From 1cc3b5224172b220abfce2a999780149c10ad22f Mon Sep 17 00:00:00 2001 From: Reshzera Date: Mon, 5 Aug 2024 17:36:16 -0300 Subject: [PATCH] feat: add endpoint to delete draft project --- src/repositories/projectRepository.test.ts | 110 +++++++++++++++++++- src/repositories/projectRepository.ts | 68 +++++++++++++ src/resolvers/projectResolver.test.ts | 113 +++++++++++++++++++++ src/resolvers/projectResolver.ts | 38 +++++++ src/utils/errorMessages.ts | 2 + src/utils/locales/en.json | 1 + src/utils/locales/es.json | 1 + test/graphqlQueries.ts | 6 ++ 8 files changed, 337 insertions(+), 2 deletions(-) diff --git a/src/repositories/projectRepository.test.ts b/src/repositories/projectRepository.test.ts index cec4c5a14..78c5ab1d2 100644 --- a/src/repositories/projectRepository.test.ts +++ b/src/repositories/projectRepository.test.ts @@ -8,18 +8,25 @@ import { findProjectsByIdArray, findProjectsBySlugArray, projectsWithoutUpdateAfterTimeFrame, + removeProjectAndRelatedEntities, updateProjectWithVerificationForm, verifyMultipleProjects, verifyProject, } from './projectRepository'; import { + createDonationData, createProjectData, generateRandomEtheriumAddress, + saveAnchorContractDirectlyToDb, + saveDonationDirectlyToDb, saveProjectDirectlyToDb, saveUserDirectlyToDb, } from '../../test/testUtils'; import { createProjectVerificationForm } from './projectVerificationRepository'; -import { PROJECT_VERIFICATION_STATUSES } from '../entities/projectVerificationForm'; +import { + PROJECT_VERIFICATION_STATUSES, + ProjectVerificationForm, +} from '../entities/projectVerificationForm'; import { NETWORK_IDS } from '../provider'; import { setPowerRound } from './powerRoundRepository'; import { refreshProjectPowerView } from './projectPowerViewRepository'; @@ -27,7 +34,7 @@ import { insertSinglePowerBoosting, takePowerBoostingSnapshot, } from './powerBoostingRepository'; -import { Project } from '../entities/project'; +import { Project, ProjectUpdate } from '../entities/project'; import { User } from '../entities/user'; import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; @@ -37,6 +44,15 @@ import { getHtmlTextSummary } from '../utils/utils'; import { generateRandomString } from '../utils/utils'; import { addOrUpdatePowerSnapshotBalances } from './powerBalanceSnapshotRepository'; import { findPowerSnapshots } from './powerSnapshotRepository'; +import { AnchorContractAddress } from '../entities/anchorContractAddress'; +import { Donation } from '../entities/donation'; +import { FeaturedUpdate } from '../entities/featuredUpdate'; +import { ProjectAddress } from '../entities/projectAddress'; +import { ProjectSocialMedia } from '../entities/projectSocialMedia'; +import { ProjectStatusHistory } from '../entities/projectStatusHistory'; +import { Reaction } from '../entities/reaction'; +import { SocialProfile } from '../entities/socialProfile'; +import { ProjectSocialMediaType } from '../types/projectSocialMediaType'; describe( 'findProjectByWalletAddress test cases', @@ -71,6 +87,11 @@ describe( findProjectsBySlugArrayTestCases, ); +describe( + 'removeProjectAndRelatedEntities test cases', + removeProjectAndRelatedEntitiesTestCase, +); + function projectsWithoutUpdateAfterTimeFrameTestCases() { it('should return projects created a long time ago', async () => { const superExpiredProject = await saveProjectDirectlyToDb({ @@ -498,3 +519,88 @@ function updateDescriptionSummaryTestCases() { ); }); } + +function removeProjectAndRelatedEntitiesTestCase() { + it('should remove project and related entities', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const projectData = createProjectData(); + projectData.adminUserId = user.id; + //It creates a project, projectUpdate, and ProjectAddress + const project = await saveProjectDirectlyToDb(projectData); + + await Promise.all([ + saveDonationDirectlyToDb( + { + ...createDonationData(), + }, + user.id, + project.id, + ), + saveAnchorContractDirectlyToDb({ + creatorId: user.id, + projectId: project.id, + }), + Reaction.create({ + projectId: project.id, + userId: user.id, + reaction: '', + }).save(), + ProjectSocialMedia.create({ + projectId: project.id, + type: ProjectSocialMediaType.FACEBOOK, + link: 'https://facebook.com', + }).save(), + ProjectStatusHistory.create({ + projectId: project.id, + createdAt: new Date(), + }).save(), + ProjectVerificationForm.create({ projectId: project.id }).save(), + FeaturedUpdate.create({ projectId: project.id }).save(), + SocialProfile.create({ projectId: project.id }).save(), + ]); + + const relatedEntitiesBefore = await Promise.all([ + Donation.findOne({ where: { projectId: project.id } }), + Reaction.findOne({ where: { projectId: project.id } }), + ProjectAddress.findOne({ where: { projectId: project.id } }), + ProjectSocialMedia.findOne({ where: { projectId: project.id } }), + AnchorContractAddress.findOne({ where: { projectId: project.id } }), + ProjectStatusHistory.findOne({ where: { projectId: project.id } }), + ProjectVerificationForm.findOne({ + where: { projectId: project.id }, + }), + FeaturedUpdate.findOne({ where: { projectId: project.id } }), + SocialProfile.findOne({ where: { projectId: project.id } }), + ProjectUpdate.findOne({ where: { projectId: project.id } }), + ]); + + relatedEntitiesBefore.forEach(entity => { + assert.isNotNull(entity); + }); + + await removeProjectAndRelatedEntities(project.id); + + const relatedEntities = await Promise.all([ + Donation.findOne({ where: { projectId: project.id } }), + Reaction.findOne({ where: { projectId: project.id } }), + ProjectAddress.findOne({ where: { projectId: project.id } }), + ProjectSocialMedia.findOne({ where: { projectId: project.id } }), + AnchorContractAddress.findOne({ where: { projectId: project.id } }), + ProjectStatusHistory.findOne({ where: { projectId: project.id } }), + ProjectVerificationForm.findOne({ + where: { projectId: project.id }, + }), + FeaturedUpdate.findOne({ where: { projectId: project.id } }), + SocialProfile.findOne({ where: { projectId: project.id } }), + ProjectUpdate.findOne({ where: { projectId: project.id } }), + ]); + + const fetchedProject = await Project.findOne({ where: { id: project.id } }); + + assert.isNull(fetchedProject); + + relatedEntities.forEach(entity => { + assert.isNull(entity); + }); + }); +} diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index a00fedd7c..b160c5d81 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -2,6 +2,7 @@ import { UpdateResult } from 'typeorm'; import { FilterField, Project, + ProjectUpdate, ProjStatus, ReviewStatus, RevokeSteps, @@ -14,6 +15,13 @@ import { publicSelectionFields } from '../entities/user'; import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver'; import { OrderDirection, ProjectResolver } from '../resolvers/projectResolver'; import { getAppropriateNetworkId } from '../services/chains'; +import { AnchorContractAddress } from '../entities/anchorContractAddress'; +import { Donation } from '../entities/donation'; +import { FeaturedUpdate } from '../entities/featuredUpdate'; +import { ProjectSocialMedia } from '../entities/projectSocialMedia'; +import { ProjectStatusHistory } from '../entities/projectStatusHistory'; +import { Reaction } from '../entities/reaction'; +import { SocialProfile } from '../entities/socialProfile'; export const findProjectById = (projectId: number): Promise => { // return Project.findOne({ id: projectId }); @@ -533,3 +541,63 @@ export const findProjectsBySlugArray = async ( }); return projects; }; + +export const removeProjectAndRelatedEntities = async ( + projectId: number, +): Promise => { + // Delete related entities + await Donation.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await Reaction.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await ProjectAddress.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await ProjectSocialMedia.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await AnchorContractAddress.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await ProjectStatusHistory.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await ProjectVerificationForm.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await FeaturedUpdate.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await SocialProfile.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await ProjectUpdate.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + + await Project.createQueryBuilder() + .delete() + .where('id = :id', { id: projectId }) + .execute(); +}; diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index bc131740a..30f9c39a7 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -20,6 +20,7 @@ import { addRecipientAddressToProjectQuery, createProjectQuery, deactivateProjectQuery, + deleteDraftProjectQuery, deleteProjectUpdateQuery, editProjectUpdateQuery, fetchFeaturedProjects, @@ -174,6 +175,8 @@ describe( describe('projectsPerDate() test cases --->', projectsPerDateTestCases); +describe('deleteDraftProject test cases --->', deleteDraftProjectTestCases); + function projectsPerDateTestCases() { it('should projects created in a time range', async () => { await saveProjectDirectlyToDb({ @@ -5668,3 +5671,113 @@ function deleteProjectUpdateTestCases() { ); }); } + +function deleteDraftProjectTestCases() { + it('should delete draft project successfully ', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const accessToken = await generateTestAccessToken(user.id); + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + adminUserId: user.id, + statusId: ProjStatus.drafted, + }); + + const result = await axios.post( + graphqlUrl, + { + query: deleteDraftProjectQuery, + variables: { + projectId: project.id, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal(result.data.data.deleteDraftProject, true); + }); + it('should can not delete draft project because of ownerShip ', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + adminUserId: user.id, + statusId: ProjStatus.drafted, + }); + const accessTokenUser1 = await generateTestAccessToken(user1.id); + + // Add projectUpdate with accessToken user1 + const result = await axios.post( + graphqlUrl, + { + query: deleteDraftProjectQuery, + variables: { + projectId: project.id, + }, + }, + { + headers: { + Authorization: `Bearer ${accessTokenUser1}`, + }, + }, + ); + assert.equal( + result.data.errors[0].message, + errorMessages.YOU_ARE_NOT_THE_OWNER_OF_PROJECT, + ); + }); + it('should can not delete draft project because of not found project ', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const accessToken = await generateTestAccessToken(user.id); + const projectCount = await Project.count(); + const result = await axios.post( + graphqlUrl, + { + query: deleteDraftProjectQuery, + variables: { + projectId: Number(projectCount + 10), + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.errors[0].message, + errorMessages.PROJECT_NOT_FOUND, + ); + }); + + it('should can not delete draft project because status ', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const accessToken = await generateTestAccessToken(user.id); + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + adminUserId: user.id, + statusId: ProjStatus.active, + }); + + const result = await axios.post( + graphqlUrl, + { + query: deleteDraftProjectQuery, + variables: { + projectId: project.id, + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + assert.equal( + result.data.errors[0].message, + errorMessages.ONLY_DRAFTED_PROJECTS_CAN_BE_DELETED, + ); + }); +} diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 3da3d5d14..dd5001bc2 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -81,6 +81,7 @@ import { filterProjectsQuery, findProjectById, findProjectIdBySlug, + removeProjectAndRelatedEntities, totalProjectsPerDate, totalProjectsPerDateByMonthAndYear, } from '../repositories/projectRepository'; @@ -2197,4 +2198,41 @@ export class ProjectResolver { throw error; } } + + @Mutation(_returns => Boolean) + async deleteDraftProject( + @Arg('projectId') projectId: number, + @Ctx() ctx: ApolloContext, + ): Promise { + const user = await getLoggedInUser(ctx); + const project = await findProjectById(projectId); + + if (!project) { + throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); + } + + if (project.adminUserId !== user.id) { + throw new Error( + i18n.__(translationErrorMessagesKeys.YOU_ARE_NOT_THE_OWNER_OF_PROJECT), + ); + } + + if (project.statusId !== ProjStatus.drafted) { + throw new Error( + i18n.__( + translationErrorMessagesKeys.ONLY_DRAFTED_PROJECTS_CAN_BE_DELETED, + ), + ); + } + + try { + await removeProjectAndRelatedEntities(project.id); + } catch (error) { + logger.error('projectResolver.deleteDraftProject() error', error); + SentryLogger.captureException(error); + throw error; + } + + return true; + } } diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index 31e835498..6d96c82e6 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -29,6 +29,7 @@ export const errorMessages = { CHAINVINE_REFERRER_NOT_FOUND: 'Chainvine referrer not found', ONRAMPER_SIGNATURE_INVALID: 'Onramper signature invalid', ONRAMPER_SIGNATURE_MISSING: 'Onramper signature missing', + ONLY_DRAFTED_PROJECTS_CAN_BE_DELETED: 'Only drafted projects can be deleted', UPLOAD_FAILED: 'Upload file failed', SPECIFY_GIV_POWER_ADAPTER: 'Specify givPower adapter', CHANGE_API_INVALID_TITLE_OR_EIN: @@ -212,6 +213,7 @@ export const translationErrorMessagesKeys = { FIAT_DONATION_ALREADY_EXISTS: 'FIAT_DONATION_ALREADY_EXISTS', ONRAMPER_SIGNATURE_INVALID: 'ONRAMPER_SIGNATURE_INVALID', ONRAMPER_SIGNATURE_MISSING: 'ONRAMPER_SIGNATURE_MISSING', + ONLY_DRAFTED_PROJECTS_CAN_BE_DELETED: 'ONLY_DRAFTED_PROJECTS_CAN_BE_DELETED', UPLOAD_FAILED: 'UPLOAD_FAILED', SPECIFY_GIV_POWER_ADAPTER: 'SPECIFY_GIV_POWER_ADAPTER', CHANGE_API_INVALID_TITLE_OR_EIN: 'SPECIFY_GIV_POWER_ADAPTER', diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 743b7ecbc..a0d45342a 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -5,6 +5,7 @@ "FIAT_DONATION_ALREADY_EXISTS": "Fiat donation already exists", "ONRAMPER_SIGNATURE_INVALID": "Request payload or signature is invalid", "ONRAMPER_SIGNATURE_MISSING": "Request headers does not contain signature", + "ONLY_DRAFTED_PROJECTS_CAN_BE_DELETED": "Only drafted projects can be deleted", "UPLOAD_FAILED": "Upload file failed", "SPECIFY_GIV_POWER_ADAPTER": "Specify givPower adapter", "CHANGE_API_INVALID_TITLE_OR_EIN": "ChangeAPI title or EIN not found or invalid", diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index 1292916d6..9ec9d8c09 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -5,6 +5,7 @@ "FIAT_DONATION_ALREADY_EXISTS": "La donación Fiat ya existe", "ONRAMPER_SIGNATURE_INVALID": "El cuerpo o firma son invalidos", "ONRAMPER_SIGNATURE_MISSING": "El encabezado no continue la firma", + "ONLY_DRAFTED_PROJECTS_CAN_BE_DELETED": "Solo los proyectos en borrador pueden ser eliminados", "UPLOAD_FAILED": "No fue posible subir el archivo", "SPECIFY_GIV_POWER_ADAPTER": "Especificar adaptador givPower", "CHANGE_API_INVALID_TITLE_OR_EIN": "ChangeAPI título de API o EIN no encontrado o no válido", diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index e46d0dcfb..2c918de63 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -2452,3 +2452,9 @@ export const fetchDonationMetricsQuery = ` } } `; + +export const deleteDraftProjectQuery = ` + mutation ($projectId: Float!) { + deleteDraftProject(projectId: $projectId) + } +`;