Added db and more routes

This commit is contained in:
Soham Chari 2025-11-28 19:45:31 +05:30
parent 2ea38ef091
commit 17e918da47
6 changed files with 355 additions and 45 deletions

21
db/pool.js Normal file
View File

@ -0,0 +1,21 @@
import pg from "pg";
import "dotenv/config";
const { Pool } = pg;
const baseConfig = {};
baseConfig.host = process.env.PGHOST || "127.0.0.1";
baseConfig.port = Number(process.env.PGPORT || 5432);
baseConfig.user = process.env.PGUSER || "postgres";
baseConfig.password = process.env.PGPASSWORD || "postgres";
baseConfig.database = process.env.PGDATABASE || "postgres";
console.log("Base Config:", baseConfig);
const pool = new Pool(baseConfig);
pool.on("error", (err) => {
console.error("Unexpected Postgres client error", err);
});
export default pool;

114
db/schema.sql Normal file
View File

@ -0,0 +1,114 @@
-- Livestock Marketplace schema (PostgreSQL)
-- Generated from specs in README.md
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
phone VARCHAR(50) UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS species (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS breeds (
id SERIAL PRIMARY KEY,
species_id INT NOT NULL REFERENCES species(id) ON DELETE RESTRICT,
name VARCHAR(100) NOT NULL,
description TEXT,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS locations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
is_saved_address BOOLEAN NOT NULL DEFAULT FALSE,
location_type VARCHAR(50),
country VARCHAR(100),
state VARCHAR(100),
district VARCHAR(100),
city_village VARCHAR(150),
pincode VARCHAR(20),
lat DECIMAL(10,7),
lng DECIMAL(10,7),
source_type VARCHAR(50) NOT NULL DEFAULT 'unknown',
source_confidence VARCHAR(50) NOT NULL DEFAULT 'medium',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS animals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
species_id INT NOT NULL REFERENCES species(id) ON DELETE RESTRICT,
breed_id INT REFERENCES breeds(id) ON DELETE SET NULL,
sex VARCHAR(20) NOT NULL,
age_months INT,
weight_kg DECIMAL(10,2),
color_markings VARCHAR(200),
quantity INT NOT NULL DEFAULT 1,
purpose VARCHAR(50) NOT NULL,
health_status VARCHAR(50) NOT NULL,
vaccinated BOOLEAN NOT NULL DEFAULT FALSE,
dewormed BOOLEAN NOT NULL DEFAULT FALSE,
previous_pregnancies_count INT,
pregnancy_status VARCHAR(50),
milk_yield_litre_per_day DECIMAL(10,2),
ear_tag_no VARCHAR(100),
description TEXT,
suggested_care TEXT,
location_id UUID REFERENCES locations(id) ON DELETE SET NULL,
created_from VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS listings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
seller_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
animal_id UUID NOT NULL UNIQUE REFERENCES animals(id) ON DELETE CASCADE,
title VARCHAR(200) NOT NULL,
price NUMERIC(12,2) NOT NULL,
currency VARCHAR(10) NOT NULL,
is_negotiable BOOLEAN NOT NULL DEFAULT TRUE,
listing_type VARCHAR(50) NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'active',
listing_score INT NOT NULL DEFAULT 0,
views_count INT NOT NULL DEFAULT 0,
bookmarks_count INT NOT NULL DEFAULT 0,
enquiries_call_count INT NOT NULL DEFAULT 0,
enquiries_whatsapp_count INT NOT NULL DEFAULT 0,
clicks_count INT NOT NULL DEFAULT 0,
listing_score_status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS listing_media (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
listing_id UUID NOT NULL REFERENCES listings(id) ON DELETE CASCADE,
media_url VARCHAR(500) NOT NULL,
media_type VARCHAR(50) NOT NULL,
is_primary BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS custom_requirements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
requirement_text TEXT NOT NULL,
animal_id UUID REFERENCES animals(id) ON DELETE SET NULL,
status VARCHAR(50) NOT NULL DEFAULT 'open',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

12
package-lock.json generated
View File

@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"pg": "^8.16.3"
}
@ -156,6 +157,17 @@
"node": ">= 0.8"
}
},
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",

View File

@ -19,6 +19,7 @@
"homepage": "https://github.com/schari2509/BuySellService_LivingAI#readme",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"pg": "^8.16.3"
}

View File

@ -1,14 +1,7 @@
import express from "express";
const router = express.Router();
import { Pool } from "pg";
import pool from "../db/pool.js";
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD_D,
port: process.env.DB_PORT,
});
const router = express.Router();
// Get all listings
router.get("/", async (req, res) => {
@ -19,37 +12,43 @@ router.get("/", async (req, res) => {
const minPrice = req.query.min_price;
const listingType = req.query.listing_type;
let baseQuery = "SELECT * FROM listings WHERE 1=1";
let baseQuery = `
SELECT l.*, a.species_id, a.breed_id, loc.state, loc.district
FROM listings l
LEFT JOIN animals a ON a.id = l.animal_id
LEFT JOIN locations loc ON loc.id = a.location_id
WHERE 1=1
`;
const queryParams = [];
let paramIndex = 1;
if (speciesId) {
baseQuery += ` AND species_id = $${paramIndex}`;
baseQuery += ` AND a.species_id = $${paramIndex}`;
queryParams.push(speciesId);
paramIndex++;
}
if (breedId) {
baseQuery += ` AND breed_id = $${paramIndex}`;
baseQuery += ` AND a.breed_id = $${paramIndex}`;
queryParams.push(breedId);
paramIndex++;
}
if (state) {
baseQuery += ` AND state = $${paramIndex}`;
baseQuery += ` AND loc.state = $${paramIndex}`;
queryParams.push(state);
paramIndex++;
}
if (district) {
baseQuery += ` AND district = $${paramIndex}`;
baseQuery += ` AND loc.district = $${paramIndex}`;
queryParams.push(district);
paramIndex++;
}
if (minPrice) {
baseQuery += ` AND price >= $${paramIndex}`;
baseQuery += ` AND l.price >= $${paramIndex}`;
queryParams.push(minPrice);
paramIndex++;
}
if (listingType) {
baseQuery += ` AND listing_type = $${paramIndex}`;
baseQuery += ` AND l.listing_type = $${paramIndex}`;
queryParams.push(listingType);
paramIndex++;
}
@ -78,7 +77,7 @@ router.get("/:id", async (req, res) => {
res.status(200).json(listingResult.rows[0]);
} catch (error) {
res.status(500).json({
error: `Internal Server Error in fetching the specified ${id} listing`,
error: `Internal Server Error in fetching the specified ${listingId} listing`,
});
}
});
@ -89,7 +88,7 @@ router.put("/:id", async (req, res) => {
const { title, description, price } = req.body;
try {
const updateResult = await pool.query(
"UPDATE listings SET title = $1, description = $2, price = $3 WHERE id = $5 RETURNING *",
"UPDATE listings SET title = $1, description = $2, price = $3, updated_at = NOW() WHERE id = $4 RETURNING *",
[title, description, price, listingId]
);
if (updateResult.rows.length === 0) {
@ -100,7 +99,7 @@ router.put("/:id", async (req, res) => {
res.status(200).json(updateResult.rows[0]);
} catch (error) {
res.status(500).json({
error: `Internal Server Error in updating the specified ${id} listing`,
error: `Internal Server Error in updating the specified ${listingId} listing`,
});
}
});
@ -123,10 +122,25 @@ router.post("/", async (req, res) => {
// Add a new location if provided
if (!animal.location_id && req.body.new_location) {
animal.location_id = addNewLocation(req.body.new_location);
const newLocation = {
...req.body.new_location,
};
if (
newLocation.save_as_address &&
!newLocation.user_id &&
seller_id
) {
newLocation.user_id = seller_id;
}
if (typeof newLocation.is_saved_address === "undefined") {
newLocation.is_saved_address = Boolean(
newLocation.save_as_address
);
}
animal.location_id = await addNewLocation(client, newLocation);
}
const animalId = await addAnimalToListing(animal);
const animalId = await addAnimalToListing(client, animal);
const listingInsertQuery =
"INSERT INTO listings (seller_id, animal_id, title, price, currency, is_negotiable, listing_type, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) RETURNING *";
@ -156,10 +170,10 @@ router.post("/", async (req, res) => {
}
});
const addAnimalToListing = async (animal) => {
const addAnimalToListing = async (client, animal) => {
try {
const animalInsertQuery =
"INSERT INTO animals (species_id, breed_id, sex, age_months, weight_kg, color_markings, quantity, purpose, health_status, vaccinated, dewormed, previous_pregnancies_count, pregnancy_status, milk_yield_litre_per_day, ear_tag_no, description, suggested_care, location_id, created_from, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, NOW(), NOW()) RETURNING id";
"INSERT INTO animals (species_id, breed_id, sex, age_months, weight_kg, color_markings, quantity, purpose, health_status, vaccinated, dewormed, previous_pregnancies_count, pregnancy_status, milk_yield_litre_per_day, ear_tag_no, description, suggested_care, location_id, created_from, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, NOW(), NOW()) RETURNING id";
const animalValues = [
animal.species_id,
animal.breed_id,
@ -179,6 +193,7 @@ const addAnimalToListing = async (animal) => {
animal.description,
animal.suggested_care,
animal.location_id,
animal.created_from || "listing",
];
const animalResult = await client.query(
animalInsertQuery,
@ -191,23 +206,38 @@ const addAnimalToListing = async (animal) => {
}
};
const addNewLocation = async (location) => {
const addNewLocation = async (client, location) => {
try {
const {
user_id = null,
is_saved_address = false,
location_type = null,
country = null,
state = null,
district = null,
city_village = null,
pincode = null,
lat = null,
lng = null,
source_type = "unknown",
source_confidence = "medium",
} = location;
const locationInsertQuery =
"INSERT INTO locations (user_id, is_saved_address, location_type, country, state, district, city_village, pincode, lat, lng, source_type, source_confidence, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW()) RETURNING id";
const locationValues = [
location.user_id,
location.is_saved_address,
location.location_type,
location.country,
location.state,
location.district,
location.city_village,
location.pincode,
location.lat,
location.lng,
location.source_type,
location.source_confidence,
user_id,
is_saved_address,
location_type,
country,
state,
district,
city_village,
pincode,
lat,
lng,
source_type,
source_confidence,
];
const locationResult = await client.query(
locationInsertQuery,
@ -219,4 +249,72 @@ const addNewLocation = async (location) => {
}
};
// Delete listing by ID
router.delete("/:id", async (req, res) => {
const listingId = req.params.id;
try {
const deleteResult = await pool.query(
"DELETE FROM listings WHERE id = $1 RETURNING *",
[listingId]
);
if (deleteResult.rows.length === 0) {
return res
.status(404)
.json({ error: "Listing not found or already deleted" });
}
res.status(200).json({
message: "Listing deleted successfully",
listing: deleteResult.rows[0],
});
} catch (error) {
res.status(500).json({
error: `Internal Server Error in deleting the specified ${listingId} listing`,
});
}
});
// Get all listings for a user
router.get("/user/:user_id", async (req, res) => {
const userId = req.params.user_id;
try {
const listingsResult = await pool.query(
"SELECT * FROM listings WHERE seller_id = $1 ORDER BY created_at DESC",
[userId]
);
res.status(200).json(listingsResult.rows);
} catch (error) {
res.status(500).json({
error: "Internal Server Error in fetching user listings",
});
}
});
// Mark listing as sold or update listing status
router.patch("/:id/status", async (req, res) => {
const listingId = req.params.id;
const { status } = req.body;
if (!status) {
return res.status(400).json({ error: "Status must be provided" });
}
try {
const updateResult = await pool.query(
"UPDATE listings SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *",
[status, listingId]
);
if (updateResult.rows.length === 0) {
return res
.status(404)
.json({ error: "Listing not found for status update" });
}
res.status(200).json({
message: "Listing status updated",
listing: updateResult.rows[0],
});
} catch (error) {
res.status(500).json({
error: `Internal Server Error updating listing status for ${listingId}`,
});
}
});
export default router;

View File

@ -1,14 +1,7 @@
import express from "express";
import { Pool } from "pg";
const router = express.Router();
import pool from "../db/pool.js";
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD_D,
port: process.env.DB_PORT,
});
const router = express.Router();
// Add a new location
router.post("/", async (req, res) => {
@ -112,6 +105,54 @@ router.put("/:id", async (req, res) => {
}
});
// Get all locations for a user
router.get("/user/:user_id", async (req, res) => {
const userId = req.params.user_id;
try {
const result = await pool.query(
"SELECT * FROM locations WHERE user_id = $1 ORDER BY created_at DESC",
[userId]
);
res.status(200).json(result.rows);
} catch (error) {
console.error("Error fetching user's locations:", error);
res.status(500).json({
error: "Internal server error fetching user's locations",
});
}
});
// TODO: Remove this route later
// Get all users
router.get("/users", async (req, res) => {
try {
const result = await pool.query(
"SELECT * FROM users ORDER BY created_at DESC"
);
res.status(200).json(result.rows);
} catch (error) {
console.error("Error fetching all users:", error);
res.status(500).json({
error: "Internal server error fetching users",
});
}
});
// Get all locations
router.get("/", async (req, res) => {
try {
const result = await pool.query(
"SELECT * FROM locations ORDER BY created_at DESC"
);
res.status(200).json(result.rows);
} catch (error) {
console.error("Error fetching all locations:", error);
res.status(500).json({
error: "Internal server error fetching locations",
});
}
});
// Get location by ID
router.get("/:id", async (req, res) => {
const locationId = req.params.id;
@ -133,4 +174,27 @@ router.get("/:id", async (req, res) => {
}
});
// Delete a location by ID
router.delete("/:id", async (req, res) => {
const locationId = req.params.id;
try {
const deleteQuery = "DELETE FROM locations WHERE id = $1 RETURNING *";
const result = await pool.query(deleteQuery, [locationId]);
if (result.rows.length === 0) {
return res.status(404).json({ error: "Location not found" });
}
res.status(200).json({
message: "Location deleted successfully",
location: result.rows[0],
});
} catch (error) {
console.error("Error deleting location:", error);
res.status(500).json({
error: "Internal server error deleting location",
});
}
});
export default router;