Skip to content

feat(properties): Prometheus metrics, query optimisation, Redis caching and per-route rate limits for the Property API - #1471

Open
ahmadogo wants to merge 2 commits into
MettaChain:mainfrom
ahmadogo:feature/propertyapi-observability-and-caching
Open

ahmadogo wants to merge 2 commits into
MettaChain:mainfrom
ahmadogo:feature/propertyapi-observability-and-caching

Conversation

@ahmadogo

Copy link
Copy Markdown
Contributor

Makes the Property API (src/properties) observable, faster and abuse-resistant. This mirrors the conventions already established for the Users API in #1457–#1463 (users-metrics.ts / users-cache.service.ts / UsersRouteRateLimitGuard), so the two modules stay symmetrical.

#1367 — Prometheus metrics

src/properties/properties-metrics.ts + interceptors/properties-metrics.interceptor.ts, applied at the controller:

Metric Type Labels
properties_requests_total counter operation, outcome (success/client_error/server_error)
properties_request_duration_seconds histogram operation

Both are surfaced on the existing /metrics endpoint. operation is a controller handler name and outcome has three values, so cardinality is bounded and does not grow with property input — the failure mode that makes property-scoped metrics unusable. Metrics are resolved through register.getSingleMetric before construction so hot reloads and the test suite do not register them twice.

→ docs/api/properties-metrics.md

#1368 — Database query optimisation

Stable pagination. GET /properties and GET /properties/search page with skip/take but guaranteed no total order. Two rows sharing a sort value could come back in a different order per call, so a property could appear on page 1 and page 2, or be skipped entirely. Both paths now order by the sort column plus id as a tie-breaker, and GET /properties defaults to createdAt desc, id desc instead of deferring to the database. findByOwnerId gets the same treatment.

Composite indexes. Every listing query filters is_deleted = false AND status = 'ACTIVE'. The schema only had status and is_deleted indexed separately (plus (status, created_at) and (status, price) from the earlier composite-index migration), so the planner intersected indexes and then sorted — on the single most common query in the product.

prisma/migrations/20261001000000_add_property_listing_indexes adds (status, is_deleted, created_at DESC), (status, is_deleted, price) and (city, state, status, is_deleted), with matching @@index entries on the Property model.

→ docs/api/properties-db-optimization.md

#1369 — Redis caching

GET /properties/:id is read through PropertiesCacheService on the shared CacheService (Redis when configured): key properties:detail:<id>, 60s TTL, also registered under CACHE_TAGS.PROPERTIES so existing tag invalidation covers it.

Deliberate decisions worth reviewing:

  • A miss is not cached. The controller turns null into a 404; caching that would hide a property created moments later for a full TTL.
  • Every write invalidates — create, update, remove, restore, transitionStatus (so approve/reject), agent assignment create/update/remove, and the amenity endpoints. transitionStatus invalidates before its follow-up findOne, otherwise the new status would be read back out of the cache.
  • Search and listing results are not cached. Their result sets change on almost every write and their query keys are unbounded.
  • Cache failures never fail a request — they fall through to the database and are logged.

PropertiesCacheService is injected with @Optional(), so PropertiesService still constructs without it and the existing unit-test module is unaffected.

→ docs/api/properties-caching.md

#1370 — Rate limiting

PropertiesRouteRateLimitGuard + @PropertiesRouteLimit({ max, windowMs }), run after the JWT guard so the caller is known (authenticated user id, falling back to client address), with an independent counter per route:

Route Limit
POST /properties 30 / hour
POST /properties/bulk/status, POST /properties/bulk/delete 20 / hour
POST /properties/bulk/export 10 / hour
GET /properties/:id/report 15 / hour

Over the limit: 429 with Retry-After. Counters are namespaced properties-route:<METHOD> <path>, so they do not interact with the users route limits or the existing in-service geocoding throttle.

→ docs/api/properties-rate-limiting.md

Scope

Module-local additions plus one doc under docs/api/ per issue, mirroring the Users API PRs. Nothing was installed, built, linted or tested — please verify in CI. The service test module needed no changes because of the @Optional() injection.

Closes #1367
Closes #1368
Closes #1369
Closes #1370

…ng, per-caller rate limits

Observability, performance and abuse protection for the Property API
(`src/properties`), following the conventions already established by the
Users API (metrics -> `src/users/users-metrics.ts`, caching ->
`users-cache.service.ts`, rate limits -> `guards/users-route-rate-limit.guard.ts`).

- MettaChain#1367: `properties_requests_total` (counter, `operation` + `outcome`) and
  `properties_request_duration_seconds` (histogram, `operation`) recorded by
  `PropertiesMetricsInterceptor` on every controller handler. Labels are bounded,
  so cardinality cannot grow with property input; docs/api/properties-metrics.md
- MettaChain#1368: listing queries now sort on the requested column plus `id`, and
  `GET /properties` defaults to `createdAt desc, id desc`, so skip/take paging
  can no longer repeat or drop a row. Added
  `prisma/migrations/20261001000000_add_property_listing_indexes`: every listing
  query filters `is_deleted = false AND status = 'ACTIVE'`, which previously
  required the planner to intersect the separate `status`/`is_deleted` indexes.
  docs/api/properties-db-optimization.md
- MettaChain#1369: `GET /properties/:id` is read through `PropertiesCacheService`
  (key `properties:detail:<id>`, 60s TTL, also registered under
  `CACHE_TAGS.PROPERTIES`). Lookups that miss are not cached, so a 404 cannot be
  cached in front of a property created moments later. Every write path
  invalidates the key, and `transitionStatus` invalidates before its follow-up
  `findOne` so a stale status is never served. Cache errors fall back to the
  database and never fail a request. docs/api/properties-caching.md
- MettaChain#1370: `PropertiesRouteRateLimitGuard` + `@PropertiesRouteLimit` cap create
  (30/h), bulk status and delete (20/h), bulk export (10/h) and PDF report
  generation (15/h) per caller, answering 429 with `Retry-After`.
  docs/api/properties-rate-limiting.md

`PropertiesCacheService` is `@Optional()`, so `PropertiesService` still
constructs without it and the existing unit test module is unaffected.

No tests were run and nothing was installed, built or linted; please verify in CI.

Closes MettaChain#1367
Closes MettaChain#1368
Closes MettaChain#1369
Closes MettaChain#1370
@drips-wave

drips-wave Bot commented Oct 18, 2026

Copy link
Copy Markdown

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

Development

Successfully merging this pull request may close these issues.

Add rate limiting to PropertyAPI Add Redis caching for PropertyAPI Optimize DB queries in PropertyAPI Add Prometheus metrics to PropertyAPI

1 participant