Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: vehicle create, update and searching with pagination #2

Merged
merged 1 commit into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading