Updates to database and API for dropdowns.
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
ModelsQuery,
|
||||
TrimsQuery,
|
||||
EnginesQuery,
|
||||
TransmissionsQuery,
|
||||
VINDecodeRequest
|
||||
} from '../models/requests';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
@@ -57,12 +58,12 @@ export class PlatformController {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/platform/models?year={year}&make_id={id}
|
||||
* GET /api/platform/models?year={year}&make={make}
|
||||
*/
|
||||
async getModels(request: FastifyRequest<{ Querystring: ModelsQuery }>, reply: FastifyReply): Promise<void> {
|
||||
try {
|
||||
const { year, make_id } = request.query;
|
||||
const models = await this.vehicleDataService.getModels(this.pool, year, make_id);
|
||||
const { year, make } = request.query as any;
|
||||
const models = await this.vehicleDataService.getModels(this.pool, year, make);
|
||||
reply.code(200).send({ models });
|
||||
} catch (error) {
|
||||
logger.error('Controller error: getModels', { error, query: request.query });
|
||||
@@ -71,12 +72,12 @@ export class PlatformController {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/platform/trims?year={year}&model_id={id}
|
||||
* GET /api/platform/trims?year={year}&make={make}&model={model}
|
||||
*/
|
||||
async getTrims(request: FastifyRequest<{ Querystring: TrimsQuery }>, reply: FastifyReply): Promise<void> {
|
||||
try {
|
||||
const { year, model_id } = request.query;
|
||||
const trims = await this.vehicleDataService.getTrims(this.pool, year, model_id);
|
||||
const { year, make, model } = request.query as any;
|
||||
const trims = await this.vehicleDataService.getTrims(this.pool, year, make, model);
|
||||
reply.code(200).send({ trims });
|
||||
} catch (error) {
|
||||
logger.error('Controller error: getTrims', { error, query: request.query });
|
||||
@@ -85,12 +86,12 @@ export class PlatformController {
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/platform/engines?year={year}&model_id={id}&trim_id={id}
|
||||
* GET /api/platform/engines?year={year}&make={make}&model={model}&trim={trim}
|
||||
*/
|
||||
async getEngines(request: FastifyRequest<{ Querystring: EnginesQuery }>, reply: FastifyReply): Promise<void> {
|
||||
try {
|
||||
const { year, model_id, trim_id } = request.query;
|
||||
const engines = await this.vehicleDataService.getEngines(this.pool, year, model_id, trim_id);
|
||||
const { year, make, model, trim } = request.query as any;
|
||||
const engines = await this.vehicleDataService.getEngines(this.pool, year, make, model, trim);
|
||||
reply.code(200).send({ engines });
|
||||
} catch (error) {
|
||||
logger.error('Controller error: getEngines', { error, query: request.query });
|
||||
@@ -98,6 +99,20 @@ export class PlatformController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/platform/transmissions?year={year}&make={make}&model={model}
|
||||
*/
|
||||
async getTransmissions(request: FastifyRequest<{ Querystring: TransmissionsQuery }>, reply: FastifyReply): Promise<void> {
|
||||
try {
|
||||
const { year, make, model } = request.query as any;
|
||||
const transmissions = await this.vehicleDataService.getTransmissions(this.pool, year, make, model);
|
||||
reply.code(200).send({ transmissions });
|
||||
} catch (error) {
|
||||
logger.error('Controller error: getTransmissions', { error, query: request.query });
|
||||
reply.code(500).send({ error: 'Failed to retrieve transmissions' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/platform/vehicle?vin={vin}
|
||||
*/
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ModelsQuery,
|
||||
TrimsQuery,
|
||||
EnginesQuery,
|
||||
TransmissionsQuery,
|
||||
VINDecodeRequest
|
||||
} from '../models/requests';
|
||||
import pool from '../../../core/config/database';
|
||||
@@ -37,6 +38,10 @@ async function platformRoutes(fastify: FastifyInstance) {
|
||||
preHandler: [fastify.authenticate]
|
||||
}, controller.getEngines.bind(controller));
|
||||
|
||||
fastify.get<{ Querystring: TransmissionsQuery }>('/platform/transmissions', {
|
||||
preHandler: [fastify.authenticate]
|
||||
}, controller.getTransmissions.bind(controller));
|
||||
|
||||
fastify.get<{ Querystring: VINDecodeRequest }>('/platform/vehicle', {
|
||||
preHandler: [fastify.authenticate]
|
||||
}, controller.decodeVIN.bind(controller));
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
/**
|
||||
* @ai-summary Vehicle data repository for hierarchical queries
|
||||
* @ai-context PostgreSQL queries against vehicles schema
|
||||
* @ai-summary Vehicle data repository for hierarchical dropdown queries
|
||||
* @ai-context Queries denormalized vehicle_options table with string-based cascade
|
||||
* @ai-migration Updated to use new ETL-generated database (1.1M+ vehicle configurations)
|
||||
*/
|
||||
import { Pool } from 'pg';
|
||||
import { MakeItem, ModelItem, TrimItem, EngineItem } from '../models/responses';
|
||||
import { VINDecodeResult } from '../models/responses';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
|
||||
export class VehicleDataRepository {
|
||||
/**
|
||||
* Get distinct years from model_year table
|
||||
* Get distinct years from vehicle_options table
|
||||
*/
|
||||
async getYears(pool: Pool): Promise<number[]> {
|
||||
const query = `
|
||||
SELECT DISTINCT year
|
||||
FROM vehicles.model_year
|
||||
FROM vehicle_options
|
||||
ORDER BY year DESC
|
||||
`;
|
||||
|
||||
|
||||
try {
|
||||
const result = await pool.query(query);
|
||||
return result.rows.map(row => row.year);
|
||||
@@ -28,23 +28,16 @@ export class VehicleDataRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get makes for a specific year
|
||||
* Get makes for a specific year using database function
|
||||
*/
|
||||
async getMakes(pool: Pool, year: number): Promise<MakeItem[]> {
|
||||
async getMakes(pool: Pool, year: number): Promise<string[]> {
|
||||
const query = `
|
||||
SELECT DISTINCT ma.id, ma.name
|
||||
FROM vehicles.make ma
|
||||
JOIN vehicles.model mo ON mo.make_id = ma.id
|
||||
JOIN vehicles.model_year my ON my.model_id = mo.id AND my.year = $1
|
||||
ORDER BY ma.name
|
||||
SELECT make FROM get_makes_for_year($1)
|
||||
`;
|
||||
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [year]);
|
||||
return result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
}));
|
||||
return result.rows.map(row => row.make);
|
||||
} catch (error) {
|
||||
logger.error('Repository error: getMakes', { error, year });
|
||||
throw new Error(`Failed to retrieve makes for year ${year}`);
|
||||
@@ -52,96 +45,114 @@ export class VehicleDataRepository {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get models for a specific year and make
|
||||
* Get models for a specific year and make using database function
|
||||
*/
|
||||
async getModels(pool: Pool, year: number, makeId: number): Promise<ModelItem[]> {
|
||||
async getModels(pool: Pool, year: number, make: string): Promise<string[]> {
|
||||
const query = `
|
||||
SELECT DISTINCT mo.id, mo.name
|
||||
FROM vehicles.model mo
|
||||
JOIN vehicles.model_year my ON my.model_id = mo.id AND my.year = $1
|
||||
WHERE mo.make_id = $2
|
||||
ORDER BY mo.name
|
||||
SELECT model FROM get_models_for_year_make($1, $2)
|
||||
`;
|
||||
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [year, makeId]);
|
||||
return result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
}));
|
||||
const result = await pool.query(query, [year, make]);
|
||||
return result.rows.map(row => row.model);
|
||||
} catch (error) {
|
||||
logger.error('Repository error: getModels', { error, year, makeId });
|
||||
throw new Error(`Failed to retrieve models for year ${year}, make ${makeId}`);
|
||||
logger.error('Repository error: getModels', { error, year, make });
|
||||
throw new Error(`Failed to retrieve models for year ${year}, make ${make}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trims for a specific year and model
|
||||
* Get trims for a specific year, make, and model using database function
|
||||
*/
|
||||
async getTrims(pool: Pool, year: number, modelId: number): Promise<TrimItem[]> {
|
||||
async getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
|
||||
const query = `
|
||||
SELECT t.id, t.name
|
||||
FROM vehicles.trim t
|
||||
JOIN vehicles.model_year my ON my.id = t.model_year_id
|
||||
WHERE my.year = $1 AND my.model_id = $2
|
||||
ORDER BY t.name
|
||||
SELECT trim_name FROM get_trims_for_year_make_model($1, $2, $3)
|
||||
`;
|
||||
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [year, modelId]);
|
||||
return result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
}));
|
||||
const result = await pool.query(query, [year, make, model]);
|
||||
return result.rows.map(row => row.trim_name);
|
||||
} catch (error) {
|
||||
logger.error('Repository error: getTrims', { error, year, modelId });
|
||||
throw new Error(`Failed to retrieve trims for year ${year}, model ${modelId}`);
|
||||
logger.error('Repository error: getTrims', { error, year, make, model });
|
||||
throw new Error(`Failed to retrieve trims for year ${year}, make ${make}, model ${model}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engines for a specific year, model, and trim
|
||||
* Get engines for a specific year, make, model, and trim
|
||||
* Returns 'N/A (Electric)' for electric vehicles with NULL engine_id
|
||||
*/
|
||||
async getEngines(pool: Pool, year: number, modelId: number, trimId: number): Promise<EngineItem[]> {
|
||||
async getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]> {
|
||||
const query = `
|
||||
SELECT DISTINCT e.id, e.name
|
||||
FROM vehicles.engine e
|
||||
JOIN vehicles.trim_engine te ON te.engine_id = e.id
|
||||
JOIN vehicles.trim t ON t.id = te.trim_id
|
||||
JOIN vehicles.model_year my ON my.id = t.model_year_id
|
||||
WHERE my.year = $1
|
||||
AND my.model_id = $2
|
||||
AND t.id = $3
|
||||
ORDER BY e.name
|
||||
SELECT DISTINCT
|
||||
CASE
|
||||
WHEN vo.engine_id IS NULL THEN 'N/A (Electric)'
|
||||
ELSE e.name
|
||||
END as engine_name
|
||||
FROM vehicle_options vo
|
||||
LEFT JOIN engines e ON e.id = vo.engine_id
|
||||
WHERE vo.year = $1
|
||||
AND vo.make = $2
|
||||
AND vo.model = $3
|
||||
AND vo.trim = $4
|
||||
ORDER BY engine_name
|
||||
`;
|
||||
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [year, modelId, trimId]);
|
||||
return result.rows.map(row => ({
|
||||
id: row.id,
|
||||
name: row.name
|
||||
}));
|
||||
const result = await pool.query(query, [year, make, model, trim]);
|
||||
return result.rows.map(row => row.engine_name);
|
||||
} catch (error) {
|
||||
logger.error('Repository error: getEngines', { error, year, modelId, trimId });
|
||||
throw new Error(`Failed to retrieve engines for year ${year}, model ${modelId}, trim ${trimId}`);
|
||||
logger.error('Repository error: getEngines', { error, year, make, model, trim });
|
||||
throw new Error(`Failed to retrieve engines for year ${year}, make ${make}, model ${model}, trim ${trim}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transmissions for a specific year, make, and model
|
||||
* Returns real transmission types from the database (not hardcoded)
|
||||
*/
|
||||
async getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
|
||||
const query = `
|
||||
SELECT DISTINCT
|
||||
CASE
|
||||
WHEN vo.transmission_id IS NULL THEN 'N/A'
|
||||
ELSE t.type
|
||||
END as transmission_type
|
||||
FROM vehicle_options vo
|
||||
LEFT JOIN transmissions t ON t.id = vo.transmission_id
|
||||
WHERE vo.year = $1
|
||||
AND vo.make = $2
|
||||
AND vo.model = $3
|
||||
ORDER BY transmission_type
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [year, make, model]);
|
||||
return result.rows.map(row => row.transmission_type);
|
||||
} catch (error) {
|
||||
logger.error('Repository error: getTransmissions', { error, year, make, model });
|
||||
throw new Error(`Failed to retrieve transmissions for year ${year}, make ${make}, model ${model}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode VIN using PostgreSQL function
|
||||
* NOTE: This function may need updates after vehicles.* schema migration
|
||||
* If the old f_decode_vin function no longer exists, it will need to be
|
||||
* reimplemented against the new vehicle_options schema
|
||||
*/
|
||||
async decodeVIN(pool: Pool, vin: string): Promise<VINDecodeResult | null> {
|
||||
const query = `
|
||||
SELECT * FROM vehicles.f_decode_vin($1)
|
||||
`;
|
||||
|
||||
|
||||
try {
|
||||
const result = await pool.query(query, [vin]);
|
||||
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const row = result.rows[0];
|
||||
return {
|
||||
make: row.make || null,
|
||||
|
||||
@@ -32,15 +32,15 @@ export class PlatformCacheService {
|
||||
/**
|
||||
* Get cached makes for year
|
||||
*/
|
||||
async getMakes(year: number): Promise<any[] | null> {
|
||||
async getMakes(year: number): Promise<string[] | null> {
|
||||
const key = this.prefix + 'vehicle-data:makes:' + year;
|
||||
return await this.cacheService.get<any[]>(key);
|
||||
return await this.cacheService.get<string[]>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached makes for year
|
||||
*/
|
||||
async setMakes(year: number, makes: any[], ttl: number = 6 * 3600): Promise<void> {
|
||||
async setMakes(year: number, makes: string[], ttl: number = 6 * 3600): Promise<void> {
|
||||
const key = this.prefix + 'vehicle-data:makes:' + year;
|
||||
await this.cacheService.set(key, makes, ttl);
|
||||
}
|
||||
@@ -48,51 +48,67 @@ export class PlatformCacheService {
|
||||
/**
|
||||
* Get cached models for year and make
|
||||
*/
|
||||
async getModels(year: number, makeId: number): Promise<any[] | null> {
|
||||
const key = this.prefix + 'vehicle-data:models:' + year + ':' + makeId;
|
||||
return await this.cacheService.get<any[]>(key);
|
||||
async getModels(year: number, make: string): Promise<string[] | null> {
|
||||
const key = this.prefix + 'vehicle-data:models:' + year + ':' + make;
|
||||
return await this.cacheService.get<string[]>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached models for year and make
|
||||
*/
|
||||
async setModels(year: number, makeId: number, models: any[], ttl: number = 6 * 3600): Promise<void> {
|
||||
const key = this.prefix + 'vehicle-data:models:' + year + ':' + makeId;
|
||||
async setModels(year: number, make: string, models: string[], ttl: number = 6 * 3600): Promise<void> {
|
||||
const key = this.prefix + 'vehicle-data:models:' + year + ':' + make;
|
||||
await this.cacheService.set(key, models, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached trims for year and model
|
||||
* Get cached trims for year, make, and model
|
||||
*/
|
||||
async getTrims(year: number, modelId: number): Promise<any[] | null> {
|
||||
const key = this.prefix + 'vehicle-data:trims:' + year + ':' + modelId;
|
||||
return await this.cacheService.get<any[]>(key);
|
||||
async getTrims(year: number, make: string, model: string): Promise<string[] | null> {
|
||||
const key = this.prefix + 'vehicle-data:trims:' + year + ':' + make + ':' + model;
|
||||
return await this.cacheService.get<string[]>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached trims for year and model
|
||||
* Set cached trims for year, make, and model
|
||||
*/
|
||||
async setTrims(year: number, modelId: number, trims: any[], ttl: number = 6 * 3600): Promise<void> {
|
||||
const key = this.prefix + 'vehicle-data:trims:' + year + ':' + modelId;
|
||||
async setTrims(year: number, make: string, model: string, trims: string[], ttl: number = 6 * 3600): Promise<void> {
|
||||
const key = this.prefix + 'vehicle-data:trims:' + year + ':' + make + ':' + model;
|
||||
await this.cacheService.set(key, trims, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached engines for year, model, and trim
|
||||
* Get cached engines for year, make, model, and trim
|
||||
*/
|
||||
async getEngines(year: number, modelId: number, trimId: number): Promise<any[] | null> {
|
||||
const key = this.prefix + 'vehicle-data:engines:' + year + ':' + modelId + ':' + trimId;
|
||||
return await this.cacheService.get<any[]>(key);
|
||||
async getEngines(year: number, make: string, model: string, trim: string): Promise<string[] | null> {
|
||||
const key = this.prefix + 'vehicle-data:engines:' + year + ':' + make + ':' + model + ':' + trim;
|
||||
return await this.cacheService.get<string[]>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached engines for year, model, and trim
|
||||
* Set cached engines for year, make, model, and trim
|
||||
*/
|
||||
async setEngines(year: number, modelId: number, trimId: number, engines: any[], ttl: number = 6 * 3600): Promise<void> {
|
||||
const key = this.prefix + 'vehicle-data:engines:' + year + ':' + modelId + ':' + trimId;
|
||||
async setEngines(year: number, make: string, model: string, trim: string, engines: string[], ttl: number = 6 * 3600): Promise<void> {
|
||||
const key = this.prefix + 'vehicle-data:engines:' + year + ':' + make + ':' + model + ':' + trim;
|
||||
await this.cacheService.set(key, engines, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached transmissions for year, make, and model
|
||||
*/
|
||||
async getTransmissions(year: number, make: string, model: string): Promise<string[] | null> {
|
||||
const key = this.prefix + 'vehicle-data:transmissions:' + year + ':' + make + ':' + model;
|
||||
return await this.cacheService.get<string[]>(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cached transmissions for year, make, and model
|
||||
*/
|
||||
async setTransmissions(year: number, make: string, model: string, transmissions: string[], ttl: number = 6 * 3600): Promise<void> {
|
||||
const key = this.prefix + 'vehicle-data:transmissions:' + year + ':' + make + ':' + model;
|
||||
await this.cacheService.set(key, transmissions, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached VIN decode result
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* @ai-summary Vehicle data service with caching
|
||||
* @ai-context Business logic for hierarchical vehicle data queries
|
||||
* @ai-summary Vehicle data service with caching for dropdown queries
|
||||
* @ai-context String-based cascade queries with Redis caching
|
||||
* @ai-migration Updated to use string parameters (not IDs)
|
||||
*/
|
||||
import { Pool } from 'pg';
|
||||
import { VehicleDataRepository } from '../data/vehicle-data.repository';
|
||||
import { PlatformCacheService } from './platform-cache.service';
|
||||
import { MakeItem, ModelItem, TrimItem, EngineItem } from '../models/responses';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
|
||||
export class VehicleDataService {
|
||||
@@ -41,7 +41,7 @@ export class VehicleDataService {
|
||||
/**
|
||||
* Get makes for a year with caching
|
||||
*/
|
||||
async getMakes(pool: Pool, year: number): Promise<MakeItem[]> {
|
||||
async getMakes(pool: Pool, year: number): Promise<string[]> {
|
||||
try {
|
||||
const cached = await this.cache.getMakes(year);
|
||||
if (cached) {
|
||||
@@ -62,62 +62,83 @@ export class VehicleDataService {
|
||||
/**
|
||||
* Get models for a year and make with caching
|
||||
*/
|
||||
async getModels(pool: Pool, year: number, makeId: number): Promise<ModelItem[]> {
|
||||
async getModels(pool: Pool, year: number, make: string): Promise<string[]> {
|
||||
try {
|
||||
const cached = await this.cache.getModels(year, makeId);
|
||||
const cached = await this.cache.getModels(year, make);
|
||||
if (cached) {
|
||||
logger.debug('Models retrieved from cache', { year, makeId });
|
||||
logger.debug('Models retrieved from cache', { year, make });
|
||||
return cached;
|
||||
}
|
||||
|
||||
const models = await this.repository.getModels(pool, year, makeId);
|
||||
await this.cache.setModels(year, makeId, models);
|
||||
logger.debug('Models retrieved from database and cached', { year, makeId, count: models.length });
|
||||
const models = await this.repository.getModels(pool, year, make);
|
||||
await this.cache.setModels(year, make, models);
|
||||
logger.debug('Models retrieved from database and cached', { year, make, count: models.length });
|
||||
return models;
|
||||
} catch (error) {
|
||||
logger.error('Service error: getModels', { error, year, makeId });
|
||||
logger.error('Service error: getModels', { error, year, make });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trims for a year and model with caching
|
||||
* Get trims for a year, make, and model with caching
|
||||
*/
|
||||
async getTrims(pool: Pool, year: number, modelId: number): Promise<TrimItem[]> {
|
||||
async getTrims(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
|
||||
try {
|
||||
const cached = await this.cache.getTrims(year, modelId);
|
||||
const cached = await this.cache.getTrims(year, make, model);
|
||||
if (cached) {
|
||||
logger.debug('Trims retrieved from cache', { year, modelId });
|
||||
logger.debug('Trims retrieved from cache', { year, make, model });
|
||||
return cached;
|
||||
}
|
||||
|
||||
const trims = await this.repository.getTrims(pool, year, modelId);
|
||||
await this.cache.setTrims(year, modelId, trims);
|
||||
logger.debug('Trims retrieved from database and cached', { year, modelId, count: trims.length });
|
||||
const trims = await this.repository.getTrims(pool, year, make, model);
|
||||
await this.cache.setTrims(year, make, model, trims);
|
||||
logger.debug('Trims retrieved from database and cached', { year, make, model, count: trims.length });
|
||||
return trims;
|
||||
} catch (error) {
|
||||
logger.error('Service error: getTrims', { error, year, modelId });
|
||||
logger.error('Service error: getTrims', { error, year, make, model });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engines for a year, model, and trim with caching
|
||||
* Get engines for a year, make, model, and trim with caching
|
||||
*/
|
||||
async getEngines(pool: Pool, year: number, modelId: number, trimId: number): Promise<EngineItem[]> {
|
||||
async getEngines(pool: Pool, year: number, make: string, model: string, trim: string): Promise<string[]> {
|
||||
try {
|
||||
const cached = await this.cache.getEngines(year, modelId, trimId);
|
||||
const cached = await this.cache.getEngines(year, make, model, trim);
|
||||
if (cached) {
|
||||
logger.debug('Engines retrieved from cache', { year, modelId, trimId });
|
||||
logger.debug('Engines retrieved from cache', { year, make, model, trim });
|
||||
return cached;
|
||||
}
|
||||
|
||||
const engines = await this.repository.getEngines(pool, year, modelId, trimId);
|
||||
await this.cache.setEngines(year, modelId, trimId, engines);
|
||||
logger.debug('Engines retrieved from database and cached', { year, modelId, trimId, count: engines.length });
|
||||
const engines = await this.repository.getEngines(pool, year, make, model, trim);
|
||||
await this.cache.setEngines(year, make, model, trim, engines);
|
||||
logger.debug('Engines retrieved from database and cached', { year, make, model, trim, count: engines.length });
|
||||
return engines;
|
||||
} catch (error) {
|
||||
logger.error('Service error: getEngines', { error, year, modelId, trimId });
|
||||
logger.error('Service error: getEngines', { error, year, make, model, trim });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transmissions for a year, make, and model with caching
|
||||
*/
|
||||
async getTransmissions(pool: Pool, year: number, make: string, model: string): Promise<string[]> {
|
||||
try {
|
||||
const cached = await this.cache.getTransmissions(year, make, model);
|
||||
if (cached) {
|
||||
logger.debug('Transmissions retrieved from cache', { year, make, model });
|
||||
return cached;
|
||||
}
|
||||
|
||||
const transmissions = await this.repository.getTransmissions(pool, year, make, model);
|
||||
await this.cache.setTransmissions(year, make, model, transmissions);
|
||||
logger.debug('Transmissions retrieved from database and cached', { year, make, model, count: transmissions.length });
|
||||
return transmissions;
|
||||
} catch (error) {
|
||||
logger.error('Service error: getTransmissions', { error, year, make, model });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +43,9 @@ export const modelsQuerySchema = z.object({
|
||||
.int('Year must be an integer')
|
||||
.min(1950, 'Year must be at least 1950')
|
||||
.max(2100, 'Year must be at most 2100'),
|
||||
make_id: z.coerce.number()
|
||||
.int('Make ID must be an integer')
|
||||
.positive('Make ID must be positive')
|
||||
make: z.string()
|
||||
.min(1, 'Make is required')
|
||||
.max(100, 'Make must be less than 100 characters')
|
||||
});
|
||||
|
||||
export type ModelsQuery = z.infer<typeof modelsQuerySchema>;
|
||||
@@ -58,9 +58,12 @@ export const trimsQuerySchema = z.object({
|
||||
.int('Year must be an integer')
|
||||
.min(1950, 'Year must be at least 1950')
|
||||
.max(2100, 'Year must be at most 2100'),
|
||||
model_id: z.coerce.number()
|
||||
.int('Model ID must be an integer')
|
||||
.positive('Model ID must be positive')
|
||||
make: z.string()
|
||||
.min(1, 'Make is required')
|
||||
.max(100, 'Make must be less than 100 characters'),
|
||||
model: z.string()
|
||||
.min(1, 'Model is required')
|
||||
.max(100, 'Model must be less than 100 characters')
|
||||
});
|
||||
|
||||
export type TrimsQuery = z.infer<typeof trimsQuerySchema>;
|
||||
@@ -73,12 +76,33 @@ export const enginesQuerySchema = z.object({
|
||||
.int('Year must be an integer')
|
||||
.min(1950, 'Year must be at least 1950')
|
||||
.max(2100, 'Year must be at most 2100'),
|
||||
model_id: z.coerce.number()
|
||||
.int('Model ID must be an integer')
|
||||
.positive('Model ID must be positive'),
|
||||
trim_id: z.coerce.number()
|
||||
.int('Trim ID must be an integer')
|
||||
.positive('Trim ID must be positive')
|
||||
make: z.string()
|
||||
.min(1, 'Make is required')
|
||||
.max(100, 'Make must be less than 100 characters'),
|
||||
model: z.string()
|
||||
.min(1, 'Model is required')
|
||||
.max(100, 'Model must be less than 100 characters'),
|
||||
trim: z.string()
|
||||
.min(1, 'Trim is required')
|
||||
.max(100, 'Trim must be less than 100 characters')
|
||||
});
|
||||
|
||||
export type EnginesQuery = z.infer<typeof enginesQuerySchema>;
|
||||
|
||||
/**
|
||||
* Transmissions query parameters validation
|
||||
*/
|
||||
export const transmissionsQuerySchema = z.object({
|
||||
year: z.coerce.number()
|
||||
.int('Year must be an integer')
|
||||
.min(1950, 'Year must be at least 1950')
|
||||
.max(2100, 'Year must be at most 2100'),
|
||||
make: z.string()
|
||||
.min(1, 'Make is required')
|
||||
.max(100, 'Make must be less than 100 characters'),
|
||||
model: z.string()
|
||||
.min(1, 'Model is required')
|
||||
.max(100, 'Model must be less than 100 characters')
|
||||
});
|
||||
|
||||
export type TransmissionsQuery = z.infer<typeof transmissionsQuerySchema>;
|
||||
|
||||
@@ -1,72 +1,37 @@
|
||||
/**
|
||||
* @ai-summary Response DTOs for platform feature
|
||||
* @ai-context Type-safe response structures matching Python API
|
||||
* @ai-context Type-safe response structures for vehicle data queries
|
||||
*/
|
||||
|
||||
/**
|
||||
* Make item response
|
||||
*/
|
||||
export interface MakeItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model item response
|
||||
*/
|
||||
export interface ModelItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim item response
|
||||
*/
|
||||
export interface TrimItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Engine item response
|
||||
*/
|
||||
export interface EngineItem {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Years response
|
||||
*/
|
||||
export type YearsResponse = number[];
|
||||
|
||||
/**
|
||||
* Makes response
|
||||
* Makes response - array of make strings
|
||||
*/
|
||||
export interface MakesResponse {
|
||||
makes: MakeItem[];
|
||||
}
|
||||
export type MakesResponse = string[];
|
||||
|
||||
/**
|
||||
* Models response
|
||||
* Models response - array of model strings
|
||||
*/
|
||||
export interface ModelsResponse {
|
||||
models: ModelItem[];
|
||||
}
|
||||
export type ModelsResponse = string[];
|
||||
|
||||
/**
|
||||
* Trims response
|
||||
* Trims response - array of trim strings
|
||||
*/
|
||||
export interface TrimsResponse {
|
||||
trims: TrimItem[];
|
||||
}
|
||||
export type TrimsResponse = string[];
|
||||
|
||||
/**
|
||||
* Engines response
|
||||
* Engines response - array of engine strings (includes 'N/A (Electric)' for EVs)
|
||||
*/
|
||||
export interface EnginesResponse {
|
||||
engines: EngineItem[];
|
||||
}
|
||||
export type EnginesResponse = string[];
|
||||
|
||||
/**
|
||||
* Transmissions response - array of transmission type strings
|
||||
*/
|
||||
export type TransmissionsResponse = string[];
|
||||
|
||||
/**
|
||||
* VIN decode result (detailed vehicle information)
|
||||
|
||||
Reference in New Issue
Block a user