Skip to content

Commit

Permalink
Merge pull request #2 from alamariful1727/feat/vehicles-modules
Browse files Browse the repository at this point in the history
feat: vehicle create, update and searching with pagination
  • Loading branch information
alamariful1727 authored Jun 3, 2024
2 parents 29b8132 + cde2564 commit 0244566
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 2 deletions.
9 changes: 8 additions & 1 deletion src/database/migrations/sql/1_create-table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ CREATE TABLE vehicles (
price NUMERIC(10, 2) NOT NULL,
vin VARCHAR(17) NOT NULL,
FOREIGN KEY (dealership_id) REFERENCES dealerships(id) ON DELETE CASCADE,
UNIQUE (dealership_id, make, model, year, price)
UNIQUE (dealership_id, vin)
);

-- Create Customers Table
Expand Down Expand Up @@ -48,14 +48,21 @@ CREATE TABLE sales (
);

-- Indexes for Vehicles Table
-- Make, Model, Year: For vehicle searches.
CREATE INDEX idx_vehicles_make_model_year ON vehicles (make, model, year);
-- Dealership ID: For inventory management.
CREATE INDEX idx_vehicles_dealership_id ON vehicles (dealership_id);

-- Indexes for Customers Table
-- Last Name: For customer lookups.
CREATE INDEX idx_customers_last_name ON customers (last_name);
-- Dealership ID: For managing customers per dealership.
CREATE INDEX idx_customers_dealership_id ON customers (dealership_id);

-- Indexes for Sales Table
-- Vehicle ID: For vehicle sales history.
CREATE INDEX idx_sales_vehicle_id ON sales (vehicle_id);
-- Customer ID: For customer purchase history.
CREATE INDEX idx_sales_customer_id ON sales (customer_id);
-- Dealership ID: For sales per dealership.
CREATE INDEX idx_sales_dealership_id ON sales (dealership_id);
12 changes: 11 additions & 1 deletion src/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { readFileSync } from "fs";
import { gql } from "graphql-tag";
import path from "path";
import dealershipResolver from "./resolvers/dealership.resolver";
import vehicleResolver from "./resolvers/vehicle.resolver";

const dealershipTypes = readFileSync(
path.join(__dirname, "./typeDefs/dealership.graphql"),
Expand All @@ -10,13 +11,22 @@ const dealershipTypes = readFileSync(
},
);

export const typeDefs = gql(`${dealershipTypes}`);
const vehicleTypes = readFileSync(
path.join(__dirname, "./typeDefs/vehicle.graphql"),
{
encoding: "utf-8",
},
);

export const typeDefs = gql(`${dealershipTypes} ${vehicleTypes}`);

export const resolvers = {
Query: {
...dealershipResolver.Query,
...vehicleResolver.Query,
},
Mutation: {
...dealershipResolver.Mutation,
...vehicleResolver.Mutation,
},
};
26 changes: 26 additions & 0 deletions src/graphql/resolvers/vehicle.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {
CreateVehicleArgs,
SearchVehiclesArgs,
UpdateVehicleArgs,
createVehicle,
searchVehicles,
updateVehicle,
} from "../services/vehicle.service";

const dealershipResolver = {
Query: {
async searchVehicles(_: unknown, args: SearchVehiclesArgs) {
return await searchVehicles(args.vehicleSearch);
},
},
Mutation: {
async createVehicle(_: unknown, args: CreateVehicleArgs) {
return await createVehicle(args.vehicle);
},
async updateVehicle(_: unknown, args: UpdateVehicleArgs) {
return await updateVehicle(args.vehicle);
},
},
};

export default dealershipResolver;
182 changes: 182 additions & 0 deletions src/graphql/services/vehicle.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import format from "pg-format";
import Database from "@src/database";
import log from "@src/utils/logger";

export interface IVehicle {
id: number;
dealership_id: number;
make: string;
model: string;
year: number;
price: number;
vin: string;
}

type TVehicleInput = Omit<IVehicle, "id">;

export interface CreateVehicleArgs {
vehicle: TVehicleInput;
}

type TVehicleUpdateInput = Partial<TVehicleInput> & Pick<IVehicle, "id">;

export interface UpdateVehicleArgs {
vehicle: TVehicleUpdateInput;
}

type TVehicleSearchParams = Partial<
Omit<TVehicleInput, "year" | "price"> & {
yearMin: number;
yearMax: number;
priceMin: number;
priceMax: number;
limit: number;
offset: number;
}
>;

export interface SearchVehiclesArgs {
vehicleSearch: TVehicleSearchParams;
}

export const createVehicle = async ({
dealership_id,
make,
model,
year,
price,
vin,
}: CreateVehicleArgs["vehicle"]) => {
try {
const result = await Database.executeQuery<IVehicle>(
format(
"INSERT INTO vehicles (dealership_id, make, model, year, price, vin) VALUES (%L, %L, %L, %L, %L, %L) RETURNING *",
dealership_id,
make,
model,
year,
price,
vin,
),
);
return result[0] || null;
} catch (err) {
log.error(err, "Error creating vehicle");
return null;
}
};

export const updateVehicle = async ({
id,
...updatedFields
}: UpdateVehicleArgs["vehicle"]) => {
// if updateFields are not provided, return null
if (Object.entries(updatedFields).length === 0) {
return null;
}

const updateValues = Object.entries(updatedFields).map(([key, value]) =>
format("%I = %L", key, value),
);

const sql = format(
"UPDATE vehicles SET %s WHERE id = %s RETURNING *",
updateValues.join(", "),
id,
);

try {
const result = await Database.executeQuery<IVehicle>(sql);
return result[0] || null;
} catch (err) {
log.error(err, "Error updating vehicle");
return null;
}
};

export const searchVehicles = async (
vehicleSearchParams: TVehicleSearchParams,
) => {
try {
const sql = buildSearchQuery(vehicleSearchParams);
log.info(sql);
return await Database.executeQuery<IVehicle>(sql);
} catch (err) {
log.error(err, "Error searching vehicles");
return [];
}
};

// Helper function to build the search query
const buildSearchQuery = (searchArgs: TVehicleSearchParams): string => {
let baseQuery = "SELECT * FROM vehicles";
const conditions: string[] = [];

// Default limit will be 5
if (!searchArgs.limit) {
searchArgs.limit = 5;
}

// Default offset will be 0
if (!searchArgs.offset) {
searchArgs.offset = 0;
}

// if minimum year is passed and maximum year is not passed, then maximum year will be counted will be current year
if (searchArgs.yearMin && !searchArgs.yearMax) {
searchArgs.yearMax = new Date().getFullYear();
}

// if maximum year is passed and minimum year is not passed, then minimum year will be counted will be 1990
if (!searchArgs.yearMin && searchArgs.yearMax) {
searchArgs.yearMin = 1990;
}

if (searchArgs.dealership_id) {
conditions.push(format("dealership_id = %s", searchArgs.dealership_id));
}

if (searchArgs.make) {
conditions.push(format("make ILIKE %L", `%${searchArgs.make}%`));
}

if (searchArgs.model) {
conditions.push(format("model ILIKE %L", `%${searchArgs.model}%`));
}

if (searchArgs.yearMin && searchArgs.yearMax) {
conditions.push(
format("year BETWEEN %L AND %L", searchArgs.yearMin, searchArgs.yearMax),
);
}

if (searchArgs.priceMin && searchArgs.priceMax) {
conditions.push(
format(
"price BETWEEN %L AND %L",
searchArgs.priceMin,
searchArgs.priceMax,
),
);
} else if (searchArgs.priceMin) {
conditions.push(format("price >= %L", searchArgs.priceMin));
} else if (searchArgs.priceMax) {
conditions.push(format("price <= %L", searchArgs.priceMax));
}

if (searchArgs.vin) {
conditions.push(format("vin = %L", searchArgs.vin));
}

if (conditions.length > 0) {
baseQuery += " WHERE " + conditions.join(" AND ");
}

baseQuery += format(
" LIMIT %L OFFSET %L",
searchArgs.limit,
searchArgs.offset,
);

return baseQuery;
};
50 changes: 50 additions & 0 deletions src/graphql/typeDefs/vehicle.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
type Vehicle {
id: Int!
dealership_id: Int!
make: String!
model: String!
year: Int!
price: Float!
vin: String!
}

input VehicleCreateInput {
dealership_id: Int!
make: String!
model: String!
year: Int!
price: Float!
vin: String!
}

input VehicleUpdateInput {
id: Int!
dealership_id: Int
make: String
model: String
year: Int
price: Float
vin: String
}

input VehicleSearchParams {
dealership_id: Int
make: String
model: String
yearMin: Int
yearMax: Int
priceMin: Float
priceMax: Float
vin: String
limit: Int
offset: Int
}

type Query {
searchVehicles(vehicleSearch: VehicleSearchParams): [Vehicle]
}

type Mutation {
createVehicle(vehicle: VehicleCreateInput!): Vehicle
updateVehicle(vehicle: VehicleUpdateInput!): Vehicle
}

0 comments on commit 0244566

Please sign in to comment.