feat: add batch insert operations to repositories (refs #26)

Add batchInsert methods to vehicles, fuel-logs, maintenance, and documents repositories. Multi-value INSERT syntax provides 10-100x performance improvement over individual operations for bulk data import.

- vehicles.repository: batchInsert for vehicles
- fuel-logs.repository: batchInsert for fuel logs
- maintenance.repository: batchInsertRecords and batchInsertSchedules
- documents.repository: batchInsert for documents
- All methods support empty array (immediate return) and optional transaction client
- Fix lint error: replace require() with ES6 import in test mock

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Eric Gullickson
2026-01-11 19:28:11 -06:00
parent bb8fdf33cf
commit e6af7ed5d5
5 changed files with 286 additions and 2 deletions

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' };
});