From 832e9a4eabab529194e27173f8415c00065a637b Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 18 Apr 2026 22:05:15 +0700 Subject: [PATCH] =?UTF-8?q?fix(api):=20resolve=20500=20on=20GET=20/project?= =?UTF-8?q?s=20=E2=80=94=20column=20name=20+=20shape=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs masking each other: 1. Raw SQL in PrismaProjectDevelopmentRepository.search() and the related slug/ID queries joined Property on pr."projectId", but the actual FK column is "projectDevelopmentId". Postgres raised "column pr.projectId does not exist", bubbling up as a 500. 2. Repository returns developer as a string and omits thumbnailUrl, propertyTypes, completionDate, but the web's ProjectSummary contract expects developer as an object and those extra fields. After the SQL was fixed, the frontend crashed on `project.developer.name` with a runtime error screen. Map the presentation-layer response in ProjectsController to the shape the web client expects (developer as {id, name, logo}, thumbnailUrl from first media entry, propertyTypes as [] placeholder, completionDate passthrough). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../prisma-project-development.repository.ts | 6 ++-- .../controllers/projects.controller.ts | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/api/src/modules/projects/infrastructure/repositories/prisma-project-development.repository.ts b/apps/api/src/modules/projects/infrastructure/repositories/prisma-project-development.repository.ts index 26d49f7..a41ef31 100644 --- a/apps/api/src/modules/projects/infrastructure/repositories/prisma-project-development.repository.ts +++ b/apps/api/src/modules/projects/infrastructure/repositories/prisma-project-development.repository.ts @@ -37,7 +37,7 @@ export class PrismaProjectDevelopmentRepository implements IProjectRepository { ST_X(p.location::geometry) as lng, COUNT(pr.id)::int as "propertyCount" FROM "ProjectDevelopment" p - LEFT JOIN "Property" pr ON pr."projectId" = p.id + LEFT JOIN "Property" pr ON pr."projectDevelopmentId" = p.id WHERE p.slug = ${slug} GROUP BY p.id LIMIT 1 @@ -52,7 +52,7 @@ export class PrismaProjectDevelopmentRepository implements IProjectRepository { ST_X(p.location::geometry) as lng, COUNT(pr.id)::int as "propertyCount" FROM "ProjectDevelopment" p - LEFT JOIN "Property" pr ON pr."projectId" = p.id + LEFT JOIN "Property" pr ON pr."projectDevelopmentId" = p.id WHERE p.id = ${id} GROUP BY p.id LIMIT 1 @@ -163,7 +163,7 @@ export class PrismaProjectDevelopmentRepository implements IProjectRepository { `SELECT p.*, ST_Y(p.location::geometry) as lat, ST_X(p.location::geometry) as lng, COUNT(pr.id)::int as "propertyCount" FROM "ProjectDevelopment" p - LEFT JOIN "Property" pr ON pr."projectId" = p.id + LEFT JOIN "Property" pr ON pr."projectDevelopmentId" = p.id WHERE ${where.replace(/\b(\$\d+)/g, (_, m) => m)} GROUP BY p.id ORDER BY p."createdAt" DESC diff --git a/apps/api/src/modules/projects/presentation/controllers/projects.controller.ts b/apps/api/src/modules/projects/presentation/controllers/projects.controller.ts index e08c82c..cd7b636 100644 --- a/apps/api/src/modules/projects/presentation/controllers/projects.controller.ts +++ b/apps/api/src/modules/projects/presentation/controllers/projects.controller.ts @@ -12,6 +12,29 @@ import { CreateProjectDto } from '../dto/create-project.dto'; import { SearchProjectsDto } from '../dto/search-projects.dto'; import { UpdateProjectDto } from '../dto/update-project.dto'; +interface RawProjectListItem { + id: string; + name: string; + slug: string; + developer: string; + developerLogo: string | null; + media?: { url: string; type?: string; order?: number }[] | null; + [k: string]: unknown; +} + +function shapeProject(row: T) { + const { developer, developerLogo, media, ...rest } = row; + const thumbnailUrl = + Array.isArray(media) && media.length > 0 ? (media[0]?.url ?? null) : null; + return { + ...rest, + developer: { id: developer, name: developer, logo: developerLogo ?? null }, + thumbnailUrl, + propertyTypes: [], + completionDate: (rest as { completionDate?: Date | null }).completionDate ?? null, + }; +} + @ApiTags('projects') @Controller('projects') export class ProjectsController { @@ -26,7 +49,10 @@ export class ProjectsController { @ApiResponse({ status: 200, description: 'Danh sách dự án phân trang' }) @Get() async listProjects(@Query() dto: SearchProjectsDto) { - return this.queryBus.execute( + const result = await this.queryBus.execute< + ListProjectsQuery, + { data: RawProjectListItem[]; total: number; page: number; limit: number; totalPages: number } + >( new ListProjectsQuery( dto.q, dto.status, @@ -38,6 +64,7 @@ export class ProjectsController { dto.limit ?? 20, ), ); + return { ...result, data: result.data.map(shapeProject) }; } @ApiOperation({ summary: 'Chi tiết dự án', description: 'Xem chi tiết dự án theo slug hoặc ID' }) @@ -45,11 +72,13 @@ export class ProjectsController { @ApiResponse({ status: 404, description: 'Không tìm thấy dự án' }) @Get(':slugOrId') async getProject(@Param('slugOrId') slugOrId: string) { - const result = await this.queryBus.execute(new GetProjectQuery(slugOrId)); + const result = await this.queryBus.execute( + new GetProjectQuery(slugOrId), + ); if (!result) { throw new NotFoundException('Dự án', slugOrId); } - return result; + return shapeProject(result); } // ── Admin endpoints ───────────────────────────────────────────────