[CL-OBJECT-ACCESS-SLUG] objectAccess: accept slug + id (fix AI Анализ "Object not found") #201

Closed
andrei wants to merge 1 commit from fix/cl-object-access-slug into master
Owner

Что сделано

Изменил api/src/services/objectAccess.service.ts:

  • Новая внутренняя функция findObjectByIdOrSlug(idOrSlug)prisma.propertyObject.findFirst({ where: { OR: [{ id }, { slug }] }, select: { id, status } }). Один query вместо двух.
  • assertObjectAccessible(idOrSlug, userId?) — теперь принимает slug ИЛИ id, использует resolved канонический object.id для hasObjectOwnershipAccess (без этого rooms relation room: { objectId } ищет по slug → false → ownership denied).
  • assertObjectPublic(idOrSlug) — то же.
  • Return narrowed { status } (не вся prisma row) — preserve API shape, не ломает existing callers.
  • Boy Scout: исправил pre-existing TS7006 (object) => object.id в getPublicVisibleObjectIds — добавил explicit type { id: string }.

Также добавил api/src/services/__tests__/objectAccess.service.test.ts — 8 vitest cases: slug→id resolved, canonical id directly, 404 on miss, ownership grants for non-public statuses, ownership deny throws, public path, non-public-status rejected, slug-not-found.

Зачем

Live evidence prod (CEO репорт скриншот #2):

CEO открыл /objects/hotel-baistrocchi-wellness-longevity-center/passport, в AI Анализ панели нажал «Обновить анализ» → 2× toast «Object not found». Анализ уже отображался (score 53/100), значит GET /api/analysis/<slug> работал (через resolveObjectId в propertyAnalysis/analysis.ts). Но POST /api/analysis/<slug>/generategenerateAnalysisUser controller → assertObjectAccessible(slug, userId)prisma.findUnique({where:{id:slug}}) → null → AppError 404.

Тот же баг ломал:

  • income.controller (assetTabAccess через assertObjectAccessible)
  • requireObjectAccess middleware (использует обе функции)
  • dao.routes (assertObjectPublic)
  • yieldDistribution.routes (dynamic import assertObjectAccessible)

Все эти endpoints возвращали 404 если controller получал slug, что для public passport URL — норма.

Fix в одной точке (objectAccess.service) автоматически чинит весь импакт-кластер.

План тестирования

  • npx vitest run src/services/__tests__/objectAccess.service.test.ts — 8/8 PASS
  • npx vitest run src/services/__tests__/objectAccess.service.test.ts src/controllers/income.controller.test.ts src/services/analytics.service.test.ts — 38/38 PASS (regression-safe для existing callers через mock {status:'ACTIVE'} return shape)
  • npx tsc --noEmit — clean (наш файл + bonus pre-existing TS7006 closed)
  • npx eslint src/services/objectAccess.service.ts src/services/__tests__/objectAccess.service.test.ts --max-warnings 0 — clean (exit=0)

После merge + deploy через bash scripts/deploy.sh:

  • POST /api/analysis/hotel-baistrocchi-wellness-longevity-center/generate должен вернуть 201 (не 404).
  • AI Анализ "Обновить" toast больше не появится.

Где могу ошибаться

  • Изменил return shape с object (полная findUnique row) на { status }. Все callers (8 мест по grep) деструктурируют как { status } или игнорируют return — проверил vitest 38/38 проходит, в т.ч. income.controller.test ожидает { status: 'ACTIVE' }. OK.
  • hasObjectOwnershipAccess(userId, canonical_id) — теперь использует resolved id. Тестируется в новом vitest case (mock проверяет room: { objectId: 'obj-002' }). Backward-compat: callers вне service используют только status, не объект.
  • Single DB query вместо двух (resolve + findUnique) → slight perf win. Никакой регрессии.
  • В отличие от prerender 404 fix (#199 frontend), этот PR backend-only. После merge effect immediate на следующем API restart, без необходимости полного prerender.
## Что сделано Изменил `api/src/services/objectAccess.service.ts`: - Новая внутренняя функция `findObjectByIdOrSlug(idOrSlug)` — `prisma.propertyObject.findFirst({ where: { OR: [{ id }, { slug }] }, select: { id, status } })`. Один query вместо двух. - `assertObjectAccessible(idOrSlug, userId?)` — теперь принимает slug ИЛИ id, использует resolved канонический `object.id` для `hasObjectOwnershipAccess` (без этого rooms relation `room: { objectId }` ищет по slug → false → ownership denied). - `assertObjectPublic(idOrSlug)` — то же. - Return narrowed `{ status }` (не вся prisma row) — preserve API shape, не ломает existing callers. - Boy Scout: исправил pre-existing TS7006 `(object) => object.id` в `getPublicVisibleObjectIds` — добавил explicit type `{ id: string }`. Также добавил `api/src/services/__tests__/objectAccess.service.test.ts` — 8 vitest cases: slug→id resolved, canonical id directly, 404 on miss, ownership grants for non-public statuses, ownership deny throws, public path, non-public-status rejected, slug-not-found. ## Зачем **Live evidence prod (CEO репорт скриншот #2):** CEO открыл `/objects/hotel-baistrocchi-wellness-longevity-center/passport`, в AI Анализ панели нажал «Обновить анализ» → 2× toast «Object not found». Анализ уже отображался (score 53/100), значит GET `/api/analysis/<slug>` работал (через `resolveObjectId` в `propertyAnalysis/analysis.ts`). Но POST `/api/analysis/<slug>/generate` → `generateAnalysisUser` controller → `assertObjectAccessible(slug, userId)` → `prisma.findUnique({where:{id:slug}})` → null → AppError 404. Тот же баг ломал: - `income.controller` (`assetTabAccess` через `assertObjectAccessible`) - `requireObjectAccess` middleware (использует обе функции) - `dao.routes` (`assertObjectPublic`) - `yieldDistribution.routes` (dynamic import `assertObjectAccessible`) Все эти endpoints возвращали 404 если controller получал slug, что для public passport URL — норма. Fix в одной точке (objectAccess.service) автоматически чинит весь импакт-кластер. ## План тестирования - `npx vitest run src/services/__tests__/objectAccess.service.test.ts` — 8/8 PASS - `npx vitest run src/services/__tests__/objectAccess.service.test.ts src/controllers/income.controller.test.ts src/services/analytics.service.test.ts` — 38/38 PASS (regression-safe для existing callers через mock `{status:'ACTIVE'}` return shape) - `npx tsc --noEmit` — clean (наш файл + bonus pre-existing TS7006 closed) - `npx eslint src/services/objectAccess.service.ts src/services/__tests__/objectAccess.service.test.ts --max-warnings 0` — clean (exit=0) После merge + deploy через `bash scripts/deploy.sh`: - POST `/api/analysis/hotel-baistrocchi-wellness-longevity-center/generate` должен вернуть 201 (не 404). - AI Анализ "Обновить" toast больше не появится. ## Где могу ошибаться - Изменил return shape с `object` (полная findUnique row) на `{ status }`. Все callers (8 мест по grep) деструктурируют как `{ status }` или игнорируют return — проверил vitest 38/38 проходит, в т.ч. income.controller.test ожидает `{ status: 'ACTIVE' }`. OK. - `hasObjectOwnershipAccess(userId, canonical_id)` — теперь использует resolved id. Тестируется в новом vitest case (mock проверяет `room: { objectId: 'obj-002' }`). Backward-compat: callers вне service используют только status, не объект. - Single DB query вместо двух (resolve + findUnique) → slight perf win. Никакой регрессии. - В отличие от prerender 404 fix (#199 frontend), этот PR backend-only. После merge effect immediate на следующем API restart, без необходимости полного prerender.
[CL-OBJECT-ACCESS-SLUG] objectAccess: accept slug + id (fix AI Анализ "Object not found" toast)
Some checks are pending
CI Debug Smoke / echo (pull_request) Waiting to run
CI / API (pull_request) Waiting to run
CI / App (pull_request) Waiting to run
CI / Contracts (pull_request) Waiting to run
CI / Telegram Mini App (pull_request) Waiting to run
CI / Python SDK (pull_request) Waiting to run
CI / Secrets Scan (pull_request) Waiting to run
CI / Prisma Migrate Gate (pull_request) Waiting to run
35220b03f2
Root cause: assertObjectAccessible + assertObjectPublic used
prisma.findUnique({where:{id}}) which rejects slug. analysis.controller
generateAnalysisUser passes raw req.params.objectId (slug for public
passport URLs) -> AppError 404 -> "Object not found" toast on CEO browse
of /objects/<slug>/passport "Обновить анализ" button.

Same pattern was breaking:
- analysis.controller (generateAnalysisUser)
- income.controller (assetTabAccess)
- requireObjectAccess middleware
- dao.routes
- yieldDistribution.routes

Fix:
- Replace findUnique({where:{id}}) with findFirst({where:{OR:[{id},{slug}]}})
  in single shared helper findObjectByIdOrSlug.
- assertObjectAccessible + assertObjectPublic now resolve slug to canonical id
  before ownership lookup (hasObjectOwnershipAccess uses canonical id, not raw
  slug, so room: {objectId} relation matches).
- Return narrowed {status} only (not the prisma row) to preserve original API.
- Boy Scout: fixed pre-existing TS7006 in getPublicVisibleObjectIds map callback.

Tests: 8 new vitest cases cover slug-resolved-to-id, canonical-id, 404 on
miss, ownership-grants-for-non-public, ownership-deny-throws, public path,
non-public-status-rejected, slug-not-found.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
andrei closed this pull request 2026-05-25 13:41:40 +00:00
Some checks are pending
CI Debug Smoke / echo (pull_request) Waiting to run
CI / API (pull_request) Waiting to run
CI / App (pull_request) Waiting to run
CI / Contracts (pull_request) Waiting to run
CI / Telegram Mini App (pull_request) Waiting to run
CI / Python SDK (pull_request) Waiting to run
CI / Secrets Scan (pull_request) Waiting to run
CI / Prisma Migrate Gate (pull_request) Waiting to run

Pull request closed

Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
europa-tech-srl/europatech!201
No description provided.