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;
|
ward: string;
|
||||||
district: string;
|
district: string;
|
||||||
city: string;
|
city: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
areaM2: number;
|
areaM2: number;
|
||||||
bedrooms: number | null;
|
bedrooms: number | null;
|
||||||
bathrooms: number | null;
|
bathrooms: number | null;
|
||||||
@@ -70,6 +72,8 @@ export interface ListingSearchItem {
|
|||||||
address: string;
|
address: string;
|
||||||
district: string;
|
district: string;
|
||||||
city: string;
|
city: string;
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
areaM2: number;
|
areaM2: number;
|
||||||
bedrooms: number | null;
|
bedrooms: number | null;
|
||||||
bathrooms: number | null;
|
bathrooms: number | null;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ describe('listing-read.queries', () => {
|
|||||||
findMany: ReturnType<typeof vi.fn>;
|
findMany: ReturnType<typeof vi.fn>;
|
||||||
count: ReturnType<typeof vi.fn>;
|
count: ReturnType<typeof vi.fn>;
|
||||||
};
|
};
|
||||||
|
$queryRaw: ReturnType<typeof vi.fn>;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -17,6 +18,7 @@ describe('listing-read.queries', () => {
|
|||||||
findMany: vi.fn().mockResolvedValue([]),
|
findMany: vi.fn().mockResolvedValue([]),
|
||||||
count: vi.fn().mockResolvedValue(0),
|
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' },
|
seller: { id: 'seller-1', fullName: 'Nguyễn Văn A', phone: '0901234567' },
|
||||||
agent: { id: 'agent-1', userId: 'user-a', agency: 'Đất Xanh' },
|
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');
|
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!.seller.fullName).toBe('Nguyễn Văn A');
|
||||||
expect(result!.agent!.agency).toBe('Đất Xanh');
|
expect(result!.agent!.agency).toBe('Đất Xanh');
|
||||||
expect(result!.publishedAt).toBe(now.toISOString());
|
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.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 });
|
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]!.id).toBe('listing-1');
|
||||||
expect(result.data[0]!.priceVND).toBe('5000000000');
|
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.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.total).toBe(1);
|
||||||
expect(result.totalPages).toBe(1);
|
expect(result.totalPages).toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ export async function findByIdWithProperty(
|
|||||||
|
|
||||||
if (!listing) return null;
|
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 {
|
return {
|
||||||
id: listing.id,
|
id: listing.id,
|
||||||
status: listing.status,
|
status: listing.status,
|
||||||
@@ -44,6 +55,8 @@ export async function findByIdWithProperty(
|
|||||||
ward: listing.property.ward,
|
ward: listing.property.ward,
|
||||||
district: listing.property.district,
|
district: listing.property.district,
|
||||||
city: listing.property.city,
|
city: listing.property.city,
|
||||||
|
latitude: geo.latitude,
|
||||||
|
longitude: geo.longitude,
|
||||||
areaM2: listing.property.areaM2,
|
areaM2: listing.property.areaM2,
|
||||||
bedrooms: listing.property.bedrooms,
|
bedrooms: listing.property.bedrooms,
|
||||||
bathrooms: listing.property.bathrooms,
|
bathrooms: listing.property.bathrooms,
|
||||||
@@ -115,36 +128,58 @@ export async function searchListings(
|
|||||||
prisma.listing.count({ where }),
|
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 {
|
return {
|
||||||
data: data.map((listing) => ({
|
data: data.map((listing) => {
|
||||||
id: listing.id,
|
const geo = geoMap.get(listing.property.id) ?? { latitude: 0, longitude: 0 };
|
||||||
status: listing.status,
|
return {
|
||||||
transactionType: listing.transactionType,
|
id: listing.id,
|
||||||
priceVND: listing.priceVND.toString(),
|
status: listing.status,
|
||||||
pricePerM2: listing.pricePerM2,
|
transactionType: listing.transactionType,
|
||||||
viewCount: listing.viewCount,
|
priceVND: listing.priceVND.toString(),
|
||||||
publishedAt: listing.publishedAt?.toISOString() ?? null,
|
pricePerM2: listing.pricePerM2,
|
||||||
property: {
|
viewCount: listing.viewCount,
|
||||||
id: listing.property.id,
|
publishedAt: listing.publishedAt?.toISOString() ?? null,
|
||||||
propertyType: listing.property.propertyType,
|
property: {
|
||||||
title: listing.property.title,
|
id: listing.property.id,
|
||||||
address: listing.property.address,
|
propertyType: listing.property.propertyType,
|
||||||
district: listing.property.district,
|
title: listing.property.title,
|
||||||
city: listing.property.city,
|
address: listing.property.address,
|
||||||
areaM2: listing.property.areaM2,
|
district: listing.property.district,
|
||||||
bedrooms: listing.property.bedrooms,
|
city: listing.property.city,
|
||||||
bathrooms: listing.property.bathrooms,
|
latitude: geo.latitude,
|
||||||
thumbnail: listing.property.media[0]?.url ?? null,
|
longitude: geo.longitude,
|
||||||
media: listing.property.media.map((m) => ({
|
areaM2: listing.property.areaM2,
|
||||||
id: m.id,
|
bedrooms: listing.property.bedrooms,
|
||||||
url: m.url,
|
bathrooms: listing.property.bathrooms,
|
||||||
type: m.type,
|
thumbnail: listing.property.media[0]?.url ?? null,
|
||||||
order: m.order,
|
media: listing.property.media.map((m) => ({
|
||||||
caption: m.caption,
|
id: m.id,
|
||||||
})),
|
url: m.url,
|
||||||
},
|
type: m.type,
|
||||||
seller: listing.seller,
|
order: m.order,
|
||||||
})),
|
caption: m.caption,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
seller: listing.seller,
|
||||||
|
};
|
||||||
|
}),
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
Reference in New Issue
Block a user