diff --git a/migration/1715728347907-AddCalculatedFieldAsColumnsForProject.ts b/migration/1715728347907-AddCalculatedFieldAsColumnsForProject.ts new file mode 100644 index 000000000..2c6a01a8a --- /dev/null +++ b/migration/1715728347907-AddCalculatedFieldAsColumnsForProject.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddCalculatedFieldAsColumnsForProject1715728347907 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "project" + ADD COLUMN IF NOT EXISTS "sumDonationValueUsdForActiveQfRound" DOUBLE PRECISION DEFAULT 0; + `); + + await queryRunner.query(` + ALTER TABLE "project" + ADD COLUMN IF NOT EXISTS "sumDonationValueUsd" DOUBLE PRECISION DEFAULT 0; + `); + + // Add new integer columns for counting unique donors with 'IF NOT EXISTS' + await queryRunner.query(` + ALTER TABLE "project" + ADD COLUMN IF NOT EXISTS "countUniqueDonorsForActiveQfRound" INTEGER DEFAULT 0; + `); + + await queryRunner.query(` + ALTER TABLE "project" + ADD COLUMN IF NOT EXISTS "countUniqueDonors" INTEGER DEFAULT 0; + `); + + await queryRunner.query(` + UPDATE "project" + SET "countUniqueDonors" = pds."uniqueDonorsCount", + "sumDonationValueUsd" = pds."sumVerifiedDonations" + FROM "project_donation_summary_view" AS pds + WHERE "project"."id" = pds."projectId"; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Use 'IF EXISTS' in the DROP statement to avoid errors in case the column does not exist + await queryRunner.query( + `ALTER TABLE "project" DROP COLUMN IF EXISTS "sumDonationValueUsdForActiveQfRound"`, + ); + await queryRunner.query( + `ALTER TABLE "project" DROP COLUMN IF EXISTS "sumDonationValueUsd"`, + ); + await queryRunner.query( + `ALTER TABLE "project" DROP COLUMN IF EXISTS "countUniqueDonorsForActiveQfRound"`, + ); + await queryRunner.query( + `ALTER TABLE "project" DROP COLUMN IF EXISTS "countUniqueDonors"`, + ); + } +} diff --git a/src/entities/project.ts b/src/entities/project.ts index 6935c0bcc..f5bfbdb0d 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -40,12 +40,6 @@ import { Category } from './category'; import { FeaturedUpdate } from './featuredUpdate'; import { getHtmlTextSummary } from '../utils/utils'; import { QfRound } from './qfRound'; -import { - countUniqueDonors, - countUniqueDonorsForRound, - sumDonationValueUsd, - sumDonationValueUsdForQfRound, -} from '../repositories/donationRepository'; import { getProjectDonationsSqrtRootSum, getQfRoundTotalProjectsDonationsSum, @@ -475,41 +469,22 @@ export class Project extends BaseEntity { createdAt: new Date(), }).save(); } - /** - * Custom Query Builders to chain together - */ @Field(_type => Float, { nullable: true }) - async sumDonationValueUsdForActiveQfRound() { - const activeQfRound = this.getActiveQfRound(); - return activeQfRound - ? await sumDonationValueUsdForQfRound({ - projectId: this.id, - qfRoundId: activeQfRound.id, - }) - : 0; - } + @Column({ type: 'float', nullable: true }) + sumDonationValueUsdForActiveQfRound: number; @Field(_type => Float, { nullable: true }) - async sumDonationValueUsd() { - return await sumDonationValueUsd(this.id); - } + @Column({ type: 'float', nullable: true }) + sumDonationValueUsd: number; @Field(_type => Int, { nullable: true }) - async countUniqueDonorsForActiveQfRound() { - const activeQfRound = this.getActiveQfRound(); - return activeQfRound - ? await countUniqueDonorsForRound({ - projectId: this.id, - qfRoundId: activeQfRound.id, - }) - : 0; - } + @Column({ type: 'int', nullable: true }) + countUniqueDonorsForActiveQfRound: number; @Field(_type => Int, { nullable: true }) - async countUniqueDonors() { - return await countUniqueDonors(this.id); - } + @Column({ type: 'int', nullable: true }) + countUniqueDonors: number; // In your main class @Field(_type => EstimatedMatching, { nullable: true }) diff --git a/src/orm.ts b/src/orm.ts index e1f399452..7961e434e 100644 --- a/src/orm.ts +++ b/src/orm.ts @@ -29,6 +29,7 @@ export class AppDataSource { schema: 'public', type: 'postgres', replication: { + defaultMode: 'master', master: { database: config.get('TYPEORM_DATABASE_NAME') as string, username: config.get('TYPEORM_DATABASE_USER') as string, diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index 025dbb9d6..854400e34 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -51,23 +51,18 @@ import { ChainType } from '../types/network'; describe('all projects test cases --->', allProjectsTestCases); function allProjectsTestCases() { - it('should return projects search by owner', async () => { + it('should return projects search by title', async () => { const result = await axios.post(graphqlUrl, { query: fetchMultiFilterAllProjectsQuery, variables: { - searchTerm: SEED_DATA.SECOND_USER.name, + searchTerm: SEED_DATA.FIRST_PROJECT.title, }, }); const projects = result.data.data.allProjects.projects; - const secondUserProjects = await Project.find({ - where: { - adminUserId: SEED_DATA.SECOND_USER.id, - }, - }); - assert.equal(projects.length, secondUserProjects.length); - assert.equal(projects[0]?.adminUserId, SEED_DATA.SECOND_USER.id); + assert.isTrue(projects.length > 0); + assert.equal(projects[0]?.adminUserId, SEED_DATA.FIRST_PROJECT.adminUserId); assert.isNotEmpty(projects[0].addresses); projects.forEach(project => { assert.isNotOk(project.adminUser.email); @@ -79,10 +74,10 @@ function allProjectsTestCases() { getHtmlTextSummary(project.description), ); assert.isNull(project.estimatedMatching); - assert.exists(project.sumDonationValueUsd); - assert.exists(project.sumDonationValueUsdForActiveQfRound); - assert.exists(project.countUniqueDonorsForActiveQfRound); - assert.exists(project.countUniqueDonors); + assert.isNull(project.sumDonationValueUsd); + assert.isNull(project.sumDonationValueUsdForActiveQfRound); + assert.isNull(project.countUniqueDonorsForActiveQfRound); + assert.isNull(project.countUniqueDonors); }); }); diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 33fa248a5..728561fa2 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -329,13 +329,13 @@ export class ProjectResolver { // .setParameter('searchTerm', searchTerm) .andWhere( new Brackets(qb => { - qb.where('project.title % :searchTerm ', { + qb.where('project.title %> :searchTerm ', { searchTerm, }) - .orWhere('project.description % :searchTerm ', { + .orWhere('project.description %> :searchTerm ', { searchTerm, }) - .orWhere('project.impactLocation % :searchTerm', { + .orWhere('project.impactLocation %> :searchTerm', { searchTerm, }); }), diff --git a/src/services/donationService.ts b/src/services/donationService.ts index d9d4a7c66..28972396d 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -44,6 +44,7 @@ import { getTransactionInfoFromNetwork } from './chains'; import { getEvmTransactionTimestamp } from './chains/evm/transactionService'; import { getOrttoPersonAttributes } from '../adapters/notifications/NotificationCenterAdapter'; import { CustomToken, getTokenPrice } from './priceService'; +import { updateProjectStatistics } from './projectService'; export const TRANSAK_COMPLETED_STATUS = 'COMPLETED'; @@ -369,6 +370,8 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { await refreshProjectEstimatedMatchingView(); await refreshProjectDonationSummaryView(); + await updateProjectStatistics(donation.projectId); + const donationStats = await getUserDonationStats(donation.userId); const donor = await findUserById(donation.userId); diff --git a/src/services/projectService.ts b/src/services/projectService.ts index e360c5235..5dbc7265a 100644 --- a/src/services/projectService.ts +++ b/src/services/projectService.ts @@ -1,4 +1,11 @@ import { Project } from '../entities/project'; +import { + countUniqueDonors, + countUniqueDonorsForRound, + sumDonationValueUsd, + sumDonationValueUsdForQfRound, +} from '../repositories/donationRepository'; +import { findProjectById } from '../repositories/projectRepository'; export const getAppropriateSlug = async ( slugBase: string, @@ -22,6 +29,35 @@ export const getAppropriateSlug = async ( return slug; }; +export const updateProjectStatistics = async (projectId: number) => { + const project = await findProjectById(projectId); + if (!project) return; + + const activeQfRound = project.getActiveQfRound(); + if (activeQfRound) { + project.sumDonationValueUsdForActiveQfRound = + await sumDonationValueUsdForQfRound({ + projectId: project.id, + qfRoundId: activeQfRound.id, + }); + project.countUniqueDonorsForActiveQfRound = await countUniqueDonorsForRound( + { + projectId: project.id, + qfRoundId: activeQfRound.id, + }, + ); + } + + if (!activeQfRound) { + project.sumDonationValueUsdForActiveQfRound = 0; + project.countUniqueDonorsForActiveQfRound = 0; + } + + project.sumDonationValueUsd = await sumDonationValueUsd(project.id); + project.countUniqueDonors = await countUniqueDonors(project.id); + await project.save(); +}; + // Current Formula: will be changed possibly in the future export const getQualityScore = (description, hasImageUpload, heartCount?) => { const heartScore = 10;