Skip to content

fix: add @IsNotEmpty, @ApiProperty, UUID pipe validation, and server-side identity derivation (closes #354 #355 #356 #357) - #469

Merged
chonilius merged 1 commit into
MergeFi:mainfrom
drips-projects:fix/no-dto-in-the-entire-app-uses-isnotempty
Sep 28, 2026
Merged

chonilius merged 1 commit into
MergeFi:mainfrom
drips-projects:fix/no-dto-in-the-entire-app-uses-isnotempty

Conversation

@Joeloo1

@Joeloo1 Joeloo1 commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR bundles fixes for four related validation and data-integrity issues reported across the DTO layer, Swagger documentation, query-parameter validation, and ownership attribution. All changes are backward-compatible at the HTTP surface; no migration is required.


Issue #354 — Required free-text fields accept blank/whitespace-only strings

Problem

A repo-wide search confirmed that no DTO used @IsNotEmpty(). Every required string field was guarded by @IsString() alone, which class-validator considers satisfied by "" or " ". Three high-value fields were directly affected:

DTO Field Before
CreateMilestoneDto title @IsString() @MaxLength(200)
CreatePoolDto name @IsString() @MaxLength(100)
CreateTeamDto name @IsString() (also missing @MaxLength)

Fix

  • Added @Transform(({ value }) => typeof value === 'string' ? value.trim() : value) before the validators so leading/trailing whitespace is stripped at the pipe stage, matching the trim-then-validate pattern already in place for other fields.
  • Added @IsNotEmpty() to all three fields.
  • Added the missing @MaxLength(100) to CreateTeamDto.name (tracked separately as CreateTeamDto.name has no @MaxLength, unlike every sibling free-text field the #151 fix covered #263 but trivially included here).
  • Added a defensive .trim() in each service's create method as a belt-and-suspenders guard at persistence time.

Files changed:

  • src/milestones/dto/create-milestone.dto.ts
  • src/maintenance-pool/dto/create-pool.dto.ts
  • src/teams/dto/create-team.dto.ts
  • src/milestones/milestones.service.ts
  • src/maintenance-pool/maintenance-pool.service.ts
  • src/teams/teams.service.ts

New test files:

  • src/milestones/dto/create-milestone.dto.spec.ts — rejects empty/whitespace title, accepts trimmed title
  • src/maintenance-pool/dto/create-pool.dto.spec.ts — rejects empty/whitespace name, accepts trimmed name
  • src/teams/dto/create-team.dto.spec.ts — rejects empty/whitespace name, accepts trimmed name

Issue #355 — Six inline controller DTOs have no @ApiProperty annotations

Problem

grep -n "ApiProperty" src/maintenance-pool/maintenance-pool.controller.ts src/bounties/bounties.controller.ts src/users/users.controller.ts src/milestones/milestones.controller.ts returned zero matches. Every DTO defined inline inside a controller file was invisible to Swagger/OpenAPI tooling, making the generated API spec incomplete and untestable via Swagger UI.

The affected DTOs were:

DTO File Fields
DepositDto maintenance-pool.controller.ts amount, funderAddress
AssignRewardDto maintenance-pool.controller.ts issueId, amount, recipientAddress, recipientId
FundBountyDto bounties.controller.ts funderAddress
SetStellarAddressDto users.controller.ts stellarAddress
FundMilestoneDto milestones.controller.ts funderAddress
ResolveIssueDto milestones.controller.ts issueId

Fix

Added @ApiProperty({ description: '...' }) to every field in each of the six DTOs, using the same annotation style already established by the dedicated dto/ files (e.g. FundEscrowDto, CreateBountyDto). Optional fields include required: false.

Files changed:

  • src/maintenance-pool/maintenance-pool.controller.ts
  • src/bounties/bounties.controller.ts
  • src/users/users.controller.ts
  • src/milestones/milestones.controller.ts

Issue #356 — repositoryId query param on GET /bounties not validated as UUID

Problem

GET /bounties accepts five query filters. Three (status, difficulty, asset) are pipe-validated. repositoryId, despite being a UUID foreign key used in a WHERE clause, was bound as a raw string? with no pipe. A malformed value (e.g. repositoryId=not-a-uuid) bypassed the application layer and reached the PostgreSQL driver, which threw invalid input syntax for type uuid — an unhandled 500 on a public, unauthenticated endpoint throttled at 1 000 req/hour.

// Before — no validation
@Query('repositoryId') repositoryId?: string,

// After — validated at the pipe layer
@Query('repositoryId', new ParseUUIDPipe({ version: '4', optional: true }))
repositoryId?: string,

TypeORM's parameterization already prevents SQL injection; this fix converts the driver-level crash into the clean 400 Bad Request that every other filter on the same route returns.

Files changed:

  • src/bounties/bounties.controller.ts

New test file:

  • src/bounties/bounties.query.spec.ts — asserts that a non-UUID repositoryId yields a 400, a valid UUID and an absent param both pass through cleanly.

Issue #357 — sponsorId/createdById persisted as null when a MAINTAINER omits the field

Problem

CreateBountyDto.sponsorId is @IsUUID() (required). The structurally identical ownership fields on the other three create DTOs are @IsOptional() @IsUUID(), meaning a MAINTAINER — who is a valid caller of all @Roles(SPONSOR, MAINTAINER)-gated create routes — can omit the field entirely, producing rows where sponsorId = null / createdById = null.

This silently breaks downstream queries in SponsorsService:

  • activeMilestones: filters WHERE milestone.sponsorId = :userId — null rows never appear
  • dashboard / milestoneProgress: same filter — the milestone is permanently invisible to any sponsor dashboard

The same null propagation affects MaintenancePoolService (pool attribution) and TeamsService (team creator attribution).

Fix — server-side identity derivation

Rather than making the DTO field required (which would break MAINTAINER callers who have no sponsor ID to supply), the server now derives the value from the authenticated JWT when the client omits it:

// MilestonesService.create
sponsorId: dto.sponsorId ?? callerUserId,

// MaintenancePoolService.create
createdById: dto.createdById ?? callerUserId,

// TeamsService.create
createdById: dto.createdById ?? callerUserId,

Each service's create method now accepts a callerUserId: string parameter. The controllers extract it from the authenticated request:

  • MilestonesController.create — already passed callerUserId (no change needed)
  • MaintenancePoolController.create — @Req() req: Request added; req.user.userId extracted and forwarded
  • TeamsController.create — uses the existing @CurrentUser() decorator (already imported for the assign endpoint)

The DTO fields remain @IsOptional() — the server handles the missing value gracefully rather than forcing clients to re-send their own user ID (which they shouldn't be trusted to self-report anyway).

Files changed:

  • src/milestones/milestones.service.ts
  • src/maintenance-pool/maintenance-pool.service.ts
  • src/teams/teams.service.ts
  • src/maintenance-pool/maintenance-pool.controller.ts
  • src/teams/teams.controller.ts

Updated test files:

  • src/maintenance-pool/maintenance-pool.service.spec.ts — updated create tests to pass callerUserId; the "defaults to null" test is replaced with "falls back to callerUserId"
  • src/teams/teams.service.spec.ts — same updates

Test coverage summary

New / updated file What it covers
create-milestone.dto.spec.ts (new) Blank/whitespace title rejected; trimmed title accepted
create-pool.dto.spec.ts (new) Blank/whitespace name rejected; trimmed name accepted
create-team.dto.spec.ts (new) Blank/whitespace name rejected; trimmed name accepted
bounties.query.spec.ts (new) Non-UUID repositoryId → 400; valid UUID and absent param pass
maintenance-pool.service.spec.ts (updated) callerUserId used as fallback for createdById
teams.service.spec.ts (updated) callerUserId used as fallback for createdById
bounties.controller.spec.ts (updated) Reflects ParseUUIDPipe on repositoryId

Closes #354
Closes #355
Closes #356
Closes #357

…y derivation (MergeFi#354 MergeFi#355 MergeFi#356 MergeFi#357)

Issue MergeFi#354 — @isnotempty() missing from required free-text fields
- CreateMilestoneDto.title: add @Transform(trim) + @isnotempty()
- CreatePoolDto.name: add @Transform(trim) + @isnotempty()
- CreateTeamDto.name: add @Transform(trim) + @isnotempty() + @maxlength(100)
- MilestonesService.create: defensive .trim() on title at persistence
- MaintenancePoolService.create: defensive .trim() on name at persistence
- TeamsService.create: defensive .trim() on name at persistence
- New unit specs: create-milestone.dto.spec.ts, create-pool.dto.spec.ts,
  create-team.dto.spec.ts covering blank/whitespace-only rejection

Issue MergeFi#355 — inline controller DTOs missing @ApiProperty
- DepositDto (amount, funderAddress) in maintenance-pool.controller.ts
- AssignRewardDto (issueId, amount, recipientAddress, recipientId) same file
- FundBountyDto (funderAddress) in bounties.controller.ts
- SetStellarAddressDto (stellarAddress) in users.controller.ts
- FundMilestoneDto (funderAddress) in milestones.controller.ts
- ResolveIssueDto (issueId) same file

Issue MergeFi#356 — repositoryId query param on GET /bounties not validated as UUID
- Add ParseUUIDPipe({ version: '4', optional: true }) to repositoryId
- New regression spec bounties.query.spec.ts: malformed UUID -> 400,
  valid UUID and omitted param pass through

Issue MergeFi#357 — sponsorId/createdById null when MAINTAINER omits the field
- MilestonesService.create: sponsorId = dto.sponsorId ?? callerUserId
- MaintenancePoolService.create: createdById = dto.createdById ?? callerUserId
- TeamsService.create: createdById = dto.createdById ?? callerUserId
- MaintenancePoolController.create: extract userId from JWT req.user, pass through
- TeamsController.create: use existing @currentuser() decorator, pass through
- Update service specs to assert fallback-to-callerUserId behaviour
@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

@Joeloo1 is attempting to deploy a commit to the chonilius' projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Sep 28, 2026

Copy link
Copy Markdown

@Joeloo1 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@chonilius
chonilius merged commit 50647ca into MergeFi:main Sep 28, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment