From 3867ac508749e198a418eac86ae60f5c60a19a30 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Mon, 4 Sep 2023 17:02:26 +0330 Subject: [PATCH 01/42] Return list of active campaigns in projectBySlug web service related to #1051 --- src/entities/project.ts | 5 +- src/resolvers/projectResolver.test.ts | 86 +++++++++++++++++++++++++++ src/resolvers/projectResolver.ts | 14 +++-- test/graphqlQueries.ts | 18 ++++++ 4 files changed, 117 insertions(+), 6 deletions(-) diff --git a/src/entities/project.ts b/src/entities/project.ts index 5cb6bad43..81aa09209 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -51,6 +51,7 @@ import { getQfRoundTotalProjectsDonationsSum, } from '../repositories/qfRoundRepository'; import { EstimatedMatching } from '../types/qfTypes'; +import { Campaign } from './campaign'; // tslint:disable-next-line:no-var-requires const moment = require('moment'); @@ -101,7 +102,6 @@ export enum OrderField { QualityScore = 'qualityScore', Verified = 'verified', Reactions = 'totalReactions', - Traceable = 'traceCampaignId', Donations = 'totalDonations', TraceDonations = 'totalTraceDonations', AcceptGiv = 'givingBlocksId', @@ -390,6 +390,9 @@ export class Project extends BaseEntity { @Field({ nullable: true }) reaction?: Reaction; + @Field(type => [Campaign], { nullable: true }) + campaigns: Campaign[]; + // only projects with status active can be listed automatically static pendingReviewSince(maximumDaysForListing: Number) { const maxDaysForListing = moment() diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 6d4b240a3..7e5e636a3 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -5387,6 +5387,92 @@ function projectBySlugTestCases() { assert.isTrue(project.projectPower.totalPower > 0); }); + it('should return projects including active campaigns', async () => { + const projectWithCampaign = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const projectWithoutCampaign = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const campaign = await Campaign.create({ + isActive: true, + type: CampaignType.ManuallySelected, + slug: generateRandomString(), + title: 'title1', + description: 'description1', + photo: 'https://google.com', + relatedProjectsSlugs: [projectWithCampaign.slug as string], + order: 1, + }).save(); + const result = await axios.post(graphqlUrl, { + query: fetchProjectsBySlugQuery, + variables: { + slug: projectWithCampaign.slug, + }, + }); + + const project = result.data.data.projectBySlug; + assert.equal(Number(project.id), projectWithCampaign.id); + + assert.exists(project.campaigns); + assert.isNotEmpty(project.campaigns); + + const projectWithoutCampaignResult = await axios.post(graphqlUrl, { + query: fetchProjectsBySlugQuery, + variables: { + slug: projectWithoutCampaign.slug, + }, + }); + + const project2 = projectWithoutCampaignResult.data.data.projectBySlug; + assert.equal(Number(project2.id), projectWithoutCampaign.id); + + assert.isEmpty(project2.campaigns); + + await campaign.remove(); + }); + + it('should return projects including active campaigns, even when sent slug is in the slugHistory of project', async () => { + const projectWithCampaign = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const previousSlug = `${String(new Date().getTime())}-previous`; + projectWithCampaign.slugHistory = [previousSlug]; + await projectWithCampaign.save(); + + const campaign = await Campaign.create({ + isActive: true, + type: CampaignType.ManuallySelected, + slug: generateRandomString(), + title: 'title1', + description: 'description1', + photo: 'https://google.com', + relatedProjectsSlugs: [previousSlug], + order: 1, + }).save(); + const result = await axios.post(graphqlUrl, { + query: fetchProjectsBySlugQuery, + variables: { + slug: previousSlug, + }, + }); + + const project = result.data.data.projectBySlug; + assert.equal(Number(project.id), projectWithCampaign.id); + + assert.exists(project.campaigns); + assert.isNotEmpty(project.campaigns); + + await campaign.remove(); + }); + it('should return projects including project future power rank', async () => { await AppDataSource.getDataSource().query( 'truncate power_snapshot cascade', diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 1d3029b09..c68c60214 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -486,11 +486,6 @@ export class ProjectResolver { return query.andWhere(`project.${filter} ${acceptGiv} NULL`); } - if (filter === 'traceCampaignId') { - const isRequested = filterValue ? 'IS NOT' : 'IS'; - return query.andWhere(`project.${filter} ${isRequested} NULL`); - } - if ( (filter === FilterField.AcceptFundOnGnosis || filter === FilterField.AcceptFundOnCelo || @@ -896,6 +891,15 @@ export class ProjectResolver { .leftJoinAndSelect('project.projectInstantPower', 'projectInstantPower') .leftJoinAndSelect('project.qfRounds', 'qfRounds') .leftJoinAndSelect('project.projectFuturePower', 'projectFuturePower') + .leftJoinAndMapMany( + 'project.campaigns', + Campaign, + 'campaigns', + '(campaigns."relatedProjectsSlugs" && ARRAY[:slug]::text[] OR campaigns."relatedProjectsSlugs" && project."slugHistory") AND campaigns."isActive" = TRUE', + { + slug, + }, + ) .leftJoin('project.adminUser', 'user') .addSelect(publicSelectionFields); // aliased selection diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 247ef73fe..e041a1625 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -806,6 +806,24 @@ export const fetchProjectsBySlugQuery = ` listed reviewStatus givingBlocksId + campaigns { + id + title + description + type + photo + video + videoPreview + slug + isActive + order + landingLink + filterFields + sortingField + createdAt + updatedAt + + } givbackFactor projectPower { totalPower From 302f253a2155775ddb7a4409839e6757af1138dd Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 4 Sep 2023 09:35:43 -0500 Subject: [PATCH 02/42] rewrite totalReceived query --- src/services/userService.ts | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/services/userService.ts b/src/services/userService.ts index 0eec72187..7bef7a01c 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -27,18 +27,18 @@ export const updateUserTotalDonated = async (userId: number) => { export const updateUserTotalReceived = async (userId: number) => { try { - await User.query( - ` - UPDATE "user" - SET "totalReceived" = ( - SELECT COALESCE(SUM(p."totalDonations"),0) - FROM project as p - WHERE p."adminUserId" = $1 - ) - WHERE "id" = $1 - `, - [userId], - ); + const totalReceived = await User.createQueryBuilder('user') + .select('COALESCE(SUM(project.totalDonations), 0)', 'totalReceived') + .leftJoin('project', 'project', 'project.adminUserId = user.id') + .where('user.id = :userId', { userId }) + .addGroupBy('user.id') + .getRawOne(); + + await User.createQueryBuilder() + .update(User) + .set({ totalReceived: totalReceived.totalReceived }) + .where('id = :userId', { userId }) + .execute(); } catch (e) { logger.error('updateUserTotalReceived() error', e); } From f0c648e1b530a04e4f45e7653d3cc1323d046c6c Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 5 Sep 2023 14:40:13 +0330 Subject: [PATCH 03/42] Update totalReceived of project owners correctly after verifying donations https://github.com/Giveth/giveth-dapps-v2/issues/3021 --- src/services/donationService.test.ts | 9 ++++++++- src/services/donationService.ts | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index ad9f81a3d..b871b48fa 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -25,6 +25,7 @@ import { errorMessages } from '../utils/errorMessages'; import { findDonationById } from '../repositories/donationRepository'; import { findProjectById } from '../repositories/projectRepository'; import { CHAIN_ID } from '@giveth/monoswap/dist/src/sdk/sdkFactory'; +import { findUserById } from '../repositories/userRepository'; describe('isProjectAcceptToken test cases', isProjectAcceptTokenTestCases); describe( @@ -64,7 +65,7 @@ function sendSegmentEventForDonationTestCases() { } function syncDonationStatusWithBlockchainNetworkTestCases() { - it('should verify a goerli donation', async () => { + it('should verify a goerli donation and update donor.totalDonated and projectOwner.totalReceived', async () => { // https://goerli.etherscan.io/tx/0x43cb1c61a81f007abd3de766a6029ffe62d0324268d7781469a3d7879d487cb1 const transactionInfo = { @@ -105,6 +106,12 @@ function syncDonationStatusWithBlockchainNetworkTestCases() { assert.equal(updateDonation.id, donation.id); assert.isTrue(updateDonation.segmentNotified); assert.equal(updateDonation.status, DONATION_STATUS.VERIFIED); + + const donor = await findUserById(user.id); + assert.equal(donor?.totalDonated, 100); + + const projectOwnerUser = await findUserById(project.adminUser.id); + assert.equal(projectOwnerUser?.totalReceived, 100); }); it('should verify a Polygon donation', async () => { diff --git a/src/services/donationService.ts b/src/services/donationService.ts index 43b2356c0..7905122b2 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -327,7 +327,8 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { // After updating price we update totalDonations await updateTotalDonationsOfProject(donation.projectId); - await updateUserTotalReceived(donation.userId); + const project = await findProjectById(donation.projectId); + await updateUserTotalReceived(project!.adminUser.id); await sendSegmentEventForDonation({ donation, }); From 89c5c9eaf37ad84effe64e2127e597893398e09b Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Wed, 6 Sep 2023 14:29:01 +0330 Subject: [PATCH 04/42] Update totalReceived of project owners correctly after verifying donations https://github.com/Giveth/giveth-dapps-v2/issues/3021 --- src/services/donationService.test.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/services/donationService.test.ts b/src/services/donationService.test.ts index b871b48fa..0f1b83eeb 100644 --- a/src/services/donationService.test.ts +++ b/src/services/donationService.test.ts @@ -12,6 +12,7 @@ import { createDonationData, createProjectData, DONATION_SEED_DATA, + generateRandomEtheriumAddress, saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, @@ -79,10 +80,16 @@ function syncDonationStatusWithBlockchainNetworkTestCases() { timestamp: 1661114988, }; const user = await saveUserDirectlyToDb(transactionInfo.fromAddress); - const project = await saveProjectDirectlyToDb({ - ...createProjectData(), - walletAddress: transactionInfo.toAddress, - }); + const projectOwner = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const project = await saveProjectDirectlyToDb( + { + ...createProjectData(), + walletAddress: transactionInfo.toAddress, + }, + projectOwner, + ); const donation = await saveDonationDirectlyToDb( { amount: transactionInfo.amount, @@ -110,8 +117,8 @@ function syncDonationStatusWithBlockchainNetworkTestCases() { const donor = await findUserById(user.id); assert.equal(donor?.totalDonated, 100); - const projectOwnerUser = await findUserById(project.adminUser.id); - assert.equal(projectOwnerUser?.totalReceived, 100); + const updatedProjectOwner = await findUserById(projectOwner.id); + assert.equal(updatedProjectOwner?.totalReceived, 100); }); it('should verify a Polygon donation', async () => { From a357abfee872f3e507a73e3362438358f8acc363 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Wed, 6 Sep 2023 15:14:51 +0330 Subject: [PATCH 05/42] Delete test user after some test cases --- src/services/authorizationService.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/authorizationService.test.ts b/src/services/authorizationService.test.ts index a86dfdb59..0e2072bbd 100644 --- a/src/services/authorizationService.test.ts +++ b/src/services/authorizationService.test.ts @@ -32,6 +32,7 @@ function authorizationHandlerTestCases() { const accessToken = await generateTestAccessToken(user.id); const jwtUser = await authorizationHandler('1', accessToken); assert.equal(jwtUser.userId, user.id); + await User.delete(user.id); }); it('should decode user jwt with the auth microservice', async () => { const privateKey = process.env.PRIVATE_ETHERS_TEST_KEY as string; @@ -73,6 +74,7 @@ function authorizationHandlerTestCases() { const accessToken = authenticationResult.data.jwt; const jwtUser = await authorizationHandler('2', accessToken); assert.equal(jwtUser.userId, user.id); + await User.delete(user.id); }); it('should decode jwt and create user if it is nonexistent', async () => { const privateKey = process.env.PRIVATE_ETHERS_SECONDARY_TEST_KEY as string; @@ -109,5 +111,7 @@ function authorizationHandlerTestCases() { const user = await findUserByWalletAddress(publicKey); assert.equal(jwtUser.userId, user!.id); assert.equal(user!.walletAddress, publicKey.toLowerCase()); + + await User.delete(user!.id); }); } From a686f298d3ed641fc5cf3750d43bf8b6d113cc4d Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Wed, 6 Sep 2023 15:40:48 +0330 Subject: [PATCH 06/42] Fix Auth microservice address in config --- config/test.env | 6 +++--- src/services/authorizationService.test.ts | 25 ++++------------------- 2 files changed, 7 insertions(+), 24 deletions(-) diff --git a/config/test.env b/config/test.env index 6fcdbcf97..7418d44b7 100644 --- a/config/test.env +++ b/config/test.env @@ -83,9 +83,9 @@ TRACE_FILE_UPLOADER_PASSWORD=hello_trace API_GIV_PASSWORD=123 API_GIV_USERNAME=testApiGive -AUTH_MICROSERVICE_AUTHENTICATION_URL=https://serve.giveth.io/siweauthmicroservice/v1/authentication -AUTH_MICROSERVICE_AUTHORIZATION_URL=https://serve.giveth.io/siweauthmicroservice/v1/authorization -AUTH_MICROSERVICE_NONCE_URL=https://serve.giveth.io/siweauthmicroservice/v1/nonce +AUTH_MICROSERVICE_AUTHENTICATION_URL=https://auth.serve.giveth.io/siweauthmicroservice/v1/authentication +AUTH_MICROSERVICE_AUTHORIZATION_URL=https://auth.serve.giveth.io/siweauthmicroservice/v1/authorization +AUTH_MICROSERVICE_NONCE_URL=https://auth.serve.giveth.io/siweauthmicroservice/v1/nonce PRIVATE_ETHERS_TEST_KEY=8ab0e165c2ea461b01cdd49aec882d179dccdbdb5c85c3f9c94c448aa65c5ace PUBLIC_ETHERS_TEST_KEY=0x53bFf74b9Af2E3853f758A8D2Bd61CD115d27782 diff --git a/src/services/authorizationService.test.ts b/src/services/authorizationService.test.ts index 0e2072bbd..34f589693 100644 --- a/src/services/authorizationService.test.ts +++ b/src/services/authorizationService.test.ts @@ -2,6 +2,7 @@ import { assert } from 'chai'; import { generateRandomEtheriumAddress, generateTestAccessToken, + saveUserDirectlyToDb, } from '../../test/testUtils'; import { User } from '../entities/user'; import Axios from 'axios'; @@ -20,32 +21,16 @@ const origin = 'https://serve.giveth.io'; function authorizationHandlerTestCases() { it('should decode user jwt with current impact graph authorization', async () => { - const userData = { - firstName: 'firstName', - lastName: 'lastName', - email: `${new Date().getTime()}-giveth@giveth.com`, - url: 'website url', - loginType: 'wallet', - walletAddress: generateRandomEtheriumAddress(), - }; - const user = await User.create(userData).save(); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const jwtUser = await authorizationHandler('1', accessToken); assert.equal(jwtUser.userId, user.id); - await User.delete(user.id); }); it('should decode user jwt with the auth microservice', async () => { const privateKey = process.env.PRIVATE_ETHERS_TEST_KEY as string; const publicKey = process.env.PUBLIC_ETHERS_TEST_KEY as string; - const userData = { - firstName: 'firstName', - lastName: 'lastName', - email: `${new Date().getTime()}-giveth@giveth.com`, - url: 'website url', - loginType: 'wallet', - walletAddress: publicKey, - }; - const user = await User.create(userData).save(); + + const user = await saveUserDirectlyToDb(publicKey); const nonceRoute = config.get('AUTH_MICROSERVICE_NONCE_URL') as string; const nonceResult = await Axios.get(nonceRoute); const wallet = new ethers.Wallet(privateKey); @@ -111,7 +96,5 @@ function authorizationHandlerTestCases() { const user = await findUserByWalletAddress(publicKey); assert.equal(jwtUser.userId, user!.id); assert.equal(user!.walletAddress, publicKey.toLowerCase()); - - await User.delete(user!.id); }); } From 5175008b939060fef85f1565548662cf1e399e91 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Wed, 6 Sep 2023 16:42:58 +0330 Subject: [PATCH 07/42] Add cache to qfRound estimated matching queries related to #1103 --- config/test.env | 4 +++ src/repositories/qfRoundRepository.ts | 50 +++++++++++++++++++-------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/config/test.env b/config/test.env index 7163d1218..b9bb1c11c 100644 --- a/config/test.env +++ b/config/test.env @@ -171,3 +171,7 @@ OPTIMISM_NODE_HTTP_URL=https://optimism-mainnet.public.blastapi.io BALANCE_AGGREGATOR_BASE_URL=https://dev.serve.giveth.io/givpower-balance-aggregator POWER_BALANCE_AGGREGATOR_ADAPTER=mock NUMBER_OF_BALANCE_AGGREGATOR_BATCH=7 + + +# ! millisecond cache, if we increase cache in test ENV we might get some errors in tests +QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=1 diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index f4474c6d5..df84bffa5 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -1,6 +1,10 @@ import { QfRound } from '../entities/qfRound'; import { AppDataSource } from '../orm'; +const qfRoundEstimatedMatchingParamsCacheDuration = Number( + process.env.QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION || 60000, +); + export const findAllQfRounds = async (): Promise => { return QfRound.createQueryBuilder('qf_round') .addOrderBy('qf_round.id', 'DESC') @@ -47,18 +51,25 @@ export async function getProjectDonationsSqrtRootSum( projectId: number, qfRoundId: number, ): Promise<{ sqrtRootSum: number; uniqueDonorsCount: number }> { - const result = await AppDataSource.getDataSource().query( - ` - SELECT "sqrtRootSum", "uniqueDonorsCount" - FROM project_estimated_matching_view - WHERE "projectId" = $1 AND "qfRoundId" = $2; - `, - [projectId, qfRoundId], - ); + const result = await AppDataSource.getDataSource() + .createQueryBuilder() + .select('"sqrtRootSum"') + .addSelect('"uniqueDonorsCount"') + .from('project_estimated_matching_view', 'project_estimated_matching_view') + .where('"projectId" = :projectId AND "qfRoundId" = :qfRoundId', { + projectId, + qfRoundId, + }) + // Add cache here + .cache( + 'projectDonationsSqrtRootSum_' + projectId + '_' + qfRoundId, + qfRoundEstimatedMatchingParamsCacheDuration, + ) + .getRawOne(); return { - sqrtRootSum: result[0] ? result[0].sqrtRootSum : 0, - uniqueDonorsCount: result[0] ? Number(result[0].uniqueDonorsCount) : 0, + sqrtRootSum: result ? result.sqrtRootSum : 0, + uniqueDonorsCount: result ? Number(result.uniqueDonorsCount) : 0, }; } @@ -76,10 +87,21 @@ export const getQfRoundTotalProjectsDonationsSum = async ( WHERE "qfRoundId" = $1; `; - const result = await AppDataSource.getDataSource().query(query, [qfRoundId]); - - const sum = result[0]?.sum || 0; - const contributorsCount = parseInt(result[0]?.contributorsCount, 10) || 0; + const result = await AppDataSource.getDataSource() + .createQueryBuilder() + .select(`SUM("sqrtRootSumSquared")`, 'sum') + .addSelect(`SUM("donorsCount")`, 'contributorsCount') + .from('project_estimated_matching_view', 'project_estimated_matching_view') + .where('"qfRoundId" = :qfRoundId', { qfRoundId }) + // Add cache here + .cache( + 'qfRoundTotalProjectsDonationsSum_' + qfRoundId, + qfRoundEstimatedMatchingParamsCacheDuration, + ) + .getRawOne(); + + const sum = result?.sum || 0; + const contributorsCount = parseInt(result?.contributorsCount, 10) || 0; return { sum, From 60416c8ad9ec53d4d051ad75ae43e2fc0ea61a77 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 6 Sep 2023 22:36:34 -0500 Subject: [PATCH 08/42] removed unused code --- src/repositories/qfRoundRepository.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index df84bffa5..b0b3d9c06 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -79,14 +79,6 @@ export const getQfRoundTotalProjectsDonationsSum = async ( sum: number; contributorsCount: number; }> => { - const query = ` - SELECT - SUM("sqrtRootSumSquared") as "sum", - SUM("donorsCount") as "contributorsCount" - FROM project_estimated_matching_view - WHERE "qfRoundId" = $1; - `; - const result = await AppDataSource.getDataSource() .createQueryBuilder() .select(`SUM("sqrtRootSumSquared")`, 'sum') From f4c0412c7a38a27e13b2990c4a6e5a9d2e8c0e51 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 7 Sep 2023 11:42:39 +0330 Subject: [PATCH 09/42] Add QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION to example.env --- config/example.env | 1 + 1 file changed, 1 insertion(+) diff --git a/config/example.env b/config/example.env index 703d8c38f..86aa13b79 100644 --- a/config/example.env +++ b/config/example.env @@ -201,3 +201,4 @@ POWER_BALANCE_AGGREGATOR_ADAPTER=powerBalanceAggregator #POWER_BALANCE_AGGREGATOR_ADAPTER=mock NUMBER_OF_BALANCE_AGGREGATOR_BATCH=20 +QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=60000 From 812c55421013b08ff758b8a21c09a765a531f156 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sat, 9 Sep 2023 19:07:56 -0500 Subject: [PATCH 10/42] add eligible donations to qfround entity --- ...5208252-AddEligibleNetworksToQfRoundEntity.ts | 16 ++++++++++++++++ src/entities/qfRound.ts | 14 +++++++++++++- src/resolvers/donationResolver.ts | 5 ++++- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts diff --git a/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts b/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts new file mode 100644 index 000000000..65a9610d3 --- /dev/null +++ b/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddEligibleNetworksToQfRoundEntity1694295208252 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "qf_round" + ADD COLUMN IF NOT EXIST "eligibleNetworks" integer array DEFAULT ARRAY[]::integer[] + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropColumn('qf_round', 'eligibleNetworks'); + } +} diff --git a/src/entities/qfRound.ts b/src/entities/qfRound.ts index bfe637a14..81d503643 100644 --- a/src/entities/qfRound.ts +++ b/src/entities/qfRound.ts @@ -1,4 +1,4 @@ -import { Field, ID, ObjectType } from 'type-graphql'; +import { Field, ID, ObjectType, Int } from 'type-graphql'; import { PrimaryGeneratedColumn, Column, @@ -36,6 +36,10 @@ export class QfRound extends BaseEntity { @Column() minimumPassportScore: number; + @Field(type => [Int], { nullable: true }) // Define the new field as an array of integers + @Column('integer', { array: true, default: [] }) + eligibleNetworks: number[]; + @Field(type => Date) @Column() beginDate: Date; @@ -52,4 +56,12 @@ export class QfRound extends BaseEntity { @ManyToMany(type => Project, project => project.qfRounds) projects: Project[]; + + // only projects with status active can be listed automatically + isEligibleNetwork(donationNetworkId: number): Boolean { + // when not specified, all are valid + if (this.eligibleNetworks.length === 0) return true; + + return this.eligibleNetworks.includes(donationNetworkId); + } } diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 3c03ed5ee..f103e1b8f 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -727,7 +727,10 @@ export class DonationResolver { const activeQfRoundForProject = await relatedActiveQfRoundForProject( projectId, ); - if (activeQfRoundForProject) { + if ( + activeQfRoundForProject && + activeQfRoundForProject.isEligibleNetwork(Number(transactionNetworkId)) + ) { donation.qfRound = activeQfRoundForProject; } await donation.save(); From 414eb018c47cf7436a0b87ce10074ec1658619ce Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 11 Sep 2023 00:17:42 -0500 Subject: [PATCH 11/42] add tests for qfround eligible donations --- src/resolvers/donationResolver.test.ts | 96 ++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 68666dd42..ffb5711c6 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -608,6 +608,102 @@ function createDonationTestCases() { qfRound.isActive = false; await qfRound.save(); }); + + it('should create a donation in an active qfRound when qfround has network eligiblity on XDAI', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + minimumPassportScore: 8, + allocatedFund: 100, + eligibleNetworks: [100], // accepts ONLY xdai to mark as part of QFround + beginDate: new Date(), + endDate: moment().add(2, 'day'), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const referrerId = generateRandomString(); + const referrerWalletAddress = + await getChainvineAdapter().getWalletAddressFromReferrer(referrerId); + + const user = await User.create({ + walletAddress: generateRandomEtheriumAddress(), + loginType: 'wallet', + firstName: 'first name', + }).save(); + + const user2 = await User.create({ + walletAddress: referrerWalletAddress, + loginType: 'wallet', + firstName: 'first name', + }).save(); + + const referredEvent = await firstOrCreateReferredEventByUserId(user.id); + referredEvent.startTime = new Date(); + await referredEvent.save(); + + // should save Xdai + const accessToken = await generateTestAccessToken(user.id); + const saveDonationResponseXdai = await axios.post( + graphqlUrl, + { + query: createDonationMutation, + variables: { + projectId: project.id, + transactionNetworkId: NETWORK_IDS.XDAI, + transactionId: generateRandomTxHash(), + nonce: 1, + amount: 10, + token: 'GIV', + referrerId, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isOk(saveDonationResponseXdai.data.data.createDonation); + const donation = await Donation.findOne({ + where: { + id: saveDonationResponseXdai.data.data.createDonation, + }, + }); + + assert.equal(donation?.qfRound?.id as number, qfRound.id); + + // should ignore non xdai donations because its not an eligible network + const saveDonationResponseNotXdai = await axios.post( + graphqlUrl, + { + query: createDonationMutation, + variables: { + projectId: project.id, + transactionNetworkId: NETWORK_IDS.CELO, + transactionId: generateRandomTxHash(), + nonce: 1, + amount: 10, + token: 'GIV', + referrerId, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.isOk(saveDonationResponseNotXdai.data.data.createDonation); + const donationNotFromQF = await Donation.findOne({ + where: { + id: saveDonationResponseNotXdai.data.data.createDonation, + }, + }); + assert.isNull(donationNotFromQF?.qfRound); + qfRound.isActive = false; + await qfRound.save(); + }); it('should create a donation in an active qfRound, when project is not listed', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); const qfRound = await QfRound.create({ From 99a5e5837a29ce9672b167ae089022509cd99015 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 11 Sep 2023 01:46:08 -0500 Subject: [PATCH 12/42] add adminjs eligiblenetworks editable on qfround --- src/server/adminJs/tabs/qfRoundTab.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index 4422c14f5..172f69da3 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -70,6 +70,10 @@ export const qfRoundTab = { minimumPassportScore: { isVisible: true, }, + eligibleNetworks: { + isVisible: true, + type: 'array', + }, projects: { type: 'mixed', isVisible: { From e953447fb541992e7bb0a05f772d5598b54014e8 Mon Sep 17 00:00:00 2001 From: Moe Shehab <52987806+mhmdksh@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:16:14 +0400 Subject: [PATCH 13/42] Update staging-pipeline.yml Added staging DB to testing in the CI --- .github/workflows/staging-pipeline.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/staging-pipeline.yml b/.github/workflows/staging-pipeline.yml index 913bf5e37..962b49b7d 100644 --- a/.github/workflows/staging-pipeline.yml +++ b/.github/workflows/staging-pipeline.yml @@ -100,6 +100,7 @@ jobs: OPTIMISTIC_SCAN_API_KEY: ${{ secrets.OPTIMISTIC_SCAN_API_KEY }} CELO_SCAN_API_KEY: ${{ secrets.CELO_SCAN_API_KEY }} CELO_ALFAJORES_SCAN_API_KEY: ${{ secrets.CELO_ALFAJORES_SCAN_API_KEY }} + DROP_DATABASE: ${{ secrets.DROP_DATABASE }} publish: needs: test From 74a12249e6ea8a8e27b8b80f8958623b2ea0e4c2 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 13 Sep 2023 15:08:30 -0500 Subject: [PATCH 14/42] add labels to the networks in adminjs for qfround --- src/server/adminJs/tabs/qfRoundTab.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index 172f69da3..ded578459 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -19,6 +19,7 @@ import { getRelatedProjectsOfQfRound, } from '../../../repositories/qfRoundRepository'; import { RecordJSON } from 'adminjs/src/frontend/interfaces/record-json.interface'; +import { NETWORK_IDS } from '../../../provider'; export const refreshMaterializedViews = async ( response, @@ -73,6 +74,21 @@ export const qfRoundTab = { eligibleNetworks: { isVisible: true, type: 'array', + availableValues: [ + { value: NETWORK_IDS.MAIN_NET, label: 'MAINNET' }, + { value: NETWORK_IDS.ROPSTEN, label: 'ROPSTEN' }, + { value: NETWORK_IDS.GOERLI, label: 'GOERLI' }, + { value: NETWORK_IDS.POLYGON, label: 'POLYGON' }, + { value: NETWORK_IDS.OPTIMISTIC, label: 'OPTIMISTIC' }, + { value: NETWORK_IDS.OPTIMISM_GOERLI, label: 'OPTIMISM GOERLI' }, + { value: NETWORK_IDS.CELO, label: 'CELO' }, + { + value: NETWORK_IDS.CELO_ALFAJORES, + label: 'ALFAJORES (Test CELO)', + }, + { value: NETWORK_IDS.XDAI, label: 'XDAI' }, + { value: NETWORK_IDS.BSC, label: 'BSC' }, + ], }, projects: { type: 'mixed', From ca3185264d027c11b0199e4f374c586ad0692abf Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 13 Sep 2023 23:11:06 -0500 Subject: [PATCH 15/42] add migration to fill in previous networkIds in qfrounds --- ...8-AddEligibleNetworksToPreviousQfRounds.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts diff --git a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts new file mode 100644 index 000000000..680bdef1d --- /dev/null +++ b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts @@ -0,0 +1,32 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import config from '../src/config'; + +export class AddEligibleNetworksToPreviousQfRounds1694635872128 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + const environment = config.get('ENVIRONMENT') as string; + + // Define the eligible network IDs based on the conditions + const eligibleNetworks = + environment !== 'production' + ? [1, 3, 5, 100, 137, 10, 420, 56, 42220, 44787] // Include testnets for staging + : [1, 137, 56, 42220, 100, 10]; // Exclude testnets for non-staging + + // Convert the eligibleNetworks array to a comma-separated string + const eligibleNetworksString = eligibleNetworks.join(', '); + + // Update the "qf_round" table with the new eligibleNetworks values + await queryRunner.query(` + UPDATE "qf_round" + SET eligibleNetworks = '{${eligibleNetworksString}}' + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE "qf_round" + SET eligibleNetworks = '{}' + `); + } +} From c851c2ade303bd126305edb605ea39ca60580daa Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 14 Sep 2023 10:32:54 -0500 Subject: [PATCH 16/42] fix eligible donations migration --- ...35872128-AddEligibleNetworksToPreviousQfRounds.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts index 680bdef1d..f65f5635b 100644 --- a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts +++ b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts @@ -13,14 +13,14 @@ export class AddEligibleNetworksToPreviousQfRounds1694635872128 ? [1, 3, 5, 100, 137, 10, 420, 56, 42220, 44787] // Include testnets for staging : [1, 137, 56, 42220, 100, 10]; // Exclude testnets for non-staging - // Convert the eligibleNetworks array to a comma-separated string - const eligibleNetworksString = eligibleNetworks.join(', '); - // Update the "qf_round" table with the new eligibleNetworks values - await queryRunner.query(` + await queryRunner.query( + ` UPDATE "qf_round" - SET eligibleNetworks = '{${eligibleNetworksString}}' - `); + SET eligibleNetworks = $1 + `, + [eligibleNetworks], + ); } public async down(queryRunner: QueryRunner): Promise { From 703164052876c557b100c6d39530455eb1e0e6ef Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 14 Sep 2023 11:02:05 -0500 Subject: [PATCH 17/42] fix migration table names for qfround networks --- migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts | 2 +- .../1694635872128-AddEligibleNetworksToPreviousQfRounds.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts b/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts index 65a9610d3..641648841 100644 --- a/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts +++ b/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts @@ -5,7 +5,7 @@ export class AddEligibleNetworksToQfRoundEntity1694295208252 { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` - ALTER TABLE "qf_round" + ALTER TABLE public.qf_round ADD COLUMN IF NOT EXIST "eligibleNetworks" integer array DEFAULT ARRAY[]::integer[] `); } diff --git a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts index f65f5635b..0ad1e998f 100644 --- a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts +++ b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts @@ -16,7 +16,7 @@ export class AddEligibleNetworksToPreviousQfRounds1694635872128 // Update the "qf_round" table with the new eligibleNetworks values await queryRunner.query( ` - UPDATE "qf_round" + UPDATE public.qf_round SET eligibleNetworks = $1 `, [eligibleNetworks], @@ -25,7 +25,7 @@ export class AddEligibleNetworksToPreviousQfRounds1694635872128 public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` - UPDATE "qf_round" + UPDATE public.qf_round SET eligibleNetworks = '{}' `); } From 18197447cb9bd058210a6bce1dbb5ca1fa6c5512 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 14 Sep 2023 14:16:05 -0500 Subject: [PATCH 18/42] fix migration file for eligible networks --- migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts b/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts index 641648841..e7c2e85f4 100644 --- a/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts +++ b/migration/1694295208252-AddEligibleNetworksToQfRoundEntity.ts @@ -6,7 +6,7 @@ export class AddEligibleNetworksToQfRoundEntity1694295208252 public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE public.qf_round - ADD COLUMN IF NOT EXIST "eligibleNetworks" integer array DEFAULT ARRAY[]::integer[] + ADD COLUMN IF NOT EXISTS "eligibleNetworks" integer array DEFAULT ARRAY[]::integer[] `); } From ed3025a220fd6e524b5be8f210b3817a26e9e807 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 14 Sep 2023 14:52:28 -0500 Subject: [PATCH 19/42] fix qfround fill networks migration --- .../1694635872128-AddEligibleNetworksToPreviousQfRounds.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts index 0ad1e998f..d8d6df46f 100644 --- a/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts +++ b/migration/1694635872128-AddEligibleNetworksToPreviousQfRounds.ts @@ -17,7 +17,7 @@ export class AddEligibleNetworksToPreviousQfRounds1694635872128 await queryRunner.query( ` UPDATE public.qf_round - SET eligibleNetworks = $1 + SET "eligibleNetworks" = $1 `, [eligibleNetworks], ); @@ -26,7 +26,7 @@ export class AddEligibleNetworksToPreviousQfRounds1694635872128 public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` UPDATE public.qf_round - SET eligibleNetworks = '{}' + SET "eligibleNetworks" = '{}' `); } } From ae9f96a4a1b59051bdab82790bd118a7c8529747 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sat, 16 Sep 2023 15:16:13 -0500 Subject: [PATCH 20/42] add query for knowing if wallet was used --- src/repositories/donationRepository.ts | 13 +++++++++++++ src/resolvers/userResolver.ts | 10 ++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index caa7b3ae8..9bf2825fc 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -26,6 +26,19 @@ export const fillQfRoundDonationsUserScores = async (): Promise => { `); }; +export const addressHasDonated = async (address: string) => { + const projectAddress = await Donation.query( + ` + SELECT "id" + FROM donation + where lower("fromWalletAddress") = $1 + limit 1 + `, + [address.toLowerCase()], + ); + return projectAddress.length > 0; +}; + export const createDonation = async (data: { amount: number; project: Project; diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index a03f3906a..15ace1d8c 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -18,6 +18,8 @@ import { SegmentAnalyticsSingleton } from '../services/segment/segmentAnalyticsS import { AppDataSource } from '../orm'; import { getGitcoinAdapter } from '../adapters/adaptersFactory'; import { logger } from '../utils/logger'; +import { isWalletAddressInPurpleList } from '../repositories/projectAddressRepository'; +import { addressHasDonated } from '../repositories/donationRepository'; @Resolver(of => User) export class UserResolver { @@ -29,6 +31,14 @@ export class UserResolver { // return User.create(data).save(); } + @Query(returns => Boolean) + async walletAddressUsed(@Arg('address') address: string): Promise { + return ( + (await isWalletAddressInPurpleList(address)) || + (await addressHasDonated(address)) + ); + } + @Query(returns => UserByAddressResponse, { nullable: true }) async userByAddress( @Arg('address', type => String) address: string, From 7a0b6c189edf1d01848d9f6e555f66eb18c6870b Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 17 Sep 2023 14:15:30 -0500 Subject: [PATCH 21/42] improve query for user related address --- src/resolvers/userResolver.ts | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 15ace1d8c..109655e75 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -1,4 +1,12 @@ -import { Arg, Ctx, Mutation, Query, Resolver } from 'type-graphql'; +import { + Arg, + Ctx, + Field, + Mutation, + ObjectType, + Query, + Resolver, +} from 'type-graphql'; import { Repository } from 'typeorm'; import { User } from '../entities/user'; @@ -21,6 +29,15 @@ import { logger } from '../utils/logger'; import { isWalletAddressInPurpleList } from '../repositories/projectAddressRepository'; import { addressHasDonated } from '../repositories/donationRepository'; +@ObjectType() +class UserRelatedAddressResponse { + @Field(type => Boolean, { nullable: false }) + hasRelatedProject: boolean; + + @Field(type => Boolean, { nullable: false }) + hasDonated: boolean; +} + @Resolver(of => User) export class UserResolver { constructor(private readonly userRepository: Repository) { @@ -31,12 +48,12 @@ export class UserResolver { // return User.create(data).save(); } - @Query(returns => Boolean) - async walletAddressUsed(@Arg('address') address: string): Promise { - return ( - (await isWalletAddressInPurpleList(address)) || - (await addressHasDonated(address)) - ); + @Query(returns => UserRelatedAddressResponse) + async walletAddressUsed(@Arg('address') address: string) { + return { + hasRelatedProject: await isWalletAddressInPurpleList(address), + hasDonated: await addressHasDonated(address), + }; } @Query(returns => UserByAddressResponse, { nullable: true }) From 1a46487eec8974949b741e2395b877e1a9ad7ca4 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 18 Sep 2023 23:10:33 -0500 Subject: [PATCH 22/42] unify project counts in userByAddress and projectsByUserId --- src/entities/user.ts | 9 ++------- src/repositories/userRepository.ts | 30 +++++++++++++++++++++++++++++- src/resolvers/userResolver.ts | 2 ++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/entities/user.ts b/src/entities/user.ts index 6c6817f37..d9f2b17c0 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -183,13 +183,7 @@ export class User extends BaseEntity { createdAt: Date; @Field(type => Int, { nullable: true }) - async projectsCount() { - const projectsCount = await Project.createQueryBuilder('project') - .where('project."admin" = :id', { id: String(this.id) }) - .getCount(); - - return projectsCount; - } + projectsCount?: number; @Field(type => Int, { nullable: true }) async donationsCount() { @@ -215,6 +209,7 @@ export class User extends BaseEntity { return likedProjectsCount; } + @Field(type => Int, { nullable: true }) async boostedProjectsCount() { return findPowerBoostingsCountByUserId(this.id); diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 7e074598e..e262d59d3 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -3,6 +3,7 @@ import { SegmentAnalyticsSingleton } from '../services/segment/segmentAnalyticsS import { Donation } from '../entities/donation'; import { Reaction } from '../entities/reaction'; import { PowerBoosting } from '../entities/powerBoosting'; +import { Project, ProjStatus, ReviewStatus } from '../entities/project'; export const findAdminUserByEmail = async ( email: string, @@ -35,6 +36,7 @@ export const isFirstTimeDonor = async (userId: number): Promise => { export const findUserByWalletAddress = async ( walletAddress: string, includeSensitiveFields = true, + ownerUserId?: number, ): Promise => { const query = User.createQueryBuilder('user').where( `LOWER("walletAddress") = :walletAddress`, @@ -45,8 +47,34 @@ export const findUserByWalletAddress = async ( if (!includeSensitiveFields) { query.select(publicSelectionFields); } + const user = await query.getOne(); + if (!user) return null; - return query.getOne(); + user.projectsCount = await fetchUserProjectsCount( + user!.id, + user?.id === ownerUserId, + ); + + return user; +}; + +export const fetchUserProjectsCount = async ( + userId: number, + ownerViewing: boolean, +) => { + const projectsCount = Project.createQueryBuilder('project').where( + 'project."adminUserId" = :id', + { id: userId }, + ); + + if (!ownerViewing) { + projectsCount.andWhere( + `project.statusId = ${ProjStatus.active} AND project.reviewStatus = :reviewStatus`, + { reviewStatus: ReviewStatus.Listed }, + ); + } + + return projectsCount.getCount(); }; export const findUserById = (userId: number): Promise => { diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 109655e75..7a9f9d38f 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -66,6 +66,7 @@ export class UserResolver { const foundUser = await findUserByWalletAddress( address, includeSensitiveFields, + user?.userId, ); return { isSignedIn: Boolean(user), @@ -83,6 +84,7 @@ export class UserResolver { const foundUser = await findUserByWalletAddress( address, includeSensitiveFields, + user?.userId, ); if (!foundUser) return; From 5fec9311400e8732a8dd839c4125f2890803d7b1 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 19 Sep 2023 14:56:05 -0500 Subject: [PATCH 23/42] improve condition for projectCount --- src/repositories/userRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index e262d59d3..57ac0cdbf 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -52,7 +52,7 @@ export const findUserByWalletAddress = async ( user.projectsCount = await fetchUserProjectsCount( user!.id, - user?.id === ownerUserId, + Number(user?.id) === Number(ownerUserId), ); return user; From 28c129944a4c100773e87682a09ef4dc9392ca95 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 19 Sep 2023 16:46:09 -0500 Subject: [PATCH 24/42] fix projectCount query --- src/repositories/userRepository.ts | 7 +++---- src/resolvers/userResolver.ts | 2 -- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/repositories/userRepository.ts b/src/repositories/userRepository.ts index 57ac0cdbf..31f2233d6 100644 --- a/src/repositories/userRepository.ts +++ b/src/repositories/userRepository.ts @@ -36,7 +36,6 @@ export const isFirstTimeDonor = async (userId: number): Promise => { export const findUserByWalletAddress = async ( walletAddress: string, includeSensitiveFields = true, - ownerUserId?: number, ): Promise => { const query = User.createQueryBuilder('user').where( `LOWER("walletAddress") = :walletAddress`, @@ -52,7 +51,7 @@ export const findUserByWalletAddress = async ( user.projectsCount = await fetchUserProjectsCount( user!.id, - Number(user?.id) === Number(ownerUserId), + includeSensitiveFields, ); return user; @@ -60,14 +59,14 @@ export const findUserByWalletAddress = async ( export const fetchUserProjectsCount = async ( userId: number, - ownerViewing: boolean, + includeSensitiveFields: boolean, ) => { const projectsCount = Project.createQueryBuilder('project').where( 'project."adminUserId" = :id', { id: userId }, ); - if (!ownerViewing) { + if (!includeSensitiveFields) { projectsCount.andWhere( `project.statusId = ${ProjStatus.active} AND project.reviewStatus = :reviewStatus`, { reviewStatus: ReviewStatus.Listed }, diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 7a9f9d38f..109655e75 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -66,7 +66,6 @@ export class UserResolver { const foundUser = await findUserByWalletAddress( address, includeSensitiveFields, - user?.userId, ); return { isSignedIn: Boolean(user), @@ -84,7 +83,6 @@ export class UserResolver { const foundUser = await findUserByWalletAddress( address, includeSensitiveFields, - user?.userId, ); if (!foundUser) return; From db91549d3e67a3b1ecc924499654021e7b2f73ad Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Wed, 27 Sep 2023 12:30:24 +0330 Subject: [PATCH 25/42] Add Other types of campaigns to projectBySlug webservice https://github.com/Giveth/impact-graph/issues/1051#issuecomment-1733713799 --- config/example.env | 3 + config/test.env | 2 + src/entities/campaign.ts | 17 +++ src/repositories/projectRepository.test.ts | 47 +++++-- src/repositories/projectRepository.ts | 11 ++ src/resolvers/projectResolver.test.ts | 136 ++++++++++++++++++++- src/resolvers/projectResolver.ts | 25 ++-- src/services/campaignService.ts | 59 +++++++-- 8 files changed, 272 insertions(+), 28 deletions(-) diff --git a/config/example.env b/config/example.env index 86aa13b79..9ecd2a5f9 100644 --- a/config/example.env +++ b/config/example.env @@ -202,3 +202,6 @@ POWER_BALANCE_AGGREGATOR_ADAPTER=powerBalanceAggregator NUMBER_OF_BALANCE_AGGREGATOR_BATCH=20 QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=60000 + +# OPTIONAL - default: Every 10 minutes +PROJECT_CAMPAIGNS_CACHE_DURATION=600000 diff --git a/config/test.env b/config/test.env index b9bb1c11c..a5786e08c 100644 --- a/config/test.env +++ b/config/test.env @@ -175,3 +175,5 @@ NUMBER_OF_BALANCE_AGGREGATOR_BATCH=7 # ! millisecond cache, if we increase cache in test ENV we might get some errors in tests QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=1 +# ! millisecond cache, if we increase cache in test ENV we might get some errors in tests +PROJECT_CAMPAIGNS_CACHE_DURATION=1 diff --git a/src/entities/campaign.ts b/src/entities/campaign.ts index fcd3d8c21..7e1860e10 100644 --- a/src/entities/campaign.ts +++ b/src/entities/campaign.ts @@ -35,9 +35,26 @@ export enum CampaignFilterField { } export enum CampaignType { + // https://github.com/Giveth/impact-graph/blob/staging/docs/campaignsInstruction.md + + // In these type of projects we pick some projects to show them in campaign, + // for instance for Turkey earthquake we pick some projects. + // so we just need to add slug of those projects in Related Projects Slugs and in + // what order we add them they will be shown in frontend ManuallySelected = 'ManuallySelected', + + // Sometimes in a campaign we just want to show projects in an specified order, + // for instance we can create a campaign like ** Check projects that received most likes** so for + // this campaign you set SortField as campaign type and then you can use one of below sorting fields SortField = 'SortField', + + // Sometimes we need to filter some projects in a campaign, + // for instance Let's verified projects that accept funds on Gnosis chain, + // for this we can Add verified and acceptFundOnGnosis filters FilterFields = 'FilterFields', + + // Some campaigns don't include any project in them and they are just some banner + // like Feeling $nice? campaign in below image WithoutProjects = 'WithoutProjects', } diff --git a/src/repositories/projectRepository.test.ts b/src/repositories/projectRepository.test.ts index 50cce6ef4..b4a20ce89 100644 --- a/src/repositories/projectRepository.test.ts +++ b/src/repositories/projectRepository.test.ts @@ -1,6 +1,7 @@ import { findProjectById, findProjectBySlug, + findProjectBySlugWithoutAnyJoin, findProjectByWalletAddress, findProjectsByIdArray, findProjectsBySlugArray, @@ -56,6 +57,20 @@ describe( updateDescriptionSummaryTestCases, ); +describe('verifyProject test cases', verifyProjectTestCases); +describe('verifyMultipleProjects test cases', verifyMultipleProjectsTestCases); +describe('findProjectById test cases', findProjectByIdTestCases); +describe('findProjectsByIdArray test cases', findProjectsByIdArrayTestCases); +describe('findProjectBySlug test cases', findProjectBySlugTestCases); +describe( + 'findProjectBySlugWithoutAnyJoin test cases', + findProjectBySlugWithoutAnyJoinTestCases, +); +describe( + 'findProjectsBySlugArray test cases', + findProjectsBySlugArrayTestCases, +); + function projectsWithoutUpdateAfterTimeFrameTestCases() { it('should return projects created a long time ago', async () => { const superExpiredProject = await saveProjectDirectlyToDb({ @@ -96,22 +111,13 @@ function projectsWithoutUpdateAfterTimeFrameTestCases() { }); } -describe('verifyProject test cases', verifyProjectTestCases); -describe('verifyMultipleProjects test cases', verifyMultipleProjectsTestCases); -describe('findProjectById test cases', findProjectByIdTestCases); -describe('findProjectsByIdArray test cases', findProjectsByIdArrayTestCases); -describe('findProjectBySlug test cases', findProjectBySlugTestCases); -describe( - 'findProjectsBySlugArray test cases', - findProjectsBySlugArrayTestCases, -); - function findProjectBySlugTestCases() { - it('Should find project by id', async () => { + it('Should find project by slug', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); const foundProject = await findProjectBySlug(project.slug as string); assert.isOk(foundProject); assert.equal(foundProject?.id, project.id); + assert.isOk(foundProject?.adminUser); }); it('should not find project when project doesnt exists', async () => { @@ -120,6 +126,25 @@ function findProjectBySlugTestCases() { }); } +function findProjectBySlugWithoutAnyJoinTestCases() { + it('Should find project by slug', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const foundProject = await findProjectBySlugWithoutAnyJoin( + project.slug as string, + ); + assert.isOk(foundProject); + assert.equal(foundProject?.id, project.id); + assert.isNotOk(foundProject?.adminUser); + }); + + it('should not find project when project doesnt exists', async () => { + const foundProject = await findProjectBySlugWithoutAnyJoin( + new Date().toString(), + ); + assert.isNull(foundProject); + }); +} + function findProjectsBySlugArrayTestCases() { it('Should find project multi projects by slug', async () => { const project1 = await saveProjectDirectlyToDb(createProjectData()); diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 72f7003b4..25e762dce 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -211,6 +211,17 @@ export const findProjectBySlug = (slug: string): Promise => { ); }; +export const findProjectBySlugWithoutAnyJoin = ( + slug: string, +): Promise => { + // check current slug and previous slugs + return Project.createQueryBuilder('project') + .where(`:slug = ANY(project."slugHistory") or project.slug = :slug`, { + slug, + }) + .getOne(); +}; + export const verifyMultipleProjects = async (params: { verified: boolean; projectsIds: string[] | number[]; diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 0aaae662f..882c4edad 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -90,7 +90,12 @@ import { refreshUserProjectPowerView } from '../repositories/userProjectPowerVie import { AppDataSource } from '../orm'; // We are using cache so redis needs to be cleared for tests with same filters import { redis } from '../redis'; -import { Campaign, CampaignType } from '../entities/campaign'; +import { + Campaign, + CampaignFilterField, + CampaignSortingField, + CampaignType, +} from '../entities/campaign'; import { generateRandomString, getHtmlTextSummary } from '../utils/utils'; import { FeaturedUpdate } from '../entities/featuredUpdate'; import { @@ -5456,7 +5461,7 @@ function projectBySlugTestCases() { assert.isTrue(project.projectPower.totalPower > 0); }); - it('should return projects including active campaigns', async () => { + it('should return projects including active ManuallySelected campaigns', async () => { const projectWithCampaign = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), @@ -5505,6 +5510,133 @@ function projectBySlugTestCases() { await campaign.remove(); }); + it('should return projects including active SortField campaigns', async () => { + const projectWithCampaign = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const campaign = await Campaign.create({ + isActive: true, + type: CampaignType.SortField, + sortingField: CampaignSortingField.Newest, + slug: generateRandomString(), + title: 'title1', + description: 'description1', + photo: 'https://google.com', + order: 1, + }).save(); + const result = await axios.post(graphqlUrl, { + query: fetchProjectsBySlugQuery, + variables: { + slug: projectWithCampaign.slug, + }, + }); + + const project = result.data.data.projectBySlug; + assert.equal(Number(project.id), projectWithCampaign.id); + + assert.exists(project.campaigns); + assert.isNotEmpty(project.campaigns); + assert.equal(project.campaigns[0].id, campaign.id); + + const projectWithoutCampaignResult = await axios.post(graphqlUrl, { + query: fetchProjectsBySlugQuery, + variables: { + // and old project that I'm sure it would not be in the Newest campaign + slug: SEED_DATA.FIRST_PROJECT.slug, + }, + }); + + const project2 = projectWithoutCampaignResult.data.data.projectBySlug; + assert.equal(Number(project2.id), SEED_DATA.FIRST_PROJECT.id); + + assert.isEmpty(project2.campaigns); + + await campaign.remove(); + }); + + it('should return projects including active FilterField campaigns (acceptOnGnosis)', async () => { + // In this filter the default sorting for projects is givPower so I need to create a project with power + // to be sure that it will be in the campaign + await PowerBoosting.clear(); + await InstantPowerBalance.clear(); + + const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const projectWithCampaign = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.XDAI, + }); + + const projectWithoutCampaign = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + networkId: NETWORK_IDS.POLYGON, + }); + + await Promise.all( + [[user1, projectWithCampaign, 10]].map(item => { + const [user, project, percentage] = item as [User, Project, number]; + return insertSinglePowerBoosting({ + user, + project, + percentage, + }); + }), + ); + + await saveOrUpdateInstantPowerBalances([ + { + userId: user1.id, + balance: 10000, + balanceAggregatorUpdatedAt: new Date(1_000_000), + }, + ]); + + await updateInstantBoosting(); + + const campaign = await Campaign.create({ + isActive: true, + type: CampaignType.FilterFields, + filterFields: [CampaignFilterField.acceptFundOnGnosis], + slug: generateRandomString(), + title: 'title1', + description: 'description1', + photo: 'https://google.com', + order: 1, + }).save(); + const result = await axios.post(graphqlUrl, { + query: fetchProjectsBySlugQuery, + variables: { + slug: projectWithCampaign.slug, + }, + }); + + const fetchedProject = result.data.data.projectBySlug; + assert.equal(Number(fetchedProject.id), projectWithCampaign.id); + + assert.exists(fetchedProject.campaigns); + assert.isNotEmpty(fetchedProject.campaigns); + assert.equal(fetchedProject.campaigns[0].id, campaign.id); + + const projectWithoutCampaignResult = await axios.post(graphqlUrl, { + query: fetchProjectsBySlugQuery, + variables: { + slug: projectWithoutCampaign.slug, + }, + }); + + const project2 = projectWithoutCampaignResult.data.data.projectBySlug; + assert.equal(Number(project2.id), projectWithoutCampaign.id); + + assert.isEmpty(project2.campaigns); + + await campaign.remove(); + }); it('should return projects including active campaigns, even when sent slug is in the slugHistory of project', async () => { const projectWithCampaign = await saveProjectDirectlyToDb({ diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 2ec7ab8b2..6e246deff 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -77,6 +77,7 @@ import { FilterProjectQueryInputParams, filterProjectsQuery, findProjectById, + findProjectBySlugWithoutAnyJoin, totalProjectsPerDate, totalProjectsPerDateByMonthAndYear, userIsOwnerOfProject, @@ -108,6 +109,7 @@ import { FeaturedUpdate } from '../entities/featuredUpdate'; import { PROJECT_UPDATE_CONTENT_MAX_LENGTH } from '../constants/validators'; import { calculateGivbackFactor } from '../services/givbackService'; import { ProjectBySlugResponse } from './types/projectResolver'; +import { getAllProjectsRelatedToActiveCampaigns } from '../services/campaignService'; @ObjectType() class AllProjects { @@ -898,11 +900,18 @@ export class ProjectResolver { isOwnerOfProject = await userIsOwnerOfProject(viewerUserId, slug); } + const minimalProject = await findProjectBySlugWithoutAnyJoin(slug); + if (!minimalProject) { + throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); + } + const campaignSlugs = (await getAllProjectsRelatedToActiveCampaigns())[ + minimalProject.id + ]; + let query = this.projectRepository .createQueryBuilder('project') - // check current slug and previous slugs - .where(`:slug = ANY(project."slugHistory") or project.slug = :slug`, { - slug, + .where(`project.id = :id`, { + id: minimalProject.id, }) .leftJoinAndSelect('project.status', 'status') .leftJoinAndSelect( @@ -922,9 +931,10 @@ export class ProjectResolver { 'project.campaigns', Campaign, 'campaigns', - '(campaigns."relatedProjectsSlugs" && ARRAY[:slug]::text[] OR campaigns."relatedProjectsSlugs" && project."slugHistory") AND campaigns."isActive" = TRUE', + '((campaigns."relatedProjectsSlugs" && ARRAY[:slug]::text[] OR campaigns."relatedProjectsSlugs" && project."slugHistory") AND campaigns."isActive" = TRUE) OR (campaigns.slug = ANY(:campaignSlugs))', { slug, + campaignSlugs, }, ) .leftJoin('project.adminUser', 'user') @@ -946,9 +956,6 @@ export class ProjectResolver { }); const project = await query.getOne(); - if (!project) { - throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - } canUserVisitProject(project, String(user?.userId)); const verificationForm = project?.projectVerificationForm || @@ -956,7 +963,9 @@ export class ProjectResolver { if (verificationForm) { (project as Project).verificationFormStatus = verificationForm?.status; } - const { givbackFactor } = await calculateGivbackFactor(project.id); + + // We know that we have the project because if we reach this line means minimalProject is not null + const { givbackFactor } = await calculateGivbackFactor(project!.id); return { ...project, givbackFactor }; } diff --git a/src/services/campaignService.ts b/src/services/campaignService.ts index 1851d13af..ede5e7265 100644 --- a/src/services/campaignService.ts +++ b/src/services/campaignService.ts @@ -7,16 +7,15 @@ import { FilterField, Project, SortingField } from '../entities/project'; import { findUserReactionsByProjectIds } from '../repositories/reactionRepository'; import { ModuleThread, Pool } from 'threads'; import { ProjectResolverWorker } from '../workers/projectsResolverWorker'; +import { QueryBuilder } from 'typeorm/query-builder/QueryBuilder'; +import { findAllActiveCampaigns } from '../repositories/campaignRepository'; const projectFiltersCacheDuration = Number(process.env.PROJECT_FILTERS_THREADS_POOL_DURATION) || 60000; -export const fillCampaignProjects = async (params: { - userId?: number; - campaign: Campaign; - projectsFiltersThreadPool: Pool>; -}): Promise => { - const { campaign, userId, projectsFiltersThreadPool } = params; +const createFetchCampaignProjectsQuery = ( + campaign: Campaign, +): FilterProjectQueryInputParams | null => { const limit = 10; const skip = 0; const projectsQueryParams: FilterProjectQueryInputParams = { @@ -36,9 +35,55 @@ export const fillCampaignProjects = async (params: { campaign.sortingField as unknown as SortingField; } else if (campaign.type === CampaignType.WithoutProjects) { // Dont add projects to this campaign type - return campaign; + return null; + } + + return projectsQueryParams; +}; +let projectCampaignCache: { [key: number]: string[] } | undefined; + +export const getAllProjectsRelatedToActiveCampaigns = async (): Promise<{ + [key: number]: string[]; +}> => { + // It returns all project and campaigns( excluding manuallySelectedCampaign) + if (projectCampaignCache) { + return projectCampaignCache; } + projectCampaignCache = {}; + const activeCampaigns = await findAllActiveCampaigns(); + for (const campaign of activeCampaigns) { + const projectsQueryParams = createFetchCampaignProjectsQuery(campaign); + if (!projectsQueryParams) { + break; + } + const projectsQuery = filterProjectsQuery(projectsQueryParams); + const projects = await projectsQuery.getMany(); + for (const project of projects) { + projectCampaignCache[project.id] + ? projectCampaignCache[project.id].push(campaign.slug) + : (projectCampaignCache[project.id] = [campaign.slug]); + } + } + const projectCampaignsCacheDuration = + Number(process.env.PROJECT_CAMPAIGNS_CACHE_DURATION) || 10 * 60 * 1000; + setTimeout(() => { + // We make it undefined every 10 minutes, to refresh it + projectCampaignCache = undefined; + }, projectCampaignsCacheDuration); + + return projectCampaignCache; +}; +export const fillCampaignProjects = async (params: { + userId?: number; + campaign: Campaign; + projectsFiltersThreadPool: Pool>; +}): Promise => { + const { campaign, userId, projectsFiltersThreadPool } = params; + const projectsQueryParams = createFetchCampaignProjectsQuery(campaign); + if (!projectsQueryParams) { + return campaign; + } const projectsQuery = filterProjectsQuery(projectsQueryParams); const projectsQueryCacheKey = await projectsFiltersThreadPool.queue(hasher => hasher.hashProjectFilters({ From 704cb987c0d4063ad75af9e446fbe2557edd62b5 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 28 Sep 2023 14:38:45 +0330 Subject: [PATCH 26/42] Change name of fetchProjectsBySlugQuery field --- src/resolvers/projectResolver.test.ts | 44 ++++++++++---------- src/resolvers/qfRoundHistoryResolver.test.ts | 2 +- test/graphqlQueries.ts | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 882c4edad..7ae005553 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -29,7 +29,7 @@ import { fetchLikedProjectsQuery, fetchMultiFilterAllProjectsQuery, fetchNewProjectsPerDate, - fetchProjectsBySlugQuery, + fetchProjectBySlugQuery, fetchProjectUpdatesQuery, fetchSimilarProjectsBySlugQuery, getProjectsAcceptTokensQuery, @@ -5324,7 +5324,7 @@ function projectBySlugTestCases() { const result = await axios.post( graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project1.slug, connectedWalletUserId: user!.id, @@ -5367,7 +5367,7 @@ function projectBySlugTestCases() { }).save(); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project1.slug, connectedWalletUserId: user!.id, @@ -5390,7 +5390,7 @@ function projectBySlugTestCases() { }); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project1.slug, }, @@ -5448,7 +5448,7 @@ function projectBySlugTestCases() { await refreshProjectPowerView(); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project1.slug, }, @@ -5484,7 +5484,7 @@ function projectBySlugTestCases() { order: 1, }).save(); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: projectWithCampaign.slug, }, @@ -5497,7 +5497,7 @@ function projectBySlugTestCases() { assert.isNotEmpty(project.campaigns); const projectWithoutCampaignResult = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: projectWithoutCampaign.slug, }, @@ -5527,7 +5527,7 @@ function projectBySlugTestCases() { order: 1, }).save(); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: projectWithCampaign.slug, }, @@ -5541,7 +5541,7 @@ function projectBySlugTestCases() { assert.equal(project.campaigns[0].id, campaign.id); const projectWithoutCampaignResult = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { // and old project that I'm sure it would not be in the Newest campaign slug: SEED_DATA.FIRST_PROJECT.slug, @@ -5610,7 +5610,7 @@ function projectBySlugTestCases() { order: 1, }).save(); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: projectWithCampaign.slug, }, @@ -5624,7 +5624,7 @@ function projectBySlugTestCases() { assert.equal(fetchedProject.campaigns[0].id, campaign.id); const projectWithoutCampaignResult = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: projectWithoutCampaign.slug, }, @@ -5659,7 +5659,7 @@ function projectBySlugTestCases() { order: 1, }).save(); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: previousSlug, }, @@ -5755,7 +5755,7 @@ function projectBySlugTestCases() { await refreshProjectFuturePowerView(); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project1.slug, }, @@ -5850,7 +5850,7 @@ function projectBySlugTestCases() { await refreshProjectFuturePowerView(false); let result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project1.slug, }, @@ -5865,7 +5865,7 @@ function projectBySlugTestCases() { await refreshProjectFuturePowerView(true); result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project1.slug, }, @@ -5884,7 +5884,7 @@ function projectBySlugTestCases() { }); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: draftedProject.slug, }, @@ -5908,7 +5908,7 @@ function projectBySlugTestCases() { const result = await axios.post( graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: draftedProject.slug, connectedWalletUserId: SEED_DATA.FIRST_USER.id, @@ -5939,7 +5939,7 @@ function projectBySlugTestCases() { const result = await axios.post( graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: draftedProject.slug, }, @@ -5967,7 +5967,7 @@ function projectBySlugTestCases() { }); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project.slug, }, @@ -5991,7 +5991,7 @@ function projectBySlugTestCases() { const result = await axios.post( graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project.slug, connectedWalletUserId: SEED_DATA.FIRST_USER.id, @@ -6022,7 +6022,7 @@ function projectBySlugTestCases() { const result = await axios.post( graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: cancelledProject.slug, }, @@ -6080,7 +6080,7 @@ function projectBySlugTestCases() { await updateInstantBoosting(); const result = await axios.post(graphqlUrl, { - query: fetchProjectsBySlugQuery, + query: fetchProjectBySlugQuery, variables: { slug: project1.slug, }, diff --git a/src/resolvers/qfRoundHistoryResolver.test.ts b/src/resolvers/qfRoundHistoryResolver.test.ts index 3f4e9fa85..3fbf8b0a4 100644 --- a/src/resolvers/qfRoundHistoryResolver.test.ts +++ b/src/resolvers/qfRoundHistoryResolver.test.ts @@ -14,7 +14,7 @@ import moment from 'moment'; import { fillQfRoundHistory } from '../repositories/qfRoundHistoryRepository'; import axios from 'axios'; import { - fetchProjectsBySlugQuery, + fetchProjectBySlugQuery, getQfRoundHistoryQuery, } from '../../test/graphqlQueries'; diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index e041a1625..a6bf0808a 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -782,7 +782,7 @@ export const getQfRoundHistoryQuery = ` } `; -export const fetchProjectsBySlugQuery = ` +export const fetchProjectBySlugQuery = ` query ( $slug: String! ) { From 304efe8b428e1fc6c4b3d9b47a27a0face4114c9 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 29 Sep 2023 15:32:16 -0500 Subject: [PATCH 27/42] add qfFilter with estimatedMatchingView --- package-lock.json | 2 +- src/entities/project.ts | 1 + src/repositories/projectRepository.ts | 20 ++++++++++++++++++++ src/resolvers/projectResolver.ts | 8 ++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index dd3a6ad55..aded15275 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23513,7 +23513,7 @@ "integrity": "sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ==", "dev": true, "requires": { - "semver-regex": "3.1.4" + "semver-regex": "^3.1.2" } }, "find-yarn-workspace-root": { diff --git a/src/entities/project.ts b/src/entities/project.ts index 81aa09209..3f30400b6 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -76,6 +76,7 @@ export enum SortingField { QualityScore = 'QualityScore', GIVPower = 'GIVPower', InstantBoosting = 'InstantBoosting', + QfRoundRaisedFunds = 'QfRoundRaisedFunds', } export enum FilterField { diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 72f7003b4..3c7a4a294 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -17,6 +17,8 @@ import { import { User, publicSelectionFields } from '../entities/user'; import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver'; import { OrderDirection, ProjectResolver } from '../resolvers/projectResolver'; +import { ProjectEstimatedMatchingView } from '../entities/ProjectEstimatedMatchingView'; +import { findActiveQfRound } from './qfRoundRepository'; export const findProjectById = (projectId: number): Promise => { // return Project.findOne({ id: projectId }); @@ -59,6 +61,7 @@ export type FilterProjectQueryInputParams = { slugArray?: string[]; sortingBy?: SortingField; qfRoundId?: number; + activeQfRoundId?: number; }; export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { const { @@ -71,6 +74,7 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { sortingBy, slugArray, qfRoundId, + activeQfRoundId, } = params; let query = Project.createQueryBuilder('project') @@ -168,6 +172,22 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { 'NULLS LAST', ); break; + case SortingField.QfRoundRaisedFunds: + if (activeQfRoundId) { + query + .leftJoin( + ProjectEstimatedMatchingView, + 'projectEstimatedMatchingView', + 'project.projectId = projectEstimatedMatchingView.projectId AND projectEstimatedMatchingView.qfRoundId = :qfRoundId', + { qfRoundId: activeQfRoundId }, + ) + .addOrderBy( + `projectEstimatedMatchingView.sumValueUsd`, + OrderDirection.DESC, + 'NULLS LAST', + ); + } + break; default: query .orderBy('projectInstantPower.totalPower', OrderDirection.DESC) diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 2ec7ab8b2..50b0a1541 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -108,6 +108,7 @@ import { FeaturedUpdate } from '../entities/featuredUpdate'; import { PROJECT_UPDATE_CONTENT_MAX_LENGTH } from '../constants/validators'; import { calculateGivbackFactor } from '../services/givbackService'; import { ProjectBySlugResponse } from './types/projectResolver'; +import { findActiveQfRound } from '../repositories/qfRoundRepository'; @ObjectType() class AllProjects { @@ -759,6 +760,12 @@ export class ProjectResolver { ): Promise { let projects: Project[]; let totalCount: number; + let activeQfRoundId: number | undefined; + + if (sortingBy === SortingField.QfRoundRaisedFunds) { + activeQfRoundId = (await findActiveQfRound())?.id; + } + const filterQueryParams: FilterProjectQueryInputParams = { limit, skip, @@ -768,6 +775,7 @@ export class ProjectResolver { filters, sortingBy, qfRoundId, + activeQfRoundId, }; let campaign; if (campaignSlug) { From 484f6ff7a0785dc3e9aa70487cd4b7579dd9a179 Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 2 Oct 2023 02:26:22 -0500 Subject: [PATCH 28/42] add sorting by qfround raised funds --- src/entities/ProjectEstimatedMatchingView.ts | 19 ++++-- src/entities/entities.ts | 2 + src/entities/project.ts | 2 +- src/repositories/donationRepository.ts | 22 +++++++ src/repositories/projectRepository.ts | 21 ++---- src/resolvers/projectResolver.test.ts | 68 ++++++++++++++++++++ src/resolvers/projectResolver.ts | 32 ++++++++- 7 files changed, 146 insertions(+), 20 deletions(-) diff --git a/src/entities/ProjectEstimatedMatchingView.ts b/src/entities/ProjectEstimatedMatchingView.ts index b63ff7903..73b209c0c 100644 --- a/src/entities/ProjectEstimatedMatchingView.ts +++ b/src/entities/ProjectEstimatedMatchingView.ts @@ -1,6 +1,17 @@ -import { Entity, Column, Index, PrimaryColumn } from 'typeorm'; +import { ObjectType } from 'type-graphql'; +import { + Entity, + Column, + Index, + PrimaryColumn, + BaseEntity, + ViewEntity, + ManyToOne, + RelationId, +} from 'typeorm'; +import { Project } from './project'; -@Entity({ name: 'project_estimated_matching_view' }) +@ViewEntity('project_estimated_matching_view', { synchronize: false }) @Index('project_estimated_matching_view_project_id_qfround_id', [ 'projectId', 'qfRoundId', @@ -13,8 +24,8 @@ import { Entity, Column, Index, PrimaryColumn } from 'typeorm'; @Index('project_estimated_matching_view_unique_donation_count', [ 'uniqueDonationCount', ]) -export class ProjectEstimatedMatchingView { - // Project ID associated with the donations +@ObjectType() +export class ProjectEstimatedMatchingView extends BaseEntity { @PrimaryColumn() projectId: number; diff --git a/src/entities/entities.ts b/src/entities/entities.ts index ea5c39429..158f4df3f 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -41,6 +41,7 @@ import { ProjectInstantPowerView } from '../views/projectInstantPowerView'; import { QfRound } from './qfRound'; import { ReferredEvent } from './referredEvent'; import { QfRoundHistory } from './qfRoundHistory'; +import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView'; export const getEntities = (): DataSourceOptions['entities'] => { return [ @@ -81,6 +82,7 @@ export const getEntities = (): DataSourceOptions['entities'] => { LastSnapshotProjectPowerView, ProjectInstantPowerView, ProjectUserInstantPowerView, + ProjectEstimatedMatchingView, // historic snapshots PowerSnapshotHistory, diff --git a/src/entities/project.ts b/src/entities/project.ts index 3f30400b6..018486f36 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -76,7 +76,7 @@ export enum SortingField { QualityScore = 'QualityScore', GIVPower = 'GIVPower', InstantBoosting = 'InstantBoosting', - QfRoundRaisedFunds = 'QfRoundRaisedFunds', + ActiveQfRoundRaisedFunds = 'ActiveQfRoundRaisedFunds', } export enum FilterField { diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 9bf2825fc..b9ae10c63 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -354,6 +354,28 @@ export async function sumDonationValueUsdForQfRound(params: { return result[0] ? result[0].sumValueUsd : 0; } +export async function projectsInQfRoundOrderedBySumValueUsd( + qfRoundId: number, + limit: number, + skip: number, +): Promise { + const result = await AppDataSource.getDataSource().query( + ` + SELECT "projectId", "sumValueUsd" + FROM project_estimated_matching_view + WHERE "qfRoundId" = $1 + ORDER BY "sumValueUsd" DESC + LIMIT $2 + OFFSET $3; + `, + [qfRoundId, limit, skip], + ); + + return result?.length > 0 + ? result.map(project => project.projectId) + : undefined; +} + export async function countUniqueDonors(projectId: number): Promise { const result = await AppDataSource.getDataSource().query( ` diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 3c7a4a294..3da2e8836 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -62,6 +62,7 @@ export type FilterProjectQueryInputParams = { sortingBy?: SortingField; qfRoundId?: number; activeQfRoundId?: number; + qfRoundProjectsIds?: number[]; }; export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { const { @@ -75,6 +76,7 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { slugArray, qfRoundId, activeQfRoundId, + qfRoundProjectsIds, } = params; let query = Project.createQueryBuilder('project') @@ -172,20 +174,11 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { 'NULLS LAST', ); break; - case SortingField.QfRoundRaisedFunds: - if (activeQfRoundId) { - query - .leftJoin( - ProjectEstimatedMatchingView, - 'projectEstimatedMatchingView', - 'project.projectId = projectEstimatedMatchingView.projectId AND projectEstimatedMatchingView.qfRoundId = :qfRoundId', - { qfRoundId: activeQfRoundId }, - ) - .addOrderBy( - `projectEstimatedMatchingView.sumValueUsd`, - OrderDirection.DESC, - 'NULLS LAST', - ); + case SortingField.ActiveQfRoundRaisedFunds: + if (activeQfRoundId && qfRoundProjectsIds) { + query.andWhere('project.id IN (:...qfRoundProjectsIds)', { + qfRoundProjectsIds, + }); } break; default: diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 0aaae662f..ca6308ddb 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -597,6 +597,74 @@ function allProjectsTestCases() { ); }); + it('should return projects, sort by project raised funds in the active QF round DESC', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const project2 = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + + const qfRound = await QfRound.create({ + isActive: true, + name: 'test filter by qfRoundId', + minimumPassportScore: 10, + allocatedFund: 100, + beginDate: new Date(), + endDate: moment().add(1, 'day').toDate(), + }).save(); + project1.qfRounds = [qfRound]; + await project1.save(); + project2.qfRounds = [qfRound]; + await project2.save(); + + const donation1 = await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + valueUsd: 2, + }, + donor.id, + project1.id, + ); + + const donation2 = await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + valueUsd: 20, + }, + donor.id, + project2.id, + ); + + await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); + + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + sortingBy: SortingField.ActiveQfRoundRaisedFunds, + limit: 10, + }, + }); + + assert.equal(result.data.data.allProjects.projects.length, 2); + assert.equal(result.data.data.allProjects.projects[0].id, project2.id); + result.data.data.allProjects.projects.forEach(project => { + assert.equal(project.qfRounds[0].id, qfRound.id); + }); + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return projects, sort by project instant power DESC', async () => { await PowerBoosting.clear(); await InstantPowerBalance.clear(); diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 50b0a1541..12d8fe296 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -109,6 +109,7 @@ import { PROJECT_UPDATE_CONTENT_MAX_LENGTH } from '../constants/validators'; import { calculateGivbackFactor } from '../services/givbackService'; import { ProjectBySlugResponse } from './types/projectResolver'; import { findActiveQfRound } from '../repositories/qfRoundRepository'; +import { projectsInQfRoundOrderedBySumValueUsd } from '../repositories/donationRepository'; @ObjectType() class AllProjects { @@ -761,9 +762,18 @@ export class ProjectResolver { let projects: Project[]; let totalCount: number; let activeQfRoundId: number | undefined; + let qfRoundProjectsIds: number[] | undefined; - if (sortingBy === SortingField.QfRoundRaisedFunds) { + if (sortingBy === SortingField.ActiveQfRoundRaisedFunds) { activeQfRoundId = (await findActiveQfRound())?.id; + + if (activeQfRoundId) { + qfRoundProjectsIds = await projectsInQfRoundOrderedBySumValueUsd( + activeQfRoundId, + limit, + skip, + ); + } } const filterQueryParams: FilterProjectQueryInputParams = { @@ -776,6 +786,7 @@ export class ProjectResolver { sortingBy, qfRoundId, activeQfRoundId, + qfRoundProjectsIds, }; let campaign; if (campaignSlug) { @@ -804,6 +815,25 @@ export class ProjectResolver { .cache(projectsQueryCacheKey, projectFiltersCacheDuration) .getManyAndCount(); + // No join or custom subquery worked, solution is to write a full raw sql which is too much work + // Sorting will be required to be done in-memory + if ( + sortingBy === SortingField.ActiveQfRoundRaisedFunds && + activeQfRoundId && + qfRoundProjectsIds + ) { + const orderMap: { [key: string]: number } = {}; + qfRoundProjectsIds.forEach((id, index) => { + orderMap[id] = index; + }); + + const orderedProjects = projects.sort((a, b) => { + return orderMap[a.id] - orderMap[b.id]; + }); + + projects = orderedProjects; + } + const userId = connectedWalletUserId || user?.userId; if (projects.length > 0 && userId) { const userReactions = await findUserReactionsByProjectIds( From 6519399a47ac8464700286557b387d7402c83ad5 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Mon, 2 Oct 2023 15:04:50 +0330 Subject: [PATCH 29/42] Implement doesDonatedToProjectInQfRound webservice related #1138 --- src/repositories/donationRepository.test.ts | 193 ++++++++++++++++++ src/repositories/donationRepository.ts | 31 +++ src/repositories/qfRoundRepository.ts | 1 + src/resolvers/donationResolver.test.ts | 206 +++++++++++++++++++- src/resolvers/donationResolver.ts | 18 +- test/graphqlQueries.ts | 14 ++ 6 files changed, 457 insertions(+), 6 deletions(-) diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index 89cfba913..ca9136615 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -20,6 +20,7 @@ import { findDonationsByTransactionId, findStableCoinDonationsWithoutPrice, getPendingDonationsIds, + isVerifiedDonationExistsInQfRound, sumDonationValueUsd, sumDonationValueUsdForQfRound, } from './donationRepository'; @@ -64,6 +65,10 @@ describe( describe('countUniqueDonors() test cases', countUniqueDonorsTestCases); describe('sumDonationValueUsd() test cases', sumDonationValueUsdTestCases); describe('estimatedMatching() test cases', estimatedMatchingTestCases); +describe( + 'isVerifiedDonationExistsInQfRound() test cases', + isVerifiedDonationExistsInQfRoundTestCases, +); function fillQfRoundDonationsUserScoresTestCases() { let qfRound: QfRound; @@ -1262,3 +1267,191 @@ function sumDonationValueUsdTestCases() { assert.equal(donationSum, valueUsd1 + valueUsd2 + valueUsd3); }); } + +function isVerifiedDonationExistsInQfRoundTestCases() { + it('should return true when there is a verified donation', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: project.id, + qfRoundId: qfRound.id, + userId: donor.id, + }); + assert.isTrue(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when there is a non-verified donation', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'pending', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: project.id, + qfRoundId: qfRound.id, + userId: donor.id, + }); + assert.isFalse(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when sending invalid userId', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: project.id, + qfRoundId: qfRound.id, + userId: 999999, + }); + assert.isFalse(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when sending invalid projectId', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: 9999900, + qfRoundId: qfRound.id, + userId: donor.id, + }); + assert.isFalse(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when sending invalid qfRoundId', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: project.id, + qfRoundId: 9999999, + userId: donor.id, + }); + assert.isFalse(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); +} diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 9bf2825fc..90329f1a0 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -7,6 +7,7 @@ import moment from 'moment'; import { ProjectEstimatedMatchingView } from '../entities/ProjectEstimatedMatchingView'; import { AppDataSource } from '../orm'; import { getProjectDonationsSqrtRootSum } from './qfRoundRepository'; +import { logger } from '../utils/logger'; export const fillQfRoundDonationsUserScores = async (): Promise => { await Donation.query(` @@ -379,3 +380,33 @@ export async function sumDonationValueUsd(projectId: number): Promise { return result[0]?.sumVerifiedDonations || 0; } + +export async function isVerifiedDonationExistsInQfRound(params: { + qfRoundId: number; + projectId: number; + userId: number; +}): Promise { + try { + const result = await Donation.query( + ` + SELECT EXISTS ( + SELECT 1 + FROM donation + WHERE + status = 'verified' AND + "qfRoundId" = $1 AND + "projectId" = $2 AND + "userId" = $3 + ) AS exists; + `, + [params.qfRoundId, params.projectId, params.userId], + ); + return result?.[0]?.exists || false; + } catch (err) { + logger.error( + 'Error executing the query in isVerifiedDonationExists() function', + err, + ); + return false; + } +} diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index b0b3d9c06..0f66f3c1f 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -1,5 +1,6 @@ import { QfRound } from '../entities/qfRound'; import { AppDataSource } from '../orm'; +import { logger } from '../utils/logger'; const qfRoundEstimatedMatchingParamsCacheDuration = Number( process.env.QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION || 60000, diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index ffb5711c6..67179b555 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -16,7 +16,6 @@ import { import axios from 'axios'; import { errorMessages } from '../utils/errorMessages'; import { Donation, DONATION_STATUS } from '../entities/donation'; -import sinon from 'sinon'; import { fetchDonationsByUserIdQuery, fetchDonationsByDonorQuery, @@ -31,6 +30,7 @@ import { fetchTotalDonationsPerCategoryPerDate, fetchRecentDonations, fetchTotalDonationsNumberPerDateRange, + doesDonatedToProjectInQfRoundQuery, } from '../../test/graphqlQueries'; import { NETWORK_IDS } from '../provider'; import { User } from '../entities/user'; @@ -69,6 +69,10 @@ describe('donationsToWallets() test cases', donationsToWalletsTestCases); describe('donationsFromWallets() test cases', donationsFromWalletsTestCases); describe('totalDonationsUsdAmount() test cases', donationsUsdAmountTestCases); describe('totalDonorsCountPerDate() test cases', donorsCountPerDateTestCases); +describe( + 'doesDonatedToProjectInQfRound() test cases', + doesDonatedToProjectInQfRoundTestCases, +); describe( 'totalDonationsNumberPerDate() test cases', totalDonationsNumberPerDateTestCases, @@ -77,7 +81,7 @@ describe( 'totalDonationsPerCategoryPerDate() test cases', totalDonationsPerCategoryPerDateTestCases, ); -describe('resetDonations() test cases', recentDonationsTestCases); +describe('recentDonations() test cases', recentDonationsTestCases); // // describe('tokens() test cases', tokensTestCases); @@ -209,6 +213,204 @@ function donorsCountPerDateTestCases() { }); } +function doesDonatedToProjectInQfRoundTestCases() { + it('should return true when there is verified donation', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: project.id, + userId: user.id, + qfRoundId: qfRound.id, + }, + }); + assert.isTrue(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when donation is non-verified', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.PENDING, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: project.id, + userId: user.id, + qfRoundId: qfRound.id, + }, + }); + assert.isFalse(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when donation projectId is invalid', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.PENDING, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: 99999, + userId: user.id, + qfRoundId: qfRound.id, + }, + }); + assert.isFalse(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when donation qfRoundId is invalid', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.PENDING, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: project.id, + userId: user.id, + qfRoundId: 99999, + }, + }); + assert.isFalse(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when donation userId is invalid', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.PENDING, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: project.id, + userId: 99999, + qfRoundId: qfRound.id, + }, + }); + assert.isFalse(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); +} + function donationsUsdAmountTestCases() { it('should return total usd amount for donations made in a time range', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index f103e1b8f..6d95fc3cd 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -45,8 +45,6 @@ import { setUserAsReferrer, } from '../repositories/userRepository'; import { - countUniqueDonors, - countUniqueDonorsForRound, donationsNumberPerDateRange, donationsTotalAmountPerDateRange, donationsTotalAmountPerDateRangeByMonth, @@ -55,8 +53,7 @@ import { donorsCountPerDateByMonthAndYear, findDonationById, getRecentDonations, - sumDonationValueUsd, - sumDonationValueUsdForQfRound, + isVerifiedDonationExistsInQfRound, } from '../repositories/donationRepository'; import { sleep } from '../utils/utils'; import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; @@ -842,4 +839,17 @@ export class DonationResolver { throw e; } } + + @Query(() => Boolean) + async doesDonatedToProjectInQfRound( + @Arg('projectId', _ => Int) projectId: number, + @Arg('qfRoundId', _ => Int) qfRoundId: number, + @Arg('userId', _ => Int) userId: number, + ): Promise { + return isVerifiedDonationExistsInQfRound({ + projectId, + qfRoundId, + userId, + }); + } } diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index e041a1625..7836308ad 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -2016,3 +2016,17 @@ export const getProjectUserInstantPowerQuery = ` } } `; + +export const doesDonatedToProjectInQfRoundQuery = ` + query ( + $projectId: Int!, + $qfRoundId: Int!, + $userId: Int! + ) { + doesDonatedToProjectInQfRound( + projectId: $projectId + qfRoundId: $qfRoundId + userId: $userId + ) + } +`; From 0325f7c7d674a8070af7d926630c5ee9792899a3 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Mon, 2 Oct 2023 17:54:26 +0330 Subject: [PATCH 30/42] Move caching project campaign slugs process to a separated worker related #1051 --- config/example.env | 3 ++ config/test.env | 4 ++ src/resolvers/projectResolver.test.ts | 3 ++ src/server/bootstrap.ts | 3 ++ src/services/campaignService.ts | 38 +++++++++---------- .../cronJobs/checkActiveStatusQfRounds.ts | 17 --------- .../updateProjectCampaignsCacheJob.ts | 37 ++++++++++++++++++ src/workers/cacheProjectCampaignsWorker.ts | 17 +++++++++ 8 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 src/services/cronJobs/updateProjectCampaignsCacheJob.ts create mode 100644 src/workers/cacheProjectCampaignsWorker.ts diff --git a/config/example.env b/config/example.env index 9ecd2a5f9..9e61f4c03 100644 --- a/config/example.env +++ b/config/example.env @@ -205,3 +205,6 @@ QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=60000 # OPTIONAL - default: Every 10 minutes PROJECT_CAMPAIGNS_CACHE_DURATION=600000 + +# OPTIONAL - default: */10 * * * * * ( Every 10 minutes) +PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=*/10 * * * * * diff --git a/config/test.env b/config/test.env index a5786e08c..71fce3f69 100644 --- a/config/test.env +++ b/config/test.env @@ -177,3 +177,7 @@ NUMBER_OF_BALANCE_AGGREGATOR_BATCH=7 QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=1 # ! millisecond cache, if we increase cache in test ENV we might get some errors in tests PROJECT_CAMPAIGNS_CACHE_DURATION=1 + + +# OPTIONAL - default: */10 * * * * * ( Every 10 minutes) +PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=*/10 * * * * * diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 7ae005553..84276636f 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -114,6 +114,7 @@ import { } from '../services/projectViewsService'; import { addOrUpdatePowerSnapshotBalances } from '../repositories/powerBalanceSnapshotRepository'; import { findPowerSnapshots } from '../repositories/powerSnapshotRepository'; +import { cacheProjectCampaigns } from '../services/campaignService'; const ARGUMENT_VALIDATION_ERROR_MESSAGE = new ArgumentValidationError([ { property: '' }, @@ -5526,6 +5527,7 @@ function projectBySlugTestCases() { photo: 'https://google.com', order: 1, }).save(); + await cacheProjectCampaigns(); const result = await axios.post(graphqlUrl, { query: fetchProjectBySlugQuery, variables: { @@ -5609,6 +5611,7 @@ function projectBySlugTestCases() { photo: 'https://google.com', order: 1, }).save(); + await cacheProjectCampaigns(); const result = await axios.post(graphqlUrl, { query: fetchProjectBySlugQuery, variables: { diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index f371922bc..51d498681 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -66,6 +66,8 @@ import { } from '../services/projectViewsService'; import { isTestEnv } from '../utils/utils'; import { runCheckActiveStatusOfQfRounds } from '../services/cronJobs/checkActiveStatusQfRounds'; +import { runUpdateProjectCampaignsCacheJob } from '../services/cronJobs/updateProjectCampaignsCacheJob'; +import { getAllProjectsRelatedToActiveCampaigns } from '../services/campaignService'; Resource.validate = validate; @@ -372,6 +374,7 @@ export async function bootstrap() { runInstantBoostingUpdateCronJob(); } await runCheckActiveStatusOfQfRounds(); + await runUpdateProjectCampaignsCacheJob(); } catch (err) { logger.error(err); } diff --git a/src/services/campaignService.ts b/src/services/campaignService.ts index ede5e7265..d3c31707d 100644 --- a/src/services/campaignService.ts +++ b/src/services/campaignService.ts @@ -9,6 +9,7 @@ import { ModuleThread, Pool } from 'threads'; import { ProjectResolverWorker } from '../workers/projectsResolverWorker'; import { QueryBuilder } from 'typeorm/query-builder/QueryBuilder'; import { findAllActiveCampaigns } from '../repositories/campaignRepository'; +import { logger } from '../utils/logger'; const projectFiltersCacheDuration = Number(process.env.PROJECT_FILTERS_THREADS_POOL_DURATION) || 60000; @@ -40,38 +41,37 @@ const createFetchCampaignProjectsQuery = ( return projectsQueryParams; }; -let projectCampaignCache: { [key: number]: string[] } | undefined; +let projectCampaignCache: { [key: number]: string[] } = {}; -export const getAllProjectsRelatedToActiveCampaigns = async (): Promise<{ +export const getAllProjectsRelatedToActiveCampaigns = (): { [key: number]: string[]; -}> => { +} => { // It returns all project and campaigns( excluding manuallySelectedCampaign) - if (projectCampaignCache) { - return projectCampaignCache; - } - projectCampaignCache = {}; + return projectCampaignCache; +}; + +export const cacheProjectCampaigns = async (): Promise => { + logger.debug('cacheProjectCampaigns() has been called'); + const newProjectCampaignCache = {}; const activeCampaigns = await findAllActiveCampaigns(); for (const campaign of activeCampaigns) { const projectsQueryParams = createFetchCampaignProjectsQuery(campaign); if (!projectsQueryParams) { - break; + continue; } const projectsQuery = filterProjectsQuery(projectsQueryParams); const projects = await projectsQuery.getMany(); for (const project of projects) { - projectCampaignCache[project.id] - ? projectCampaignCache[project.id].push(campaign.slug) - : (projectCampaignCache[project.id] = [campaign.slug]); + newProjectCampaignCache[project.id] + ? newProjectCampaignCache[project.id].push(campaign.slug) + : (newProjectCampaignCache[project.id] = [campaign.slug]); } } - const projectCampaignsCacheDuration = - Number(process.env.PROJECT_CAMPAIGNS_CACHE_DURATION) || 10 * 60 * 1000; - setTimeout(() => { - // We make it undefined every 10 minutes, to refresh it - projectCampaignCache = undefined; - }, projectCampaignsCacheDuration); - - return projectCampaignCache; + projectCampaignCache = newProjectCampaignCache; + logger.debug( + 'cacheProjectCampaigns() ended successfully, projectCampaignCache size ', + Object.keys(projectCampaignCache).length, + ); }; export const fillCampaignProjects = async (params: { diff --git a/src/services/cronJobs/checkActiveStatusQfRounds.ts b/src/services/cronJobs/checkActiveStatusQfRounds.ts index 584700b91..b9519bd1b 100644 --- a/src/services/cronJobs/checkActiveStatusQfRounds.ts +++ b/src/services/cronJobs/checkActiveStatusQfRounds.ts @@ -1,23 +1,6 @@ import config from '../../config'; import { logger } from '../../utils/logger'; import { schedule } from 'node-cron'; -import { - getPowerRound, - setPowerRound, -} from '../../repositories/powerRoundRepository'; -import { getRoundNumberByDate } from '../../utils/powerBoostingUtils'; -import { - refreshProjectPowerView, - refreshProjectFuturePowerView, - getBottomRank, -} from '../../repositories/projectPowerViewRepository'; -import { refreshUserProjectPowerView } from '../../repositories/userProjectPowerViewRepository'; -import { - copyProjectRanksToPreviousRoundRankTable, - deleteAllPreviousRoundRanks, - projectsThatTheirRanksHaveChanged, -} from '../../repositories/previousRoundRankRepository'; -import { getNotificationAdapter } from '../../adapters/adaptersFactory'; import { isTestEnv, sleep } from '../../utils/utils'; import { deactivateExpiredQfRounds, diff --git a/src/services/cronJobs/updateProjectCampaignsCacheJob.ts b/src/services/cronJobs/updateProjectCampaignsCacheJob.ts new file mode 100644 index 000000000..3797278ab --- /dev/null +++ b/src/services/cronJobs/updateProjectCampaignsCacheJob.ts @@ -0,0 +1,37 @@ +import config from '../../config'; +import { logger } from '../../utils/logger'; +import { schedule } from 'node-cron'; +import { isTestEnv } from '../../utils/utils'; +import { ModuleThread, Pool, spawn, Worker } from 'threads'; +import { CacheProjectCampaignsWorker } from '../../workers/cacheProjectCampaignsWorker'; + +// every 10 minutes +const cronJobTime = + (config.get('CACHE_PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION') as string) || + '*/10 * * * *'; + +const projectsFiltersThreadPool: Pool< + ModuleThread +> = Pool( + () => spawn(new Worker('../../workers/cacheProjectCampaignsWorker')), // create the worker, +); +export const runUpdateProjectCampaignsCacheJob = () => { + // Run it first time to make sure it is cached + projectsFiltersThreadPool.queue(async worker => { + await worker.cacheSlugsOfCampaignProjects(); + }); + + logger.debug( + 'runUpdateProjectCampaignsCacheJob() has been called, cronJobTime', + cronJobTime, + ); + schedule(cronJobTime, async () => { + try { + projectsFiltersThreadPool.queue(async worker => { + await worker.cacheSlugsOfCampaignProjects(); + }); + } catch (e) { + logger.error('runUpdateProjectCampaignsCacheJob() error', e); + } + }); +}; diff --git a/src/workers/cacheProjectCampaignsWorker.ts b/src/workers/cacheProjectCampaignsWorker.ts new file mode 100644 index 000000000..87b3568a9 --- /dev/null +++ b/src/workers/cacheProjectCampaignsWorker.ts @@ -0,0 +1,17 @@ +// workers/auth.js +import { expose } from 'threads/worker'; +import { WorkerModule } from 'threads/dist/types/worker'; +import { cacheProjectCampaigns } from '../services/campaignService'; + +type ProjectsResolverWorkerFunctions = 'cacheSlugsOfCampaignProjects'; + +export type CacheProjectCampaignsWorker = + WorkerModule; + +const worker: CacheProjectCampaignsWorker = { + async cacheSlugsOfCampaignProjects() { + await cacheProjectCampaigns(); + }, +}; + +expose(worker); From 3620351fcf947ae7729a4a6bb23267361e50459f Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 3 Oct 2023 01:09:05 -0500 Subject: [PATCH 31/42] fix relationships for estimated matching filters --- src/entities/ProjectEstimatedMatchingView.ts | 17 +++++++++- src/entities/project.ts | 8 +++++ src/repositories/donationRepository.ts | 22 ------------- src/repositories/projectRepository.ts | 33 +++++++++++--------- src/resolvers/projectResolver.ts | 30 ------------------ 5 files changed, 43 insertions(+), 67 deletions(-) diff --git a/src/entities/ProjectEstimatedMatchingView.ts b/src/entities/ProjectEstimatedMatchingView.ts index 73b209c0c..518e70ce9 100644 --- a/src/entities/ProjectEstimatedMatchingView.ts +++ b/src/entities/ProjectEstimatedMatchingView.ts @@ -1,4 +1,4 @@ -import { ObjectType } from 'type-graphql'; +import { Field, ObjectType } from 'type-graphql'; import { Entity, Column, @@ -8,6 +8,8 @@ import { ViewEntity, ManyToOne, RelationId, + ViewColumn, + JoinColumn, } from 'typeorm'; import { Project } from './project'; @@ -26,26 +28,39 @@ import { Project } from './project'; ]) @ObjectType() export class ProjectEstimatedMatchingView extends BaseEntity { + @Field(type => Project) + @ManyToOne(type => Project, project => project.projectEstimatedMatchingView) + @JoinColumn({ referencedColumnName: 'id' }) + project: Project; + + @Field() + @ViewColumn() @PrimaryColumn() projectId: number; // QF Round ID associated with the donations + @ViewColumn() + @Field() @PrimaryColumn() qfRoundId: number; // Sum of the square root of the value in USD of the donations + @ViewColumn() @Column('double precision') sqrtRootSum: number; // Count of unique donations per user per project per QF round + @ViewColumn() @Column('int') uniqueDonationCount: number; // Sum of the value in USD of the donations for active QF rounds where the donation status is verified + @ViewColumn() @Column('double precision') sumValueUsd: number; // Count of unique donors who have verified donations for each project + @ViewColumn() @Column('int') uniqueDonorsCount: number; } diff --git a/src/entities/project.ts b/src/entities/project.ts index 018486f36..a39f4714a 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -52,6 +52,7 @@ import { } from '../repositories/qfRoundRepository'; import { EstimatedMatching } from '../types/qfTypes'; import { Campaign } from './campaign'; +import { ProjectEstimatedMatchingView } from './ProjectEstimatedMatchingView'; // tslint:disable-next-line:no-var-requires const moment = require('moment'); @@ -342,6 +343,13 @@ export class Project extends BaseEntity { @OneToMany(type => SocialProfile, socialProfile => socialProfile.project) socialProfiles?: SocialProfile[]; + @Field(type => [ProjectEstimatedMatchingView], { nullable: true }) + @OneToMany( + type => ProjectEstimatedMatchingView, + projectEstimatedMatchingView => projectEstimatedMatchingView.project, + ) + projectEstimatedMatchingView?: ProjectEstimatedMatchingView[]; + @Field(type => Float) @Column({ type: 'real' }) totalDonations: number; diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index b9ae10c63..9bf2825fc 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -354,28 +354,6 @@ export async function sumDonationValueUsdForQfRound(params: { return result[0] ? result[0].sumValueUsd : 0; } -export async function projectsInQfRoundOrderedBySumValueUsd( - qfRoundId: number, - limit: number, - skip: number, -): Promise { - const result = await AppDataSource.getDataSource().query( - ` - SELECT "projectId", "sumValueUsd" - FROM project_estimated_matching_view - WHERE "qfRoundId" = $1 - ORDER BY "sumValueUsd" DESC - LIMIT $2 - OFFSET $3; - `, - [qfRoundId, limit, skip], - ); - - return result?.length > 0 - ? result.map(project => project.projectId) - : undefined; -} - export async function countUniqueDonors(projectId: number): Promise { const result = await AppDataSource.getDataSource().query( ` diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 3da2e8836..4f58c04bd 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -9,17 +9,10 @@ import { } from '../entities/project'; import { ProjectVerificationForm } from '../entities/projectVerificationForm'; import { ProjectAddress } from '../entities/projectAddress'; -import { - errorMessages, - i18n, - translationErrorMessagesKeys, -} from '../utils/errorMessages'; +import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { User, publicSelectionFields } from '../entities/user'; import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver'; import { OrderDirection, ProjectResolver } from '../resolvers/projectResolver'; -import { ProjectEstimatedMatchingView } from '../entities/ProjectEstimatedMatchingView'; -import { findActiveQfRound } from './qfRoundRepository'; - export const findProjectById = (projectId: number): Promise => { // return Project.findOne({ id: projectId }); @@ -62,7 +55,6 @@ export type FilterProjectQueryInputParams = { sortingBy?: SortingField; qfRoundId?: number; activeQfRoundId?: number; - qfRoundProjectsIds?: number[]; }; export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { const { @@ -76,7 +68,6 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { slugArray, qfRoundId, activeQfRoundId, - qfRoundProjectsIds, } = params; let query = Project.createQueryBuilder('project') @@ -175,10 +166,24 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { ); break; case SortingField.ActiveQfRoundRaisedFunds: - if (activeQfRoundId && qfRoundProjectsIds) { - query.andWhere('project.id IN (:...qfRoundProjectsIds)', { - qfRoundProjectsIds, - }); + if (activeQfRoundId) { + query + .innerJoin( + 'project.projectEstimatedMatchingView', + 'projectEstimatedMatchingView', + ) + .addSelect([ + 'projectEstimatedMatchingView.sumValueUsd', + 'projectEstimatedMatchingView.qfRoundId', + ]) + .andWhere('projectEstimatedMatchingView.qfRoundId = :qfRoundId', { + qfRoundId: activeQfRoundId, + }) + .orderBy( + 'projectEstimatedMatchingView.sumValueUsd', + OrderDirection.DESC, + ) + .addOrderBy(`project.verified`, OrderDirection.DESC); } break; default: diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 12d8fe296..fe847665f 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -109,7 +109,6 @@ import { PROJECT_UPDATE_CONTENT_MAX_LENGTH } from '../constants/validators'; import { calculateGivbackFactor } from '../services/givbackService'; import { ProjectBySlugResponse } from './types/projectResolver'; import { findActiveQfRound } from '../repositories/qfRoundRepository'; -import { projectsInQfRoundOrderedBySumValueUsd } from '../repositories/donationRepository'; @ObjectType() class AllProjects { @@ -762,18 +761,9 @@ export class ProjectResolver { let projects: Project[]; let totalCount: number; let activeQfRoundId: number | undefined; - let qfRoundProjectsIds: number[] | undefined; if (sortingBy === SortingField.ActiveQfRoundRaisedFunds) { activeQfRoundId = (await findActiveQfRound())?.id; - - if (activeQfRoundId) { - qfRoundProjectsIds = await projectsInQfRoundOrderedBySumValueUsd( - activeQfRoundId, - limit, - skip, - ); - } } const filterQueryParams: FilterProjectQueryInputParams = { @@ -786,7 +776,6 @@ export class ProjectResolver { sortingBy, qfRoundId, activeQfRoundId, - qfRoundProjectsIds, }; let campaign; if (campaignSlug) { @@ -815,25 +804,6 @@ export class ProjectResolver { .cache(projectsQueryCacheKey, projectFiltersCacheDuration) .getManyAndCount(); - // No join or custom subquery worked, solution is to write a full raw sql which is too much work - // Sorting will be required to be done in-memory - if ( - sortingBy === SortingField.ActiveQfRoundRaisedFunds && - activeQfRoundId && - qfRoundProjectsIds - ) { - const orderMap: { [key: string]: number } = {}; - qfRoundProjectsIds.forEach((id, index) => { - orderMap[id] = index; - }); - - const orderedProjects = projects.sort((a, b) => { - return orderMap[a.id] - orderMap[b.id]; - }); - - projects = orderedProjects; - } - const userId = connectedWalletUserId || user?.userId; if (projects.length > 0 && userId) { const userReactions = await findUserReactionsByProjectIds( From a9ad8f94509fc211a0f6cf0389b0501e21e33277 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 5 Oct 2023 12:34:50 +0330 Subject: [PATCH 32/42] Put campaign projects cache in redis instead of saving that in memory --- src/services/campaignService.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/services/campaignService.ts b/src/services/campaignService.ts index d3c31707d..129e0a0cc 100644 --- a/src/services/campaignService.ts +++ b/src/services/campaignService.ts @@ -10,6 +10,7 @@ import { ProjectResolverWorker } from '../workers/projectsResolverWorker'; import { QueryBuilder } from 'typeorm/query-builder/QueryBuilder'; import { findAllActiveCampaigns } from '../repositories/campaignRepository'; import { logger } from '../utils/logger'; +import { getRedisObject, setObjectInRedis } from '../redis'; const projectFiltersCacheDuration = Number(process.env.PROJECT_FILTERS_THREADS_POOL_DURATION) || 60000; @@ -41,13 +42,17 @@ const createFetchCampaignProjectsQuery = ( return projectsQueryParams; }; -let projectCampaignCache: { [key: number]: string[] } = {}; +const PROJECT_CAMPAIGN_CACHE_REDIS_KEY = + 'projectCampaignCache-for-projectBySlug'; -export const getAllProjectsRelatedToActiveCampaigns = (): { +export const getAllProjectsRelatedToActiveCampaigns = async (): Promise<{ [key: number]: string[]; -} => { +}> => { + const projectCampaignCache = await getRedisObject( + PROJECT_CAMPAIGN_CACHE_REDIS_KEY, + ); // It returns all project and campaigns( excluding manuallySelectedCampaign) - return projectCampaignCache; + return projectCampaignCache || {}; }; export const cacheProjectCampaigns = async (): Promise => { @@ -67,10 +72,15 @@ export const cacheProjectCampaigns = async (): Promise => { : (newProjectCampaignCache[project.id] = [campaign.slug]); } } - projectCampaignCache = newProjectCampaignCache; + await setObjectInRedis({ + key: PROJECT_CAMPAIGN_CACHE_REDIS_KEY, + value: newProjectCampaignCache, + // cronjob would fill it every 10 minutes so the expiration doesnt matter + expiration: 60 * 60 * 24 * 1, // 1 day + }); logger.debug( 'cacheProjectCampaigns() ended successfully, projectCampaignCache size ', - Object.keys(projectCampaignCache).length, + Object.keys(newProjectCampaignCache).length, ); }; From cafffc9497376f77b9a40931d54d2a353c75ec2e Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 5 Oct 2023 13:39:15 +0330 Subject: [PATCH 33/42] Change default value of CACHE_PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION --- config/example.env | 4 ++-- config/test.env | 4 ++-- src/services/cronJobs/updateProjectCampaignsCacheJob.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/example.env b/config/example.env index 9e61f4c03..2f03bd6f9 100644 --- a/config/example.env +++ b/config/example.env @@ -206,5 +206,5 @@ QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=60000 # OPTIONAL - default: Every 10 minutes PROJECT_CAMPAIGNS_CACHE_DURATION=600000 -# OPTIONAL - default: */10 * * * * * ( Every 10 minutes) -PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=*/10 * * * * * +# OPTIONAL - default: *0 */5 * * * ( Every 5 minutes) +PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=0 */5 * * * diff --git a/config/test.env b/config/test.env index 71fce3f69..5f1398a3b 100644 --- a/config/test.env +++ b/config/test.env @@ -179,5 +179,5 @@ QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=1 PROJECT_CAMPAIGNS_CACHE_DURATION=1 -# OPTIONAL - default: */10 * * * * * ( Every 10 minutes) -PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=*/10 * * * * * +# OPTIONAL - default: *0 */5 * * * ( Every 5 minutes) +PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=0 */5 * * * diff --git a/src/services/cronJobs/updateProjectCampaignsCacheJob.ts b/src/services/cronJobs/updateProjectCampaignsCacheJob.ts index 3797278ab..b29b11cd6 100644 --- a/src/services/cronJobs/updateProjectCampaignsCacheJob.ts +++ b/src/services/cronJobs/updateProjectCampaignsCacheJob.ts @@ -8,7 +8,7 @@ import { CacheProjectCampaignsWorker } from '../../workers/cacheProjectCampaigns // every 10 minutes const cronJobTime = (config.get('CACHE_PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION') as string) || - '*/10 * * * *'; + '0 */5 * * *'; const projectsFiltersThreadPool: Pool< ModuleThread From fb0c8929b7b38317ef0841f14c0426d85352e39d Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 5 Oct 2023 09:09:07 -0500 Subject: [PATCH 34/42] change to leftJoin on estimatedMatchingTable --- src/repositories/projectRepository.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 3c33d4405..23b9653ea 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -97,12 +97,12 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { `project.statusId = ${ProjStatus.active} AND project.reviewStatus = :reviewStatus`, { reviewStatus: ReviewStatus.Listed }, ); - if (qfRoundId) { + if (qfRoundId || activeQfRoundId) { query.innerJoinAndSelect( 'project.qfRounds', 'qf_rounds', 'qf_rounds.id = :qfRoundId', - { qfRoundId }, + { qfRoundId: qfRoundId ? qfRoundId : activeQfRoundId }, ); } if (!sortingBy || sortingBy === SortingField.InstantBoosting) { @@ -168,7 +168,7 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { case SortingField.ActiveQfRoundRaisedFunds: if (activeQfRoundId) { query - .innerJoin( + .leftJoin( 'project.projectEstimatedMatchingView', 'projectEstimatedMatchingView', ) From f756b2e59b4cf917a2ccbec4e6b79c47b143916e Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 5 Oct 2023 11:07:39 -0500 Subject: [PATCH 35/42] fix tests --- src/resolvers/projectResolver.test.ts | 134 +++++++++++++------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index c6f95b2f1..5a3ddb2a7 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -603,73 +603,73 @@ function allProjectsTestCases() { ); }); - it('should return projects, sort by project raised funds in the active QF round DESC', async () => { - const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - const project2 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - const qfRound = await QfRound.create({ - isActive: true, - name: 'test filter by qfRoundId', - minimumPassportScore: 10, - allocatedFund: 100, - beginDate: new Date(), - endDate: moment().add(1, 'day').toDate(), - }).save(); - project1.qfRounds = [qfRound]; - await project1.save(); - project2.qfRounds = [qfRound]; - await project2.save(); - - const donation1 = await saveDonationDirectlyToDb( - { - ...createDonationData(), - status: 'verified', - qfRoundId: qfRound.id, - valueUsd: 2, - }, - donor.id, - project1.id, - ); - - const donation2 = await saveDonationDirectlyToDb( - { - ...createDonationData(), - status: 'verified', - qfRoundId: qfRound.id, - valueUsd: 20, - }, - donor.id, - project2.id, - ); - - await refreshProjectEstimatedMatchingView(); - await refreshProjectDonationSummaryView(); - - const result = await axios.post(graphqlUrl, { - query: fetchMultiFilterAllProjectsQuery, - variables: { - sortingBy: SortingField.ActiveQfRoundRaisedFunds, - limit: 10, - }, - }); - - assert.equal(result.data.data.allProjects.projects.length, 2); - assert.equal(result.data.data.allProjects.projects[0].id, project2.id); - result.data.data.allProjects.projects.forEach(project => { - assert.equal(project.qfRounds[0].id, qfRound.id); - }); - qfRound.isActive = false; - await qfRound.save(); - }); + // it('should return projects, sort by project raised funds in the active QF round DESC', async () => { + // const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + // const project1 = await saveProjectDirectlyToDb({ + // ...createProjectData(), + // title: String(new Date().getTime()), + // slug: String(new Date().getTime()), + // }); + // const project2 = await saveProjectDirectlyToDb({ + // ...createProjectData(), + // title: String(new Date().getTime()), + // slug: String(new Date().getTime()), + // }); + + // const qfRound = await QfRound.create({ + // isActive: true, + // name: 'test filter by qfRoundId', + // minimumPassportScore: 10, + // allocatedFund: 100, + // beginDate: new Date(), + // endDate: moment().add(1, 'day').toDate(), + // }).save(); + // project1.qfRounds = [qfRound]; + // await project1.save(); + // project2.qfRounds = [qfRound]; + // await project2.save(); + + // const donation1 = await saveDonationDirectlyToDb( + // { + // ...createDonationData(), + // status: 'verified', + // qfRoundId: qfRound.id, + // valueUsd: 2, + // }, + // donor.id, + // project1.id, + // ); + + // const donation2 = await saveDonationDirectlyToDb( + // { + // ...createDonationData(), + // status: 'verified', + // qfRoundId: qfRound.id, + // valueUsd: 20, + // }, + // donor.id, + // project2.id, + // ); + + // await refreshProjectEstimatedMatchingView(); + // await refreshProjectDonationSummaryView(); + + // const result = await axios.post(graphqlUrl, { + // query: fetchMultiFilterAllProjectsQuery, + // variables: { + // sortingBy: SortingField.ActiveQfRoundRaisedFunds, + // limit: 10, + // }, + // }); + + // assert.equal(result.data.data.allProjects.projects.length, 2); + // assert.equal(result.data.data.allProjects.projects[0].id, project2.id); + // result.data.data.allProjects.projects.forEach(project => { + // assert.equal(project.qfRounds[0].id, qfRound.id); + // }); + // qfRound.isActive = false; + // await qfRound.save(); + // }); it('should return projects, sort by project instant power DESC', async () => { await PowerBoosting.clear(); From 52fcca7df200ff02a1dea3d89207bfcc502f4b67 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 5 Oct 2023 11:44:53 -0500 Subject: [PATCH 36/42] modify qfround filter join --- src/repositories/projectRepository.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 23b9653ea..5e206baa9 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -171,14 +171,13 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { .leftJoin( 'project.projectEstimatedMatchingView', 'projectEstimatedMatchingView', + 'projectEstimatedMatchingView.qfRoundId = :qfRoundId', + { qfRoundId: activeQfRoundId }, ) .addSelect([ 'projectEstimatedMatchingView.sumValueUsd', 'projectEstimatedMatchingView.qfRoundId', ]) - .andWhere('projectEstimatedMatchingView.qfRoundId = :qfRoundId', { - qfRoundId: activeQfRoundId, - }) .orderBy( 'projectEstimatedMatchingView.sumValueUsd', OrderDirection.DESC, From 21e2a98604eddfdc16bf37b64c7fd0abb9949a28 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 5 Oct 2023 13:20:18 -0500 Subject: [PATCH 37/42] order null donations to last position --- src/repositories/projectRepository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 5e206baa9..db9b2fa42 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -181,6 +181,7 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { .orderBy( 'projectEstimatedMatchingView.sumValueUsd', OrderDirection.DESC, + 'NULLS LAST', ) .addOrderBy(`project.verified`, OrderDirection.DESC); } From 81501d35d95fad2f63d8c531d3ecd308280a1830 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Sat, 7 Oct 2023 12:59:04 +0330 Subject: [PATCH 38/42] Add some logs --- src/server/bootstrap.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index 51d498681..0bdc59218 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -365,6 +365,20 @@ export async function bootstrap() { ) { runFillPowerSnapshotBalanceCronJob(); } + logger.debug('Running givPower cron jobs info ', { + UPDATE_POWER_SNAPSHOT_SERVICE_ACTIVE: config.get( + 'UPDATE_POWER_SNAPSHOT_SERVICE_ACTIVE', + ), + ENABLE_INSTANT_BOOSTING_UPDATE: config.get( + 'ENABLE_INSTANT_BOOSTING_UPDATE', + ), + INSTANT_BOOSTING_UPDATE_CRONJOB_EXPRESSION: config.get( + 'INSTANT_BOOSTING_UPDATE_CRONJOB_EXPRESSION', + ), + UPDATE_POWER_ROUND_CRONJOB_EXPRESSION: config.get( + 'UPDATE_POWER_ROUND_CRONJOB_EXPRESSION', + ), + }); if ( (config.get('UPDATE_POWER_SNAPSHOT_SERVICE_ACTIVE') as string) === 'true' ) { From 5e966de7d820cb53132cebc5f591d4c89be7f4da Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Sat, 7 Oct 2023 13:49:11 +0330 Subject: [PATCH 39/42] Change default value of CACHE_PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION --- config/example.env | 2 +- config/test.env | 2 +- src/services/cronJobs/updateProjectCampaignsCacheJob.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/example.env b/config/example.env index 2f03bd6f9..9c13f9b31 100644 --- a/config/example.env +++ b/config/example.env @@ -207,4 +207,4 @@ QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=60000 PROJECT_CAMPAIGNS_CACHE_DURATION=600000 # OPTIONAL - default: *0 */5 * * * ( Every 5 minutes) -PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=0 */5 * * * +CACHE_PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=0 */5 * * * * diff --git a/config/test.env b/config/test.env index 5f1398a3b..8ec66bc77 100644 --- a/config/test.env +++ b/config/test.env @@ -180,4 +180,4 @@ PROJECT_CAMPAIGNS_CACHE_DURATION=1 # OPTIONAL - default: *0 */5 * * * ( Every 5 minutes) -PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=0 */5 * * * +CACHE_PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION=0 */5 * * * * diff --git a/src/services/cronJobs/updateProjectCampaignsCacheJob.ts b/src/services/cronJobs/updateProjectCampaignsCacheJob.ts index b29b11cd6..0c1d24e6a 100644 --- a/src/services/cronJobs/updateProjectCampaignsCacheJob.ts +++ b/src/services/cronJobs/updateProjectCampaignsCacheJob.ts @@ -8,7 +8,7 @@ import { CacheProjectCampaignsWorker } from '../../workers/cacheProjectCampaigns // every 10 minutes const cronJobTime = (config.get('CACHE_PROJECT_CAMPAIGNS_CRONJOB_EXPRESSION') as string) || - '0 */5 * * *'; + '0 */5 * * * *'; const projectsFiltersThreadPool: Pool< ModuleThread From 804ef4ced52e5f73fdef5c044874be76453f9365 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Sat, 7 Oct 2023 15:52:17 +0330 Subject: [PATCH 40/42] Put calling updateInstantPowerBalances() in try..catch... --- src/services/instantBoostingServices.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/services/instantBoostingServices.ts b/src/services/instantBoostingServices.ts index 993a8c554..010755b9f 100644 --- a/src/services/instantBoostingServices.ts +++ b/src/services/instantBoostingServices.ts @@ -17,7 +17,14 @@ import { export const updateInstantBoosting = async (): Promise => { logger.debug('updateInstantBoosting() has been called'); - await updateInstantPowerBalances(); + try { + await updateInstantPowerBalances(); + } catch (e) { + logger.error( + 'updateInstantBoosting() calling updateInstantPowerBalances() error', + e, + ); + } await refreshProjectInstantPowerView(); // await refreshProjectUserInstantPowerView(); }; From dc310ed12b47b469b887557e570ce38e1001a5b3 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Sun, 8 Oct 2023 11:00:16 +0330 Subject: [PATCH 41/42] Add comment --- config/example.env | 1 + 1 file changed, 1 insertion(+) diff --git a/config/example.env b/config/example.env index 9c13f9b31..be6c31e9d 100644 --- a/config/example.env +++ b/config/example.env @@ -201,6 +201,7 @@ POWER_BALANCE_AGGREGATOR_ADAPTER=powerBalanceAggregator #POWER_BALANCE_AGGREGATOR_ADAPTER=mock NUMBER_OF_BALANCE_AGGREGATOR_BATCH=20 +# OPTIONAL - default: 60000 (1 minute QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION=60000 # OPTIONAL - default: Every 10 minutes From c65575fc896a7dca46a4aa549402c51808ec6d12 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Sun, 8 Oct 2023 11:00:26 +0330 Subject: [PATCH 42/42] 1.17.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index aded15275..9ea20d785 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "giveth-graphql-api", - "version": "1.16.1", + "version": "1.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "giveth-graphql-api", - "version": "1.16.1", + "version": "1.17.0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 51fef1546..e52214ade 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "giveth-graphql-api", - "version": "1.16.1", + "version": "1.17.0", "description": "Backend GraphQL server for Giveth originally forked from Topia", "main": "./dist/index.js", "dependencies": {