fix(listings): extract PostGIS coordinates in read queries instead of returning 0,0
findByIdWithProperty and searchListings used Prisma include which cannot extract PostGIS geometry(Point,4326) columns. Added raw SQL with ST_Y/ST_X to return actual lat/lng. Search uses batch extraction via ANY() for efficiency. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -23,6 +23,8 @@ export interface ListingDetailData {
|
||||
ward: string;
|
||||
district: string;
|
||||
city: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
areaM2: number;
|
||||
bedrooms: number | null;
|
||||
bathrooms: number | null;
|
||||
@@ -70,6 +72,8 @@ export interface ListingSearchItem {
|
||||
address: string;
|
||||
district: string;
|
||||
city: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
areaM2: number;
|
||||
bedrooms: number | null;
|
||||
bathrooms: number | null;
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('listing-read.queries', () => {
|
||||
findMany: ReturnType<typeof vi.fn>;
|
||||
count: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
$queryRaw: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -17,6 +18,7 @@ describe('listing-read.queries', () => {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
count: vi.fn().mockResolvedValue(0),
|
||||
},
|
||||
$queryRaw: vi.fn().mockResolvedValue([]),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -69,6 +71,7 @@ describe('listing-read.queries', () => {
|
||||
seller: { id: 'seller-1', fullName: 'Nguyễn Văn A', phone: '0901234567' },
|
||||
agent: { id: 'agent-1', userId: 'user-a', agency: 'Đất Xanh' },
|
||||
});
|
||||
mockPrisma.$queryRaw.mockResolvedValue([{ latitude: 10.7769, longitude: 106.7009 }]);
|
||||
|
||||
const result = await findByIdWithProperty(mockPrisma as any, 'listing-1');
|
||||
|
||||
@@ -81,6 +84,8 @@ describe('listing-read.queries', () => {
|
||||
expect(result!.seller.fullName).toBe('Nguyễn Văn A');
|
||||
expect(result!.agent!.agency).toBe('Đất Xanh');
|
||||
expect(result!.publishedAt).toBe(now.toISOString());
|
||||
expect(result!.property.latitude).toBe(10.7769);
|
||||
expect(result!.property.longitude).toBe(106.7009);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,6 +130,7 @@ describe('listing-read.queries', () => {
|
||||
},
|
||||
]);
|
||||
mockPrisma.listing.count.mockResolvedValue(1);
|
||||
mockPrisma.$queryRaw.mockResolvedValue([{ id: 'prop-1', latitude: 10.7769, longitude: 106.7009 }]);
|
||||
|
||||
const result = await searchListings(mockPrisma as any, { status: 'ACTIVE', page: 1, limit: 20 });
|
||||
|
||||
@@ -132,6 +138,8 @@ describe('listing-read.queries', () => {
|
||||
expect(result.data[0]!.id).toBe('listing-1');
|
||||
expect(result.data[0]!.priceVND).toBe('5000000000');
|
||||
expect(result.data[0]!.property.thumbnail).toBe('https://cdn.example.com/thumb.jpg');
|
||||
expect(result.data[0]!.property.latitude).toBe(10.7769);
|
||||
expect(result.data[0]!.property.longitude).toBe(106.7009);
|
||||
expect(result.total).toBe(1);
|
||||
expect(result.totalPages).toBe(1);
|
||||
});
|
||||
|
||||
@@ -22,6 +22,17 @@ export async function findByIdWithProperty(
|
||||
|
||||
if (!listing) return null;
|
||||
|
||||
// Extract lat/lng from PostGIS geometry via raw SQL
|
||||
const geoRows = await prisma.$queryRaw<{ latitude: number; longitude: number }[]>`
|
||||
SELECT
|
||||
ST_Y("location"::geometry) AS latitude,
|
||||
ST_X("location"::geometry) AS longitude
|
||||
FROM "Property"
|
||||
WHERE "id" = ${listing.property.id}
|
||||
LIMIT 1
|
||||
`;
|
||||
const geo = geoRows[0] ?? { latitude: 0, longitude: 0 };
|
||||
|
||||
return {
|
||||
id: listing.id,
|
||||
status: listing.status,
|
||||
@@ -44,6 +55,8 @@ export async function findByIdWithProperty(
|
||||
ward: listing.property.ward,
|
||||
district: listing.property.district,
|
||||
city: listing.property.city,
|
||||
latitude: geo.latitude,
|
||||
longitude: geo.longitude,
|
||||
areaM2: listing.property.areaM2,
|
||||
bedrooms: listing.property.bedrooms,
|
||||
bathrooms: listing.property.bathrooms,
|
||||
@@ -115,36 +128,58 @@ export async function searchListings(
|
||||
prisma.listing.count({ where }),
|
||||
]);
|
||||
|
||||
// Batch-extract lat/lng for all properties in the result set
|
||||
const propertyIds = data.map((l) => l.property.id);
|
||||
const geoMap = new Map<string, { latitude: number; longitude: number }>();
|
||||
if (propertyIds.length > 0) {
|
||||
const geoRows = await prisma.$queryRaw<{ id: string; latitude: number; longitude: number }[]>`
|
||||
SELECT
|
||||
"id",
|
||||
ST_Y("location"::geometry) AS latitude,
|
||||
ST_X("location"::geometry) AS longitude
|
||||
FROM "Property"
|
||||
WHERE "id" = ANY(${propertyIds})
|
||||
`;
|
||||
for (const row of geoRows) {
|
||||
geoMap.set(row.id, { latitude: row.latitude, longitude: row.longitude });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: data.map((listing) => ({
|
||||
id: listing.id,
|
||||
status: listing.status,
|
||||
transactionType: listing.transactionType,
|
||||
priceVND: listing.priceVND.toString(),
|
||||
pricePerM2: listing.pricePerM2,
|
||||
viewCount: listing.viewCount,
|
||||
publishedAt: listing.publishedAt?.toISOString() ?? null,
|
||||
property: {
|
||||
id: listing.property.id,
|
||||
propertyType: listing.property.propertyType,
|
||||
title: listing.property.title,
|
||||
address: listing.property.address,
|
||||
district: listing.property.district,
|
||||
city: listing.property.city,
|
||||
areaM2: listing.property.areaM2,
|
||||
bedrooms: listing.property.bedrooms,
|
||||
bathrooms: listing.property.bathrooms,
|
||||
thumbnail: listing.property.media[0]?.url ?? null,
|
||||
media: listing.property.media.map((m) => ({
|
||||
id: m.id,
|
||||
url: m.url,
|
||||
type: m.type,
|
||||
order: m.order,
|
||||
caption: m.caption,
|
||||
})),
|
||||
},
|
||||
seller: listing.seller,
|
||||
})),
|
||||
data: data.map((listing) => {
|
||||
const geo = geoMap.get(listing.property.id) ?? { latitude: 0, longitude: 0 };
|
||||
return {
|
||||
id: listing.id,
|
||||
status: listing.status,
|
||||
transactionType: listing.transactionType,
|
||||
priceVND: listing.priceVND.toString(),
|
||||
pricePerM2: listing.pricePerM2,
|
||||
viewCount: listing.viewCount,
|
||||
publishedAt: listing.publishedAt?.toISOString() ?? null,
|
||||
property: {
|
||||
id: listing.property.id,
|
||||
propertyType: listing.property.propertyType,
|
||||
title: listing.property.title,
|
||||
address: listing.property.address,
|
||||
district: listing.property.district,
|
||||
city: listing.property.city,
|
||||
latitude: geo.latitude,
|
||||
longitude: geo.longitude,
|
||||
areaM2: listing.property.areaM2,
|
||||
bedrooms: listing.property.bedrooms,
|
||||
bathrooms: listing.property.bathrooms,
|
||||
thumbnail: listing.property.media[0]?.url ?? null,
|
||||
media: listing.property.media.map((m) => ({
|
||||
id: m.id,
|
||||
url: m.url,
|
||||
type: m.type,
|
||||
order: m.order,
|
||||
caption: m.caption,
|
||||
})),
|
||||
},
|
||||
seller: listing.seller,
|
||||
};
|
||||
}),
|
||||
total,
|
||||
page,
|
||||
limit,
|
||||
|
||||
Reference in New Issue
Block a user