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:
@@ -90,6 +90,64 @@ export class DocumentsRepository {
|
|||||||
return res.rows.map(row => this.mapDocumentRecord(row));
|
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> {
|
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]);
|
await this.db.query(`UPDATE documents SET deleted_at = NOW() WHERE id = $1 AND user_id = $2`, [id, userId]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,52 @@ export class FuelLogsRepository {
|
|||||||
return this.mapRow(result.rows[0]);
|
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> {
|
async delete(id: string): Promise<boolean> {
|
||||||
const query = 'DELETE FROM fuel_logs WHERE id = $1';
|
const query = 'DELETE FROM fuel_logs WHERE id = $1';
|
||||||
const result = await this.pool.query(query, [id]);
|
const result = await this.pool.query(query, [id]);
|
||||||
|
|||||||
@@ -172,6 +172,62 @@ export class MaintenanceRepository {
|
|||||||
return res.rows[0] ? this.mapMaintenanceRecord(res.rows[0]) : null;
|
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> {
|
async deleteRecord(id: string, userId: string): Promise<void> {
|
||||||
await this.db.query(
|
await this.db.query(
|
||||||
`DELETE FROM maintenance_records WHERE id = $1 AND user_id = $2`,
|
`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;
|
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> {
|
async deleteSchedule(id: string, userId: string): Promise<void> {
|
||||||
await this.db.query(
|
await this.db.query(
|
||||||
`DELETE FROM maintenance_schedules WHERE id = $1 AND user_id = $2`,
|
`DELETE FROM maintenance_schedules WHERE id = $1 AND user_id = $2`,
|
||||||
|
|||||||
@@ -164,6 +164,57 @@ export class VehiclesRepository {
|
|||||||
return this.mapRow(result.rows[0]);
|
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> {
|
async softDelete(id: string): Promise<boolean> {
|
||||||
const query = `
|
const query = `
|
||||||
UPDATE vehicles
|
UPDATE vehicles
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import fastifyPlugin from 'fastify-plugin';
|
|||||||
|
|
||||||
// Mock auth plugin to bypass JWT validation in tests
|
// Mock auth plugin to bypass JWT validation in tests
|
||||||
jest.mock('../../../../core/plugins/auth.plugin', () => {
|
jest.mock('../../../../core/plugins/auth.plugin', () => {
|
||||||
const fp = require('fastify-plugin');
|
|
||||||
return {
|
return {
|
||||||
default: fp(async function(fastify: any) {
|
default: fastifyPlugin(async function(fastify: any) {
|
||||||
fastify.decorate('authenticate', async function(request: any, _reply: any) {
|
fastify.decorate('authenticate', async function(request: any, _reply: any) {
|
||||||
request.user = { sub: 'test-user-123' };
|
request.user = { sub: 'test-user-123' };
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user