← back to Watches
database/repositories/WatchRepository.js
332 lines
/**
* Watch Repository
* Implements repository pattern for watch data access
* Provides abstraction layer between business logic and database
*/
import pool from '../connection.js';
export class WatchRepository {
/**
* Get all watches with optional filtering and pagination
*/
async findAll(options = {}) {
const {
series,
minYear,
maxYear,
limit = 100,
offset = 0,
sortBy = 'year_introduced',
sortOrder = 'DESC'
} = options;
let query = `
SELECT
w.*,
json_build_object(
'caseSize', s.case_size,
'caseMaterial', s.case_material,
'movement', s.movement_calibre,
'waterResistance', s.water_resistance
) as specifications,
(SELECT COUNT(*) FROM price_history ph WHERE ph.watch_id = w.id) as price_point_count
FROM watches w
LEFT JOIN specifications s ON w.id = s.watch_id
WHERE w.is_active = true
`;
const params = [];
let paramCount = 0;
if (series) {
paramCount++;
query += ` AND w.series = $${paramCount}`;
params.push(series);
}
if (minYear) {
paramCount++;
query += ` AND w.year_introduced >= $${paramCount}`;
params.push(minYear);
}
if (maxYear) {
paramCount++;
query += ` AND w.year_introduced <= $${paramCount}`;
params.push(maxYear);
}
query += ` ORDER BY ${sortBy} ${sortOrder}`;
paramCount++;
query += ` LIMIT $${paramCount}`;
params.push(limit);
paramCount++;
query += ` OFFSET $${paramCount}`;
params.push(offset);
const result = await pool.query(query, params);
return result.rows;
}
/**
* Find watch by ID or slug
*/
async findById(identifier) {
const query = `
SELECT
w.*,
json_build_object(
'caseSize', s.case_size,
'caseMaterial', s.case_material,
'caseThickness', s.case_thickness,
'lugWidth', s.lug_width,
'movement', s.movement_calibre,
'movementType', s.movement_type,
'powerReserve', s.power_reserve,
'waterResistance', s.water_resistance,
'crystal', s.crystal_type,
'dialColor', s.dial_color,
'bezel', s.bezel_type,
'weight', s.weight
) as specifications,
(
SELECT json_agg(f.name)
FROM features f
INNER JOIN watch_features wf ON f.id = wf.feature_id
WHERE wf.watch_id = w.id
) as features
FROM watches w
LEFT JOIN specifications s ON w.id = s.watch_id
WHERE (w.id::text = $1 OR w.slug = $1)
AND w.is_active = true
`;
const result = await pool.query(query, [identifier]);
return result.rows[0] || null;
}
/**
* Get watch with complete price history
*/
async findByIdWithPriceHistory(identifier) {
const watch = await this.findById(identifier);
if (!watch) return null;
const priceQuery = `
SELECT
year,
price,
condition,
source,
currency,
is_estimate,
confidence_level
FROM price_history
WHERE watch_id = $1
ORDER BY year ASC
`;
const priceResult = await pool.query(priceQuery, [watch.id]);
watch.priceHistory = priceResult.rows;
return watch;
}
/**
* Full-text search across watches
*/
async search(searchTerm, options = {}) {
const { limit = 20, offset = 0 } = options;
const query = `
SELECT
w.*,
ts_rank(w.search_vector, plainto_tsquery('english', $1)) as rank
FROM watches w
WHERE w.search_vector @@ plainto_tsquery('english', $1)
AND w.is_active = true
ORDER BY rank DESC
LIMIT $2 OFFSET $3
`;
const result = await pool.query(query, [searchTerm, limit, offset]);
return result.rows;
}
/**
* Get watches by series
*/
async findBySeries(series) {
const query = `
SELECT w.*, s.*
FROM watches w
LEFT JOIN specifications s ON w.id = s.watch_id
WHERE w.series = $1
AND w.is_active = true
ORDER BY w.year_introduced DESC
`;
const result = await pool.query(query, [series]);
return result.rows;
}
/**
* Get trending watches (most viewed recently)
*/
async getTrending(daysBack = 7, limit = 10) {
const result = await pool.query(
'SELECT * FROM get_trending_watches($1, $2)',
[daysBack, limit]
);
return result.rows;
}
/**
* Get watch statistics
*/
async getStatistics(watchId) {
const query = `
SELECT * FROM watch_statistics
WHERE id = $1
`;
const result = await pool.query(query, [watchId]);
return result.rows[0] || null;
}
/**
* Get all statistics (from materialized view)
*/
async getAllStatistics(options = {}) {
const {
sortBy = 'appreciation_percentage',
sortOrder = 'DESC',
limit = 100,
offset = 0
} = options;
const query = `
SELECT * FROM watch_statistics
ORDER BY ${sortBy} ${sortOrder} NULLS LAST
LIMIT $1 OFFSET $2
`;
const result = await pool.query(query, [limit, offset]);
return result.rows;
}
/**
* Create new watch
*/
async create(watchData) {
const {
slug,
model,
series,
reference,
yearIntroduced,
description,
detailedDescription,
imageUrl,
isLimitedEdition,
limitedEditionCount
} = watchData;
const query = `
INSERT INTO watches (
slug, model, series, reference, year_introduced,
description, detailed_description, image_url,
is_limited_edition, limited_edition_count
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *
`;
const result = await pool.query(query, [
slug,
model,
series,
reference,
yearIntroduced,
description,
detailedDescription,
imageUrl,
isLimitedEdition || false,
limitedEditionCount || null
]);
return result.rows[0];
}
/**
* Update watch
*/
async update(watchId, updateData) {
const allowedFields = [
'model', 'series', 'reference', 'year_introduced',
'description', 'detailed_description', 'image_url',
'is_active', 'is_limited_edition', 'limited_edition_count'
];
const updates = [];
const params = [];
let paramCount = 0;
for (const [key, value] of Object.entries(updateData)) {
const snakeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
if (allowedFields.includes(snakeKey)) {
paramCount++;
updates.push(`${snakeKey} = $${paramCount}`);
params.push(value);
}
}
if (updates.length === 0) {
throw new Error('No valid fields to update');
}
paramCount++;
params.push(watchId);
const query = `
UPDATE watches
SET ${updates.join(', ')}
WHERE id = $${paramCount}
RETURNING *
`;
const result = await pool.query(query, params);
return result.rows[0];
}
/**
* Track view (for trending analytics)
*/
async trackView(watchId, metadata = {}) {
const { sessionId, userAgent, ipAddress } = metadata;
const query = `
INSERT INTO watch_views (watch_id, session_id, user_agent, ip_address)
VALUES ($1, $2, $3, $4)
`;
await pool.query(query, [
watchId,
sessionId || null,
userAgent || null,
ipAddress || null
]);
}
/**
* Refresh materialized views
*/
async refreshStatistics() {
await pool.query('REFRESH MATERIALIZED VIEW CONCURRENTLY watch_statistics');
}
}
export default new WatchRepository();