809 lines
26 KiB
JavaScript
809 lines
26 KiB
JavaScript
import express from "express";
|
|
import { insert, select, update, execute } from "../db/queryHelper/index.js";
|
|
|
|
const router = express.Router();
|
|
|
|
// Helper function to validate UUID format
|
|
const isValidUUID = (uuid) => {
|
|
if (!uuid || typeof uuid !== 'string') return false;
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
return uuidRegex.test(uuid);
|
|
};
|
|
|
|
const createNewLocation = async (trx, 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;
|
|
|
|
// Validate user exists if this is a saved address
|
|
let finalUserId = userId;
|
|
if (is_saved_address && userId) {
|
|
const userCheck = await trx('users')
|
|
.select('id')
|
|
.where({ id: userId, deleted: false })
|
|
.first();
|
|
if (!userCheck) {
|
|
throw new Error(`User with id ${userId} does not exist. Cannot create saved address for non-existent user.`);
|
|
}
|
|
} else if (!is_saved_address) {
|
|
// For captured locations (not saved addresses), user_id can be NULL
|
|
finalUserId = null;
|
|
} else if (is_saved_address && !userId) {
|
|
throw new Error("user_id is required when is_saved_address is true");
|
|
}
|
|
|
|
// Insert into locations
|
|
const locationDataToInsert = {
|
|
user_id: finalUserId,
|
|
lat,
|
|
lng,
|
|
source_type: source_type || "manual",
|
|
source_confidence: source_confidence || "unknown",
|
|
selected_location: selected_location || false,
|
|
is_saved_address: is_saved_address || false,
|
|
location_type: location_type || "other",
|
|
country,
|
|
state,
|
|
district,
|
|
city_village,
|
|
pincode
|
|
};
|
|
|
|
try {
|
|
const locationResult = await trx('locations')
|
|
.insert(locationDataToInsert)
|
|
.returning('id');
|
|
if (locationResult.length === 0) {
|
|
throw new Error("Failed to insert location record");
|
|
}
|
|
return locationResult[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 {
|
|
// Parse and validate query parameters
|
|
const status = req.query.status || "active";
|
|
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
|
|
const offset = parseInt(req.query.offset) || 0;
|
|
|
|
// Parse filter parameters
|
|
const species_id = req.query.species_id;
|
|
const breed_id = req.query.breed_id;
|
|
const sex = req.query.sex;
|
|
const pregnancy_status = req.query.pregnancy_status;
|
|
const age_min = req.query.age_min ? parseInt(req.query.age_min) : undefined;
|
|
const age_max = req.query.age_max ? parseInt(req.query.age_max) : undefined;
|
|
const weight_min = req.query.weight_min ? parseFloat(req.query.weight_min) : undefined;
|
|
const weight_max = req.query.weight_max ? parseFloat(req.query.weight_max) : undefined;
|
|
const calving_number_min = req.query.calving_number_min ? parseInt(req.query.calving_number_min) : undefined;
|
|
const calving_number_max = req.query.calving_number_max ? parseInt(req.query.calving_number_max) : undefined;
|
|
const milking_capacity_min = req.query.milking_capacity_min ? parseFloat(req.query.milking_capacity_min) : undefined;
|
|
const milking_capacity_max = req.query.milking_capacity_max ? parseFloat(req.query.milking_capacity_max) : undefined;
|
|
const price_min = req.query.price_min ? parseFloat(req.query.price_min) : undefined;
|
|
const price_max = req.query.price_max ? parseFloat(req.query.price_max) : undefined;
|
|
|
|
// Build where conditions using JSON-based structure
|
|
const where = { deleted: false, status };
|
|
if (species_id) where.filter_species_id = species_id;
|
|
if (breed_id) where.filter_breed_id = breed_id;
|
|
if (sex) where.filter_sex = sex;
|
|
if (pregnancy_status) where.filter_pregnancy_status = pregnancy_status;
|
|
|
|
// Use raw builder for complex range queries (queryHelper doesn't support range operators directly)
|
|
const result = await execute({
|
|
type: 'raw-builder',
|
|
handler: async (knex) => {
|
|
let query = knex('listings')
|
|
.select('*')
|
|
.where(where);
|
|
|
|
// Add range conditions using JSON-based where structure
|
|
if (age_min !== undefined) {
|
|
query = query.where('filter_age_months', '>=', age_min);
|
|
}
|
|
if (age_max !== undefined) {
|
|
query = query.where('filter_age_months', '<=', age_max);
|
|
}
|
|
if (weight_min !== undefined) {
|
|
query = query.where('filter_weight_kg', '>=', weight_min);
|
|
}
|
|
if (weight_max !== undefined) {
|
|
query = query.where('filter_weight_kg', '<=', weight_max);
|
|
}
|
|
if (calving_number_min !== undefined) {
|
|
query = query.where('filter_calving_number', '>=', calving_number_min);
|
|
}
|
|
if (calving_number_max !== undefined) {
|
|
query = query.where('filter_calving_number', '<=', calving_number_max);
|
|
}
|
|
if (milking_capacity_min !== undefined) {
|
|
query = query.where('filter_milking_capacity', '>=', milking_capacity_min);
|
|
}
|
|
if (milking_capacity_max !== undefined) {
|
|
query = query.where('filter_milking_capacity', '<=', milking_capacity_max);
|
|
}
|
|
if (price_min !== undefined) {
|
|
query = query.where('price', '>=', price_min);
|
|
}
|
|
if (price_max !== undefined) {
|
|
query = query.where('price', '<=', price_max);
|
|
}
|
|
|
|
return await query
|
|
.orderBy('created_at', 'desc')
|
|
.limit(limit)
|
|
.offset(offset);
|
|
}
|
|
});
|
|
|
|
res.json(result);
|
|
} 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 {
|
|
// Parse and validate query parameters
|
|
const lat = parseFloat(req.query.lat);
|
|
const lng = parseFloat(req.query.lng);
|
|
const radius_meters = parseInt(req.query.radius_meters) || 100000;
|
|
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
|
|
const offset = parseInt(req.query.offset) || 0;
|
|
|
|
if (!lat || !lng || isNaN(lat) || isNaN(lng)) {
|
|
return res.status(400).json({ error: "Valid latitude and longitude are required" });
|
|
}
|
|
|
|
// Use raw builder for PostGIS spatial queries (queryHelper doesn't support PostGIS functions)
|
|
const result = await execute({
|
|
type: 'raw-builder',
|
|
handler: async (knex) => {
|
|
return await knex('listings')
|
|
.select('*')
|
|
.where({ deleted: false, status: 'active' })
|
|
.whereRaw(
|
|
'ST_DWithin(filter_location_geog, ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography, ?)',
|
|
[lng, lat, radius_meters]
|
|
)
|
|
.orderByRaw(
|
|
'filter_location_geog <-> ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography',
|
|
[lng, lat]
|
|
)
|
|
.limit(limit)
|
|
.offset(offset);
|
|
}
|
|
});
|
|
|
|
res.json(result);
|
|
} 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 {
|
|
// Parse and validate query parameters
|
|
const q = req.query.q?.trim();
|
|
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
|
|
const offset = parseInt(req.query.offset) || 0;
|
|
|
|
if (!q) {
|
|
return res.status(400).json({ error: "Search query 'q' is required" });
|
|
}
|
|
|
|
// Use raw builder for full-text search (queryHelper doesn't support PostgreSQL full-text search)
|
|
const result = await execute({
|
|
type: 'raw-builder',
|
|
handler: async (knex) => {
|
|
return await knex('listings')
|
|
.select('*')
|
|
.where({ deleted: false })
|
|
.whereRaw("search_vector @@ plainto_tsquery('english', ?)", [q])
|
|
.limit(limit)
|
|
.offset(offset);
|
|
}
|
|
});
|
|
|
|
res.json(result);
|
|
} 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 {
|
|
// Parse and validate parameters
|
|
const { sellerId } = req.params;
|
|
const status = req.query.status; // Optional status filter
|
|
const limit = Math.min(parseInt(req.query.limit) || 100, 100);
|
|
const offset = parseInt(req.query.offset) || 0;
|
|
|
|
// Build where conditions using JSON-based structure
|
|
const where = { deleted: false, seller_id: sellerId };
|
|
if (status) where.status = status;
|
|
|
|
// Use queryHelper select with JSON-based where conditions
|
|
const result = await select({
|
|
table: 'listings',
|
|
where,
|
|
orderBy: { column: 'created_at', direction: 'desc' },
|
|
limit,
|
|
offset
|
|
});
|
|
|
|
res.json(result);
|
|
} 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) => {
|
|
try {
|
|
// Parse and extract listing data from request body
|
|
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;
|
|
|
|
// Validate required fields
|
|
if (!seller_id) {
|
|
return res.status(400).json({ error: "seller_id is required" });
|
|
}
|
|
if (!isValidUUID(seller_id)) {
|
|
return res.status(400).json({ error: "seller_id must be a valid UUID" });
|
|
}
|
|
if (!title || !title.trim()) {
|
|
return res.status(400).json({ error: "title is required" });
|
|
}
|
|
if (!animal) {
|
|
return res.status(400).json({ error: "animal data is required" });
|
|
}
|
|
|
|
// Validate animal required fields
|
|
if (!animal.species_id) {
|
|
return res.status(400).json({ error: "animal.species_id is required" });
|
|
}
|
|
if (!isValidUUID(animal.species_id)) {
|
|
return res.status(400).json({ error: "animal.species_id must be a valid UUID" });
|
|
}
|
|
if (animal.breed_id && !isValidUUID(animal.breed_id)) {
|
|
return res.status(400).json({ error: "animal.breed_id must be a valid UUID" });
|
|
}
|
|
if (animal.location_id && !isValidUUID(animal.location_id)) {
|
|
return res.status(400).json({ error: "animal.location_id must be a valid UUID" });
|
|
}
|
|
|
|
// Validate seller exists using queryHelper
|
|
const sellerCheck = await select({
|
|
table: 'users',
|
|
columns: ['id'],
|
|
where: { id: seller_id, deleted: false },
|
|
limit: 1
|
|
});
|
|
if (sellerCheck.length === 0) {
|
|
return res.status(400).json({ error: `Seller with id ${seller_id} does not exist. Please create the user first.` });
|
|
}
|
|
|
|
const result = await execute({
|
|
type: 'transaction',
|
|
handler: async (trx) => {
|
|
let final_location_id = animal?.location_id;
|
|
|
|
// 1. Create Location (if needed)
|
|
if (!final_location_id && animal?.new_location) {
|
|
final_location_id = await createNewLocation(trx, seller_id, animal.new_location);
|
|
}
|
|
|
|
// 2. Create Animal - Parse and build JSON data object
|
|
const animalData = {
|
|
species_id: animal.species_id, // Already validated as UUID
|
|
breed_id: animal.breed_id && isValidUUID(animal.breed_id) ? animal.breed_id : null,
|
|
location_id: final_location_id, // Can be null or valid UUID
|
|
sex: animal.sex || null,
|
|
age_months: animal.age_months ? parseInt(animal.age_months) : null,
|
|
weight_kg: animal.weight_kg ? parseFloat(animal.weight_kg) : null,
|
|
color_markings: animal.color_markings || null,
|
|
quantity: animal.quantity ? parseInt(animal.quantity) : 1,
|
|
purpose: animal.purpose || null,
|
|
health_status: animal.health_status || "healthy",
|
|
vaccinated: animal.vaccinated === true || animal.vaccinated === 'true',
|
|
dewormed: animal.dewormed === true || animal.dewormed === 'true',
|
|
pregnancy_status: animal.pregnancy_status || "unknown",
|
|
calving_number: animal.calving_number ? parseInt(animal.calving_number) : null,
|
|
milk_yield_litre_per_day: animal.milk_yield_litre_per_day ? parseFloat(animal.milk_yield_litre_per_day) : null,
|
|
ear_tag_no: animal.ear_tag_no || null,
|
|
description: animal.description || null,
|
|
};
|
|
|
|
const animalResult = await trx('animals')
|
|
.insert(animalData)
|
|
.returning('id');
|
|
const animal_id = animalResult[0].id;
|
|
|
|
// 3. Create Listing - Parse and build JSON data object
|
|
const listingData = {
|
|
seller_id,
|
|
animal_id,
|
|
title: title.trim(),
|
|
price: price ? parseFloat(price) : null,
|
|
currency: currency || null,
|
|
is_negotiable: is_negotiable === true || is_negotiable === 'true',
|
|
listing_type: listing_type || null
|
|
};
|
|
|
|
const listingResult = await trx('listings')
|
|
.insert(listingData)
|
|
.returning('*');
|
|
const listing_id = listingResult[0].id;
|
|
|
|
// 4. Create Listing Media - Parse and build JSON data array
|
|
if (media && Array.isArray(media) && media.length > 0) {
|
|
const mediaData = media.map(item => ({
|
|
listing_id,
|
|
media_url: item.media_url?.trim() || null,
|
|
media_type: item.media_type || 'image',
|
|
is_primary: item.is_primary === true || item.is_primary === 'true',
|
|
sort_order: item.sort_order ? parseInt(item.sort_order) : 0
|
|
}));
|
|
await trx('listing_media').insert(mediaData);
|
|
}
|
|
|
|
return listingResult[0];
|
|
}
|
|
});
|
|
|
|
res.status(201).json(result);
|
|
} catch (err) {
|
|
console.error("Error creating listing with animal:", err);
|
|
|
|
// Provide more specific error messages
|
|
if (err.code === '22P02') {
|
|
// Invalid UUID format
|
|
return res.status(400).json({
|
|
error: "Invalid UUID format. Please ensure all UUID fields (species_id, breed_id, location_id) are valid UUIDs."
|
|
});
|
|
}
|
|
|
|
if (err.code === '23503') {
|
|
// Foreign key violation
|
|
return res.status(400).json({
|
|
error: err.detail || "Foreign key constraint violation. Please ensure species_id, breed_id, and location_id reference existing records."
|
|
});
|
|
}
|
|
|
|
if (err.code === '23505') {
|
|
// Unique constraint violation
|
|
return res.status(400).json({
|
|
error: err.detail || "A record with these values already exists."
|
|
});
|
|
}
|
|
|
|
res.status(500).json({ error: err.message || "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// GET Single Listing
|
|
router.get("/:id", async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
// Use raw builder for complex join with JSON aggregation
|
|
const result = await execute({
|
|
type: 'raw-builder',
|
|
handler: async (knex) => {
|
|
return await knex('listings as l')
|
|
.select(
|
|
'l.*',
|
|
knex.raw('row_to_json(a) as animal'),
|
|
knex.raw(`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`)
|
|
)
|
|
.join('animals as a', 'l.animal_id', 'a.id')
|
|
.where('l.id', id)
|
|
.where('l.deleted', false)
|
|
.first();
|
|
}
|
|
});
|
|
|
|
if (!result) {
|
|
return res.status(404).json({ error: "Listing not found" });
|
|
}
|
|
|
|
res.json(result);
|
|
} 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;
|
|
// Parse and extract update data from request body
|
|
const {
|
|
title, price, currency, is_negotiable, listing_type, status,
|
|
animal // Animal object
|
|
} = req.body;
|
|
|
|
const result = await execute({
|
|
type: 'transaction',
|
|
handler: async (trx) => {
|
|
// 1. Update Listing - Parse and build JSON update object
|
|
const listingUpdates = {};
|
|
if (title !== undefined) listingUpdates.title = title ? title.trim() : null;
|
|
if (price !== undefined) listingUpdates.price = price !== null ? parseFloat(price) : null;
|
|
if (currency !== undefined) listingUpdates.currency = currency || null;
|
|
if (is_negotiable !== undefined) listingUpdates.is_negotiable = is_negotiable === true || is_negotiable === 'true';
|
|
if (listing_type !== undefined) listingUpdates.listing_type = listing_type || null;
|
|
if (status !== undefined) listingUpdates.status = status || null;
|
|
|
|
const listingResult = await trx('listings')
|
|
.where({ id, deleted: false })
|
|
.update(listingUpdates)
|
|
.returning('*');
|
|
|
|
if (listingResult.length === 0) {
|
|
throw new Error("Listing not found");
|
|
}
|
|
|
|
const listing = listingResult[0];
|
|
|
|
// 2. Update Animal (if provided) - Parse and build JSON update object
|
|
if (animal) {
|
|
const animalUpdates = {};
|
|
if (animal.species_id !== undefined) animalUpdates.species_id = animal.species_id;
|
|
if (animal.breed_id !== undefined) animalUpdates.breed_id = animal.breed_id;
|
|
if (animal.sex !== undefined) animalUpdates.sex = animal.sex;
|
|
if (animal.age_months !== undefined) animalUpdates.age_months = animal.age_months ? parseInt(animal.age_months) : null;
|
|
if (animal.weight_kg !== undefined) animalUpdates.weight_kg = animal.weight_kg ? parseFloat(animal.weight_kg) : null;
|
|
if (animal.color_markings !== undefined) animalUpdates.color_markings = animal.color_markings || null;
|
|
if (animal.quantity !== undefined) animalUpdates.quantity = animal.quantity ? parseInt(animal.quantity) : null;
|
|
if (animal.purpose !== undefined) animalUpdates.purpose = animal.purpose || null;
|
|
if (animal.health_status !== undefined) animalUpdates.health_status = animal.health_status;
|
|
if (animal.vaccinated !== undefined) animalUpdates.vaccinated = animal.vaccinated === true || animal.vaccinated === 'true';
|
|
if (animal.dewormed !== undefined) animalUpdates.dewormed = animal.dewormed === true || animal.dewormed === 'true';
|
|
if (animal.pregnancy_status !== undefined) animalUpdates.pregnancy_status = animal.pregnancy_status;
|
|
if (animal.calving_number !== undefined) animalUpdates.calving_number = animal.calving_number ? parseInt(animal.calving_number) : null;
|
|
if (animal.milk_yield_litre_per_day !== undefined) animalUpdates.milk_yield_litre_per_day = animal.milk_yield_litre_per_day ? parseFloat(animal.milk_yield_litre_per_day) : null;
|
|
if (animal.ear_tag_no !== undefined) animalUpdates.ear_tag_no = animal.ear_tag_no || null;
|
|
if (animal.description !== undefined) animalUpdates.description = animal.description || null;
|
|
|
|
await trx('animals')
|
|
.where({ id: listing.animal_id })
|
|
.update(animalUpdates);
|
|
}
|
|
|
|
// 3. Update Media (if provided) - Parse and build JSON data array
|
|
if (req.body.media && Array.isArray(req.body.media)) {
|
|
const media = req.body.media;
|
|
|
|
// Soft delete existing media
|
|
await trx('listing_media')
|
|
.where({ listing_id: id })
|
|
.update({ deleted: true });
|
|
|
|
// Insert new media
|
|
if (media.length > 0) {
|
|
const mediaData = media.map(item => ({
|
|
listing_id: id,
|
|
media_url: item.media_url?.trim() || null,
|
|
media_type: item.media_type || 'image',
|
|
is_primary: item.is_primary === true || item.is_primary === 'true',
|
|
sort_order: item.sort_order ? parseInt(item.sort_order) : 0
|
|
}));
|
|
await trx('listing_media').insert(mediaData);
|
|
}
|
|
}
|
|
|
|
// Fetch complete updated data
|
|
const completeResult = await trx('listings as l')
|
|
.select('l.*', trx.raw('row_to_json(a) as animal'))
|
|
.join('animals as a', 'l.animal_id', 'a.id')
|
|
.where('l.id', id)
|
|
.first();
|
|
|
|
return completeResult;
|
|
}
|
|
});
|
|
|
|
res.json(result);
|
|
} catch (err) {
|
|
if (err.message === "Listing not found") {
|
|
return res.status(404).json({ error: err.message });
|
|
}
|
|
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;
|
|
// Parse and extract delete reason from request body
|
|
const { deleted_reason } = req.body;
|
|
|
|
// Build update data object using JSON-based structure
|
|
const updateData = {
|
|
deleted: true,
|
|
status: 'deleted'
|
|
};
|
|
if (deleted_reason !== undefined) {
|
|
updateData.deleted_reason = deleted_reason.trim();
|
|
}
|
|
|
|
// Use queryHelper update with JSON-based where conditions
|
|
const result = await update({
|
|
table: 'listings',
|
|
data: updateData,
|
|
where: {
|
|
id,
|
|
deleted: false // Only delete if not already deleted
|
|
},
|
|
returning: '*'
|
|
});
|
|
|
|
if (result.length === 0) {
|
|
return res.status(404).json({ error: "Listing not found or already deleted" });
|
|
}
|
|
|
|
res.json({ message: "Listing deleted successfully", listing: result[0] });
|
|
} catch (err) {
|
|
console.error("Error deleting listing:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
|
|
// Get the list of species
|
|
router.get("/species", async (req, res) => {
|
|
try {
|
|
const result = await select({
|
|
table: 'species',
|
|
where: { deleted: false },
|
|
orderBy: { column: 'name', direction: 'asc' }
|
|
});
|
|
res.json(result);
|
|
} catch (err) {
|
|
console.error("Error fetching species:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
// Get the list of breeds
|
|
router.get("/breeds", async (req, res) => {
|
|
try {
|
|
// Parse and validate query parameters
|
|
const species_id = req.query.species_id;
|
|
|
|
// Build where conditions using JSON-based structure
|
|
const where = { deleted: false };
|
|
if (species_id) where.species_id = species_id;
|
|
|
|
// Use queryHelper select with JSON-based where conditions
|
|
const result = await select({
|
|
table: 'breeds',
|
|
where,
|
|
orderBy: { column: 'name', direction: 'asc' }
|
|
});
|
|
res.json(result);
|
|
} 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 result = await execute({
|
|
type: 'raw-builder',
|
|
handler: async (knex) => {
|
|
return await knex('favorites as f')
|
|
.select('l.*', knex.raw('row_to_json(a) as animal'))
|
|
.join('listings as l', 'f.listing_id', 'l.id')
|
|
.join('animals as a', 'l.animal_id', 'a.id')
|
|
.where('f.user_id', userId)
|
|
.where('f.deleted', false)
|
|
.where('l.deleted', false)
|
|
.orderBy('f.created_at', 'desc');
|
|
}
|
|
});
|
|
res.json(result);
|
|
} 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 result = await execute({
|
|
type: 'raw-builder',
|
|
handler: async (knex) => {
|
|
return await knex('listings as l')
|
|
.select('l.*', knex.raw('row_to_json(a) as animal'))
|
|
.join('animals as a', 'l.animal_id', 'a.id')
|
|
.where('l.deleted', false)
|
|
.where('l.seller_id', userId)
|
|
.orderBy('l.created_at', 'desc');
|
|
}
|
|
});
|
|
res.json(result);
|
|
} 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;
|
|
|
|
// Use queryHelper update with JSON-based where conditions
|
|
const result = await update({
|
|
table: 'listings',
|
|
data: {
|
|
status: 'active',
|
|
updated_at: new Date() // Will be converted to NOW() by database
|
|
},
|
|
where: {
|
|
id,
|
|
status: 'expired',
|
|
deleted: false
|
|
},
|
|
returning: '*'
|
|
});
|
|
|
|
if (result.length === 0) {
|
|
return res.status(404).json({ error: "Expired listing not found or already active" });
|
|
}
|
|
|
|
res.json(result[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;
|
|
// Parse and extract update data from request body
|
|
const { listing_score, listing_score_status } = req.body;
|
|
|
|
// Build update data object using JSON-based structure (only include fields that are provided)
|
|
const updateData = {};
|
|
if (listing_score !== undefined) {
|
|
updateData.listing_score = parseFloat(listing_score);
|
|
}
|
|
if (listing_score_status !== undefined) {
|
|
updateData.listing_score_status = listing_score_status;
|
|
}
|
|
|
|
// Validate that at least one field is being updated
|
|
if (Object.keys(updateData).length === 0) {
|
|
return res.status(400).json({ error: "At least one field (listing_score or listing_score_status) must be provided" });
|
|
}
|
|
|
|
// Use queryHelper update with JSON-based where conditions
|
|
const result = await update({
|
|
table: 'listings',
|
|
data: updateData,
|
|
where: {
|
|
id,
|
|
deleted: false
|
|
},
|
|
returning: '*'
|
|
});
|
|
|
|
if (result.length === 0) {
|
|
return res.status(404).json({ error: "Listing not found" });
|
|
}
|
|
|
|
res.json(result[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) => {
|
|
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" });
|
|
}
|
|
|
|
const result = await execute({
|
|
type: 'transaction',
|
|
handler: async (trx) => {
|
|
// Check if listing exists
|
|
const listing = await trx('listings')
|
|
.select('id')
|
|
.where({ id, deleted: false })
|
|
.first();
|
|
|
|
if (!listing) {
|
|
throw new Error("Listing not found");
|
|
}
|
|
|
|
// Insert media
|
|
const mediaData = media.map(item => ({
|
|
listing_id: id,
|
|
media_url: item.media_url,
|
|
media_type: item.media_type,
|
|
is_primary: item.is_primary || false,
|
|
sort_order: item.sort_order || 0
|
|
}));
|
|
|
|
return await trx('listing_media')
|
|
.insert(mediaData)
|
|
.returning('*');
|
|
}
|
|
});
|
|
|
|
res.status(201).json(result);
|
|
} catch (err) {
|
|
if (err.message === "Listing not found") {
|
|
return res.status(404).json({ error: err.message });
|
|
}
|
|
console.error("Error adding media to listing:", err);
|
|
res.status(500).json({ error: "Internal server error" });
|
|
}
|
|
});
|
|
|
|
export default router;
|