-
Notifications
You must be signed in to change notification settings - Fork 48
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
Conversation
There was a problem hiding this 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
totalFunds Float? @map("total_funds") | ||
fundsForPeriod Float? @map("funds_for_period") | ||
spentFundsForPeriod Float? @map("spent_funds_for_period") |
There was a problem hiding this comment.
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.
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) |
There was a problem hiding this comment.
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 ✌️
CREATE TYPE "campaign_report_file_type" AS ENUM ('photo', 'document'); | ||
|
||
-- CreateTable | ||
CREATE TABLE "campaign_reports" ( |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
@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 |
There was a problem hiding this 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 } })) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice!
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') | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
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') | ||
} |
There was a problem hiding this comment.
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
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') | ||
} |
There was a problem hiding this comment.
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
const campaign = await this.campaignService.getCampaignByIdWithPersonIds(campaignId) | ||
const userCanUploadReport = [ | ||
campaign?.beneficiary.person?.keycloakId, | ||
campaign?.organizer?.person.keycloakId, | ||
campaign?.coordinator.person.keycloakId, | ||
].includes(user.sub) |
There was a problem hiding this comment.
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 } } },
],
},
campaignReports?: CampaignReport[] | ||
campaignReportFiles?: CampaignReportFile[] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
creatorId String @map("creator_id") @db.Uuid | ||
creator Person @relation(fields: [creatorId], references: [id]) |
There was a problem hiding this comment.
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
personId String @map("person_id") @db.Uuid | ||
person Person @relation(fields: [personId], references: [id]) |
There was a problem hiding this comment.
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".
✅ Tests will run for this PR. Once they succeed it can be merged. |
-- AddForeignKey | ||
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_company_id_fkey" FOREIGN KEY ("company_id") REFERENCES "companies"("id") ON DELETE SET NULL ON UPDATE CASCADE; | ||
|
There was a problem hiding this comment.
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
@batebobo Can you please update your PR and resolve the open discussions in order to get this PR moving? 🙏 |
closing in favor of #453 |
Summary
Implements #368
Created CRUD endpoints for campaign reports. They are part of a separate module called
campaign-report
, but the controller is under thecampaign
base route.New entities:
Both entities support soft delete and have an owner (uploader).
Screenshot of how the new routes look like:
Todo