Skip to content

feat(properties): OpenAPI specs, input sanitization, search-builder extraction and retry with backoff for the Property API - #1472

Open
ayshadogo wants to merge 2 commits into
MettaChain:mainfrom
ayshadogo:feature/propertyapi-openapi-sanitization-refactor-retry
Open

ayshadogo wants to merge 2 commits into
MettaChain:mainfrom
ayshadogo:feature/propertyapi-openapi-sanitization-refactor-retry

Conversation

@ayshadogo

Copy link
Copy Markdown
Contributor

Documentation, input hygiene and resilience work on the Property API (src/properties), following the conventions of the Users API pass in #1457–#1463.

#1371 — OpenAPI specs

Every PropertiesController route now carries @ApiOperation (with a stable operationId), @ApiParam, @ApiBody, @ApiProduces and the @ApiResponse codes the route actually returns — including the 401/403 role gates that were previously undocumented.

One thing worth flagging: the project does not enable the @nestjs/swagger CLI plugin (nest-cli.json has no plugins entry), so @ApiBody({ type: SomeDto }) on its own emits an empty schema. @ApiProperty/@ApiPropertyOptional were therefore added to the DTOs a body references:

DTO Used by
CreatePropertyDto POST /properties
UpdatePropertyDto PUT /properties/:id
TransitionPropertyStatusDto PATCH /properties/:id/status
BulkPropertyStatusUpdateDto / BulkPropertyDeleteDto / BulkPropertyExportDto POST /properties/bulk/*
CreateAmenityDto / UpdateAmenityDto POST/PATCH /properties/:id/amenities

AssignAgentDto / UpdateAgentAssignmentDto already had @ApiProperty.

Examples are given where the accepted format is not obvious (currency, enums, date-time, URI, UUID), and status on UpdatePropertyDto points at PATCH /properties/:id/status, which is the route that actually enforces the lifecycle workflow.

GET /properties and GET /properties/search are deliberately not covered here — they are documented in the ListingAPI OpenAPI PR.

→ docs/api/properties-openapi.md

#1372 — Input sanitization

src/properties/sanitize.util.ts normalises every string in a property write body, wired in as class-transformer @Transform callbacks on CreatePropertyDto/UpdatePropertyDto. The global ValidationPipe in src/main.ts already runs with transform: true, so the pipe configuration did not need to change.

Helper Applied to Effect
CleanText title, description, hoaName, hoaContactInfo NFKC, strip control chars + HTML tags, collapse whitespace, trim
TrimIdentifier address, city, state, zipCode, country, propertyType, category NFKC, strip control chars, trim
CleanIdentifierList features, tags, hoaAmenities TrimIdentifier per element

Two of these are specifically about this data model:

  • NFKC normalisation stops visually identical values (full-width digits, composed/decomposed accents) from being stored as separate listings — which otherwise lets the duplicate-address check in create be bypassed and produces two "different" versions of the same property.
  • HTML stripping in free text removes the usual vector for payloads stored in a description that later get rendered on an admin surface.

Values are normalised, not rejected. Invalid input still fails validation with 400 exactly as before, so no existing client starts receiving errors it did not get already.

→ docs/api/properties-input-sanitization.md

#1373 — Refactor: search filter building

PropertiesService had grown past 1200 lines and buildSearchWhereClause alone was a 155-line private method. It reads a SearchPropertiesDto, does no I/O and uses no this — a pure function that was stuck inside a large class.

It moved verbatim to src/properties/search-where.builder.ts as an exported buildSearchWhereClause(dto). No filter was added, removed or reordered, so the existing quirks are all preserved: exact bedrooms/bathrooms filters beating the corresponding ranges, ACTIVE-only results on the public search regardless of the status passed, location matching address/city/state/zip with OR, and a radius approximated with a bounding box that is rejected unless lat/lng come as a pair. Geo validation still throws BadRequestException.

→ docs/api/properties-refactor.md

#1374 — Retry with backoff

Geocoding is the only outbound call the Property API makes, and it runs inside attemptGeocode on create and on address change. It now runs under retryWithBackoff: 3 attempts, 200 ms base delay, 2 s cap, up to 50% jitter so concurrent callers do not retry in lockstep.

isTransientGeocodeError decides what is worth another attempt — 429, any 5xx, TimeoutError/AbortError, and socket errors (ECONNRESET, ECONNREFUSED, EAI_AGAIN, ENOTFOUND, socket hang up, fetch failed). A 4xx other than 429 is not retried: the provider already rejected the request, and repeating it only burns the per-minute geocoding budget that attemptGeocode already enforces.

Behaviour deliberately unchanged:

  • Exhausting the retries does not fail the request — the property is still created without coordinates and markGeocodingFailed still runs, so a transient provider outage costs latency rather than the whole create.
  • A provider that legitimately finds nothing returns null, not an error, so it is never retried.
  • The rate-limit check runs once, before the retries, so a retry storm cannot exceed the quota.

sleep and random are injectable so the delay schedule is testable without waiting in real time.

→ docs/api/properties-retry.md

Scope

Four self-contained changes plus one doc under docs/api/ per issue. Nothing was installed, built, linted or tested — please verify in CI. The existing service spec was checked by reading, not running: it asserts geocodeAddress is not called when coordinates are supplied, which the retry wrapper does not change.

Closes #1371
Closes #1372
Closes #1373
Closes #1374

…xtraction, retry with backoff

Documentation, input hygiene and resilience work on the Property API
(`src/properties`), following the conventions of the Users API pass in
MettaChain#1457-MettaChain#1463.

- MettaChain#1371: every PropertiesController route now carries @apioperation (with a
  stable operationId), @ApiParam, @ApiBody, @ApiProduces and the @apiresponse
  codes the route really returns, including the 401/403 role gates that were
  undocumented. The project does not enable the @nestjs/swagger CLI plugin
  (nest-cli.json has no `plugins`), so @ApiBody({ type }) would emit an empty
  schema; @ApiProperty/@ApiPropertyOptional were added to CreatePropertyDto,
  UpdatePropertyDto, TransitionPropertyStatusDto, the bulk-operation DTOs and
  the amenity DTOs. docs/api/properties-openapi.md
- MettaChain#1372: sanitize.util.ts normalises every string in a property write body via
  class-transformer @Transform callbacks on Create/UpdatePropertyDto, which the
  existing global ValidationPipe already runs. Free text (title, description, HOA
  fields) is stripped of control characters, HTML tags and whitespace runs;
  identifiers (address, city, state, zip, country, type, category) are trimmed;
  string arrays are cleaned element-wise. NFKC normalisation matters here
  because visually identical values otherwise defeat the duplicate-address
  check. Values are normalised, not rejected, so no client starts getting 400s
  it did not get before. docs/api/properties-input-sanitization.md
- MettaChain#1373: buildSearchWhereClause moved verbatim out of the 1200-line service into
  search-where.builder.ts. It is a pure function of its input with no I/O and no
  `this`, and it was a 155-line private method. No filter was added, removed or
  reordered, so the existing quirks (exact bedroom/bathroom filters beating the
  ranges, ACTIVE-only public results, radius approximated as a bounding box) are
  all preserved. docs/api/properties-refactor.md
- MettaChain#1374: retryWithBackoff now wraps the geocoding provider call in
  attemptGeocode (3 attempts, 200ms base, 2s cap, up to 50% jitter).
  isTransientGeocodeError retries 429, 5xx, timeouts and socket errors but not a
  4xx, because repeating a request the provider already rejected only burns the
  per-minute geocoding budget. Exhausting the retries still creates the property
  without coordinates, exactly as before. docs/api/properties-retry.md

Nothing was installed, built, linted or tested; please verify in CI.

Closes MettaChain#1371
Closes MettaChain#1372
Closes MettaChain#1373
Closes MettaChain#1374
@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

@ayshadogo 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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant