Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend for campaign reports #378

Closed
wants to merge 12 commits into from
Closed

Conversation

batebobo
Copy link
Contributor

@batebobo batebobo commented Dec 2, 2022

Summary

Implements #368

Created CRUD endpoints for campaign reports. They are part of a separate module called campaign-report, but the controller is under the campaign base route.

New entities:

  • CampaignReport
  • CampaignReportFile

Both entities support soft delete and have an owner (uploader).

Screenshot of how the new routes look like:

Screenshot 2022-12-02 at 4 35 58 PM

Todo

  • Endpoint for downloading files from reports
  • Tests for update service
  • Tests for update controller
  • Tests for delete service
  • Tests for delete controller
  • Some manual testing of the whole process
  • Manual test of s3 upload/download
  • Squash migrations

Copy link
Member

@kachar kachar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@batebobo Whooooah, incredible increment 🚀 🌔

Kudos for the time spent on this!

I've noticed some incompatibilities with our current workflow regarding the storage of currency amounts. Please check them out and let's discuss them.

schema.prisma Outdated
Comment on lines 536 to 538
totalFunds Float? @map("total_funds")
fundsForPeriod Float? @map("funds_for_period")
spentFundsForPeriod Float? @map("spent_funds_for_period")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@batebobo Normally we use only Integer types for storing the amounts, so I would suggest that we keep that practice here as well to be consistent with the other use cases.

Stripe also provides the amounts in the smallest currency value.

Since the columns are optional we might add a default value of 0 so we always have a value.

Suggested change
totalFunds Float? @map("total_funds")
fundsForPeriod Float? @map("funds_for_period")
spentFundsForPeriod Float? @map("spent_funds_for_period")
totalFunds Int? @map("total_funds") @default(0)
fundsForPeriod Int? @map("funds_for_period") @default(0)
spentFundsForPeriod Int? @map("spent_funds_for_period") @default(0)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, makes sense, thanks :) I'll update the schema ✌️

Dockerfile.migrations Outdated Show resolved Hide resolved
CREATE TYPE "campaign_report_file_type" AS ENUM ('photo', 'document');

-- CreateTable
CREATE TABLE "campaign_reports" (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating new tables breaks our database backup job currently due to permission changes. Please whoever merges this PR, ping me in a private message on Discord when it's merged. I will need to adjust the permissions before this is shipped to prod

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for letting us know! Is there I can do on this PR to avoid breaking the backup job? Can you also please post a reference to this job, so that I can understand better what it does and why creating tables is breaking it 🙏

}

private async softDeleteFile(fileId: string): Promise<CampaignReportFile> {
return this.prisma.campaignReportFile.update({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when do we actually delete the files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't for the moment as per this reply from @igoychev. We could implement the actual deletion in several different ways, but I'd propose to leave this for a future task if it's not absolutely required for the initial version of the feature.

@kachar
Copy link
Member

kachar commented Dec 4, 2022

@imilchev The PR check for scanning k8s manifests with mondo showed an error that you might be interested in:

Target:     K8s Manifest dev-manifests
✕ Errors:   rpc error: code = InvalidArgument desc = asset does not match any of the activated policies

https://github.com/podkrepi-bg/api/actions/runs/3608695854/jobs/6081448709

Copy link
Contributor

@igoychev igoychev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great start and happy to see this code starts to take the shape!
I added some comments to help you with Prisma and naming conventions

@@ -35,7 +35,7 @@ export class CampaignFileController {
) {}

@Post(':campaign_id')
@UseInterceptors(FilesInterceptor('file', 10, { limits: { fileSize: 20485760 } })) //limit uploaded files to 5 at once and 10MB each
@UseInterceptors(FilesInterceptor('file', 10, { limits: { fileSize: 1024 * 1024 * 10 } }))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

Comment on lines 52 to 56
const campaignReports = await this.campaignReportService.getReports(campaignId)

if (!campaignReports.map((report) => report.id).includes(reportId)) {
throw new NotFoundException('The given report is not part of the selected campaign')
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like it will be more optimal to pass the reportId as optional parameter of the function campaignReportService.getReports(campaignId), so that it returns only the required report from the database.

if you did this because of the prisma - it has the nice feature that if in the where clause you pass an optional parameter with missing value it returns all, so no complex logic required there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getReports and getReport have different return types. In getReports we want the basic information about a report without fetching any of the relations. In getReport we'd like a more complicated fetch. Does that clarify the need for the two service functions?

Comment on lines 82 to 89
const fileIsInReport = [
...(report?.photos ?? []).map(photo => photo.id),
...(report?.documents ?? []).map(document => document.id)]
.includes(fileId)

if (!fileIsInReport) {
throw new NotFoundException('The given file is not part of the report')
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's give this check to the database too

Comment on lines 184 to 188
const campaignReports = await this.campaignReportService.getReports(campaignId)

if (!campaignReports.map((report) => report.id).includes(reportId)) {
throw new NotFoundException('The given report is not part of the selected campaign')
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be more optimal instead of getting the reports to add a where clause in the database query

Comment on lines 197 to 202
const campaign = await this.campaignService.getCampaignByIdWithPersonIds(campaignId)
const userCanUploadReport = [
campaign?.beneficiary.person?.keycloakId,
campaign?.organizer?.person.keycloakId,
campaign?.coordinator.person.keycloakId,
].includes(user.sub)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more optimal will be to check in db again, see how it is done in getUserCampaigns(). It can be extended with an optional campaignId parameter and the query becomes:

this.prisma.campaign.findMany({
where: {
id: campaignId,
OR: [
{ beneficiary: { person: { keycloakId } } },
{ coordinator: { person: { keycloakId } } },
{ organizer: { person: { keycloakId } } },
],
},

Comment on lines +41 to +42
campaignReports?: CampaignReport[]
campaignReportFiles?: CampaignReportFile[]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't the relation be from person to campaignReport and then from campaignReport to the files?
In other words the Person should not have direct relation to the Files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's one possibility, yes. I chose to include the relation Person -> Files because there might be different uploaders for the same report and in my mind they should have different permissions for their own uploaded files.

schema.prisma Outdated
Comment on lines 542 to 543
creatorId String @map("creator_id") @db.Uuid
creator Person @relation(fields: [creatorId], references: [id])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's unify the naming convention:
creator could be createdBy or uploadedBy

This will allow in the next iterations to add columns for 'approvedBy' or 'deletedBy'. Similarly we have the need to add some timestamps like 'createdAt, deletedAt to record when such actions happened

schema.prisma Outdated
Comment on lines 563 to 564
personId String @map("person_id") @db.Uuid
person Person @relation(fields: [personId], references: [id])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[naming convention] let's have these uploadedBy to indicate that they indicate who uploaded them. In other context someone may confuse the file is "about the person".

@github-actions
Copy link

github-actions bot commented Dec 7, 2022

✅ Tests will run for this PR. Once they succeed it can be merged.

@batebobo batebobo marked this pull request as ready for review December 7, 2022 14:15
@batebobo batebobo requested review from igoychev and kachar and removed request for igoychev and kachar December 7, 2022 14:15
@igoychev igoychev added the run tests Allows running the tests workflows for forked repos label Dec 22, 2022
@github-actions github-actions bot removed the run tests Allows running the tests workflows for forked repos label Dec 22, 2022
Comment on lines +33 to +35
-- AddForeignKey
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_company_id_fkey" FOREIGN KEY ("company_id") REFERENCES "companies"("id") ON DELETE SET NULL ON UPDATE CASCADE;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was done also on master and now having it two times breaks the migrations

@kachar
Copy link
Member

kachar commented Feb 9, 2023

@batebobo Can you please update your PR and resolve the open discussions in order to get this PR moving? 🙏

@igoychev
Copy link
Contributor

igoychev commented Apr 3, 2023

closing in favor of #453

@igoychev igoychev closed this Apr 3, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants