fix(api): resolve 500 on GET /projects — column name + shape mismatch
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 6s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 48s
Deploy / Build API Image (push) Failing after 16s
Deploy / Build Web Image (push) Failing after 9s
Deploy / Build AI Services Image (push) Failing after 9s
E2E Tests / Playwright E2E (push) Failing after 8s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 28s
Security Scanning / Trivy Scan — Web Image (push) Failing after 31s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 27s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Security Scanning / Trivy Filesystem Scan (push) Failing after 26s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 6s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 48s
Deploy / Build API Image (push) Failing after 16s
Deploy / Build Web Image (push) Failing after 9s
Deploy / Build AI Services Image (push) Failing after 9s
E2E Tests / Playwright E2E (push) Failing after 8s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 28s
Security Scanning / Trivy Scan — Web Image (push) Failing after 31s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 27s
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Security Scanning / Trivy Filesystem Scan (push) Failing after 26s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<T extends RawProjectListItem>(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<GetProjectQuery, RawProjectListItem | null>(
|
||||
new GetProjectQuery(slugOrId),
|
||||
);
|
||||
if (!result) {
|
||||
throw new NotFoundException('Dự án', slugOrId);
|
||||
}
|
||||
return result;
|
||||
return shapeProject(result);
|
||||
}
|
||||
|
||||
// ── Admin endpoints ───────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user