690 lines
21 KiB
JavaScript
690 lines
21 KiB
JavaScript
import express from "express";
|
|
import pool from "../db/pool.js";
|
|
|
|
const router = express.Router();
|
|
|
|
const createNewLocation = async (client, userId, locationData) => {
|
|
const {
|
|
lat,
|
|
lng,
|
|
source_type,
|
|
source_confidence,
|
|
selected_location,
|
|
// Location Details
|
|
is_saved_address,
|
|
location_type,
|
|
country,
|
|
state,
|
|
district,
|
|
city_village,
|
|
pincode,
|
|
} = locationData;
|
|
|
|
// 1. Insert into locations (Merged Table)
|
|
const insertLocationQuery = `
|
|
INSERT INTO locations (
|
|
user_id, lat, lng, source_type, source_confidence, selected_location,
|
|
is_saved_address, location_type,
|
|
country, state, district, city_village, pincode
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
|
RETURNING id
|
|
`;
|
|
const locationValues = [
|
|
userId,
|
|
lat,
|
|
lng,
|
|
source_type || "manual",
|
|
source_confidence || "unknown",
|
|
selected_location || false,
|
|
is_saved_address || false,
|
|
location_type || "other",
|
|
country,
|
|
state,
|
|
district,
|
|
city_village,
|
|
pincode
|
|
];
|
|
|
|
try {
|
|
const locationResult = await client.query(insertLocationQuery, locationValues);
|
|
if (locationResult.rows.length === 0) {
|
|
throw new Error("Failed to insert location record");
|
|
}
|
|
return locationResult.rows[0].id;
|
|
} catch (error) {
|
|
console.error("Error creating new location:", error);
|
|
throw error; // Propagate error to trigger transaction rollback
|
|
}
|
|
};
|
|
|
|
// 1. GET / (Main Feed) - Optimized with idx_listings_feed_optimized
|
|
router.get("/", async (req, res) => {
|
|
try {
|
|
const { status = "active", species_id, price_min, price_max, limit = 20, offset = 0 } = req.query;
|
|
|
|
let queryText = `
|
|
SELECT * FROM listings
|
|
WHERE deleted = FALSE
|
|
AND status = $1
|
|
`;
|
|
const queryParams = [status];
|
|
let paramCount = 1;
|
|
|
|
if (species_id) {
|
|
paramCount++;
|
|
queryText += ` AND filter_species_id = $${paramCount}`;
|
|
queryParams.push(species_id);
|
|
}
|
|
|
|
// New Filters
|
|
if (req.query.breed_id) {
|
|
paramCount++;
|
|
queryText += ` AND filter_breed_id = $${paramCount}`;
|
|
queryParams.push(req.query.breed_id);
|
|
}
|
|
|
|
if (req.query.sex) {
|
|
paramCount++;
|
|
queryText += ` AND filter_sex = $${paramCount}`;
|
|
queryParams.push(req.query.sex);
|
|
}
|
|
|
|
if (req.query.pregnancy_status) {
|
|
paramCount++;
|
|
queryText += ` AND filter_pregnancy_status = $${paramCount}`;
|
|
queryParams.push(req.query.pregnancy_status);
|
|
}
|
|
|
|
// Range Filters
|
|
if (req.query.age_min) {
|
|
paramCount++;
|
|
queryText += ` AND filter_age_months >= $${paramCount}`;
|
|
queryParams.push(req.query.age_min);
|
|
}
|
|
if (req.query.age_max) {
|
|
paramCount++;
|
|
queryText += ` AND filter_age_months <= $${paramCount}`;
|
|
queryParams.push(req.query.age_max);
|
|
}
|
|
|
|
if (req.query.weight_min) {
|
|
paramCount++;
|
|
queryText += ` AND filter_weight_kg >= $${paramCount}`;
|
|
queryParams.push(req.query.weight_min);
|
|
}
|
|
if (req.query.weight_max) {
|
|
paramCount++;
|
|
queryText += ` AND filter_weight_kg <= $${paramCount}`;
|
|
queryParams.push(req.query.weight_max);
|
|
}
|
|
|
|
if (req.query.calving_number_min) {
|
|
paramCount++;
|
|
queryText += ` AND filter_calving_number >= $${paramCount}`;
|
|
queryParams.push(req.query.calving_number_min);
|
|
}
|
|
if (req.query.calving_number_max) {
|
|
paramCount++;
|
|
queryText += ` AND filter_calving_number <= $${paramCount}`;
|
|
queryParams.push(req.query.calving_number_max);
|
|
}
|
|
|
|
if (req.query.milking_capacity_min) {
|
|
paramCount++;
|
|
queryText += ` AND filter_milking_capacity >= $${paramCount}`;
|
|
queryParams.push(req.query.milking_capacity_min);
|
|
}
|
|
if (req.query.milking_capacity_max) {
|
|
paramCount++;
|
|
queryText += ` AND filter_milking_capacity <= $${paramCount}`;
|
|
queryParams.push(req.query.milking_capacity_max);
|
|
}
|
|
|
|
if (price_min) {
|
|
paramCount++;
|
|
queryText += ` AND price >= $${paramCount}`;
|
|
queryParams.push(price_min);
|
|
}
|
|
|
|
if (price_max) {
|
|
paramCount++;
|
|
queryText += ` AND price <= $${paramCount}`;
|
|
queryParams.push(price_max);
|
|
}
|
|
|
|
queryText += ` ORDER BY created_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`;
|
|
queryParams.push(limit, offset);
|
|
|
|
const result = await pool.query(queryText, queryParams);
|
|
res.json(result.rows);
|
|
} catch (err) {
|
|
console.error("Error fetching listings feed:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// 2. GET /near-me (Spatial Search) - Optimized with idx_listings_spatial
|
|
router.get("/near-me", async (req, res) => {
|
|
try {
|
|
const { lat, lng, radius_meters = 100000, limit = 20, offset = 0 } = req.query;
|
|
|
|
if (!lat || !lng) {
|
|
return res.status(400).json({ error: "Latitude and Longitude are required" });
|
|
}
|
|
|
|
const queryText = `
|
|
SELECT * FROM listings
|
|
WHERE deleted = FALSE
|
|
AND status = 'active'
|
|
AND ST_DWithin(filter_location_geog, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3)
|
|
ORDER BY filter_location_geog <-> ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography
|
|
LIMIT $4 OFFSET $5
|
|
`;
|
|
const queryParams = [lng, lat, radius_meters, limit, offset]; // Note: PostGIS uses (lng, lat) for Points
|
|
|
|
const result = await pool.query(queryText, queryParams);
|
|
res.json(result.rows);
|
|
} catch (err) {
|
|
console.error("Error fetching nearby listings:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// 3. GET /search TODO: (Full Text Search) - Optimized with idx_listings_search_gin
|
|
router.get("/search", async (req, res) => {
|
|
try {
|
|
const { q, limit = 20, offset = 0 } = req.query;
|
|
|
|
if (!q) {
|
|
return res.status(400).json({ error: "Search query 'q' is required" });
|
|
}
|
|
|
|
const queryText = `
|
|
SELECT * FROM listings
|
|
WHERE deleted = FALSE
|
|
AND search_vector @@ plainto_tsquery('english', $1)
|
|
LIMIT $2 OFFSET $3
|
|
`;
|
|
const queryParams = [q, limit, offset];
|
|
|
|
const result = await pool.query(queryText, queryParams);
|
|
res.json(result.rows);
|
|
} catch (err) {
|
|
console.error("Error searching listings:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// 4. GET /seller/:sellerId (My Listings) - Optimized with idx_listings_seller_status
|
|
router.get("/seller/:sellerId", async (req, res) => {
|
|
try {
|
|
const { sellerId } = req.params;
|
|
const { status } = req.query;
|
|
|
|
let queryText = `
|
|
SELECT * FROM listings
|
|
WHERE deleted = FALSE
|
|
AND seller_id = $1
|
|
`;
|
|
const queryParams = [sellerId];
|
|
let paramCount = 1;
|
|
|
|
if (status) {
|
|
paramCount++;
|
|
queryText += ` AND status = $${paramCount}`;
|
|
queryParams.push(status);
|
|
}
|
|
|
|
queryText += ` ORDER BY created_at DESC`;
|
|
|
|
const result = await pool.query(queryText, queryParams);
|
|
res.json(result.rows);
|
|
} catch (err) {
|
|
console.error("Error fetching seller listings:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// 5. CRUD Operations
|
|
|
|
// CREATE Listing
|
|
router.post("/", async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query("BEGIN");
|
|
|
|
const {
|
|
// Listing details
|
|
seller_id,
|
|
title,
|
|
price,
|
|
currency,
|
|
is_negotiable,
|
|
listing_type,
|
|
animal,
|
|
media // Array of { media_url, media_type, is_primary, sort_order }
|
|
} = req.body;
|
|
|
|
let final_location_id = animal?.location_id;
|
|
|
|
// 1. Create Location (if needed)
|
|
if (!final_location_id && animal?.new_location) {
|
|
final_location_id = await createNewLocation(client, seller_id, animal.new_location);
|
|
}
|
|
|
|
// 2. Create Animal
|
|
const insertAnimalQuery = `
|
|
INSERT INTO animals (
|
|
species_id, breed_id, location_id, sex, age_months, weight_kg,
|
|
color_markings, quantity, purpose, health_status, vaccinated,
|
|
dewormed, pregnancy_status, calving_number, milk_yield_litre_per_day, ear_tag_no, description
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
|
RETURNING id
|
|
`;
|
|
const animalValues = [
|
|
animal.species_id,
|
|
animal.breed_id,
|
|
final_location_id,
|
|
animal.sex,
|
|
animal.age_months,
|
|
animal.weight_kg,
|
|
animal.color_markings,
|
|
animal.quantity || 1, // Default to 1
|
|
animal.purpose,
|
|
animal.health_status || "healthy", // Default
|
|
animal.vaccinated || false,
|
|
animal.dewormed || false,
|
|
animal.pregnancy_status || "unknown", // Default
|
|
animal.calving_number, // Added
|
|
animal.milk_yield_litre_per_day,
|
|
animal.ear_tag_no,
|
|
animal.description,
|
|
];
|
|
|
|
const animalResult = await client.query(insertAnimalQuery, animalValues);
|
|
const animal_id = animalResult.rows[0].id;
|
|
|
|
// 3. Create Listing
|
|
const insertListingQuery = `
|
|
INSERT INTO listings (seller_id, animal_id, title, price, currency, is_negotiable, listing_type)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
RETURNING *
|
|
`;
|
|
const listingValues = [
|
|
seller_id,
|
|
animal_id,
|
|
title,
|
|
price,
|
|
currency,
|
|
is_negotiable,
|
|
listing_type
|
|
];
|
|
|
|
const listingResult = await client.query(insertListingQuery, listingValues);
|
|
const listing_id = listingResult.rows[0].id;
|
|
|
|
// 4. Create Listing Media
|
|
if (media && media.length > 0) {
|
|
const mediaInsertQuery = `
|
|
INSERT INTO listing_media (listing_id, media_url, media_type, is_primary, sort_order)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
`;
|
|
|
|
for (const item of media) {
|
|
await client.query(mediaInsertQuery, [
|
|
listing_id,
|
|
item.media_url,
|
|
item.media_type, // 'image', 'video'
|
|
item.is_primary || false,
|
|
item.sort_order || 0
|
|
]);
|
|
}
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
|
|
res.status(201).json(listingResult.rows[0]);
|
|
} catch (err) {
|
|
await client.query("ROLLBACK");
|
|
console.error("Error creating listing with animal:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
// GET Single Listing
|
|
router.get("/:id", async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const queryText = `
|
|
SELECT
|
|
l.*,
|
|
row_to_json(a) as animal,
|
|
COALESCE(
|
|
(
|
|
SELECT json_agg(m ORDER BY m.is_primary DESC, m.sort_order ASC)
|
|
FROM listing_media m
|
|
WHERE m.listing_id = l.id AND m.deleted = FALSE
|
|
),
|
|
'[]'
|
|
) as media
|
|
FROM listings l
|
|
JOIN animals a ON l.animal_id = a.id
|
|
WHERE l.id = $1 AND l.deleted = FALSE
|
|
`;
|
|
const result = await pool.query(queryText, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "Listing not found" });
|
|
}
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("Error fetching listing:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// UPDATE Listing
|
|
router.put("/:id", async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const { id } = req.params;
|
|
const {
|
|
title, price, currency, is_negotiable, listing_type, status,
|
|
animal // Animal object
|
|
} = req.body;
|
|
|
|
await client.query("BEGIN");
|
|
|
|
// 1. Update Listing
|
|
const updateListingQuery = `
|
|
UPDATE listings
|
|
SET title = COALESCE($1, title),
|
|
price = COALESCE($2, price),
|
|
currency = COALESCE($3, currency),
|
|
is_negotiable = COALESCE($4, is_negotiable),
|
|
listing_type = COALESCE($5, listing_type),
|
|
status = COALESCE($6, status)
|
|
WHERE id = $7 AND deleted = FALSE
|
|
RETURNING *
|
|
`;
|
|
const listingParams = [title, price, currency, is_negotiable, listing_type, status, id];
|
|
const listingResult = await client.query(updateListingQuery, listingParams);
|
|
|
|
if (listingResult.rows.length === 0) {
|
|
await client.query("ROLLBACK");
|
|
return res.status(404).json({ error: "Listing not found" });
|
|
}
|
|
|
|
const listing = listingResult.rows[0];
|
|
|
|
// 2. Update Animal (if provided)
|
|
if (animal) {
|
|
const updateAnimalQuery = `
|
|
UPDATE animals
|
|
SET species_id = COALESCE($1, species_id),
|
|
breed_id = COALESCE($2, breed_id),
|
|
sex = COALESCE($3, sex),
|
|
age_months = COALESCE($4, age_months),
|
|
weight_kg = COALESCE($5, weight_kg),
|
|
color_markings = COALESCE($6, color_markings),
|
|
quantity = COALESCE($7, quantity),
|
|
purpose = COALESCE($8, purpose),
|
|
health_status = COALESCE($9, health_status),
|
|
vaccinated = COALESCE($10, vaccinated),
|
|
dewormed = COALESCE($11, dewormed),
|
|
pregnancy_status = COALESCE($12, pregnancy_status),
|
|
calving_number = COALESCE($13, calving_number),
|
|
milk_yield_litre_per_day = COALESCE($14, milk_yield_litre_per_day),
|
|
ear_tag_no = COALESCE($15, ear_tag_no),
|
|
description = COALESCE($16, description)
|
|
WHERE id = $17
|
|
RETURNING *
|
|
`;
|
|
const animalParams = [
|
|
animal.species_id, animal.breed_id, animal.sex, animal.age_months,
|
|
animal.weight_kg, animal.color_markings, animal.quantity, animal.purpose,
|
|
animal.health_status, animal.vaccinated, animal.dewormed, animal.pregnancy_status,
|
|
animal.calving_number, animal.milk_yield_litre_per_day, animal.ear_tag_no, animal.description,
|
|
listing.animal_id
|
|
];
|
|
await client.query(updateAnimalQuery, animalParams);
|
|
}
|
|
|
|
// 3. Update Media (if provided)
|
|
// Check if media is provided (it can be an empty array if the user wants to remove all media)
|
|
if (req.body.media && Array.isArray(req.body.media)) {
|
|
const media = req.body.media;
|
|
|
|
// Soft delete existing media
|
|
await client.query(
|
|
"UPDATE listing_media SET deleted = TRUE WHERE listing_id = $1",
|
|
[id]
|
|
);
|
|
|
|
// Insert new media
|
|
if (media.length > 0) {
|
|
const mediaInsertQuery = `
|
|
INSERT INTO listing_media (listing_id, media_url, media_type, is_primary, sort_order)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
`;
|
|
for (const item of media) {
|
|
await client.query(mediaInsertQuery, [
|
|
id,
|
|
item.media_url,
|
|
item.media_type,
|
|
item.is_primary || false,
|
|
item.sort_order || 0
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
|
|
// Fetch complete updated data
|
|
const completeResult = await client.query(`
|
|
SELECT l.*, row_to_json(a) as animal
|
|
FROM listings l
|
|
JOIN animals a ON l.animal_id = a.id
|
|
WHERE l.id = $1
|
|
`, [id]);
|
|
|
|
res.json(completeResult.rows[0]);
|
|
} catch (err) {
|
|
await client.query("ROLLBACK");
|
|
console.error("Error updating listing:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
// SOFT DELETE Listing
|
|
router.delete("/:id", async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { deleted_reason } = req.body;
|
|
|
|
const queryText = `
|
|
UPDATE listings
|
|
SET deleted = TRUE, status = 'deleted', deleted_reason = $1
|
|
WHERE id = $2
|
|
RETURNING *
|
|
`;
|
|
const result = await pool.query(queryText, [deleted_reason, id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "Listing not found" });
|
|
}
|
|
|
|
res.json({ message: "Listing deleted successfully", listing: result.rows[0] });
|
|
} catch (err) {
|
|
console.error("Error deleting listing:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
|
|
// Get the list of breeds
|
|
router.get("/breeds", async (req, res) => {
|
|
try {
|
|
const queryText = "SELECT * FROM breeds";
|
|
const result = await pool.query(queryText);
|
|
res.json(result.rows);
|
|
} catch (err) {
|
|
console.error("Error fetching breeds:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
|
|
// Get user favorite listings
|
|
router.get("/user/:userId/favorites", async (req, res) => {
|
|
try {
|
|
const { userId } = req.params;
|
|
const queryText = `
|
|
SELECT l.*, row_to_json(a) as animal
|
|
FROM favorites f
|
|
JOIN listings l ON f.listing_id = l.id
|
|
JOIN animals a ON l.animal_id = a.id
|
|
WHERE f.user_id = $1 AND f.deleted = FALSE AND l.deleted = FALSE
|
|
ORDER BY f.created_at DESC
|
|
`;
|
|
const result = await pool.query(queryText, [userId]);
|
|
res.json(result.rows);
|
|
} catch (err) {
|
|
console.error("Error fetching favorite listings:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Get listings created by user
|
|
router.get("/user/:userId", async (req, res) => {
|
|
try {
|
|
const { userId } = req.params;
|
|
const queryText = `
|
|
SELECT l.*, row_to_json(a) as animal
|
|
FROM listings l
|
|
JOIN animals a ON l.animal_id = a.id
|
|
WHERE l.deleted = FALSE AND l.seller_id = $1
|
|
ORDER BY l.created_at DESC
|
|
`;
|
|
const result = await pool.query(queryText, [userId]);
|
|
res.json(result.rows);
|
|
} catch (err) {
|
|
console.error("Error fetching user listings:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Reactivate Expired Listing
|
|
router.put("/:id/relist", async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const queryText = `
|
|
UPDATE listings
|
|
SET status = 'active', updated_at = NOW()
|
|
WHERE id = $1 AND status = 'expired'
|
|
RETURNING *
|
|
`;
|
|
const result = await pool.query(queryText, [id]);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "Expired listing not found or already active" });
|
|
}
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("Error relisting item:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Update listing score & status
|
|
router.patch("/:id/score", async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { listing_score, listing_score_status } = req.body;
|
|
|
|
const queryText = `
|
|
UPDATE listings
|
|
SET listing_score = COALESCE($1, listing_score),
|
|
listing_score_status = COALESCE($2, listing_score_status)
|
|
WHERE id = $3 AND deleted = FALSE
|
|
RETURNING *
|
|
`;
|
|
const queryParams = [listing_score, listing_score_status, id];
|
|
|
|
const result = await pool.query(queryText, queryParams);
|
|
|
|
if (result.rows.length === 0) {
|
|
return res.status(404).json({ error: "Listing not found" });
|
|
}
|
|
|
|
res.json(result.rows[0]);
|
|
} catch (err) {
|
|
console.error("Error updating listing:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
|
|
// Add Media to Listing
|
|
router.post("/:id/media", async (req, res) => {
|
|
const client = await pool.connect();
|
|
try {
|
|
const { id } = req.params;
|
|
const { media } = req.body; // Array of { media_url, media_type, is_primary, sort_order }
|
|
|
|
if (!media || !Array.isArray(media) || media.length === 0) {
|
|
return res.status(400).json({ error: "Media array is required" });
|
|
}
|
|
|
|
await client.query("BEGIN");
|
|
|
|
// Check if listing exists
|
|
const checkQuery = "SELECT id FROM listings WHERE id = $1 AND deleted = FALSE";
|
|
const checkResult = await client.query(checkQuery, [id]);
|
|
if (checkResult.rows.length === 0) {
|
|
await client.query("ROLLBACK");
|
|
return res.status(404).json({ error: "Listing not found" });
|
|
}
|
|
|
|
const mediaInsertQuery = `
|
|
INSERT INTO listing_media (listing_id, media_url, media_type, is_primary, sort_order)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING *
|
|
`;
|
|
|
|
const insertedMedia = [];
|
|
for (const item of media) {
|
|
const result = await client.query(mediaInsertQuery, [
|
|
id,
|
|
item.media_url,
|
|
item.media_type,
|
|
item.is_primary || false,
|
|
item.sort_order || 0
|
|
]);
|
|
insertedMedia.push(result.rows[0]);
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
res.status(201).json(insertedMedia);
|
|
} catch (err) {
|
|
await client.query("ROLLBACK");
|
|
console.error("Error adding media to listing:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
});
|
|
|
|
export default router;
|