233 lines
6.5 KiB
TypeScript
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,
|
|
};
|
|
}
|
|
}
|