diff --git a/.DS_Store b/.DS_Store index 64174a1f7..49156d3d4 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/migration-old-backup/1714018700116-add_archived_QFRound_fields.ts b/migration-old-backup/1714018700116-add_archived_QFRound_fields.ts index b4767109a..e7263ab65 100644 --- a/migration-old-backup/1714018700116-add_archived_QFRound_fields.ts +++ b/migration-old-backup/1714018700116-add_archived_QFRound_fields.ts @@ -6,11 +6,7 @@ export class AddArchivedQFRoundFields1714018700116 public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "qf_round" - ADD COLUMN IF NOT EXISTS "bannerBgImage" character varying - `); - - await queryRunner.query(` - ALTER TABLE "qf_round" + ADD COLUMN IF NOT EXISTS "bannerBgImage" character varying, ADD COLUMN IF NOT EXISTS "sponsorsImgs" character varying[] DEFAULT '{}' NOT NULL `); } @@ -18,11 +14,7 @@ export class AddArchivedQFRoundFields1714018700116 public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE "qf_round" - DROP COLUMN IF EXISTS "bannerBgImage" - `); - - await queryRunner.query(` - ALTER TABLE "qf_round" + DROP COLUMN IF EXISTS "bannerBgImage", DROP COLUMN IF EXISTS "sponsorsImgs" `); } diff --git a/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts b/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts index 81798e6fb..6edde95ef 100644 --- a/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts +++ b/migration/1654415838996-fillRelatedAddressesFromProjectsTable.ts @@ -15,7 +15,7 @@ const insertRelatedAddress = async (params: { "networkId", address, "projectId", "userId", "isRecipient") VALUES (${networkId}, '${project.walletAddress?.toLowerCase()}', ${ project.id - }, ${Number(project.admin)}, true); + }, ${project.adminUserId}, true); `, ); }; diff --git a/migration/1712853017092-UserNewRoleQfManager.ts b/migration/1712853017092-UserNewRoleQfManager.ts index 3da4baa5f..0ff47a775 100644 --- a/migration/1712853017092-UserNewRoleQfManager.ts +++ b/migration/1712853017092-UserNewRoleQfManager.ts @@ -4,22 +4,24 @@ export class UserNewRoleQfManager1712853017092 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { // add enum qfManager to user table column role await queryRunner.query( - ` - ALTER TYPE user_role_enum ADD VALUE 'qfManager'; - ALTER TABLE "user" ALTER COLUMN "role" TYPE user_role_enum USING "role"::text::user_role_enum; - ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'restricted'; + `DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_enum + WHERE pg_enum.enumtypid = 'user_role_enum'::regtype + AND pg_enum.enumlabel = 'qfManager' + ) THEN + BEGIN + EXECUTE 'ALTER TYPE user_role_enum ADD VALUE ''qfManager'''; + END; + END IF; + END $$; `, ); } public async down(queryRunner: QueryRunner): Promise { - // remove enum qfManager from user table column role - await queryRunner.query( - ` - ALTER TYPE user_role_enum DROP VALUE 'qfManager'; - ALTER TABLE "user" ALTER COLUMN "role" TYPE user_role_enum USING "role"::text::user_role_enum; - ALTER TABLE "user" ALTER COLUMN "role" SET DEFAULT 'restricted'; - `, - ); + await queryRunner.query(''); // no need to remove enum value } } diff --git a/migration/1715521134568-relate_current_mini_stream_donations_to_qf_Rounds.ts b/migration/1715521134568-relate_current_mini_stream_donations_to_qf_Rounds.ts new file mode 100644 index 000000000..e25afa95d --- /dev/null +++ b/migration/1715521134568-relate_current_mini_stream_donations_to_qf_Rounds.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RelateCurrentMiniStreamDonationsToQfRounds1715521134568 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE donation + SET "qfRoundId" = qf_round.id + FROM project_qf_rounds_qf_round, qf_round + WHERE donation."recurringDonationId" IS NOT NULL + AND donation."projectId" = project_qf_rounds_qf_round."projectId" + AND project_qf_rounds_qf_round."qfRoundId" = qf_round."id" + AND donation."createdAt" BETWEEN qf_round."beginDate" AND qf_round."endDate" + `); + } + + public async down(_queryRunner: QueryRunner): Promise { + // Since the `up` migration changes data based on existing conditions rather than schema, + // rolling back would ideally require prior knowledge of the previous state, which might + // not be practical or possible to restore. + // Therefore, typically for data migrations, the down method might be left empty or + // could reset changes based on specific requirements. + } +} diff --git a/migration/1715556030126-dropAdminColumn.ts b/migration/1715556030126-dropAdminColumn.ts new file mode 100644 index 000000000..1e1579f15 --- /dev/null +++ b/migration/1715556030126-dropAdminColumn.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class DropAdminColumn1715556030126 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "project" + DROP COLUMN "admin"; + `); + } + public async down(_queryRunner: QueryRunner): Promise {} +} diff --git a/package-lock.json b/package-lock.json index 044ba777c..cabf95684 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "form-data": "^3.0.1", "google-spreadsheet": "^3.2.0", "graphql": "16.8.1", + "graphql-fields": "^2.0.3", "graphql-tag": "^2.12.6", "graphql-upload": "15.0.2", "handlebars": "4.7.7", @@ -11943,6 +11944,11 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/graphql-fields": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/graphql-fields/-/graphql-fields-2.0.3.tgz", + "integrity": "sha512-x3VE5lUcR4XCOxPIqaO4CE+bTK8u6gVouOdpQX9+EKHr+scqtK5Pp/l8nIGqIpN1TUlkKE6jDCCycm/WtLRAwA==" + }, "node_modules/graphql-query-complexity": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/graphql-query-complexity/-/graphql-query-complexity-0.12.0.tgz", diff --git a/package.json b/package.json index c65bb0c29..4174475a4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "form-data": "^3.0.1", "google-spreadsheet": "^3.2.0", "graphql": "16.8.1", + "graphql-fields": "^2.0.3", "graphql-tag": "^2.12.6", "graphql-upload": "15.0.2", "handlebars": "4.7.7", @@ -175,6 +176,7 @@ "test:fillSnapshotBalance": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/fillSnapshotBalances.test.ts", "test:donationService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/qfRoundHistoryRepository.test.ts ./src/services/donationService.test.ts", "test:draftDonationService": "NODE_ENV=test mocha -t 99999 ./test/pre-test-scripts.ts src/services/chains/evm/draftDonationService.test.ts src/repositories/draftDonationRepository.test.ts src/workers/draftDonationMatchWorker.test.ts src/resolvers/draftDonationResolver.test.ts", + "test:draftDonationWorker": "NODE_ENV=test mocha -t 99999 ./test/pre-test-scripts.ts src/workers/draftDonationMatchWorker.test.ts", "test:draftRecurringDonationService": "NODE_ENV=test mocha -t 99999 ./test/pre-test-scripts.ts src/services/chains/evm/draftRecurringDonationService.test.ts", "test:userService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/userService.test.ts", "test:lostDonations": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/importLostDonationsJob.test.ts", @@ -201,6 +203,7 @@ "test:bootstrap": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/bootstrap.test.ts", "test:utils": "NODE_ENV=test mocha ./src/utils/utils.test.ts", "start": "NODE_ENV=development ts-node-dev --project ./tsconfig.json --respawn ./src/index.ts", + "start:test": "NODE_ENV=development ts-node-dev --project ./tsconfig.json --respawn ./test.ts", "serve": "pm2 startOrRestart ecosystem.config.js --node-args='--max-old-space-size=8192'", "db:migrate:run:test": "NODE_ENV=test npx typeorm-ts-node-commonjs migration:run -d ./src/ormconfig.ts", "db:migrate:revert:test": "NODE_ENV=test npx typeorm-ts-node-commonjs migration:revert -d ./src/ormconfig.ts", diff --git a/src/adapters/notifications/NotificationCenterAdapter.ts b/src/adapters/notifications/NotificationCenterAdapter.ts index d9cb1c6d9..884ef7e92 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -980,7 +980,6 @@ const getEmailDataDonationAttributes = async (params: { title: project.title, firstName: user.firstName, userId: user.id, - projectOwnerId: project.admin, slug: project.slug, projectLink: `${process.env.WEBSITE_URL}/project/${project.slug}`, amount, diff --git a/src/entities/project.ts b/src/entities/project.ts index 8e89219f2..109ebbe29 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -151,10 +151,6 @@ export class Project extends BaseEntity { @Column('text', { array: true, default: '{}' }) slugHistory?: string[]; - @Field({ nullable: true }) - @Column({ nullable: true }) - admin?: string; - @Field({ nullable: true }) @Column({ nullable: true }) description?: string; @@ -269,11 +265,6 @@ export class Project extends BaseEntity { @Column('jsonb', { nullable: true }) contacts: ProjectContacts[]; - @ManyToMany(_type => User, user => user.projects) - @Field(_type => [User], { nullable: true }) - @JoinTable() - users: User[]; - @Field(() => [Reaction], { nullable: true }) @OneToMany(_type => Reaction, reaction => reaction.project) reactions?: Reaction[]; @@ -318,6 +309,7 @@ export class Project extends BaseEntity { adminUser: User; @Column({ nullable: true }) + @Field(_type => Int) @RelationId((project: Project) => project.adminUser) adminUserId: number; @@ -582,10 +574,6 @@ export class Project extends BaseEntity { } } - owner() { - return this.users[0]; - } - @BeforeUpdate() async updateProjectDescriptionSummary() { await Project.update( diff --git a/src/entities/projectDonationSummaryView.ts b/src/entities/projectDonationSummaryView.ts new file mode 100644 index 000000000..e55fdcb6f --- /dev/null +++ b/src/entities/projectDonationSummaryView.ts @@ -0,0 +1,25 @@ +import { Field, ObjectType } from 'type-graphql'; +import { + Column, + PrimaryColumn, + BaseEntity, + ViewEntity, + ViewColumn, +} from 'typeorm'; + +@ViewEntity('project_donation_summary_view', { synchronize: false }) +@ObjectType() +export class ProjectDonationSummaryView extends BaseEntity { + @Field() + @ViewColumn() + @PrimaryColumn() + projectId: number; + + @ViewColumn() + @Column('double precision') + sumVerifiedDonations: number; + + @ViewColumn() + @Column('int') + uniqueDonorsCount: number; +} diff --git a/src/entities/user.ts b/src/entities/user.ts index d09ef9528..497eecacd 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -5,14 +5,12 @@ import { CreateDateColumn, Entity, Index, - JoinTable, - ManyToMany, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm'; -import { Project, ProjStatus, ReviewStatus } from './project'; +import { ProjStatus, ReviewStatus } from './project'; import { Donation, DONATION_STATUS } from './donation'; import { Reaction } from './reaction'; import { AccountVerification } from './accountVerification'; @@ -161,11 +159,6 @@ export class User extends BaseEntity { }) referredEvent?: ReferredEvent; - @Field(_type => [Project]) - @ManyToMany(_type => Project, project => project.users) - @JoinTable() - projects?: Project[]; - @Column('bool', { default: false }) segmentIdentified: boolean; @@ -216,6 +209,10 @@ export class User extends BaseEntity { status: DONATION_STATUS.VERIFIED, }) .andWhere(`donation."recurringDonationId" IS NULL`) + .cache( + `user-donationsCount-normal-${this.id}`, + Number(process.env.USER_STATS_CACHE_TIME || 60000), + ) .getCount(); // Count for recurring donations @@ -224,6 +221,10 @@ export class User extends BaseEntity { ) .where(`recurring_donation."donorId" = :donorId`, { donorId: this.id }) .andWhere('recurring_donation.totalUsdStreamed > 0') + .cache( + `user-donationsCount-recurring-${this.id}`, + Number(process.env.USER_STATS_CACHE_TIME || 60000), + ) .getCount(); // Sum of both counts @@ -239,6 +240,10 @@ export class User extends BaseEntity { `project.statusId = ${ProjStatus.active} AND project.reviewStatus = :reviewStatus`, { reviewStatus: ReviewStatus.Listed }, ) + .cache( + `user-likedProjectsCount-recurring-${this.id}`, + Number(process.env.USER_STATS_CACHE_TIME || 60000), + ) .getCount(); return likedProjectsCount; diff --git a/src/repositories/powerBoostingRepository.ts b/src/repositories/powerBoostingRepository.ts index d2082eb8e..9b255d488 100644 --- a/src/repositories/powerBoostingRepository.ts +++ b/src/repositories/powerBoostingRepository.ts @@ -165,7 +165,11 @@ export const findPowerBoostingsCountByUserId = async ( .leftJoin('powerBoosting.user', 'user') .addSelect(publicSelectionFields) .where(`percentage > 0`) - .andWhere(`"userId" =${userId}`); + .andWhere(`"userId" =${userId}`) + .cache( + `findPowerBoostingsCountByUserId-recurring-${userId}`, + Number(process.env.USER_STATS_CACHE_TIME || 60000), + ); return query.getCount(); }; diff --git a/src/repositories/previousRoundRankRepository.test.ts b/src/repositories/previousRoundRankRepository.test.ts index ad3c5c00c..cd418446a 100644 --- a/src/repositories/previousRoundRankRepository.test.ts +++ b/src/repositories/previousRoundRankRepository.test.ts @@ -13,6 +13,7 @@ import { saveProjectDirectlyToDb, saveUserDirectlyToDb, SEED_DATA, + dbIndependentTests, } from '../../test/testUtils'; import { insertSinglePowerBoosting, @@ -44,12 +45,19 @@ describe( projectsThatTheirRanksHaveChangedTestCases, ); -beforeEach(async () => { - await AppDataSource.getDataSource().query('truncate power_snapshot cascade'); +beforeEach(async function () { + const { title } = this.currentTest?.parent || {}; + + if (title && dbIndependentTests.includes(title)) { + return; + } + + await AppDataSource.getDataSource().query('TRUNCATE power_snapshot CASCADE'); await PowerBalanceSnapshot.clear(); await PowerBoostingSnapshot.clear(); await PreviousRoundRank.clear(); await PowerRound.clear(); + await createSomeSampleProjectsAndPowerViews(); }); diff --git a/src/repositories/projectAddressRepository.test.ts b/src/repositories/projectAddressRepository.test.ts index 5debdec7f..1df448209 100644 --- a/src/repositories/projectAddressRepository.test.ts +++ b/src/repositories/projectAddressRepository.test.ts @@ -142,7 +142,7 @@ function addNewProjectAddressTestCases() { ...createProjectData(), walletAddress, verified: true, - admin: String(user.id), + adminUserId: user.id, }); const newAddress = generateRandomEtheriumAddress(); const newRelatedAddress = await addNewProjectAddress({ @@ -199,7 +199,7 @@ function addBulkNewProjectAddressTestCases() { ...createProjectData(), walletAddress, verified: true, - admin: String(user.id), + adminUserId: user.id, }); const newAddress = generateRandomEtheriumAddress(); await addBulkNewProjectAddress([ @@ -224,7 +224,7 @@ function addBulkNewProjectAddressTestCases() { ...createProjectData(), walletAddress, verified: true, - admin: String(user.id), + adminUserId: user.id, }); const newAddress1 = generateRandomEtheriumAddress(); const newAddress2 = generateRandomEtheriumAddress(); diff --git a/src/repositories/projectRepository.test.ts b/src/repositories/projectRepository.test.ts index 4bf7b678f..28bb1a761 100644 --- a/src/repositories/projectRepository.test.ts +++ b/src/repositories/projectRepository.test.ts @@ -248,7 +248,7 @@ function updateProjectWithVerificationFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, networkId: NETWORK_IDS.GOERLI, }); @@ -298,7 +298,7 @@ function verifyProjectTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); assert.isFalse(project?.verified); @@ -314,7 +314,7 @@ function verifyProjectTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: true, }); assert.isTrue(project?.verified); @@ -333,12 +333,12 @@ function verifyMultipleProjectsTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); assert.isFalse(project?.verified); @@ -357,12 +357,12 @@ function verifyMultipleProjectsTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: true, }); const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: true, }); assert.isTrue(project?.verified); diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index f5eb813e3..4a669bbe0 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -92,7 +92,6 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { let query = Project.createQueryBuilder('project') .leftJoinAndSelect('project.status', 'status') - .leftJoinAndSelect('project.users', 'users') .leftJoinAndSelect('project.addresses', 'addresses') // We dont need it right now, but I comment it because we may need it later // .leftJoinAndSelect('project.anchorContracts', 'anchor_contract_address') @@ -325,6 +324,16 @@ export const findProjectBySlug = (slug: string): Promise => { ); }; +export const findProjectIdBySlug = (slug: string): Promise => { + // check current slug and previous slugs + return Project.createQueryBuilder('project') + .select('project.id') + .where(`:slug = ANY(project."slugHistory") or project.slug = :slug`, { + slug, + }) + .getOne(); +}; + export const findProjectBySlugWithoutAnyJoin = ( slug: string, ): Promise => { diff --git a/src/repositories/projectVerificationRepository.test.ts b/src/repositories/projectVerificationRepository.test.ts index cd2a9e719..b1c60e628 100644 --- a/src/repositories/projectVerificationRepository.test.ts +++ b/src/repositories/projectVerificationRepository.test.ts @@ -73,7 +73,7 @@ function createProjectVerificationFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -92,7 +92,7 @@ function updateProjectPersonalInfoOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -123,7 +123,7 @@ function updateProjectRegistryOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -153,7 +153,7 @@ function updateProjectContactsOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -187,7 +187,7 @@ function updateProjectVerificationLastStepTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -212,7 +212,7 @@ function updateMilestonesOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -253,7 +253,7 @@ function updateManagingFundsOfProjectVerificationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -295,7 +295,7 @@ function getInProgressProjectVerificationRequestTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -318,7 +318,7 @@ function getInProgressProjectVerificationRequestTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -344,7 +344,7 @@ function verifyFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -374,7 +374,7 @@ function verifyFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -407,7 +407,7 @@ function makeFormDraftTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -435,7 +435,7 @@ function makeFormDraftTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -472,7 +472,7 @@ function verifyMultipleFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -483,7 +483,7 @@ function verifyMultipleFormTestCases() { await projectVerificationForm.save(); const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm2 = await createProjectVerificationForm({ @@ -527,7 +527,7 @@ function verifyMultipleFormTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = await createProjectVerificationForm({ @@ -538,7 +538,7 @@ function verifyMultipleFormTestCases() { await projectVerificationForm.save(); const project2 = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm2 = await createProjectVerificationForm({ diff --git a/src/repositories/projectVerificationRepository.ts b/src/repositories/projectVerificationRepository.ts index d101ff977..c3299900f 100644 --- a/src/repositories/projectVerificationRepository.ts +++ b/src/repositories/projectVerificationRepository.ts @@ -2,18 +2,19 @@ import { UpdateResult } from 'typeorm'; import { ManagingFunds, Milestones, - PROJECT_VERIFICATION_STATUSES, PersonalInfo, + PROJECT_VERIFICATION_STATUSES, + PROJECT_VERIFICATION_STEPS, ProjectContacts, ProjectRegistry, ProjectVerificationForm, - PROJECT_VERIFICATION_STEPS, } from '../entities/projectVerificationForm'; import { findProjectById } from './projectRepository'; import { findUserById } from './userRepository'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; import { User } from '../entities/user'; import { getAppropriateNetworkId } from '../services/chains'; +import { logger } from '../utils/logger'; export const createProjectVerificationForm = async (params: { userId: number; @@ -168,18 +169,28 @@ export const updateProjectPersonalInfoOfProjectVerification = async (params: { projectVerificationId: number; personalInfo: PersonalInfo; }): Promise => { - const { personalInfo, projectVerificationId } = params; - const projectVerificationForm = await findProjectVerificationFormById( - projectVerificationId, - ); - if (!projectVerificationForm) { - throw new Error( - i18n.__(translationErrorMessagesKeys.PROJECT_VERIFICATION_FORM_NOT_FOUND), + try { + const { personalInfo, projectVerificationId } = params; + const projectVerificationForm = await findProjectVerificationFormById( + projectVerificationId, + ); + if (!projectVerificationForm) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.PROJECT_VERIFICATION_FORM_NOT_FOUND, + ), + ); + } + + projectVerificationForm.personalInfo = personalInfo; + return projectVerificationForm?.save(); + } catch (error) { + logger.debug( + 'updateProjectPersonalInfoOfProjectVerification error: ', + error, ); + throw new Error(error); } - - projectVerificationForm.personalInfo = personalInfo; - return projectVerificationForm?.save(); }; export const updateProjectRegistryOfProjectVerification = async (params: { @@ -317,6 +328,17 @@ export const updateManagingFundsOfProjectVerification = async (params: { return projectVerificationForm?.save(); }; +export const getVerificationFormStatusByProjectId = async ( + projectId: number, +): Promise => { + return ProjectVerificationForm.createQueryBuilder('project_verification_form') + .select(['project_verification_form.status']) + .where(`project_verification_form.projectId=:projectId`, { + projectId, + }) + .getOne(); +}; + export const getVerificationFormByProjectId = async ( projectId: number, ): Promise => { diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index cd1b08426..bc8e6f3ac 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -42,6 +42,12 @@ export class QFArchivedRounds { @Field(_type => Int) allocatedFund: number; + @Field(_type => Int, { nullable: true }) + allocatedFundUSD?: number | null; + + @Field(_type => String, { nullable: true }) + allocatedTokenSymbol?: string | null; + @Field(_type => [Int]) eligibleNetworks: number; @@ -83,6 +89,8 @@ export const findArchivedQfRounds = async ( .addSelect('SUM(donation.amount)', 'totalDonations') .addSelect('COUNT(DISTINCT donation.fromWalletAddress)', 'uniqueDonors') .addSelect('qfRound.allocatedFund', 'allocatedFund') + .addSelect('qfRound.allocatedFundUSD', 'allocatedFundUSD') + .addSelect('qfRound.allocatedTokenSymbol', 'allocatedTokenSymbol') .addSelect('qfRound.beginDate', 'beginDate') .groupBy('qfRound.id') .orderBy(fieldMap[field], direction, 'NULLS LAST') @@ -191,7 +199,7 @@ export const deactivateExpiredQfRounds = async (): Promise => { export const getRelatedProjectsOfQfRound = async ( qfRoundId: number, -): Promise<{ slug: string; name: string }[]> => { +): Promise<{ slug: string; name: string; id: number }[]> => { const query = ` SELECT "p"."slug", "p"."title" , p.id FROM "project" "p" diff --git a/src/repositories/socialProfileRepository.test.ts b/src/repositories/socialProfileRepository.test.ts index f5628c96f..c4a7116b4 100644 --- a/src/repositories/socialProfileRepository.test.ts +++ b/src/repositories/socialProfileRepository.test.ts @@ -28,7 +28,7 @@ function removeSocialProfileByIdTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -54,7 +54,7 @@ function findSocialProfilesByProjectIdTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -78,7 +78,7 @@ function findSocialProfilesByProjectIdTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const socialProfiles = await findSocialProfilesByProjectId({ diff --git a/src/repositories/userRepository.test.ts b/src/repositories/userRepository.test.ts index 508099a95..585e4b4a5 100644 --- a/src/repositories/userRepository.test.ts +++ b/src/repositories/userRepository.test.ts @@ -488,7 +488,7 @@ function findUsersWhoSupportProjectTestCases() { ); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(projectOwner.id), + adminUserId: projectOwner.id, }); const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 39eed727b..10edd7f07 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -657,6 +657,74 @@ function donationsTestCases() { allDonationsCount, ); }); + it('should get result with recurring donations joined (for streamed mini donations)', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const anchorAddress = generateRandomEtheriumAddress(); + + const anchorContractAddress = await addNewAnchorAddress({ + project, + owner: user, + creator: user, + address: anchorAddress, + networkId: NETWORK_IDS.OPTIMISTIC, + txHash: generateRandomEvmTxHash(), + }); + const currency = 'USD'; + + const recurringDonation = await createNewRecurringDonation({ + txHash: generateRandomEvmTxHash(), + networkId: NETWORK_IDS.OPTIMISTIC, + donor: user, + anchorContractAddress, + flowRate: '100', + currency, + project, + anonymous: false, + isBatch: false, + totalUsdStreamed: 1, + }); + recurringDonation.status = RECURRING_DONATION_STATUS.ACTIVE; + await recurringDonation.save(); + const donation = await saveDonationDirectlyToDb( + { + ...createDonationData(), + }, + user.id, + project.id, + ); + donation.recurringDonation = recurringDonation; + await donation.save(); + + // Use moment to parse the createdAt string + const momentDate = moment(donation.createdAt, 'YYYYMMDD HH:mm:ss'); + + // Create fromDate as one second before + const fromDate = momentDate + .clone() + .subtract(1, 'seconds') + .format('YYYYMMDD HH:mm:ss'); + + // Create toDate as one second after + const toDate = momentDate + .clone() + .add(1, 'seconds') + .format('YYYYMMDD HH:mm:ss'); + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchAllDonationsQuery, + variables: { + fromDate, + toDate, + }, + }); + assert.isOk(donationsResponse.data.data.donations); + assert.equal(donationsResponse.data.data.donations.length, 1); + assert.equal( + Number(donationsResponse.data.data.donations[0].recurringDonation.id), + recurringDonation.id, + ); + }); it('should get result when sending fromDate', async () => { const oldDonation = await saveDonationDirectlyToDb( createDonationData(), @@ -817,6 +885,16 @@ function donationsTestCases() { assert.isArray(donation.project.categories); }); }); + it('should project include categories', async () => { + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchAllDonationsQuery, + variables: {}, + }); + assert.isOk(donationsResponse.data.data.donations); + donationsResponse.data.data.donations.forEach(donation => { + assert.isArray(donation.project.categories); + }); + }); } function createDonationTestCases() { @@ -3643,7 +3721,7 @@ function donationsByUserIdTestCases() { updatedAt: new Date(), slug: title, // firstUser's id - admin: String(user.id), + adminUserId: user.id, qualityScore: 30, // just need the initial value to be different than 0 totalDonations: 10, @@ -3723,7 +3801,7 @@ function donationsByUserIdTestCases() { updatedAt: new Date(), slug: title, // firstUser's id - admin: String(user.id), + adminUserId: user.id, qualityScore: 30, // just need the initial value to be different than 0 totalDonations: 10, diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 4a00c1ebc..0540d7f22 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -238,6 +238,7 @@ export class DonationResolver { .leftJoin('donation.user', 'user') .addSelect(publicSelectionFields) .leftJoinAndSelect('donation.project', 'project') + .leftJoinAndSelect('donation.recurringDonation', 'recurringDonation') .leftJoinAndSelect('project.categories', 'categories') .leftJoin('project.projectPower', 'projectPower') .addSelect([ diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index 5d1ae0678..025dbb9d6 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -62,12 +62,12 @@ function allProjectsTestCases() { const projects = result.data.data.allProjects.projects; const secondUserProjects = await Project.find({ where: { - admin: String(SEED_DATA.SECOND_USER.id), + adminUserId: SEED_DATA.SECOND_USER.id, }, }); assert.equal(projects.length, secondUserProjects.length); - assert.equal(Number(projects[0]?.admin), SEED_DATA.SECOND_USER.id); + assert.equal(projects[0]?.adminUserId, SEED_DATA.SECOND_USER.id); assert.isNotEmpty(projects[0].addresses); projects.forEach(project => { assert.isNotOk(project.adminUser.email); diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 5138d9554..5d82a7df0 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -359,7 +359,7 @@ function projectsByUserIdTestCases() { ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), - admin: String(user.id), + adminUserId: user.id, }); const verificationForm = await ProjectVerificationForm.create({ @@ -387,7 +387,7 @@ function projectsByUserIdTestCases() { const projects = result.data.data.projectsByUserId.projects; const projectWithAnotherOwner = projects.find( - project => Number(project.admin) !== user!.id, + project => project.adminUserId !== user!.id, ); assert.isNotOk(projectWithAnotherOwner); projects.forEach(project => { @@ -410,7 +410,7 @@ function projectsByUserIdTestCases() { }); const projects = result.data.data.projectsByUserId.projects; const projectWithAnotherOwner = projects.find( - project => Number(project.admin) !== userId, + project => project.adminUserId !== userId, ); assert.isNotOk(projectWithAnotherOwner); projects.forEach(project => { @@ -481,7 +481,7 @@ function projectsByUserIdTestCases() { const userId = SEED_DATA.FIRST_USER.id; const draftProject = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, statusId: ProjStatus.drafted, }); const result = await axios.post(graphqlUrl, { @@ -505,7 +505,7 @@ function projectsByUserIdTestCases() { const userId = SEED_DATA.FIRST_USER.id; const notListedProject = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, listed: false, reviewStatus: ReviewStatus.NotListed, }); @@ -553,7 +553,7 @@ function createProjectTestCases() { const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); const sampleProject1 = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -563,7 +563,7 @@ function createProjectTestCases() { }; const sampleProject2 = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -571,7 +571,7 @@ function createProjectTestCases() { }, ], }; - const promise1 = axios.post( + const res1 = await axios.post( graphqlUrl, { query: createProjectQuery, @@ -585,7 +585,7 @@ function createProjectTestCases() { }, }, ); - const promise2 = axios.post( + const res2 = await axios.post( graphqlUrl, { query: createProjectQuery, @@ -599,20 +599,15 @@ function createProjectTestCases() { }, }, ); - const [result1, result2] = await Promise.all([promise1, promise2]); - const isResult1Ok = !!result1.data.data?.createProject; - const isResult2Ok = !!result2.data.data?.createProject; - - // Exactly one should be ok - const exactlyOneOk = - (isResult1Ok && !isResult2Ok) || (!isResult1Ok && isResult2Ok); - - assert.isTrue(exactlyOneOk, 'Exactly one operation should be successful'); + const isRes1Ok = !!res1.data.data?.createProject; + const isRes2Ok = !!res2.data.data?.createProject; + assert.isTrue(isRes1Ok); + assert.isFalse(isRes2Ok); }); it('Create Project should return <>, calling without token IN ENGLISH when no-lang header is sent', async () => { const sampleProject = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -641,7 +636,7 @@ function createProjectTestCases() { it('Create Project should return <>, calling without token IN ENGLISH when non supported language is sent', async () => { const sampleProject = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -674,7 +669,7 @@ function createProjectTestCases() { it('Create Project should return <>, calling without token IN SPANISH', async () => { const sampleProject = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -706,7 +701,7 @@ function createProjectTestCases() { it('Create Project should return <>, calling without token', async () => { const sampleProject = { title: 'title1', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -740,7 +735,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: ['invalid category'], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -782,7 +777,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [nonActiveCategory.name], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -815,7 +810,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: SEED_DATA.FOOD_SUB_CATEGORIES, description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -858,7 +853,7 @@ function createProjectTestCases() { SEED_DATA.FOOD_SUB_CATEGORIES[1], ], description: '
Sample Project Creation
', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -895,7 +890,7 @@ function createProjectTestCases() { SEED_DATA.FOOD_SUB_CATEGORIES[1], ], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -939,7 +934,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: SEED_DATA.MALFORMED_ETHEREUM_ADDRESS, @@ -972,7 +967,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: SEED_DATA.MALFORMED_SOLANA_ADDRESS, @@ -1006,7 +1001,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: SEED_DATA.FIRST_PROJECT.walletAddress, @@ -1043,7 +1038,7 @@ function createProjectTestCases() { title: String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: SEED_DATA.DAI_SMART_CONTRACT_ADDRESS, @@ -1081,7 +1076,7 @@ function createProjectTestCases() { title: SEED_DATA.FIRST_PROJECT.title, categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1134,7 +1129,7 @@ function createProjectTestCases() { description: 'a'.repeat(PROJECT_DESCRIPTION_MAX_LENGTH + 1), image: 'https://gateway.pinata.cloud/ipfs/QmauSzWacQJ9rPkPJgr3J3pdgfNRGAaDCr1yAToVWev2QS', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1196,7 +1191,7 @@ function createProjectTestCases() { description: 'description', image: 'https://gateway.pinata.cloud/ipfs/QmauSzWacQJ9rPkPJgr3J3pdgfNRGAaDCr1yAToVWev2QS', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1280,7 +1275,7 @@ function createProjectTestCases() { description: 'description', image: 'https://gateway.pinata.cloud/ipfs/QmauSzWacQJ9rPkPJgr3J3pdgfNRGAaDCr1yAToVWev2QS', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1323,7 +1318,7 @@ function createProjectTestCases() { categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', isDraft: true, - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -1367,8 +1362,8 @@ function createProjectTestCases() { ); assert.equal( - result.data.data.createProject.admin, - String(SEED_DATA.FIRST_USER.id), + result.data.data.createProject.adminUserId, + SEED_DATA.FIRST_USER.id, ); assert.equal(result.data.data.createProject.verified, false); assert.equal( @@ -1580,7 +1575,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -1652,7 +1647,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); const editProjectResult = await axios.post( @@ -1697,7 +1692,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const ethAddress = generateRandomEtheriumAddress(); const solanaAddress = generateRandomSolanaAddress(); @@ -1754,7 +1749,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); const newWalletAddress2 = generateRandomEtheriumAddress(); @@ -1820,7 +1815,7 @@ function updateProjectTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, walletAddress, }); const newWalletAddress = project.walletAddress; @@ -1882,7 +1877,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); const editProjectResult = await axios.post( @@ -1915,7 +1910,7 @@ function updateProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); const editProjectResult = await axios.post( @@ -1980,7 +1975,7 @@ function updateProjectTestCases() { title: 'test ' + String(new Date().getTime()), categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -2038,7 +2033,7 @@ function updateProjectTestCases() { title, categories: [SEED_DATA.FOOD_SUB_CATEGORIES[0]], description: 'description', - admin: String(SEED_DATA.FIRST_USER.id), + adminUserId: SEED_DATA.FIRST_USER.id, addresses: [ { address: generateRandomEtheriumAddress(), @@ -2096,7 +2091,7 @@ function updateProjectTestCases() { const walletAddress = generateRandomEtheriumAddress(); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, walletAddress, }); @@ -2566,7 +2561,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -2602,7 +2597,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomSolanaAddress(); @@ -2639,7 +2634,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -2673,7 +2668,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -2707,7 +2702,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, listed: true, reviewStatus: ReviewStatus.Listed, }); @@ -2748,7 +2743,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, listed: false, reviewStatus: ReviewStatus.NotListed, }); @@ -2790,7 +2785,7 @@ function addRecipientAddressToProjectTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const newWalletAddress = generateRandomEtheriumAddress(); @@ -3877,7 +3872,7 @@ function walletAddressIsPurpleListedTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); await addNewProjectAddress({ @@ -3973,7 +3968,7 @@ function getPurpleListTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); await addNewProjectAddress({ @@ -4292,7 +4287,7 @@ function getProjectUpdatesTestCases() { } function projectBySlugTestCases() { - it('should return projects with indicated slug and verification form if owner', async () => { + it('should return projects with indicated slug and verification form status if owner', async () => { const project1 = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), @@ -4301,7 +4296,7 @@ function projectBySlugTestCases() { const user = (await User.findOne({ where: { - id: Number(project1.admin), + id: project1.adminUserId, }, })) as User; @@ -4331,68 +4326,53 @@ function projectBySlugTestCases() { const project = result.data.data.projectBySlug; assert.equal(Number(project.id), project1.id); - assert.isOk(project.projectVerificationForm); - assert.equal(project.projectVerificationForm.id, verificationForm.id); + assert.isOk(project.verificationFormStatus); + assert.equal(project.verificationFormStatus, verificationForm.status); assert.isOk(project.adminUser.walletAddress); assert.isOk(project.adminUser.firstName); assert.isNotOk(project.adminUser.email); assert.isOk(project.categories[0].mainCategory.title); }); - it('should return verificationFormStatus if its not owner', async () => { - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - }); - - const user = - (await User.findOne({ - where: { - id: Number(project1.admin), - }, - })) || undefined; - - const verificationForm = await ProjectVerificationForm.create({ - project: project1, - user, - status: PROJECT_VERIFICATION_STATUSES.DRAFT, - }).save(); - - const result = await axios.post(graphqlUrl, { - query: fetchProjectBySlugQuery, - variables: { - slug: project1.slug, - connectedWalletUserId: user!.id, - }, - }); - - const project = result.data.data.projectBySlug; - assert.equal(Number(project.id), project1.id); - assert.isNotOk(project.projectVerificationForm); - assert.equal(project.verificationFormStatus, verificationForm.status); - }); it('should return projects with indicated slug', async () => { const walletAddress = generateRandomEtheriumAddress(); - const project1 = await saveProjectDirectlyToDb({ - ...createProjectData(), - title: String(new Date().getTime()), - slug: String(new Date().getTime()), - walletAddress, - }); - + const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); + const sampleProject1 = { + title: walletAddress, + adminUserId: SEED_DATA.FIRST_USER.id, + addresses: [ + { + address: walletAddress, + networkId: NETWORK_IDS.XDAI, + chainType: ChainType.EVM, + }, + ], + }; + const res1 = await axios.post( + graphqlUrl, + { + query: createProjectQuery, + variables: { + project: sampleProject1, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + const _project = res1.data.data.createProject; const result = await axios.post(graphqlUrl, { query: fetchProjectBySlugQuery, variables: { - slug: project1.slug, + slug: _project.slug, }, }); - const project = result.data.data.projectBySlug; - assert.equal(Number(project.id), project1.id); + assert.equal(Number(project.id), Number(_project.id)); assert.isOk(project.adminUser.walletAddress); assert.isOk(project.givbackFactor); - assert.isNull(project.projectVerificationForm); assert.isOk(project.adminUser.firstName); assert.isNotOk(project.adminUser.email); assert.isNotEmpty(project.addresses); @@ -5192,8 +5172,8 @@ function similarProjectsBySlugTestCases() { const [, relatedCount] = await Project.createQueryBuilder('project') .innerJoinAndSelect('project.categories', 'categories') .where('project.id != :id', { id: viewedProject?.id }) - .andWhere('project.admin = :ownerId', { - ownerId: String(SEED_DATA.FIRST_USER.id), + .andWhere('project.adminUserId = :ownerId', { + ownerId: SEED_DATA.FIRST_USER.id, }) .andWhere( `project.statusId = ${ProjStatus.active} AND project.reviewStatus = :reviewStatus`, @@ -5237,7 +5217,7 @@ function addProjectUpdateTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const result = await axios.post( @@ -5323,7 +5303,7 @@ function addProjectUpdateTestCases() { const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const accessTokenUser1 = await generateTestAccessToken(user1.id); @@ -5422,7 +5402,7 @@ function editProjectUpdateTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const updateProject = await ProjectUpdate.create({ userId: user.id, @@ -5549,7 +5529,7 @@ function deleteProjectUpdateTestCases() { const accessToken = await generateTestAccessToken(user.id); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const updateProject = await ProjectUpdate.create({ @@ -5582,7 +5562,7 @@ function deleteProjectUpdateTestCases() { const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, }); const accessTokenUser1 = await generateTestAccessToken(user1.id); diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index fb38730ed..e68fc17e8 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1,5 +1,5 @@ import { Max, Min } from 'class-validator'; -import { Brackets, Repository } from 'typeorm'; +import { Brackets, getMetadataArgsStorage, Repository } from 'typeorm'; import { Service } from 'typedi'; import { Arg, @@ -7,6 +7,7 @@ import { ArgsType, Ctx, Field, + Info, InputType, Int, Mutation, @@ -15,6 +16,7 @@ import { registerEnumType, Resolver, } from 'type-graphql'; +import graphqlFields from 'graphql-fields'; import { SelectQueryBuilder } from 'typeorm/query-builder/SelectQueryBuilder'; import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; import { Reaction } from '../entities/reaction'; @@ -77,15 +79,14 @@ import { FilterProjectQueryInputParams, filterProjectsQuery, findProjectById, - findProjectBySlugWithoutAnyJoin, + findProjectIdBySlug, totalProjectsPerDate, totalProjectsPerDateByMonthAndYear, - userIsOwnerOfProject, } from '../repositories/projectRepository'; import { sortTokensByOrderAndAlphabets } from '../utils/tokenUtils'; import { getNotificationAdapter } from '../adapters/adaptersFactory'; import { NETWORK_IDS } from '../provider'; -import { getVerificationFormByProjectId } from '../repositories/projectVerificationRepository'; +import { getVerificationFormStatusByProjectId } from '../repositories/projectVerificationRepository'; import { resourcePerDateReportValidator, validateWithJoiSchema, @@ -464,9 +465,9 @@ export class ProjectResolver { query: SelectQueryBuilder, take: number, skip: number, - ownerId?: string | null, + ownerId?: number | null, ): Promise { - query.andWhere('project.admin = :ownerId', { ownerId }); + query.andWhere('project.adminUserId = :ownerId', { ownerId }); const [projects, totalCount] = await query .orderBy('project.creationDate', 'DESC') .take(take) @@ -844,56 +845,103 @@ export class ProjectResolver { query = ProjectResolver.addUserReaction(query, connectedWalletUserId, user); const project = await query.getOne(); - canUserVisitProject(project, String(user?.userId)); + canUserVisitProject(project, user?.userId); return project; } + // Helper method to get the fields of the Project entity + private getEntityFields(entity: typeof Project): string[] { + const metadata = getMetadataArgsStorage(); + const columns = metadata.columns.filter(col => col.target === entity); + return columns.map(col => col.propertyName); + } + @Query(_returns => ProjectBySlugResponse) async projectBySlug( @Arg('slug') slug: string, @Arg('connectedWalletUserId', _type => Int, { nullable: true }) connectedWalletUserId: number, @Ctx() { req: { user } }: ApolloContext, + @Info() info: any, ) { - const viewerUserId = connectedWalletUserId || user?.userId; - let isOwnerOfProject = false; - - // ensure it's the owner - if (viewerUserId) { - isOwnerOfProject = await userIsOwnerOfProject(viewerUserId, slug); - } - - const minimalProject = await findProjectBySlugWithoutAnyJoin(slug); + const minimalProject = await findProjectIdBySlug(slug); if (!minimalProject) { throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); } - const campaignSlugs = (await getAllProjectsRelatedToActiveCampaigns())[ - minimalProject.id - ]; + + // Extract requested fields + const fields = graphqlFields(info); + const projectFields = this.getEntityFields(Project); + + // Filter requested fields to only include those in the Project entity + const selectedFields = Object.keys(fields).filter(field => + projectFields.includes(field), + ); + + // Dynamically build the select fields + const selectFields = selectedFields.map(field => `project.${field}`); let query = this.projectRepository .createQueryBuilder('project') + .select(selectFields) .where(`project.id = :id`, { id: minimalProject.id, }) - .leftJoinAndSelect('project.status', 'status') - .leftJoinAndSelect( - 'project.categories', - 'categories', - 'categories.isActive = :isActive', - { isActive: true }, - ) - .leftJoinAndSelect('categories.mainCategory', 'mainCategory') - .leftJoinAndSelect('project.organization', 'organization') - .leftJoinAndSelect('project.addresses', 'addresses') - .leftJoinAndSelect('project.socialMedia', 'socialMedia') - .leftJoinAndSelect('project.anchorContracts', 'anchor_contract_address') - .leftJoinAndSelect('project.projectPower', 'projectPower') - .leftJoinAndSelect('project.projectInstantPower', 'projectInstantPower') - .leftJoinAndSelect('project.qfRounds', 'qfRounds') - .leftJoinAndSelect('project.projectFuturePower', 'projectFuturePower') - .leftJoinAndMapMany( + .leftJoinAndSelect('project.status', 'status'); + + if (fields.categories) { + query = query + .leftJoinAndSelect( + 'project.categories', + 'categories', + 'categories.isActive = :isActive', + { isActive: true }, + ) + .leftJoinAndSelect('categories.mainCategory', 'mainCategory') + .orderBy({ + 'mainCategory.title': 'ASC', + 'categories.name': 'ASC', + }); + } + if (fields.organization) { + query = query.leftJoinAndSelect('project.organization', 'organization'); + } + if (fields.addresses) { + query = query.leftJoinAndSelect('project.addresses', 'addresses'); + } + if (fields.socialMedia) { + query = query.leftJoinAndSelect('project.socialMedia', 'socialMedia'); + } + if (fields.anchorContracts) { + query = query.leftJoinAndSelect( + 'project.anchorContracts', + 'anchor_contract_address', + ); + } + if (fields.projectPower) { + query = query.leftJoinAndSelect('project.projectPower', 'projectPower'); + } + if (fields.projectInstantPower) { + query = query.leftJoinAndSelect( + 'project.projectInstantPower', + 'projectInstantPower', + ); + } + if (fields.qfRounds) { + query = query.leftJoinAndSelect('project.qfRounds', 'qfRounds'); + } + if (fields.projectFuturePower) { + query = query.leftJoinAndSelect( + 'project.projectFuturePower', + 'projectFuturePower', + ); + } + if (fields.campaigns) { + const campaignSlugs = (await getAllProjectsRelatedToActiveCampaigns())[ + minimalProject.id + ]; + query = query.leftJoinAndMapMany( 'project.campaigns', Campaign, 'campaigns', @@ -902,38 +950,44 @@ export class ProjectResolver { slug, campaignSlugs, }, - ) - .leftJoin('project.adminUser', 'user') - .addSelect(publicSelectionFields); // aliased selection - - query = ProjectResolver.addUserReaction(query, connectedWalletUserId, user); - - if (isOwnerOfProject) { - query = ProjectResolver.addProjectVerificationForm( + ); + } + if (fields.adminUser) { + const adminUserFields = Object.keys(fields.adminUser).map( + field => `user.${field}`, + ); + const filterByPublicFields = publicSelectionFields.filter(field => + adminUserFields.includes(field), + ); + query = query + .leftJoin('project.adminUser', 'user') + .addSelect(filterByPublicFields); // aliased selection + } + if (fields.reaction) { + query = ProjectResolver.addUserReaction( query, connectedWalletUserId, user, ); } - query = query.orderBy({ - 'mainCategory.title': 'ASC', - 'categories.name': 'ASC', - }); - const project = await query.getOne(); - canUserVisitProject(project, String(user?.userId)); - const verificationForm = - project?.projectVerificationForm || - (await getVerificationFormByProjectId(project?.id as number)); - if (verificationForm) { - (project as Project).verificationFormStatus = verificationForm?.status; - } + canUserVisitProject(project, user?.userId); + if (fields.verificationFormStatus) { + const verificationForm = await getVerificationFormStatusByProjectId( + project?.id as number, + ); + if (verificationForm) { + (project as Project).verificationFormStatus = verificationForm?.status; + } + } + if (fields.givbackFactor) { + const { givbackFactor } = await calculateGivbackFactor(project!.id); + return { ...project, givbackFactor }; + } // 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 }; + return project; } @Mutation(_returns => Project) @@ -954,10 +1008,10 @@ export class ProjectResolver { if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - logger.debug(`project.admin ---> : ${project.admin}`); + logger.debug(`project.adminUserId ---> : ${project.adminUserId}`); logger.debug(`user.userId ---> : ${user.userId}`); logger.debug(`updateProject, inputData :`, newProjectData); - if (project.admin !== String(user.userId)) + if (project.adminUserId !== user.userId) throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1063,7 +1117,7 @@ export class ProjectResolver { } } - const adminUser = (await findUserById(Number(project.admin))) as User; + const adminUser = (await findUserById(project.adminUserId)) as User; if (newProjectData.addresses) { await removeRecipientAddressOfProject({ project }); await addBulkNewProjectAddress( @@ -1116,7 +1170,7 @@ export class ProjectResolver { if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - if (project.admin !== String(user.userId)) { + if (project.adminUserId !== user.userId) { throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1124,7 +1178,7 @@ export class ProjectResolver { await validateProjectWalletAddress(address, projectId, chainType); - const adminUser = (await findUserById(Number(project.admin))) as User; + const adminUser = (await findUserById(project.adminUserId)) as User; await addNewProjectAddress({ project, user: adminUser, @@ -1312,7 +1366,6 @@ export class ProjectResolver { const project = Project.create({ ...projectInput, - categories: categories as Category[], organization: organization as Organization, image, @@ -1320,8 +1373,7 @@ export class ProjectResolver { updatedAt: now, slug: slug.toLowerCase(), slugHistory: [], - admin: String(ctx.req.user.userId), - users: [user], + adminUserId: ctx.req.user.userId, status: status as ProjectStatus, qualityScore, totalDonations: 0, @@ -1426,7 +1478,7 @@ export class ProjectResolver { if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - if (project.admin !== String(user.userId)) + if (project.adminUserId !== user.userId) throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1486,7 +1538,7 @@ export class ProjectResolver { const project = await Project.findOne({ where: { id: update.projectId } }); if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - if (project.admin !== String(user.userId)) + if (project.adminUserId !== user.userId) throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1518,7 +1570,7 @@ export class ProjectResolver { const project = await Project.findOne({ where: { id: update.projectId } }); if (!project) throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); - if (project.admin !== String(user.userId)) + if (project.adminUserId !== user.userId) throw new Error( i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), ); @@ -1711,7 +1763,7 @@ export class ProjectResolver { ); } - query = query.where('project.admin = :userId', { userId: String(userId) }); + query = query.where('project.adminUserId = :userId', { userId }); if (userId !== user?.userId) { query = query.andWhere( @@ -1852,7 +1904,7 @@ export class ProjectResolver { query, take, skip, - viewedProject?.admin, + viewedProject?.adminUserId, ); } } diff --git a/src/resolvers/projectVerificationFormResolver.test.ts b/src/resolvers/projectVerificationFormResolver.test.ts index d2a1acb6b..695e86d59 100644 --- a/src/resolvers/projectVerificationFormResolver.test.ts +++ b/src/resolvers/projectVerificationFormResolver.test.ts @@ -72,7 +72,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -113,7 +113,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user1.id), + adminUserId: user1.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -144,7 +144,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: true, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -190,7 +190,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -208,7 +208,7 @@ function createProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -352,7 +352,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -409,7 +409,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -462,7 +462,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -519,7 +519,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -575,7 +575,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -635,7 +635,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -715,7 +715,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -784,7 +784,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -839,7 +839,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -893,7 +893,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -948,7 +948,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1002,7 +1002,7 @@ function updateProjectVerificationFormMutationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1054,7 +1054,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: true, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1090,7 +1090,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1146,7 +1146,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1202,7 +1202,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1258,7 +1258,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1298,7 +1298,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: true, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1323,7 +1323,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user1.id), + adminUserId: user1.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1359,7 +1359,7 @@ function getCurrentProjectVerificationFormTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.deactive, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1431,7 +1431,7 @@ function projectVerificationSendEmailConfirmationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1488,7 +1488,7 @@ function projectVerificationSendEmailConfirmationTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1537,7 +1537,7 @@ function projectVerificationConfirmEmailTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, @@ -1604,7 +1604,7 @@ function projectVerificationConfirmEmailTestCases() { const project = await saveProjectDirectlyToDb({ ...createProjectData(), statusId: ProjStatus.active, - admin: String(user.id), + adminUserId: user.id, verified: false, listed: false, reviewStatus: ReviewStatus.NotListed, diff --git a/src/resolvers/projectVerificationFormResolver.ts b/src/resolvers/projectVerificationFormResolver.ts index eee286f20..100ca8d78 100644 --- a/src/resolvers/projectVerificationFormResolver.ts +++ b/src/resolvers/projectVerificationFormResolver.ts @@ -213,7 +213,7 @@ export class ProjectVerificationFormResolver { i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), ); } - if (Number(project.admin) !== userId) { + if (project.adminUserId !== userId) { throw new Error( i18n.__( translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT, @@ -315,7 +315,7 @@ export class ProjectVerificationFormResolver { i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), ); } - if (Number(project.admin) !== userId) { + if (project.adminUserId !== userId) { throw new Error( i18n.__( translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT, diff --git a/src/resolvers/qfRoundResolver.ts b/src/resolvers/qfRoundResolver.ts index 14ad60ae5..3201278c4 100644 --- a/src/resolvers/qfRoundResolver.ts +++ b/src/resolvers/qfRoundResolver.ts @@ -35,6 +35,9 @@ export class QfRoundStatsResponse { @Field() matchingPool: number; + + @Field() + qfRound: QfRound; } @ObjectType() @@ -136,6 +139,7 @@ export class QfRoundResolver { uniqueDonors: contributorsCount, allDonationsUsdValue: totalDonationsSum, matchingPool: qfRound.allocatedFund, + qfRound, }; } } diff --git a/src/resolvers/reactionResolver.test.ts b/src/resolvers/reactionResolver.test.ts index 32a0195b4..624a5e08c 100644 --- a/src/resolvers/reactionResolver.test.ts +++ b/src/resolvers/reactionResolver.test.ts @@ -5,6 +5,7 @@ import { graphqlUrl, PROJECT_UPDATE_SEED_DATA, SEED_DATA, + dbIndependentTests, } from '../../test/testUtils'; import { likeProjectQuery, @@ -65,7 +66,13 @@ function likeUnlikeProjectTestCases() { }, ); - beforeEach(async () => { + beforeEach(async function () { + const { title } = this.currentTest?.parent || {}; + + if (title && dbIndependentTests.includes(title)) { + return; + } + firstUserAccessToken = await generateTestAccessToken(USER_DATA.id); projectBefore = await Project.findOne({ where: { id: PROJECT_DATA.id } }); }); @@ -196,7 +203,13 @@ function likeUnlikeProjectUpdateTestCases() { }, ); - beforeEach(async () => { + beforeEach(async function () { + const { title } = this.currentTest?.parent || {}; + + if (title && dbIndependentTests.includes(title)) { + return; + } + firstUserAccessToken = await generateTestAccessToken(USER_DATA.id); projectUpdateBefore = await ProjectUpdate.findOne({ where: { diff --git a/src/resolvers/recurringDonationResolver.test.ts b/src/resolvers/recurringDonationResolver.test.ts index fb263b010..a5c60996a 100644 --- a/src/resolvers/recurringDonationResolver.test.ts +++ b/src/resolvers/recurringDonationResolver.test.ts @@ -19,6 +19,7 @@ import { updateRecurringDonationQuery, updateRecurringDonationQueryById, updateRecurringDonationStatusMutation, + fetchRecurringDonationStatsQuery, } from '../../test/graphqlQueries'; describe( 'createRecurringDonation test cases', @@ -54,6 +55,11 @@ describe( updateRecurringDonationByIdTestCases, ); +describe( + 'getRecurringDonationStatsTestCases test cases', + getRecurringDonationStatsTestCases, +); + function createRecurringDonationTestCases() { it('should create recurringDonation successfully', async () => { const projectOwner = await saveUserDirectlyToDb( @@ -1745,7 +1751,6 @@ function updateRecurringDonationByIdTestCases() { ); }); } - function recurringDonationsByProjectIdTestCases() { it('should sort by the createdAt DESC', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); @@ -2893,3 +2898,235 @@ function updateRecurringDonationStatusTestCases() { ); }); } + +function getRecurringDonationStatsTestCases() { + const lastYear = new Date().getFullYear() - 1; + const beginDate = `${lastYear}-01-01`; + const endDate = `${lastYear}-03-01`; + + it(`should return the correct stats for a given date range (${beginDate} to ${endDate})`, async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + createdAt: new Date(`${lastYear}-01-02`), + }, + }); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(`${lastYear}-01-24`), + }, + }); + + // we are querying from January 1st of last year to the 1st of March of last year + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate, + endDate, + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 0); + assert.equal(stats.totalStreamedUsdValue, 500); + }); + + it(`should return the correct stats for a given date range (${beginDate} -> ${endDate}) and currency`, async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + currency: 'DAI', + createdAt: new Date(`${lastYear}-01-01`), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + currency: 'USDT', + createdAt: new Date(`${lastYear}-02-01`), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate, + endDate, + currency: 'USDT', + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 0); + assert.equal(stats.totalStreamedUsdValue, 600); + }); + + it(`should return the correct stats for a given date range (${beginDate} -> ${endDate}) with correct active count`, async () => { + const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const donor2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor1.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + createdAt: new Date(`${lastYear}-01-01`), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor2.id, + totalUsdStreamed: 100, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + createdAt: new Date(`${lastYear}-02-01`), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor1.id, + totalUsdStreamed: 200, + currency: 'DAI', + createdAt: new Date(`${lastYear}-02-01`), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor2.id, + totalUsdStreamed: 100, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'USDT', + createdAt: new Date(`${lastYear}-02-01`), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate, + endDate, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 2); + assert.equal(stats.totalStreamedUsdValue, 1100); + }); + + it('should return the correct stats for the given date range where beginDate', async () => { + const lastYear15thOfJanuary = new Date(`${lastYear}-01-15T09:00:00`); + + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: lastYear15thOfJanuary, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: lastYear15thOfJanuary, + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: `${lastYear}-01-15T09:00:00`, + endDate: `${lastYear}-01-15T09:00:00`, + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + + assert.equal(stats.activeRecurringDonationsCount, 1); + assert.equal(stats.totalStreamedUsdValue, 500); + }); + + it(`should return empty stats for the given date range where beginDate is same as endDate`, async () => { + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: `${lastYear}-04-01`, + endDate: `${lastYear}-05-01`, + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 0); + assert.equal(stats.totalStreamedUsdValue, 0); + }); + + it('should return an error for the given an empty date range', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: new Date(), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: '', + endDate: '', + }, + }); + + assert.isNotNull(result.data.errors); + }); + + it('should return an error for the given an invalid date range', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: new Date(), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: 'invalid date', + endDate: 'invalid date', + }, + }); + + assert.isNotNull(result.data.errors); + }); +} diff --git a/src/resolvers/recurringDonationResolver.ts b/src/resolvers/recurringDonationResolver.ts index a58f4609e..56c63b5fa 100644 --- a/src/resolvers/recurringDonationResolver.ts +++ b/src/resolvers/recurringDonationResolver.ts @@ -4,6 +4,7 @@ import { ArgsType, Ctx, Field, + Float, InputType, Int, Mutation, @@ -39,6 +40,7 @@ import { import { detectAddressChainType } from '../utils/networks'; import { logger } from '../utils/logger'; import { + getRecurringDonationStatsArgsValidator, updateDonationQueryValidator, validateWithJoiSchema, } from '../utils/validators/graphqlQueryValidators'; @@ -154,6 +156,28 @@ class UserRecurringDonations { totalCount: number; } +@Service() +@ArgsType() +class GetRecurringDonationStatsArgs { + @Field(_type => String) + beginDate: string; + + @Field(_type => String) + endDate: string; + + @Field(_type => String, { nullable: true }) + currency?: string; +} + +@ObjectType() +class RecurringDonationStats { + @Field(_type => Float) + activeRecurringDonationsCount: number; + + @Field(_type => Float) + totalStreamedUsdValue: number; +} + @Resolver(_of => AnchorContractAddress) export class RecurringDonationResolver { @Mutation(_returns => RecurringDonation, { nullable: true }) @@ -630,4 +654,42 @@ export class RecurringDonationResolver { throw e; } } + + @Query(_returns => RecurringDonationStats) + async getRecurringDonationStats( + @Args() { beginDate, endDate, currency }: GetRecurringDonationStatsArgs, + ): Promise { + try { + validateWithJoiSchema( + { beginDate, endDate }, + getRecurringDonationStatsArgsValidator, + ); + + const query = RecurringDonation.createQueryBuilder('recurring_donation') + .select([ + 'COUNT(CASE WHEN recurring_donation.status = :active THEN 1 END)', + 'SUM(recurring_donation.totalUsdStreamed)', + ]) + .setParameter('active', 'active') + .where( + `recurring_donation.createdAt >= :beginDate AND recurring_donation.createdAt <= :endDate`, + { beginDate, endDate }, + ); + + if (currency) { + query.andWhere(`recurring_donation.currency = :currency`, { currency }); + } + + const [result] = await query.getRawMany(); + + return { + activeRecurringDonationsCount: parseInt(result.count), + totalStreamedUsdValue: parseFloat(result.sum) || 0, + }; + } catch (e) { + SentryLogger.captureException(e); + logger.error('getRecurringDonationStats() error ', e); + throw e; + } + } } diff --git a/src/resolvers/socialProfilesResolver.test.ts b/src/resolvers/socialProfilesResolver.test.ts index 02466bb18..0afe05808 100644 --- a/src/resolvers/socialProfilesResolver.test.ts +++ b/src/resolvers/socialProfilesResolver.test.ts @@ -33,7 +33,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -63,7 +63,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -99,7 +99,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -147,7 +147,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -181,7 +181,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -216,7 +216,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -251,7 +251,7 @@ function addNewSocialProfileFormMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -371,7 +371,7 @@ function removeSocialProfileMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -406,7 +406,7 @@ function removeSocialProfileMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = @@ -458,7 +458,7 @@ function removeSocialProfileMutationTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const project = await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, verified: false, }); const projectVerificationForm = diff --git a/src/resolvers/types/project-input.ts b/src/resolvers/types/project-input.ts index 444df5016..251405c37 100644 --- a/src/resolvers/types/project-input.ts +++ b/src/resolvers/types/project-input.ts @@ -32,7 +32,7 @@ class ProjectInput { title: string; @Field({ nullable: true }) - admin?: string; + adminUserId?: number; @Field({ nullable: true }) @MaxLength(PROJECT_DESCRIPTION_MAX_LENGTH, { diff --git a/src/server/adminJs/adminJs-types.ts b/src/server/adminJs/adminJs-types.ts index 254890507..4a13ece68 100644 --- a/src/server/adminJs/adminJs-types.ts +++ b/src/server/adminJs/adminJs-types.ts @@ -52,7 +52,7 @@ export const projectHeaders = [ 'id', 'title', 'slug', - 'admin', + 'adminUserId', 'creationDate', 'updatedAt', 'impactLocation', diff --git a/src/server/adminJs/adminJsPermissions.test.ts b/src/server/adminJs/adminJsPermissions.test.ts index aff23d379..6e30c8d3c 100644 --- a/src/server/adminJs/adminJsPermissions.test.ts +++ b/src/server/adminJs/adminJsPermissions.test.ts @@ -271,7 +271,7 @@ const canAccessAction = ( }; // TODO Should uncomment it after https://github.com/Giveth/impact-graph/issues/1481 ( I commented this to reduce the test execution time) -describe.skip('canAccessUserAction test cases', () => { +describe('AdminJsPermissions', () => { roles.forEach(role => { Object.keys(actionsPerRole[role]).forEach(page => { actions.forEach(action => { diff --git a/src/server/adminJs/tabs/components/ProjectsInQfRound.tsx b/src/server/adminJs/tabs/components/ProjectsInQfRound.tsx index dfec3613f..c5347e8ae 100644 --- a/src/server/adminJs/tabs/components/ProjectsInQfRound.tsx +++ b/src/server/adminJs/tabs/components/ProjectsInQfRound.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { withTheme } from 'styled-components'; import { Section, Label, Link } from '@adminjs/design-system'; diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 508f5eeb1..832c5d930 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -433,7 +433,7 @@ function verifyProjectsTestCases() { const projectVerificationForm = await createProjectVerificationForm({ projectId: project.id, - userId: Number(project.admin), + userId: project.adminUserId, }); projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.VERIFIED; @@ -496,7 +496,7 @@ function verifyProjectsTestCases() { const projectVerificationForm = await createProjectVerificationForm({ projectId: project.id, - userId: Number(project.admin), + userId: project.adminUserId, }); projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.DRAFT; @@ -583,7 +583,7 @@ function verifyProjectsTestCases() { }); const projectVerificationForm = await createProjectVerificationForm({ projectId: project.id, - userId: Number(project.admin), + userId: project.adminUserId, }); projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.VERIFIED; diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index fecb8aed3..176ed5ff2 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -479,7 +479,7 @@ const sendProjectsToGoogleSheet = async ( id: project.id, title: project.title, slug: project.slug, - admin: project.admin, + admin: project.adminUserId, creationDate: project.creationDate, updatedAt: project.updatedAt, impactLocation: project.impactLocation || '', @@ -951,7 +951,7 @@ export const projectsTab = { NOTIFICATIONS_EVENT_NAMES.PROJECT_NOT_REVIEWED, ); } - if (request?.payload?.admin !== project?.admin) { + if (Number(request?.payload?.admin) !== project?.adminUserId) { request.payload.adminChanged = true; } @@ -972,7 +972,7 @@ export const projectsTab = { if (project) { if (request?.record?.params?.adminChanged) { const adminUser = await User.findOne({ - where: { id: Number(project.admin) }, + where: { id: project.adminUserId }, }); project.adminUser = adminUser!; await project.save(); diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index b719a1135..7ac744ff8 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -33,9 +33,19 @@ import { pinFile } from '../../../middleware/pinataUtils'; export const refreshMaterializedViews = async ( response, ): Promise> => { + const projectIds = await getRelatedProjectsOfQfRound( + response.record.params.id, + ); await refreshProjectEstimatedMatchingView(); await refreshProjectDonationSummaryView(); await refreshProjectActualMatchingView(); + response.record = { + ...response.record, + params: { + ...response.record.params, + projectIdsList: projectIds.map(project => project.id).join(','), + }, + }; return response; }; @@ -119,7 +129,7 @@ export const qfRoundTab = { resource: QfRound, options: { properties: { - addProjectIdsList: { + projectIdsList: { type: 'textarea', // projectIds separated By comma isVisible: { @@ -308,10 +318,10 @@ export const qfRoundTab = { request.payload.isActive = qfRound.isActive; } else if ( qfRound.isActive && - request?.payload?.addProjectIdsList?.split(',')?.length > 0 + request?.payload?.projectIdsList?.split(',')?.length > 0 ) { await relateManyProjectsToQfRound({ - projectIds: request.payload.addProjectIdsList.split(','), + projectIds: request.payload.projectIdsList.split(','), qfRound, add: true, }); diff --git a/src/services/chains/evm/draftDonationService.ts b/src/services/chains/evm/draftDonationService.ts index dfbdeb1e9..4007e369a 100644 --- a/src/services/chains/evm/draftDonationService.ts +++ b/src/services/chains/evm/draftDonationService.ts @@ -319,8 +319,9 @@ export async function runDraftDonationMatchWorker() { () => spawn(new Worker('./../../../workers/draftDonationMatchWorker')), { name: 'draftDonationMatchWorker', - concurrency: 4, - size: 2, + concurrency: 1, + maxQueuedJobs: 1, + size: 1, }, ); } diff --git a/src/services/donationService.ts b/src/services/donationService.ts index f88aed761..d9d4a7c66 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -198,9 +198,6 @@ export const updateTotalDonationsOfProject = async ( `, [projectId], ); - - // we want to update the project donation summary view after updating the total donations - refreshProjectDonationSummaryView(); } catch (e) { logger.error('updateTotalDonationsOfAProject error', e); } diff --git a/src/services/googleSheets.ts b/src/services/googleSheets.ts index 64ef44457..abb340cd7 100644 --- a/src/services/googleSheets.ts +++ b/src/services/googleSheets.ts @@ -10,7 +10,7 @@ interface ProjectExport { id: number; title: string; slug?: string | null; - admin?: string | null; + adminUserId?: number | null; creationDate: Date; updatedAt: Date; impactLocation?: string | null; diff --git a/src/services/onramper/donationService.ts b/src/services/onramper/donationService.ts index 443011b72..be187b74b 100644 --- a/src/services/onramper/donationService.ts +++ b/src/services/onramper/donationService.ts @@ -147,7 +147,7 @@ export const createFiatDonationFromOnramper = async ( // After updating price we update totalDonations await updateTotalDonationsOfProject(project.id); - await updateUserTotalReceived(Number(project.admin)); + await updateUserTotalReceived(project.adminUserId); } catch (e) { SentryLogger.captureException(e); logger.error('createFiatDonationFromOnramper() error', e); diff --git a/src/services/poignArt/syncPoignArtDonationCronJob.ts b/src/services/poignArt/syncPoignArtDonationCronJob.ts index 63557813f..ae3df3893 100644 --- a/src/services/poignArt/syncPoignArtDonationCronJob.ts +++ b/src/services/poignArt/syncPoignArtDonationCronJob.ts @@ -63,7 +63,7 @@ const importPoignArtDonations = async () => { for (const poignArtWithdrawal of poignArtWithdrawals) { await createPoignArtDonationInDb(poignArtWithdrawal, unchainProject); } - await updateUserTotalReceived(Number(unchainProject.admin)); + await updateUserTotalReceived(unchainProject.adminUserId); await updateTotalDonationsOfProject(unchainProject.id); } catch (e) { logger.error('importPoignArtDonations() error', e); diff --git a/src/services/projectUpdatesService.test.ts b/src/services/projectUpdatesService.test.ts index 4a1ff8ab4..e7e24c353 100644 --- a/src/services/projectUpdatesService.test.ts +++ b/src/services/projectUpdatesService.test.ts @@ -31,7 +31,7 @@ function updateTotalProjectUpdatesOfAProjectTestCases() { INSERT INTO public.project_update ( "userId","projectId",content,title,"createdAt","isMain" ) VALUES ( - ${Number(project.admin)}, ${project.id}, '', '', '${ + ${project.adminUserId}, ${project.id}, '', '', '${ new Date().toISOString().split('T')[0] }', false )`); diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 537fc55e8..284174b6c 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -37,6 +37,7 @@ import { updateUserTotalDonated, updateUserTotalReceived } from './userService'; import config from '../config'; import { User } from '../entities/user'; import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; +import { relatedActiveQfRoundForProject } from './qfRoundService'; // Initially it will only be monthly data export const priceDisplay = 'month'; @@ -212,17 +213,16 @@ export const createRelatedDonationsToStream = async ( amount: donation.amount, }); - // TODO - uncomment this when QF is enabled - // const activeQfRoundForProject = await relatedActiveQfRoundForProject( - // project.id, - // ); - // - // if ( - // activeQfRoundForProject && - // activeQfRoundForProject.isEligibleNetwork(networkId) - // ) { - // donation.qfRound = activeQfRoundForProject; - // } + const activeQfRoundForProject = await relatedActiveQfRoundForProject( + project.id, + ); + + if ( + activeQfRoundForProject && + activeQfRoundForProject.isEligibleNetwork(networkId) + ) { + donation.qfRound = activeQfRoundForProject; + } const { givbackFactor, projectRank, bottomRankInRound, powerRound } = await calculateGivbackFactor(project.id); diff --git a/src/services/the-giving-blocks/syncProjectsCronJob.ts b/src/services/the-giving-blocks/syncProjectsCronJob.ts index e591ba81f..3ae30789b 100644 --- a/src/services/the-giving-blocks/syncProjectsCronJob.ts +++ b/src/services/the-giving-blocks/syncProjectsCronJob.ts @@ -134,7 +134,6 @@ const createGivingProject = async (data: { image: givingBlockProject.logo, slugHistory: [], givingBlocksId: String(givingBlockProject.id), - admin: adminId, statusId: activeStatus?.id, qualityScore, totalDonations: 0, diff --git a/src/services/userService.test.ts b/src/services/userService.test.ts index 63ee3d723..85e386344 100644 --- a/src/services/userService.test.ts +++ b/src/services/userService.test.ts @@ -82,7 +82,7 @@ function updateUserTotalReceivedTestCases() { }).save(); await saveProjectDirectlyToDb({ ...createProjectData(), - admin: String(user.id), + adminUserId: user.id, organizationLabel: ORGANIZATION_LABELS.GIVING_BLOCK, totalDonations: 180, }); diff --git a/src/types/projectSocialMediaType.ts b/src/types/projectSocialMediaType.ts index 38ede741c..af901a2bf 100644 --- a/src/types/projectSocialMediaType.ts +++ b/src/types/projectSocialMediaType.ts @@ -11,6 +11,7 @@ export enum ProjectSocialMediaType { FARCASTER = 'FARCASTER', LENS = 'LENS', WEBSITE = 'WEBSITE', + TELEGRAM = 'TELEGRAM', } registerEnumType(ProjectSocialMediaType, { diff --git a/src/user/MeResolver.ts b/src/user/MeResolver.ts index ab214002a..c31d2c16f 100644 --- a/src/user/MeResolver.ts +++ b/src/user/MeResolver.ts @@ -50,7 +50,7 @@ export class MeResolver { const user = await getLoggedInUser(ctx); const projects = this.projectRepository.find({ - where: { admin: user.id?.toString() }, + where: { adminUserId: user.id }, // relations: ['status', 'donations', 'reactions'], relations: ['status', 'reactions'], order: { diff --git a/src/utils/validators/graphqlQueryValidators.ts b/src/utils/validators/graphqlQueryValidators.ts index 95da5af03..ea232be2b 100644 --- a/src/utils/validators/graphqlQueryValidators.ts +++ b/src/utils/validators/graphqlQueryValidators.ts @@ -166,6 +166,17 @@ export const updateDonationQueryValidator = Joi.object({ status: Joi.string().valid(DONATION_STATUS.VERIFIED, DONATION_STATUS.FAILED), }); +export const getRecurringDonationStatsArgsValidator = Joi.object({ + beginDate: Joi.string().pattern(resourcePerDateRegex).messages({ + 'string.base': errorMessages.INVALID_FROM_DATE, + 'string.pattern.base': errorMessages.INVALID_DATE_FORMAT, + }), + endDate: Joi.string().pattern(resourcePerDateRegex).messages({ + 'string.base': errorMessages.INVALID_FROM_DATE, + 'string.pattern.base': errorMessages.INVALID_DATE_FORMAT, + }), +}); + export const createProjectVerificationRequestValidator = Joi.object({ slug: Joi.string().required(), }); diff --git a/src/utils/validators/projectValidator.ts b/src/utils/validators/projectValidator.ts index 9352b3027..e1027a2b4 100644 --- a/src/utils/validators/projectValidator.ts +++ b/src/utils/validators/projectValidator.ts @@ -160,7 +160,7 @@ async function isSmartContract(provider, projectWalletAddress) { export const canUserVisitProject = ( project?: Project | null, - userId?: string, + userId?: number, ) => { if (!project) { throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); @@ -169,7 +169,7 @@ export const canUserVisitProject = ( (project.status.id === ProjStatus.drafted || project.status.id === ProjStatus.cancelled) && // If project is draft or cancelled, just owner can view it - project.admin !== userId + project.adminUserId !== userId ) { throw new Error( i18n.__( diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 3e9146d6a..094a908d3 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -115,7 +115,7 @@ export const createProjectQuery = ` title description descriptionSummary - admin + adminUserId image impactLocation slug @@ -166,7 +166,7 @@ export const updateProjectQuery = ` verified slugHistory creationDate - admin + adminUserId walletAddress impactLocation categories { @@ -203,7 +203,7 @@ export const addRecipientAddressToProjectQuery = ` slugHistory creationDate updatedAt - admin + adminUserId walletAddress impactLocation categories { @@ -672,6 +672,10 @@ export const fetchAllDonationsQuery = ` anonymous valueUsd amount + recurringDonation{ + id + txHash + } user { id walletAddress @@ -683,7 +687,7 @@ export const fetchAllDonationsQuery = ` reviewStatus verified slug - admin + adminUserId title categories { name @@ -777,7 +781,7 @@ export const fetchFeaturedProjects = ` slug creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -873,7 +877,7 @@ export const fetchMultiFilterAllProjectsQuery = ` descriptionSummary creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -978,6 +982,12 @@ export const qfRoundStatsQuery = ` uniqueDonors allDonationsUsdValue matchingPool + qfRound{ + allocatedFund + allocatedFundUSD + allocatedFundUSDPreferred + allocatedTokenSymbol + } } } `; @@ -1020,7 +1030,7 @@ export const fetchProjectBySlugQuery = ` slug creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -1077,53 +1087,6 @@ export const fetchProjectBySlugQuery = ` name isActive } - projectVerificationForm { - status - id - isTermAndConditionsAccepted - emailConfirmationTokenExpiredAt - email - emailConfirmationToken - emailConfirmationSent - emailConfirmationSentAt - emailConfirmedAt - emailConfirmed - projectRegistry { - organizationDescription - isNonProfitOrganization - organizationCountry - organizationWebsite - attachments - organizationName - } - personalInfo { - email - walletAddress - fullName - } - projectContacts { - name - url - } - milestones { - mission - foundationDate - achievedMilestones - achievedMilestonesProofs - problem - plans - impact - } - managingFunds { - description - relatedAddresses { - address - networkId - chainType - title - } - } - } status { id symbol @@ -1157,6 +1120,7 @@ export const fetchProjectBySlugQuery = ` email firstName walletAddress + email } totalReactions totalDonations @@ -1194,7 +1158,7 @@ export const fetchSimilarProjectsBySlugQuery = ` slug creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -1255,7 +1219,7 @@ export const fetchLikedProjectsQuery = ` slug creationDate updatedAt - admin + adminUserId description walletAddress impactLocation @@ -1502,7 +1466,7 @@ export const projectsBySlugsQuery = ` image slug creationDate - admin + adminUserId walletAddress impactLocation listed @@ -1550,7 +1514,7 @@ export const projectsByUserIdQuery = ` image slug creationDate - admin + adminUserId walletAddress impactLocation listed @@ -1654,7 +1618,7 @@ export const projectByIdQuery = ` reviewStatus description, walletAddress - admin + adminUserId categories{ name } @@ -2300,6 +2264,8 @@ export const fetchQFArchivedRounds = ` slug isActive allocatedFund + allocatedFundUSD + allocatedTokenSymbol eligibleNetworks beginDate endDate @@ -2422,3 +2388,20 @@ export const updateRecurringDonationQuery = ` } } `; + +export const fetchRecurringDonationStatsQuery = ` + query ( + $beginDate: String! + $endDate: String! + $currency: String + ) { + getRecurringDonationStats( + beginDate: $beginDate + endDate: $endDate + currency: $currency + ) { + totalStreamedUsdValue, + activeRecurringDonationsCount, + } + } +`; diff --git a/test/testUtils.ts b/test/testUtils.ts index 58ba6b75e..bff29abb5 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -123,7 +123,7 @@ export interface CreateProjectData { title: string; slug: string; description: string; - admin: string; + adminUserId: number; // relatedAddresses: RelatedAddressInputType[]; walletAddress: string; categories: string[]; @@ -233,7 +233,7 @@ export const saveProjectDirectlyToDb = async ( owner || ((await User.findOne({ where: { - id: Number(projectData.admin), + id: projectData.adminUserId, }, })) as User); const categoriesPromise = Promise.all( @@ -253,9 +253,8 @@ export const saveProjectDirectlyToDb = async ( status, organization, categories, - users: [user], adminUser: user, - admin: String(user.id), + adminUserId: user.id, }).save(); if (projectData.networkId) { @@ -315,7 +314,7 @@ export const createProjectData = (): CreateProjectData => { updatedAt: new Date(), slug: title, // firstUser's id - admin: '1', + adminUserId: 1, qualityScore: 30, // just need the initial value to be different than 0 totalDonations: 10, @@ -398,7 +397,7 @@ export const SEED_DATA = { slug: 'first-project', description: 'first description', id: 1, - admin: '1', + adminUserId: 1, }, SECOND_PROJECT: { ...createProjectData(), @@ -406,7 +405,7 @@ export const SEED_DATA = { slug: 'second-project', description: 'second description', id: 2, - admin: '2', + adminUserId: 2, }, TRANSAK_PROJECT: { ...createProjectData(), @@ -414,7 +413,7 @@ export const SEED_DATA = { slug: 'transak-project', description: 'transak description', id: 3, - admin: '3', + adminUserId: 3, }, FOURTH_PROJECT: { ...createProjectData(), @@ -422,7 +421,7 @@ export const SEED_DATA = { slug: 'forth-project', description: 'forth description', id: 4, - admin: '1', + adminUserId: 1, }, FIFTH_PROJECT: { ...createProjectData(), @@ -430,7 +429,7 @@ export const SEED_DATA = { slug: 'fifth-project', description: 'forth description', id: 5, - admin: '1', + adminUserId: 1, }, SIXTH_PROJECT: { ...createProjectData(), @@ -438,7 +437,7 @@ export const SEED_DATA = { slug: 'sixth-project', description: 'forth description', id: 6, - admin: '1', + adminUserId: 1, }, NON_VERIFIED_PROJECT: { ...createProjectData(), @@ -446,8 +445,8 @@ export const SEED_DATA = { slug: 'non-verified-project', description: 'non verified description', id: 7, - admin: '1', verified: false, + adminUserId: 1, }, MAIN_CATEGORIES: ['drink', 'food', 'nonProfit'], NON_PROFIT_SUB_CATEGORIES: [CATEGORY_NAMES.registeredNonProfits], @@ -1958,6 +1957,7 @@ export const saveRecurringDonationDirectlyToDb = async (params?: { donorId, projectId, anchorContractAddressId, + createdAt: params?.donationData?.createdAt || moment(), }).save(); }; @@ -2011,3 +2011,6 @@ export function generateRandomSolanaTxHash() { const length = Math.floor(Math.random() * 3) + 86; return generateRandomAlphanumeric(length); } + +// list of test cases titles that doesn't require DB interaction +export const dbIndependentTests = ['AdminJsPermissions'];