api-v1/routes/listingRoutes.js

401 lines
11 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,
// Location Details
is_saved_address,
location_type,
country,
state,
district,
city_village,
pincode,
} = locationData;
// 1a. Insert into locations
const insertLocationQuery = `
INSERT INTO locations (user_id, lat, lng, source_type)
VALUES ($1, $2, $3, $4)
RETURNING id
`;
const locationValues = [userId, lat, lng, source_type || "manual"];
const locationResult = await client.query(insertLocationQuery, locationValues);
const locationId = locationResult.rows[0].id;
// 1b. Insert into location_details
const insertLocationDetailsQuery = `
INSERT INTO location_details (
location_id, is_saved_address, location_type,
country, state, district, city_village, pincode
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`;
const detailsValues = [
locationId,
is_saved_address || false,
location_type || "other",
country,
state,
district,
city_village,
pincode,
];
await client.query(insertLocationDetailsQuery, detailsValues);
return locationId;
};
// 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);
}
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 = 50000, 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 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,
status,
// Animal details
species_id,
breed_id,
location_id,
sex,
age_months,
weight_kg,
color_markings,
quantity,
purpose,
health_status,
vaccinated,
dewormed,
pregnancy_status,
milk_yield_litre_per_day,
ear_tag_no,
description,
// New Location details
new_location,
} = req.body;
let final_location_id = location_id;
// 1. Create Location (if needed)
if (!final_location_id && new_location) {
final_location_id = await createNewLocation(client, seller_id, 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, 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)
RETURNING id
`;
const animalValues = [
species_id,
breed_id,
final_location_id,
sex,
age_months,
weight_kg,
color_markings,
quantity || 1, // Default to 1
purpose,
health_status || "healthy", // Default
vaccinated || false,
dewormed || false,
pregnancy_status || "unknown", // Default
milk_yield_litre_per_day,
ear_tag_no,
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, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING *
`;
const listingValues = [
seller_id,
animal_id,
title,
price,
currency,
is_negotiable,
listing_type,
status,
];
const listingResult = await client.query(insertListingQuery, listingValues);
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
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) => {
try {
const { id } = req.params;
const { title, price, currency, is_negotiable, listing_type, status } = req.body;
const queryText = `
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 queryParams = [title, price, currency, is_negotiable, listing_type, 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" });
}
});
// 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, 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" });
}
});
export default router;