Files
motovaultpro/backend/src/features/vehicles/data/vehicles.repository.ts
2025-12-15 21:39:51 -06:00

233 lines
6.5 KiB
TypeScript

/**
* @ai-summary Data access layer for vehicles
* @ai-context All database operations, no business logic
*/
import { Pool } from 'pg';
import { Vehicle, CreateVehicleRequest, VehicleImageMeta } from '../domain/vehicles.types';
export class VehiclesRepository {
constructor(private pool: Pool) {}
async create(data: CreateVehicleRequest & { userId: string, make?: string, model?: string, year?: number }): Promise<Vehicle> {
const query = `
INSERT INTO vehicles (
user_id, vin, make, model, year,
engine, transmission, trim_level, drive_type, fuel_type,
nickname, color, license_plate, odometer_reading
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
RETURNING *
`;
const values = [
data.userId,
(data.vin && data.vin.trim().length > 0) ? data.vin.trim() : null,
data.make,
data.model,
data.year,
data.engine,
data.transmission,
data.trimLevel,
data.driveType,
data.fuelType,
data.nickname,
data.color,
data.licensePlate,
data.odometerReading || 0
];
const result = await this.pool.query(query, values);
return this.mapRow(result.rows[0]);
}
async findByUserId(userId: string): Promise<Vehicle[]> {
const query = `
SELECT * FROM vehicles
WHERE user_id = $1 AND is_active = true
ORDER BY created_at DESC
`;
const result = await this.pool.query(query, [userId]);
return result.rows.map(row => this.mapRow(row));
}
async findById(id: string): Promise<Vehicle | null> {
const query = 'SELECT * FROM vehicles WHERE id = $1';
const result = await this.pool.query(query, [id]);
if (result.rows.length === 0) {
return null;
}
return this.mapRow(result.rows[0]);
}
async findByUserAndVIN(userId: string, vin: string): Promise<Vehicle | null> {
const query = 'SELECT * FROM vehicles WHERE user_id = $1 AND vin = $2';
const result = await this.pool.query(query, [userId, vin]);
if (result.rows.length === 0) {
return null;
}
return this.mapRow(result.rows[0]);
}
async update(id: string, data: Partial<Vehicle>): Promise<Vehicle | null> {
const fields = [];
const values = [];
let paramCount = 1;
// Build dynamic update query
if (data.make !== undefined) {
fields.push(`make = $${paramCount++}`);
values.push(data.make);
}
if (data.model !== undefined) {
fields.push(`model = $${paramCount++}`);
values.push(data.model);
}
if (data.year !== undefined) {
fields.push(`year = $${paramCount++}`);
values.push(data.year);
}
if (data.engine !== undefined) {
fields.push(`engine = $${paramCount++}`);
values.push(data.engine);
}
if (data.transmission !== undefined) {
fields.push(`transmission = $${paramCount++}`);
values.push(data.transmission);
}
if (data.trimLevel !== undefined) {
fields.push(`trim_level = $${paramCount++}`);
values.push(data.trimLevel);
}
if (data.driveType !== undefined) {
fields.push(`drive_type = $${paramCount++}`);
values.push(data.driveType);
}
if (data.fuelType !== undefined) {
fields.push(`fuel_type = $${paramCount++}`);
values.push(data.fuelType);
}
if (data.nickname !== undefined) {
fields.push(`nickname = $${paramCount++}`);
values.push(data.nickname);
}
if (data.color !== undefined) {
fields.push(`color = $${paramCount++}`);
values.push(data.color);
}
if (data.licensePlate !== undefined) {
fields.push(`license_plate = $${paramCount++}`);
values.push(data.licensePlate);
}
if (data.odometerReading !== undefined) {
fields.push(`odometer_reading = $${paramCount++}`);
values.push(data.odometerReading);
}
if (fields.length === 0) {
return this.findById(id);
}
values.push(id);
const query = `
UPDATE vehicles
SET ${fields.join(', ')}
WHERE id = $${paramCount}
RETURNING *
`;
const result = await this.pool.query(query, values);
if (result.rows.length === 0) {
return null;
}
return this.mapRow(result.rows[0]);
}
async softDelete(id: string): Promise<boolean> {
const query = `
UPDATE vehicles
SET is_active = false, deleted_at = NOW()
WHERE id = $1
`;
const result = await this.pool.query(query, [id]);
return (result.rowCount ?? 0) > 0;
}
async updateImageMeta(id: string, userId: string, meta: VehicleImageMeta | null): Promise<Vehicle | null> {
if (meta === null) {
const query = `
UPDATE vehicles SET
image_storage_bucket = NULL,
image_storage_key = NULL,
image_file_name = NULL,
image_content_type = NULL,
image_file_size = NULL,
updated_at = NOW()
WHERE id = $1 AND user_id = $2
RETURNING *
`;
const result = await this.pool.query(query, [id, userId]);
return result.rows[0] ? this.mapRow(result.rows[0]) : null;
}
const query = `
UPDATE vehicles SET
image_storage_bucket = $1,
image_storage_key = $2,
image_file_name = $3,
image_content_type = $4,
image_file_size = $5,
updated_at = NOW()
WHERE id = $6 AND user_id = $7
RETURNING *
`;
const result = await this.pool.query(query, [
meta.imageStorageBucket,
meta.imageStorageKey,
meta.imageFileName,
meta.imageContentType,
meta.imageFileSize,
id,
userId
]);
return result.rows[0] ? this.mapRow(result.rows[0]) : null;
}
private mapRow(row: any): Vehicle {
return {
id: row.id,
userId: row.user_id,
vin: row.vin,
make: row.make,
model: row.model,
year: row.year,
engine: row.engine,
transmission: row.transmission,
trimLevel: row.trim_level,
driveType: row.drive_type,
fuelType: row.fuel_type,
nickname: row.nickname,
color: row.color,
licensePlate: row.license_plate,
odometerReading: row.odometer_reading,
isActive: row.is_active,
deletedAt: row.deleted_at,
createdAt: row.created_at,
updatedAt: row.updated_at,
imageStorageBucket: row.image_storage_bucket,
imageStorageKey: row.image_storage_key,
imageFileName: row.image_file_name,
imageContentType: row.image_content_type,
imageFileSize: row.image_file_size,
};
}
}