feat: Add user data import feature (Fixes #26) #27

Merged
egullickson merged 11 commits from issue-26-add-user-data-import into main 2026-01-12 03:22:32 +00:00
5 changed files with 286 additions and 2 deletions
Showing only changes of commit e6af7ed5d5 - Show all commits

View File

@@ -90,6 +90,64 @@ export class DocumentsRepository {
return res.rows.map(row => this.mapDocumentRecord(row));
}
async batchInsert(
documents: Array<{
id: string;
userId: string;
vehicleId: string;
documentType: DocumentType;
title: string;
notes?: string | null;
details?: any;
issuedDate?: string | null;
expirationDate?: string | null;
emailNotifications?: boolean;
scanForMaintenance?: boolean;
}>,
client?: any
): Promise<DocumentRecord[]> {
if (documents.length === 0) {
return [];
}
// Multi-value INSERT for performance (avoids N round-trips)
const queryClient = client || this.db;
const placeholders: string[] = [];
const values: any[] = [];
let paramCount = 1;
documents.forEach((doc) => {
const docParams = [
doc.id,
doc.userId,
doc.vehicleId,
doc.documentType,
doc.title,
doc.notes ?? null,
doc.details ?? null,
doc.issuedDate ?? null,
doc.expirationDate ?? null,
doc.emailNotifications ?? false,
doc.scanForMaintenance ?? false
];
const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`;
placeholders.push(placeholder);
values.push(...docParams);
});
const query = `
INSERT INTO documents (
id, user_id, vehicle_id, document_type, title, notes, details, issued_date, expiration_date, email_notifications, scan_for_maintenance
)
VALUES ${placeholders.join(', ')}
RETURNING *
`;
const result = await queryClient.query(query, values);
return result.rows.map((row: any) => this.mapDocumentRecord(row));
}
async softDelete(id: string, userId: string): Promise<void> {
await this.db.query(`UPDATE documents SET deleted_at = NOW() WHERE id = $1 AND user_id = $2`, [id, userId]);
}

View File

@@ -148,6 +148,52 @@ export class FuelLogsRepository {
return this.mapRow(result.rows[0]);
}
async batchInsert(
logs: Array<CreateFuelLogRequest & { userId: string }>,
client?: any
): Promise<FuelLog[]> {
if (logs.length === 0) {
return [];
}
// Multi-value INSERT for performance (avoids N round-trips)
const queryClient = client || this.pool;
const placeholders: string[] = [];
const values: any[] = [];
let paramCount = 1;
logs.forEach((log) => {
const logParams = [
log.userId,
log.vehicleId,
log.date,
log.odometer,
log.gallons,
log.pricePerGallon,
log.totalCost,
log.station,
log.location,
log.notes
];
const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`;
placeholders.push(placeholder);
values.push(...logParams);
});
const query = `
INSERT INTO fuel_logs (
user_id, vehicle_id, date, odometer, gallons,
price_per_gallon, total_cost, station, location, notes
)
VALUES ${placeholders.join(', ')}
RETURNING *
`;
const result = await queryClient.query(query, values);
return result.rows.map((row: any) => this.mapRow(row));
}
async delete(id: string): Promise<boolean> {
const query = 'DELETE FROM fuel_logs WHERE id = $1';
const result = await this.pool.query(query, [id]);

View File

@@ -172,6 +172,62 @@ export class MaintenanceRepository {
return res.rows[0] ? this.mapMaintenanceRecord(res.rows[0]) : null;
}
async batchInsertRecords(
records: Array<{
id: string;
userId: string;
vehicleId: string;
category: MaintenanceCategory;
subtypes: string[];
date: string;
odometerReading?: number | null;
cost?: number | null;
shopName?: string | null;
notes?: string | null;
}>,
client?: any
): Promise<MaintenanceRecord[]> {
if (records.length === 0) {
return [];
}
// Multi-value INSERT for performance (avoids N round-trips)
const queryClient = client || this.db;
const placeholders: string[] = [];
const values: any[] = [];
let paramCount = 1;
records.forEach((record) => {
const recordParams = [
record.id,
record.userId,
record.vehicleId,
record.category,
record.subtypes,
record.date,
record.odometerReading ?? null,
record.cost ?? null,
record.shopName ?? null,
record.notes ?? null
];
const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}::text[], $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`;
placeholders.push(placeholder);
values.push(...recordParams);
});
const query = `
INSERT INTO maintenance_records (
id, user_id, vehicle_id, category, subtypes, date, odometer_reading, cost, shop_name, notes
)
VALUES ${placeholders.join(', ')}
RETURNING *
`;
const result = await queryClient.query(query, values);
return result.rows.map((row: any) => this.mapMaintenanceRecord(row));
}
async deleteRecord(id: string, userId: string): Promise<void> {
await this.db.query(
`DELETE FROM maintenance_records WHERE id = $1 AND user_id = $2`,
@@ -336,6 +392,80 @@ export class MaintenanceRepository {
return res.rows[0] ? this.mapMaintenanceSchedule(res.rows[0]) : null;
}
async batchInsertSchedules(
schedules: Array<{
id: string;
userId: string;
vehicleId: string;
category: MaintenanceCategory;
subtypes: string[];
intervalMonths?: number | null;
intervalMiles?: number | null;
lastServiceDate?: string | null;
lastServiceMileage?: number | null;
nextDueDate?: string | null;
nextDueMileage?: number | null;
isActive: boolean;
emailNotifications?: boolean;
scheduleType?: string;
fixedDueDate?: string | null;
reminderDays1?: number | null;
reminderDays2?: number | null;
reminderDays3?: number | null;
}>,
client?: any
): Promise<MaintenanceSchedule[]> {
if (schedules.length === 0) {
return [];
}
// Multi-value INSERT for performance (avoids N round-trips)
const queryClient = client || this.db;
const placeholders: string[] = [];
const values: any[] = [];
let paramCount = 1;
schedules.forEach((schedule) => {
const scheduleParams = [
schedule.id,
schedule.userId,
schedule.vehicleId,
schedule.category,
schedule.subtypes,
schedule.intervalMonths ?? null,
schedule.intervalMiles ?? null,
schedule.lastServiceDate ?? null,
schedule.lastServiceMileage ?? null,
schedule.nextDueDate ?? null,
schedule.nextDueMileage ?? null,
schedule.isActive,
schedule.emailNotifications ?? false,
schedule.scheduleType ?? 'interval',
schedule.fixedDueDate ?? null,
schedule.reminderDays1 ?? null,
schedule.reminderDays2 ?? null,
schedule.reminderDays3 ?? null
];
const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}::text[], $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`;
placeholders.push(placeholder);
values.push(...scheduleParams);
});
const query = `
INSERT INTO maintenance_schedules (
id, user_id, vehicle_id, category, subtypes, interval_months, interval_miles,
last_service_date, last_service_mileage, next_due_date, next_due_mileage, is_active, email_notifications,
schedule_type, fixed_due_date, reminder_days_1, reminder_days_2, reminder_days_3
)
VALUES ${placeholders.join(', ')}
RETURNING *
`;
const result = await queryClient.query(query, values);
return result.rows.map((row: any) => this.mapMaintenanceSchedule(row));
}
async deleteSchedule(id: string, userId: string): Promise<void> {
await this.db.query(
`DELETE FROM maintenance_schedules WHERE id = $1 AND user_id = $2`,

View File

@@ -164,6 +164,57 @@ export class VehiclesRepository {
return this.mapRow(result.rows[0]);
}
async batchInsert(
vehicles: Array<CreateVehicleRequest & { userId: string, make?: string, model?: string, year?: number }>,
client?: any
): Promise<Vehicle[]> {
if (vehicles.length === 0) {
return [];
}
// Multi-value INSERT for performance (avoids N round-trips)
const queryClient = client || this.pool;
const placeholders: string[] = [];
const values: any[] = [];
let paramCount = 1;
vehicles.forEach((vehicle) => {
const vehicleParams = [
vehicle.userId,
(vehicle.vin && vehicle.vin.trim().length > 0) ? vehicle.vin.trim() : null,
vehicle.make,
vehicle.model,
vehicle.year,
vehicle.engine,
vehicle.transmission,
vehicle.trimLevel,
vehicle.driveType,
vehicle.fuelType,
vehicle.nickname,
vehicle.color,
vehicle.licensePlate,
vehicle.odometerReading || 0
];
const placeholder = `($${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++}, $${paramCount++})`;
placeholders.push(placeholder);
values.push(...vehicleParams);
});
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 ${placeholders.join(', ')}
RETURNING *
`;
const result = await queryClient.query(query, values);
return result.rows.map((row: any) => this.mapRow(row));
}
async softDelete(id: string): Promise<boolean> {
const query = `
UPDATE vehicles

View File

@@ -12,9 +12,8 @@ import fastifyPlugin from 'fastify-plugin';
// Mock auth plugin to bypass JWT validation in tests
jest.mock('../../../../core/plugins/auth.plugin', () => {
const fp = require('fastify-plugin');
return {
default: fp(async function(fastify: any) {
default: fastifyPlugin(async function(fastify: any) {
fastify.decorate('authenticate', async function(request: any, _reply: any) {
request.user = { sub: 'test-user-123' };
});