Vehicle drop down and Gas Station fixes

This commit is contained in:
Eric Gullickson
2025-12-17 10:49:29 -06:00
parent 0925a31fd4
commit cd0cfa8913
26 changed files with 133025 additions and 1779 deletions

View File

@@ -10,15 +10,12 @@ import { pool } from '../../../core/config/database';
import { logger } from '../../../core/logging/logger';
import { CreateVehicleBody, UpdateVehicleBody, VehicleParams } from '../domain/vehicles.types';
import { getStorageService } from '../../../core/storage/storage.service';
import { Transform, TransformCallback, Readable } from 'stream';
import crypto from 'crypto';
import FileType from 'file-type';
import path from 'path';
export class VehiclesController {
private vehiclesService: VehiclesService;
private static readonly MIN_YEAR = 2017;
private static readonly MAX_YEAR = 2022;
constructor() {
const repository = new VehiclesRepository(pool);
@@ -160,13 +157,13 @@ export class VehiclesController {
async getDropdownMakes(request: FastifyRequest<{ Querystring: { year: number } }>, reply: FastifyReply) {
try {
const { year } = request.query;
if (!year || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR) {
if (!year || isNaN(year)) {
return reply.code(400).send({
error: 'Bad Request',
message: `Valid year parameter is required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
message: 'Valid year parameter is required'
});
}
const makes = await this.vehiclesService.getDropdownMakes(year);
return reply.code(200).send(makes);
} catch (error) {
@@ -181,10 +178,10 @@ export class VehiclesController {
async getDropdownModels(request: FastifyRequest<{ Querystring: { year: number; make: string } }>, reply: FastifyReply) {
try {
const { year, make } = request.query;
if (!year || !make || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0) {
if (!year || isNaN(year) || !make || make.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: `Valid year and make parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
message: 'Valid year and make parameters are required'
});
}
@@ -202,10 +199,10 @@ export class VehiclesController {
async getDropdownTransmissions(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string } }>, reply: FastifyReply) {
try {
const { year, make, model, trim } = request.query;
if (!year || !make || !model || !trim || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
if (!year || isNaN(year) || !make || !model || !trim || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: `Valid year, make, model, and trim parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
message: 'Valid year, make, model, and trim parameters are required'
});
}
@@ -223,10 +220,10 @@ export class VehiclesController {
async getDropdownEngines(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string } }>, reply: FastifyReply) {
try {
const { year, make, model, trim } = request.query;
if (!year || !make || !model || !trim || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
if (!year || isNaN(year) || !make || !model || !trim || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: `Valid year, make, model, and trim parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
message: 'Valid year, make, model, and trim parameters are required'
});
}
@@ -244,10 +241,10 @@ export class VehiclesController {
async getDropdownTrims(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string } }>, reply: FastifyReply) {
try {
const { year, make, model } = request.query;
if (!year || !make || !model || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0 || model.trim().length === 0) {
if (!year || isNaN(year) || !make || !model || make.trim().length === 0 || model.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: `Valid year, make, and model parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
message: 'Valid year, make, and model parameters are required'
});
}
@@ -279,10 +276,10 @@ export class VehiclesController {
async getDropdownOptions(request: FastifyRequest<{ Querystring: { year: number; make: string; model: string; trim: string; engine?: string; transmission?: string } }>, reply: FastifyReply) {
try {
const { year, make, model, trim, engine, transmission } = request.query;
if (!year || !make || !model || !trim || year < VehiclesController.MIN_YEAR || year > VehiclesController.MAX_YEAR || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
if (!year || isNaN(year) || !make || !model || !trim || make.trim().length === 0 || model.trim().length === 0 || trim.trim().length === 0) {
return reply.code(400).send({
error: 'Bad Request',
message: `Valid year, make, model, and trim parameters are required (${VehiclesController.MIN_YEAR}-${VehiclesController.MAX_YEAR})`
message: 'Valid year, make, model, and trim parameters are required'
});
}
@@ -350,23 +347,16 @@ export class VehiclesController {
});
}
// Read first 4100 bytes to detect file type via magic bytes
// Buffer the entire file for reliable processing
// Vehicle images are typically small (< 10MB) so this is safe
const chunks: Buffer[] = [];
let totalBytes = 0;
const targetBytes = 4100;
for await (const chunk of mp.file) {
chunks.push(chunk);
totalBytes += chunk.length;
if (totalBytes >= targetBytes) {
break;
}
}
const headerBuffer = Buffer.concat(chunks);
const fileBuffer = Buffer.concat(chunks);
// Validate actual file content using magic bytes
const detectedType = await FileType.fromBuffer(headerBuffer);
const detectedType = await FileType.fromBuffer(fileBuffer);
if (!detectedType) {
logger.warn('Unable to detect file type from content', {
@@ -424,39 +414,20 @@ export class VehiclesController {
const originalName: string = mp.filename || 'vehicle-image';
const ext = contentType === 'image/jpeg' ? 'jpg' : 'png';
class CountingStream extends Transform {
public bytes = 0;
override _transform(chunk: any, _enc: BufferEncoding, cb: TransformCallback) {
this.bytes += chunk.length || 0;
cb(null, chunk);
}
}
const counter = new CountingStream();
// Create a new readable stream from the header buffer + remaining file chunks
const headerStream = Readable.from([headerBuffer]);
const remainingStream = mp.file;
// Pipe header first, then remaining content through counter
headerStream.pipe(counter, { end: false });
headerStream.on('end', () => {
remainingStream.pipe(counter);
});
const storage = getStorageService();
const bucket = 'vehicle-images';
const unique = crypto.randomBytes(32).toString('hex');
const key = `vehicle-images/${userId}/${vehicleId}/${unique}.${ext}`;
await storage.putObject(bucket, key, counter, contentType, { 'x-original-filename': originalName });
// Write buffer directly to storage
await storage.putObject(bucket, key, fileBuffer, contentType, { 'x-original-filename': originalName });
const updated = await this.vehiclesService.updateVehicleImage(vehicleId, userId, {
imageStorageBucket: bucket,
imageStorageKey: key,
imageFileName: originalName,
imageContentType: contentType,
imageFileSize: counter.bytes,
imageFileSize: fileBuffer.length,
});
logger.info('Vehicle image upload completed', {
@@ -466,7 +437,7 @@ export class VehiclesController {
fileName: originalName,
contentType,
detectedType: detectedType.mime,
fileSize: counter.bytes,
fileSize: fileBuffer.length,
storageKey: key,
});