Updated listing and location routes as per new DB schema
This commit is contained in:
parent
49721086f3
commit
b0e51dd6da
BIN
db/ERD.png
BIN
db/ERD.png
Binary file not shown.
|
Before Width: | Height: | Size: 551 KiB After Width: | Height: | Size: 533 KiB |
|
|
@ -112,6 +112,12 @@ CREATE TABLE users (
|
||||||
user_type user_type_enum NOT NULL DEFAULT 'user',
|
user_type user_type_enum NOT NULL DEFAULT 'user',
|
||||||
country_code VARCHAR(10) NOT NULL DEFAULT '+91',
|
country_code VARCHAR(10) NOT NULL DEFAULT '+91',
|
||||||
|
|
||||||
|
-- Legacy role field (for backward compatibility with auth code)
|
||||||
|
role listing_role_enum, -- Deprecated: use user_type for system roles, roles[] for marketplace roles
|
||||||
|
|
||||||
|
-- Token version for global logout
|
||||||
|
token_version INT NOT NULL DEFAULT 1,
|
||||||
|
|
||||||
-- Seller Ratings (Cached for speed)
|
-- Seller Ratings (Cached for speed)
|
||||||
rating_average NUMERIC(3, 2) DEFAULT 0.00,
|
rating_average NUMERIC(3, 2) DEFAULT 0.00,
|
||||||
rating_count INT DEFAULT 0,
|
rating_count INT DEFAULT 0,
|
||||||
|
|
@ -145,23 +151,64 @@ CREATE TABLE user_devices (
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
device_identifier TEXT,
|
device_identifier TEXT,
|
||||||
device_platform TEXT NOT NULL,
|
device_platform TEXT NOT NULL,
|
||||||
|
device_model TEXT,
|
||||||
|
os_version TEXT,
|
||||||
|
app_version TEXT,
|
||||||
|
language_code TEXT,
|
||||||
|
timezone TEXT,
|
||||||
fcm_token TEXT, -- For Push Notifications
|
fcm_token TEXT, -- For Push Notifications
|
||||||
lat NUMERIC(10,7),
|
lat NUMERIC(10,7),
|
||||||
lng NUMERIC(10,7),
|
lng NUMERIC(10,7),
|
||||||
|
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
last_seen_at TIMESTAMPTZ,
|
last_seen_at TIMESTAMPTZ,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
UNIQUE(user_id, device_identifier)
|
UNIQUE(user_id, device_identifier)
|
||||||
);
|
);
|
||||||
|
CREATE INDEX idx_user_devices_user ON user_devices (user_id);
|
||||||
|
CREATE INDEX idx_user_devices_device_identifier ON user_devices (device_identifier);
|
||||||
|
CREATE INDEX idx_user_devices_platform ON user_devices (device_platform);
|
||||||
|
CREATE INDEX idx_user_devices_last_seen ON user_devices (last_seen_at);
|
||||||
|
|
||||||
CREATE TABLE refresh_tokens (
|
CREATE TABLE refresh_tokens (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
device_id VARCHAR(255) NOT NULL,
|
||||||
|
token_id UUID NOT NULL UNIQUE,
|
||||||
token_hash VARCHAR(255) NOT NULL,
|
token_hash VARCHAR(255) NOT NULL,
|
||||||
|
user_agent TEXT,
|
||||||
|
ip_address VARCHAR(45),
|
||||||
expires_at TIMESTAMPTZ NOT NULL,
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
revoked_at TIMESTAMPTZ,
|
||||||
|
reuse_detected_at TIMESTAMPTZ,
|
||||||
|
rotated_from_id UUID REFERENCES refresh_tokens(id) ON DELETE SET NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
CREATE INDEX idx_refresh_tokens_user_device ON refresh_tokens(user_id, device_id);
|
||||||
|
CREATE INDEX idx_refresh_tokens_expires ON refresh_tokens(expires_at);
|
||||||
|
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
|
||||||
|
CREATE INDEX idx_refresh_tokens_revoked ON refresh_tokens(revoked_at);
|
||||||
|
|
||||||
|
-- Authentication Audit Logging (for security monitoring)
|
||||||
|
CREATE TABLE auth_audit (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
action VARCHAR(100) NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL,
|
||||||
|
risk_level VARCHAR(20) DEFAULT 'INFO',
|
||||||
|
device_id VARCHAR(255),
|
||||||
|
ip_address VARCHAR(45),
|
||||||
|
user_agent TEXT,
|
||||||
|
meta JSONB,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_auth_audit_user ON auth_audit(user_id);
|
||||||
|
CREATE INDEX idx_auth_audit_created ON auth_audit(created_at);
|
||||||
|
CREATE INDEX idx_auth_audit_action ON auth_audit(action);
|
||||||
|
CREATE INDEX idx_auth_audit_status ON auth_audit(status);
|
||||||
|
CREATE INDEX idx_auth_audit_risk_level ON auth_audit(risk_level);
|
||||||
|
|
||||||
-- 5. MASTER DATA (Species & Breeds)
|
-- 5. MASTER DATA (Species & Breeds)
|
||||||
-- ======================================================
|
-- ======================================================
|
||||||
|
|
@ -216,6 +263,8 @@ CREATE TABLE locations (
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
CREATE INDEX idx_locations_geog ON locations USING GIST (geog); -- Spatial Index
|
CREATE INDEX idx_locations_geog ON locations USING GIST (geog); -- Spatial Index
|
||||||
|
CREATE INDEX idx_locations_user ON locations (user_id);
|
||||||
|
CREATE INDEX idx_locations_lat_lng ON locations (lat, lng);
|
||||||
CREATE TRIGGER trg_locations_updated_at BEFORE UPDATE ON locations FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
CREATE TRIGGER trg_locations_updated_at BEFORE UPDATE ON locations FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ const createNewLocation = async (client, userId, locationData) => {
|
||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
source_type,
|
source_type,
|
||||||
|
source_confidence,
|
||||||
|
selected_location,
|
||||||
// Location Details
|
// Location Details
|
||||||
is_saved_address,
|
is_saved_address,
|
||||||
location_type,
|
location_type,
|
||||||
|
|
@ -18,37 +20,42 @@ const createNewLocation = async (client, userId, locationData) => {
|
||||||
pincode,
|
pincode,
|
||||||
} = locationData;
|
} = locationData;
|
||||||
|
|
||||||
// 1a. Insert into locations
|
// 1. Insert into locations (Merged Table)
|
||||||
const insertLocationQuery = `
|
const insertLocationQuery = `
|
||||||
INSERT INTO locations (user_id, lat, lng, source_type)
|
INSERT INTO locations (
|
||||||
VALUES ($1, $2, $3, $4)
|
user_id, lat, lng, source_type, source_confidence, selected_location,
|
||||||
RETURNING id
|
is_saved_address, location_type,
|
||||||
`;
|
|
||||||
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
|
country, state, district, city_village, pincode
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||||
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
const detailsValues = [
|
const locationValues = [
|
||||||
locationId,
|
userId,
|
||||||
is_saved_address || false,
|
lat,
|
||||||
|
lng,
|
||||||
|
source_type || "manual",
|
||||||
|
source_confidence || "unknown",
|
||||||
|
selected_location || false,
|
||||||
|
is_saved_address || false,
|
||||||
location_type || "other",
|
location_type || "other",
|
||||||
country,
|
country,
|
||||||
state,
|
state,
|
||||||
district,
|
district,
|
||||||
city_village,
|
city_village,
|
||||||
pincode,
|
pincode
|
||||||
];
|
];
|
||||||
await client.query(insertLocationDetailsQuery, detailsValues);
|
|
||||||
|
|
||||||
return locationId;
|
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
|
// 1. GET / (Main Feed) - Optimized with idx_listings_feed_optimized
|
||||||
|
|
@ -64,12 +71,76 @@ router.get("/", async (req, res) => {
|
||||||
const queryParams = [status];
|
const queryParams = [status];
|
||||||
let paramCount = 1;
|
let paramCount = 1;
|
||||||
|
|
||||||
if (species_id) {
|
if (species_id) {
|
||||||
paramCount++;
|
paramCount++;
|
||||||
queryText += ` AND filter_species_id = $${paramCount}`;
|
queryText += ` AND filter_species_id = $${paramCount}`;
|
||||||
queryParams.push(species_id);
|
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) {
|
if (price_min) {
|
||||||
paramCount++;
|
paramCount++;
|
||||||
queryText += ` AND price >= $${paramCount}`;
|
queryText += ` AND price >= $${paramCount}`;
|
||||||
|
|
@ -190,33 +261,15 @@ router.post("/", async (req, res) => {
|
||||||
currency,
|
currency,
|
||||||
is_negotiable,
|
is_negotiable,
|
||||||
listing_type,
|
listing_type,
|
||||||
status,
|
animal,
|
||||||
// Animal details
|
media // Array of { media_url, media_type, is_primary, sort_order }
|
||||||
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;
|
} = req.body;
|
||||||
|
|
||||||
let final_location_id = location_id;
|
let final_location_id = animal?.location_id;
|
||||||
|
|
||||||
// 1. Create Location (if needed)
|
// 1. Create Location (if needed)
|
||||||
if (!final_location_id && new_location) {
|
if (!final_location_id && animal?.new_location) {
|
||||||
final_location_id = await createNewLocation(client, seller_id, new_location);
|
final_location_id = await createNewLocation(client, seller_id, animal.new_location);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Create Animal
|
// 2. Create Animal
|
||||||
|
|
@ -224,28 +277,29 @@ router.post("/", async (req, res) => {
|
||||||
INSERT INTO animals (
|
INSERT INTO animals (
|
||||||
species_id, breed_id, location_id, sex, age_months, weight_kg,
|
species_id, breed_id, location_id, sex, age_months, weight_kg,
|
||||||
color_markings, quantity, purpose, health_status, vaccinated,
|
color_markings, quantity, purpose, health_status, vaccinated,
|
||||||
dewormed, pregnancy_status, milk_yield_litre_per_day, ear_tag_no, description
|
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
const animalValues = [
|
const animalValues = [
|
||||||
species_id,
|
animal.species_id,
|
||||||
breed_id,
|
animal.breed_id,
|
||||||
final_location_id,
|
final_location_id,
|
||||||
sex,
|
animal.sex,
|
||||||
age_months,
|
animal.age_months,
|
||||||
weight_kg,
|
animal.weight_kg,
|
||||||
color_markings,
|
animal.color_markings,
|
||||||
quantity || 1, // Default to 1
|
animal.quantity || 1, // Default to 1
|
||||||
purpose,
|
animal.purpose,
|
||||||
health_status || "healthy", // Default
|
animal.health_status || "healthy", // Default
|
||||||
vaccinated || false,
|
animal.vaccinated || false,
|
||||||
dewormed || false,
|
animal.dewormed || false,
|
||||||
pregnancy_status || "unknown", // Default
|
animal.pregnancy_status || "unknown", // Default
|
||||||
milk_yield_litre_per_day,
|
animal.calving_number, // Added
|
||||||
ear_tag_no,
|
animal.milk_yield_litre_per_day,
|
||||||
description,
|
animal.ear_tag_no,
|
||||||
|
animal.description,
|
||||||
];
|
];
|
||||||
|
|
||||||
const animalResult = await client.query(insertAnimalQuery, animalValues);
|
const animalResult = await client.query(insertAnimalQuery, animalValues);
|
||||||
|
|
@ -253,8 +307,8 @@ router.post("/", async (req, res) => {
|
||||||
|
|
||||||
// 3. Create Listing
|
// 3. Create Listing
|
||||||
const insertListingQuery = `
|
const insertListingQuery = `
|
||||||
INSERT INTO listings (seller_id, animal_id, title, price, currency, is_negotiable, listing_type, status)
|
INSERT INTO listings (seller_id, animal_id, title, price, currency, is_negotiable, listing_type)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
const listingValues = [
|
const listingValues = [
|
||||||
|
|
@ -264,11 +318,29 @@ router.post("/", async (req, res) => {
|
||||||
price,
|
price,
|
||||||
currency,
|
currency,
|
||||||
is_negotiable,
|
is_negotiable,
|
||||||
listing_type,
|
listing_type
|
||||||
status,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const listingResult = await client.query(insertListingQuery, listingValues);
|
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");
|
await client.query("COMMIT");
|
||||||
|
|
||||||
|
|
@ -397,4 +469,51 @@ router.get("/user/:userId/favorites", async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ router.post("/", async (req, res) => {
|
||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
source_type,
|
source_type,
|
||||||
// Location Details
|
source_confidence,
|
||||||
|
selected_location,
|
||||||
is_saved_address,
|
is_saved_address,
|
||||||
location_type,
|
location_type,
|
||||||
country,
|
country,
|
||||||
|
|
@ -26,25 +27,20 @@ router.post("/", async (req, res) => {
|
||||||
|
|
||||||
// 1. Insert into locations
|
// 1. Insert into locations
|
||||||
const insertLocationQuery = `
|
const insertLocationQuery = `
|
||||||
INSERT INTO locations (user_id, lat, lng, source_type)
|
INSERT INTO locations (
|
||||||
VALUES ($1, $2, $3, $4)
|
user_id, lat, lng, source_type, source_confidence, selected_location,
|
||||||
RETURNING id, user_id, lat, lng, source_type, created_at
|
is_saved_address, location_type, country, state, district, city_village, pincode
|
||||||
`;
|
|
||||||
const locationValues = [user_id, lat, lng, source_type || "manual"];
|
|
||||||
const locationResult = await client.query(insertLocationQuery, locationValues);
|
|
||||||
const location = locationResult.rows[0];
|
|
||||||
|
|
||||||
// 2. Insert into location_details
|
|
||||||
const insertDetailsQuery = `
|
|
||||||
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)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
const detailsValues = [
|
const locationValues = [
|
||||||
location.id,
|
user_id,
|
||||||
|
lat,
|
||||||
|
lng,
|
||||||
|
source_type || "manual",
|
||||||
|
source_confidence || "low",
|
||||||
|
selected_location || false,
|
||||||
is_saved_address || false,
|
is_saved_address || false,
|
||||||
location_type || "other",
|
location_type || "other",
|
||||||
country,
|
country,
|
||||||
|
|
@ -53,13 +49,12 @@ router.post("/", async (req, res) => {
|
||||||
city_village,
|
city_village,
|
||||||
pincode,
|
pincode,
|
||||||
];
|
];
|
||||||
const detailsResult = await client.query(insertDetailsQuery, detailsValues);
|
const locationResult = await client.query(insertLocationQuery, locationValues);
|
||||||
|
|
||||||
await client.query("COMMIT");
|
await client.query("COMMIT");
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
...location,
|
...locationResult.rows[0],
|
||||||
details: detailsResult.rows[0],
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await client.query("ROLLBACK");
|
await client.query("ROLLBACK");
|
||||||
|
|
@ -75,11 +70,9 @@ router.get("/user/:userId", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { userId } = req.params;
|
const { userId } = req.params;
|
||||||
const queryText = `
|
const queryText = `
|
||||||
SELECT l.*, row_to_json(ld) as details
|
SELECT * FROM locations
|
||||||
FROM locations l
|
WHERE user_id = $1 AND deleted = FALSE
|
||||||
LEFT JOIN location_details ld ON l.id = ld.location_id
|
ORDER BY created_at DESC
|
||||||
WHERE l.user_id = $1 AND l.deleted = FALSE
|
|
||||||
ORDER BY l.created_at DESC
|
|
||||||
`;
|
`;
|
||||||
const result = await pool.query(queryText, [userId]);
|
const result = await pool.query(queryText, [userId]);
|
||||||
res.json(result.rows);
|
res.json(result.rows);
|
||||||
|
|
@ -94,10 +87,8 @@ router.get("/:id", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const queryText = `
|
const queryText = `
|
||||||
SELECT l.*, row_to_json(ld) as details
|
SELECT * FROM locations
|
||||||
FROM locations l
|
WHERE id = $1 AND deleted = FALSE
|
||||||
LEFT JOIN location_details ld ON l.id = ld.location_id
|
|
||||||
WHERE l.id = $1 AND l.deleted = FALSE
|
|
||||||
`;
|
`;
|
||||||
const result = await pool.query(queryText, [id]);
|
const result = await pool.query(queryText, [id]);
|
||||||
|
|
||||||
|
|
@ -114,14 +105,14 @@ router.get("/:id", async (req, res) => {
|
||||||
|
|
||||||
// 4. UPDATE Location
|
// 4. UPDATE Location
|
||||||
router.put("/:id", async (req, res) => {
|
router.put("/:id", async (req, res) => {
|
||||||
const client = await pool.connect();
|
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const {
|
const {
|
||||||
lat,
|
lat,
|
||||||
lng,
|
lng,
|
||||||
source_type,
|
source_type,
|
||||||
// Location Details
|
source_confidence,
|
||||||
|
selected_location,
|
||||||
is_saved_address,
|
is_saved_address,
|
||||||
location_type,
|
location_type,
|
||||||
country,
|
country,
|
||||||
|
|
@ -131,62 +122,40 @@ router.put("/:id", async (req, res) => {
|
||||||
pincode,
|
pincode,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
await client.query("BEGIN");
|
const updateQuery = `
|
||||||
|
|
||||||
// 1. Update locations
|
|
||||||
const updateLocationQuery = `
|
|
||||||
UPDATE locations
|
UPDATE locations
|
||||||
SET lat = COALESCE($1, lat),
|
SET lat = COALESCE($1, lat),
|
||||||
lng = COALESCE($2, lng),
|
lng = COALESCE($2, lng),
|
||||||
source_type = COALESCE($3, source_type)
|
source_type = COALESCE($3, source_type),
|
||||||
WHERE id = $4 AND deleted = FALSE
|
source_confidence = COALESCE($4, source_confidence),
|
||||||
|
selected_location = COALESCE($5, selected_location),
|
||||||
|
is_saved_address = COALESCE($6, is_saved_address),
|
||||||
|
location_type = COALESCE($7, location_type),
|
||||||
|
country = COALESCE($8, country),
|
||||||
|
state = COALESCE($9, state),
|
||||||
|
district = COALESCE($10, district),
|
||||||
|
city_village = COALESCE($11, city_village),
|
||||||
|
pincode = COALESCE($12, pincode)
|
||||||
|
WHERE id = $13 AND deleted = FALSE
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
const locationResult = await client.query(updateLocationQuery, [lat, lng, source_type, id]);
|
|
||||||
|
|
||||||
if (locationResult.rows.length === 0) {
|
const values = [
|
||||||
await client.query("ROLLBACK");
|
lat, lng, source_type, source_confidence, selected_location,
|
||||||
|
is_saved_address, location_type, country, state, district, city_village, pincode,
|
||||||
|
id
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = await pool.query(updateQuery, values);
|
||||||
|
|
||||||
|
if (result.rows.length === 0) {
|
||||||
return res.status(404).json({ error: "Location not found" });
|
return res.status(404).json({ error: "Location not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Update location_details
|
res.json(result.rows[0]);
|
||||||
// Note: location_details might not exist depending on legacy data, so we use ON CONFLICT or just UPDATE/INSERT logic.
|
|
||||||
// For simplicity and assuming details always exist creation:
|
|
||||||
const updateDetailsQuery = `
|
|
||||||
UPDATE location_details
|
|
||||||
SET is_saved_address = COALESCE($1, is_saved_address),
|
|
||||||
location_type = COALESCE($2, location_type),
|
|
||||||
country = COALESCE($3, country),
|
|
||||||
state = COALESCE($4, state),
|
|
||||||
district = COALESCE($5, district),
|
|
||||||
city_village = COALESCE($6, city_village),
|
|
||||||
pincode = COALESCE($7, pincode)
|
|
||||||
WHERE location_id = $8
|
|
||||||
RETURNING *
|
|
||||||
`;
|
|
||||||
const detailsResult = await client.query(updateDetailsQuery, [
|
|
||||||
is_saved_address,
|
|
||||||
location_type,
|
|
||||||
country,
|
|
||||||
state,
|
|
||||||
district,
|
|
||||||
city_village,
|
|
||||||
pincode,
|
|
||||||
id,
|
|
||||||
]);
|
|
||||||
|
|
||||||
await client.query("COMMIT");
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
...locationResult.rows[0],
|
|
||||||
details: detailsResult.rows[0] || null,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await client.query("ROLLBACK");
|
|
||||||
console.error("Error updating location:", err);
|
console.error("Error updating location:", err);
|
||||||
res.status(500).json({ error: "Internal server error" });
|
res.status(500).json({ error: "Internal server error" });
|
||||||
} finally {
|
|
||||||
client.release();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -206,11 +175,6 @@ router.delete("/:id", async (req, res) => {
|
||||||
return res.status(404).json({ error: "Location not found" });
|
return res.status(404).json({ error: "Location not found" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optionally mark details as deleted if they had a deleted column, but they don't seem to based on schema view earlier?
|
|
||||||
// Checking schema: location_details has 'deleted'.
|
|
||||||
|
|
||||||
await pool.query("UPDATE location_details SET deleted = TRUE WHERE location_id = $1", [id]);
|
|
||||||
|
|
||||||
res.json({ message: "Location deleted successfully" });
|
res.json({ message: "Location deleted successfully" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error deleting location:", err);
|
console.error("Error deleting location:", err);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue