diff --git a/api/src/repositories/BaseRepository.ts b/api/src/repositories/BaseRepository.ts new file mode 100644 index 0000000..90c0b48 --- /dev/null +++ b/api/src/repositories/BaseRepository.ts @@ -0,0 +1,140 @@ +/** + * Base Repository Class + * + * Provides generic CRUD operations for all repositories. + * Child repositories can extend this class and add custom methods. + */ + +import { DatabaseConnection } from '../db/sqlite'; +import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; + +/** + * Configuration interface for repository initialization + */ +export interface RepositoryConfig { + tableName: string; + idField: string; // Database column name (snake_case), e.g., 'product_id' + entityName: string; // Human-readable name for error messages, e.g., 'Product' +} + +/** + * Base repository class with generic CRUD operations + */ +export abstract class BaseRepository { + protected db: DatabaseConnection; + protected config: RepositoryConfig; + + constructor(db: DatabaseConnection, config: RepositoryConfig) { + this.db = db; + this.config = config; + } + + /** + * Get all entities + */ + async findAll(): Promise { + try { + const rows = await this.db.all( + `SELECT * FROM ${this.config.tableName} ORDER BY ${this.config.idField}` + ); + return rows.map((row) => objectToCamelCase(row) as T); + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Get entity by ID + */ + async findById(id: number): Promise { + try { + const row = await this.db.get( + `SELECT * FROM ${this.config.tableName} WHERE ${this.config.idField} = ?`, + [id] + ); + return row ? (objectToCamelCase(row) as T) : null; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Create a new entity + */ + async create(entity: any): Promise { + try { + const { sql, values } = buildInsertSQL(this.config.tableName, entity); + const result = await this.db.run(sql, values); + + const createdEntity = await this.findById(result.lastID!); + if (!createdEntity) { + throw new Error(`Failed to retrieve created ${this.config.entityName}`); + } + + return createdEntity; + } catch (error) { + handleDatabaseError(error); + } + } + + /** + * Update entity by ID + */ + async update(id: number, entity: any): Promise { + try { + const { sql, values } = buildUpdateSQL( + this.config.tableName, + entity, + `${this.config.idField} = ?` + ); + const result = await this.db.run(sql, [...values, id]); + + if (result.changes === 0) { + throw new NotFoundError(this.config.entityName, id); + } + + const updatedEntity = await this.findById(id); + if (!updatedEntity) { + throw new Error(`Failed to retrieve updated ${this.config.entityName}`); + } + + return updatedEntity; + } catch (error) { + handleDatabaseError(error, this.config.entityName, id); + } + } + + /** + * Delete entity by ID + */ + async delete(id: number): Promise { + try { + const result = await this.db.run( + `DELETE FROM ${this.config.tableName} WHERE ${this.config.idField} = ?`, + [id] + ); + + if (result.changes === 0) { + throw new NotFoundError(this.config.entityName, id); + } + } catch (error) { + handleDatabaseError(error, this.config.entityName, id); + } + } + + /** + * Check if entity exists + */ + async exists(id: number): Promise { + try { + const result = await this.db.get<{ count: number }>( + `SELECT COUNT(*) as count FROM ${this.config.tableName} WHERE ${this.config.idField} = ?`, + [id] + ); + return (result?.count || 0) > 0; + } catch (error) { + handleDatabaseError(error); + } + } +} diff --git a/api/src/repositories/branchesRepo.ts b/api/src/repositories/branchesRepo.ts index ce7babf..9313c69 100644 --- a/api/src/repositories/branchesRepo.ts +++ b/api/src/repositories/branchesRepo.ts @@ -4,110 +4,17 @@ import { getDatabase, DatabaseConnection } from '../db/sqlite'; import { Branch } from '../models/branch'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; -import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; - -export class BranchesRepository { - private db: DatabaseConnection; +import { handleDatabaseError } from '../utils/errors'; +import { objectToCamelCase } from '../utils/sql'; +import { BaseRepository } from './BaseRepository'; +export class BranchesRepository extends BaseRepository { constructor(db: DatabaseConnection) { - this.db = db; - } - - /** - * Get all branches - */ - async findAll(): Promise { - try { - const rows = await this.db.all('SELECT * FROM branches ORDER BY branch_id'); - return rows.map((row) => objectToCamelCase(row) as Branch); - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Get branch by ID - */ - async findById(id: number): Promise { - try { - const row = await this.db.get('SELECT * FROM branches WHERE branch_id = ?', [id]); - return row ? (objectToCamelCase(row) as Branch) : null; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Create a new branch - */ - async create(branch: Omit): Promise { - try { - const { sql, values } = buildInsertSQL('branches', branch); - const result = await this.db.run(sql, values); - - const createdBranch = await this.findById(result.lastID!); - if (!createdBranch) { - throw new Error('Failed to retrieve created branch'); - } - - return createdBranch; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Update branch by ID - */ - async update(id: number, branch: Partial>): Promise { - try { - const { sql, values } = buildUpdateSQL('branches', branch, 'branch_id = ?'); - const result = await this.db.run(sql, [...values, id]); - - if (result.changes === 0) { - throw new NotFoundError('Branch', id); - } - - const updatedBranch = await this.findById(id); - if (!updatedBranch) { - throw new Error('Failed to retrieve updated branch'); - } - - return updatedBranch; - } catch (error) { - handleDatabaseError(error, 'Branch', id); - } - } - - /** - * Delete branch by ID - */ - async delete(id: number): Promise { - try { - const result = await this.db.run('DELETE FROM branches WHERE branch_id = ?', [id]); - - if (result.changes === 0) { - throw new NotFoundError('Branch', id); - } - } catch (error) { - handleDatabaseError(error, 'Branch', id); - } - } - - /** - * Check if branch exists - */ - async exists(id: number): Promise { - try { - const result = await this.db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM branches WHERE branch_id = ?', - [id], - ); - return (result?.count || 0) > 0; - } catch (error) { - handleDatabaseError(error); - } + super(db, { + tableName: 'branches', + idField: 'branch_id', + entityName: 'Branch', + }); } /** diff --git a/api/src/repositories/deliveriesRepo.ts b/api/src/repositories/deliveriesRepo.ts index 68dae33..91d180a 100644 --- a/api/src/repositories/deliveriesRepo.ts +++ b/api/src/repositories/deliveriesRepo.ts @@ -4,110 +4,17 @@ import { getDatabase, DatabaseConnection } from '../db/sqlite'; import { Delivery } from '../models/delivery'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; -import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; - -export class DeliveriesRepository { - private db: DatabaseConnection; +import { handleDatabaseError } from '../utils/errors'; +import { objectToCamelCase } from '../utils/sql'; +import { BaseRepository } from './BaseRepository'; +export class DeliveriesRepository extends BaseRepository { constructor(db: DatabaseConnection) { - this.db = db; - } - - /** - * Get all deliveries - */ - async findAll(): Promise { - try { - const rows = await this.db.all('SELECT * FROM deliveries ORDER BY delivery_id'); - return rows.map((row) => objectToCamelCase(row) as Delivery); - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Get delivery by ID - */ - async findById(id: number): Promise { - try { - const row = await this.db.get('SELECT * FROM deliveries WHERE delivery_id = ?', [id]); - return row ? (objectToCamelCase(row) as Delivery) : null; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Create a new delivery - */ - async create(delivery: Omit): Promise { - try { - const { sql, values } = buildInsertSQL('deliveries', delivery); - const result = await this.db.run(sql, values); - - const createdDelivery = await this.findById(result.lastID!); - if (!createdDelivery) { - throw new Error('Failed to retrieve created delivery'); - } - - return createdDelivery; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Update delivery by ID - */ - async update(id: number, delivery: Partial>): Promise { - try { - const { sql, values } = buildUpdateSQL('deliveries', delivery, 'delivery_id = ?'); - const result = await this.db.run(sql, [...values, id]); - - if (result.changes === 0) { - throw new NotFoundError('Delivery', id); - } - - const updatedDelivery = await this.findById(id); - if (!updatedDelivery) { - throw new Error('Failed to retrieve updated delivery'); - } - - return updatedDelivery; - } catch (error) { - handleDatabaseError(error, 'Delivery', id); - } - } - - /** - * Delete delivery by ID - */ - async delete(id: number): Promise { - try { - const result = await this.db.run('DELETE FROM deliveries WHERE delivery_id = ?', [id]); - - if (result.changes === 0) { - throw new NotFoundError('Delivery', id); - } - } catch (error) { - handleDatabaseError(error, 'Delivery', id); - } - } - - /** - * Check if delivery exists - */ - async exists(id: number): Promise { - try { - const result = await this.db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM deliveries WHERE delivery_id = ?', - [id], - ); - return (result?.count || 0) > 0; - } catch (error) { - handleDatabaseError(error); - } + super(db, { + tableName: 'deliveries', + idField: 'delivery_id', + entityName: 'Delivery', + }); } /** diff --git a/api/src/repositories/headquartersRepo.ts b/api/src/repositories/headquartersRepo.ts index 112dbdc..31346c5 100644 --- a/api/src/repositories/headquartersRepo.ts +++ b/api/src/repositories/headquartersRepo.ts @@ -4,115 +4,17 @@ import { getDatabase, DatabaseConnection } from '../db/sqlite'; import { Headquarters } from '../models/headquarters'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; -import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; - -export class HeadquartersRepository { - private db: DatabaseConnection; +import { handleDatabaseError } from '../utils/errors'; +import { objectToCamelCase } from '../utils/sql'; +import { BaseRepository } from './BaseRepository'; +export class HeadquartersRepository extends BaseRepository { constructor(db: DatabaseConnection) { - this.db = db; - } - - /** - * Get all headquarters - */ - async findAll(): Promise { - try { - const rows = await this.db.all('SELECT * FROM headquarters ORDER BY headquarters_id'); - return rows.map((row) => objectToCamelCase(row) as Headquarters); - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Get headquarters by ID - */ - async findById(id: number): Promise { - try { - const row = await this.db.get('SELECT * FROM headquarters WHERE headquarters_id = ?', [ - id, - ]); - return row ? (objectToCamelCase(row) as Headquarters) : null; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Create a new headquarters - */ - async create(headquarters: Omit): Promise { - try { - const { sql, values } = buildInsertSQL('headquarters', headquarters); - const result = await this.db.run(sql, values); - - const createdHeadquarters = await this.findById(result.lastID!); - if (!createdHeadquarters) { - throw new Error('Failed to retrieve created headquarters'); - } - - return createdHeadquarters; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Update headquarters by ID - */ - async update( - id: number, - headquarters: Partial>, - ): Promise { - try { - const { sql, values } = buildUpdateSQL('headquarters', headquarters, 'headquarters_id = ?'); - const result = await this.db.run(sql, [...values, id]); - - if (result.changes === 0) { - throw new NotFoundError('Headquarters', id); - } - - const updatedHeadquarters = await this.findById(id); - if (!updatedHeadquarters) { - throw new Error('Failed to retrieve updated headquarters'); - } - - return updatedHeadquarters; - } catch (error) { - handleDatabaseError(error, 'Headquarters', id); - } - } - - /** - * Delete headquarters by ID - */ - async delete(id: number): Promise { - try { - const result = await this.db.run('DELETE FROM headquarters WHERE headquarters_id = ?', [id]); - - if (result.changes === 0) { - throw new NotFoundError('Headquarters', id); - } - } catch (error) { - handleDatabaseError(error, 'Headquarters', id); - } - } - - /** - * Check if headquarters exists - */ - async exists(id: number): Promise { - try { - const result = await this.db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM headquarters WHERE headquarters_id = ?', - [id], - ); - return (result?.count || 0) > 0; - } catch (error) { - handleDatabaseError(error); - } + super(db, { + tableName: 'headquarters', + idField: 'headquarters_id', + entityName: 'Headquarters', + }); } /** diff --git a/api/src/repositories/orderDetailDeliveriesRepo.ts b/api/src/repositories/orderDetailDeliveriesRepo.ts index eec2eac..e73d56a 100644 --- a/api/src/repositories/orderDetailDeliveriesRepo.ts +++ b/api/src/repositories/orderDetailDeliveriesRepo.ts @@ -4,127 +4,17 @@ import { getDatabase, DatabaseConnection } from '../db/sqlite'; import { OrderDetailDelivery } from '../models/orderDetailDelivery'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; -import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; - -export class OrderDetailDeliveriesRepository { - private db: DatabaseConnection; +import { handleDatabaseError } from '../utils/errors'; +import { objectToCamelCase } from '../utils/sql'; +import { BaseRepository } from './BaseRepository'; +export class OrderDetailDeliveriesRepository extends BaseRepository { constructor(db: DatabaseConnection) { - this.db = db; - } - - /** - * Get all order detail deliveries - */ - async findAll(): Promise { - try { - const rows = await this.db.all( - 'SELECT * FROM order_detail_deliveries ORDER BY order_detail_delivery_id', - ); - return rows.map((row) => objectToCamelCase(row) as OrderDetailDelivery); - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Get order detail delivery by ID - */ - async findById(id: number): Promise { - try { - const row = await this.db.get( - 'SELECT * FROM order_detail_deliveries WHERE order_detail_delivery_id = ?', - [id], - ); - return row ? (objectToCamelCase(row) as OrderDetailDelivery) : null; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Create a new order detail delivery - */ - async create( - orderDetailDelivery: Omit, - ): Promise { - try { - const { sql, values } = buildInsertSQL('order_detail_deliveries', orderDetailDelivery); - const result = await this.db.run(sql, values); - - const createdOrderDetailDelivery = await this.findById(result.lastID!); - if (!createdOrderDetailDelivery) { - throw new Error('Failed to retrieve created order detail delivery'); - } - - return createdOrderDetailDelivery; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Update order detail delivery by ID - */ - async update( - id: number, - orderDetailDelivery: Partial>, - ): Promise { - try { - const { sql, values } = buildUpdateSQL( - 'order_detail_deliveries', - orderDetailDelivery, - 'order_detail_delivery_id = ?', - ); - const result = await this.db.run(sql, [...values, id]); - - if (result.changes === 0) { - throw new NotFoundError('OrderDetailDelivery', id); - } - - const updatedOrderDetailDelivery = await this.findById(id); - if (!updatedOrderDetailDelivery) { - throw new Error('Failed to retrieve updated order detail delivery'); - } - - return updatedOrderDetailDelivery; - } catch (error) { - handleDatabaseError(error, 'OrderDetailDelivery', id); - } - } - - /** - * Delete order detail delivery by ID - */ - async delete(id: number): Promise { - try { - const result = await this.db.run( - 'DELETE FROM order_detail_deliveries WHERE order_detail_delivery_id = ?', - [id], - ); - - if (result.changes === 0) { - throw new NotFoundError('OrderDetailDelivery', id); - } - } catch (error) { - handleDatabaseError(error, 'OrderDetailDelivery', id); - } - } - - /** - * Check if order detail delivery exists - */ - async exists(id: number): Promise { - try { - const result = await this.db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM order_detail_deliveries WHERE order_detail_delivery_id = ?', - [id], - ); - return (result?.count || 0) > 0; - } catch (error) { - handleDatabaseError(error); - } + super(db, { + tableName: 'order_detail_deliveries', + idField: 'order_detail_delivery_id', + entityName: 'OrderDetailDelivery', + }); } /** diff --git a/api/src/repositories/orderDetailsRepo.ts b/api/src/repositories/orderDetailsRepo.ts index 5e1c0dc..127bf08 100644 --- a/api/src/repositories/orderDetailsRepo.ts +++ b/api/src/repositories/orderDetailsRepo.ts @@ -4,115 +4,17 @@ import { getDatabase, DatabaseConnection } from '../db/sqlite'; import { OrderDetail } from '../models/orderDetail'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; -import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; - -export class OrderDetailsRepository { - private db: DatabaseConnection; +import { handleDatabaseError } from '../utils/errors'; +import { objectToCamelCase } from '../utils/sql'; +import { BaseRepository } from './BaseRepository'; +export class OrderDetailsRepository extends BaseRepository { constructor(db: DatabaseConnection) { - this.db = db; - } - - /** - * Get all order details - */ - async findAll(): Promise { - try { - const rows = await this.db.all('SELECT * FROM order_details ORDER BY order_detail_id'); - return rows.map((row) => objectToCamelCase(row) as OrderDetail); - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Get order detail by ID - */ - async findById(id: number): Promise { - try { - const row = await this.db.get('SELECT * FROM order_details WHERE order_detail_id = ?', [ - id, - ]); - return row ? (objectToCamelCase(row) as OrderDetail) : null; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Create a new order detail - */ - async create(orderDetail: Omit): Promise { - try { - const { sql, values } = buildInsertSQL('order_details', orderDetail); - const result = await this.db.run(sql, values); - - const createdOrderDetail = await this.findById(result.lastID!); - if (!createdOrderDetail) { - throw new Error('Failed to retrieve created order detail'); - } - - return createdOrderDetail; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Update order detail by ID - */ - async update( - id: number, - orderDetail: Partial>, - ): Promise { - try { - const { sql, values } = buildUpdateSQL('order_details', orderDetail, 'order_detail_id = ?'); - const result = await this.db.run(sql, [...values, id]); - - if (result.changes === 0) { - throw new NotFoundError('OrderDetail', id); - } - - const updatedOrderDetail = await this.findById(id); - if (!updatedOrderDetail) { - throw new Error('Failed to retrieve updated order detail'); - } - - return updatedOrderDetail; - } catch (error) { - handleDatabaseError(error, 'OrderDetail', id); - } - } - - /** - * Delete order detail by ID - */ - async delete(id: number): Promise { - try { - const result = await this.db.run('DELETE FROM order_details WHERE order_detail_id = ?', [id]); - - if (result.changes === 0) { - throw new NotFoundError('OrderDetail', id); - } - } catch (error) { - handleDatabaseError(error, 'OrderDetail', id); - } - } - - /** - * Check if order detail exists - */ - async exists(id: number): Promise { - try { - const result = await this.db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM order_details WHERE order_detail_id = ?', - [id], - ); - return (result?.count || 0) > 0; - } catch (error) { - handleDatabaseError(error); - } + super(db, { + tableName: 'order_details', + idField: 'order_detail_id', + entityName: 'OrderDetail', + }); } /** diff --git a/api/src/repositories/ordersRepo.ts b/api/src/repositories/ordersRepo.ts index 8445235..7478d94 100644 --- a/api/src/repositories/ordersRepo.ts +++ b/api/src/repositories/ordersRepo.ts @@ -4,110 +4,17 @@ import { getDatabase, DatabaseConnection } from '../db/sqlite'; import { Order } from '../models/order'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; -import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; - -export class OrdersRepository { - private db: DatabaseConnection; +import { handleDatabaseError } from '../utils/errors'; +import { objectToCamelCase } from '../utils/sql'; +import { BaseRepository } from './BaseRepository'; +export class OrdersRepository extends BaseRepository { constructor(db: DatabaseConnection) { - this.db = db; - } - - /** - * Get all orders - */ - async findAll(): Promise { - try { - const rows = await this.db.all('SELECT * FROM orders ORDER BY order_id'); - return rows.map((row) => objectToCamelCase(row) as Order); - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Get order by ID - */ - async findById(id: number): Promise { - try { - const row = await this.db.get('SELECT * FROM orders WHERE order_id = ?', [id]); - return row ? (objectToCamelCase(row) as Order) : null; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Create a new order - */ - async create(order: Omit): Promise { - try { - const { sql, values } = buildInsertSQL('orders', order); - const result = await this.db.run(sql, values); - - const createdOrder = await this.findById(result.lastID!); - if (!createdOrder) { - throw new Error('Failed to retrieve created order'); - } - - return createdOrder; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Update order by ID - */ - async update(id: number, order: Partial>): Promise { - try { - const { sql, values } = buildUpdateSQL('orders', order, 'order_id = ?'); - const result = await this.db.run(sql, [...values, id]); - - if (result.changes === 0) { - throw new NotFoundError('Order', id); - } - - const updatedOrder = await this.findById(id); - if (!updatedOrder) { - throw new Error('Failed to retrieve updated order'); - } - - return updatedOrder; - } catch (error) { - handleDatabaseError(error, 'Order', id); - } - } - - /** - * Delete order by ID - */ - async delete(id: number): Promise { - try { - const result = await this.db.run('DELETE FROM orders WHERE order_id = ?', [id]); - - if (result.changes === 0) { - throw new NotFoundError('Order', id); - } - } catch (error) { - handleDatabaseError(error, 'Order', id); - } - } - - /** - * Check if order exists - */ - async exists(id: number): Promise { - try { - const result = await this.db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM orders WHERE order_id = ?', - [id], - ); - return (result?.count || 0) > 0; - } catch (error) { - handleDatabaseError(error); - } + super(db, { + tableName: 'orders', + idField: 'order_id', + entityName: 'Order', + }); } /** diff --git a/api/src/repositories/productsRepo.ts b/api/src/repositories/productsRepo.ts index def3f4f..5e45eed 100644 --- a/api/src/repositories/productsRepo.ts +++ b/api/src/repositories/productsRepo.ts @@ -4,110 +4,17 @@ import { getDatabase, DatabaseConnection } from '../db/sqlite'; import { Product } from '../models/product'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; -import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; - -export class ProductsRepository { - private db: DatabaseConnection; +import { handleDatabaseError } from '../utils/errors'; +import { objectToCamelCase } from '../utils/sql'; +import { BaseRepository } from './BaseRepository'; +export class ProductsRepository extends BaseRepository { constructor(db: DatabaseConnection) { - this.db = db; - } - - /** - * Get all products - */ - async findAll(): Promise { - try { - const rows = await this.db.all('SELECT * FROM products ORDER BY product_id'); - return rows.map((row) => objectToCamelCase(row) as Product); - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Get product by ID - */ - async findById(id: number): Promise { - try { - const row = await this.db.get('SELECT * FROM products WHERE product_id = ?', [id]); - return row ? (objectToCamelCase(row) as Product) : null; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Create a new product - */ - async create(product: Omit): Promise { - try { - const { sql, values } = buildInsertSQL('products', product); - const result = await this.db.run(sql, values); - - const createdProduct = await this.findById(result.lastID!); - if (!createdProduct) { - throw new Error('Failed to retrieve created product'); - } - - return createdProduct; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Update product by ID - */ - async update(id: number, product: Partial>): Promise { - try { - const { sql, values } = buildUpdateSQL('products', product, 'product_id = ?'); - const result = await this.db.run(sql, [...values, id]); - - if (result.changes === 0) { - throw new NotFoundError('Product', id); - } - - const updatedProduct = await this.findById(id); - if (!updatedProduct) { - throw new Error('Failed to retrieve updated product'); - } - - return updatedProduct; - } catch (error) { - handleDatabaseError(error, 'Product', id); - } - } - - /** - * Delete product by ID - */ - async delete(id: number): Promise { - try { - const result = await this.db.run('DELETE FROM products WHERE product_id = ?', [id]); - - if (result.changes === 0) { - throw new NotFoundError('Product', id); - } - } catch (error) { - handleDatabaseError(error, 'Product', id); - } - } - - /** - * Check if product exists - */ - async exists(id: number): Promise { - try { - const result = await this.db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM products WHERE product_id = ?', - [id], - ); - return (result?.count || 0) > 0; - } catch (error) { - handleDatabaseError(error); - } + super(db, { + tableName: 'products', + idField: 'product_id', + entityName: 'Product', + }); } /** @@ -131,7 +38,8 @@ export class ProductsRepository { async findByName(name: string): Promise { try { const rows = await this.db.all( - `SELECT * FROM products WHERE name LIKE '%${name}%' ORDER BY name`, + 'SELECT * FROM products WHERE name LIKE ? ORDER BY name', + [`%${name}%`], ); return rows.map((row) => objectToCamelCase(row) as Product); } catch (error) { diff --git a/api/src/repositories/suppliersRepo.ts b/api/src/repositories/suppliersRepo.ts index 77ccdea..72fb06a 100644 --- a/api/src/repositories/suppliersRepo.ts +++ b/api/src/repositories/suppliersRepo.ts @@ -4,110 +4,17 @@ import { getDatabase, DatabaseConnection } from '../db/sqlite'; import { Supplier } from '../models/supplier'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; -import { buildInsertSQL, buildUpdateSQL, objectToCamelCase } from '../utils/sql'; - -export class SuppliersRepository { - private db: DatabaseConnection; +import { handleDatabaseError } from '../utils/errors'; +import { objectToCamelCase } from '../utils/sql'; +import { BaseRepository } from './BaseRepository'; +export class SuppliersRepository extends BaseRepository { constructor(db: DatabaseConnection) { - this.db = db; - } - - /** - * Get all suppliers - */ - async findAll(): Promise { - try { - const rows = await this.db.all('SELECT * FROM suppliers ORDER BY supplier_id'); - return rows.map((row) => objectToCamelCase(row) as Supplier); - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Get supplier by ID - */ - async findById(id: number): Promise { - try { - const row = await this.db.get('SELECT * FROM suppliers WHERE supplier_id = ?', [id]); - return row ? (objectToCamelCase(row) as Supplier) : null; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Create a new supplier - */ - async create(supplier: Omit): Promise { - try { - const { sql, values } = buildInsertSQL('suppliers', supplier); - const result = await this.db.run(sql, values); - - const createdSupplier = await this.findById(result.lastID!); - if (!createdSupplier) { - throw new Error('Failed to retrieve created supplier'); - } - - return createdSupplier; - } catch (error) { - handleDatabaseError(error); - } - } - - /** - * Update supplier by ID - */ - async update(id: number, supplier: Partial>): Promise { - try { - const { sql, values } = buildUpdateSQL('suppliers', supplier, 'supplier_id = ?'); - const result = await this.db.run(sql, [...values, id]); - - if (result.changes === 0) { - throw new NotFoundError('Supplier', id); - } - - const updatedSupplier = await this.findById(id); - if (!updatedSupplier) { - throw new Error('Failed to retrieve updated supplier'); - } - - return updatedSupplier; - } catch (error) { - handleDatabaseError(error, 'Supplier', id); - } - } - - /** - * Delete supplier by ID - */ - async delete(id: number): Promise { - try { - const result = await this.db.run('DELETE FROM suppliers WHERE supplier_id = ?', [id]); - - if (result.changes === 0) { - throw new NotFoundError('Supplier', id); - } - } catch (error) { - handleDatabaseError(error, 'Supplier', id); - } - } - - /** - * Check if supplier exists - */ - async exists(id: number): Promise { - try { - const result = await this.db.get<{ count: number }>( - 'SELECT COUNT(*) as count FROM suppliers WHERE supplier_id = ?', - [id], - ); - return (result?.count || 0) > 0; - } catch (error) { - handleDatabaseError(error); - } + super(db, { + tableName: 'suppliers', + idField: 'supplier_id', + entityName: 'Supplier', + }); } /** diff --git a/api/src/routes/branch.ts b/api/src/routes/branch.ts index dec4ac2..ae4cec3 100644 --- a/api/src/routes/branch.ts +++ b/api/src/routes/branch.ts @@ -99,78 +99,14 @@ * description: Branch not found */ -import express from 'express'; -import { Branch } from '../models/branch'; import { getBranchesRepository } from '../repositories/branchesRepo'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { createCrudRouter } from '../utils/createCrudRouter'; -const router = express.Router(); - -// Create a new branch -router.post('/', async (req, res, next) => { - try { - const repo = await getBranchesRepository(); - const newBranch = await repo.create(req.body as Omit); - res.status(201).json(newBranch); - } catch (error) { - next(error); - } -}); - -// Get all branches -router.get('/', async (req, res, next) => { - try { - const repo = await getBranchesRepository(); - const branches = await repo.findAll(); - res.json(branches); - } catch (error) { - next(error); - } -}); - -// Get a branch by ID -router.get('/:id', async (req, res, next) => { - try { - const repo = await getBranchesRepository(); - const branch = await repo.findById(parseInt(req.params.id)); - if (branch) { - res.json(branch); - } else { - res.status(404).send('Branch not found'); - } - } catch (error) { - next(error); - } -}); - -// Update a branch by ID -router.put('/:id', async (req, res, next) => { - try { - const repo = await getBranchesRepository(); - const updatedBranch = await repo.update(parseInt(req.params.id), req.body); - res.json(updatedBranch); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Branch not found'); - } else { - next(error); - } - } -}); - -// Delete a branch by ID -router.delete('/:id', async (req, res, next) => { - try { - const repo = await getBranchesRepository(); - await repo.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Branch not found'); - } else { - next(error); - } - } +const router = createCrudRouter({ + getRepository: getBranchesRepository, + entityName: 'Branch', + entityNamePlural: 'Branches', + schemaName: 'Branch', }); export default router; diff --git a/api/src/routes/delivery.ts b/api/src/routes/delivery.ts index 12c12f5..37754ff 100644 --- a/api/src/routes/delivery.ts +++ b/api/src/routes/delivery.ts @@ -99,101 +99,14 @@ * description: Delivery not found */ -import express from 'express'; -import { Delivery } from '../models/delivery'; import { getDeliveriesRepository } from '../repositories/deliveriesRepo'; -import { NotFoundError } from '../utils/errors'; -import { exec } from 'child_process'; +import { createCrudRouter } from '../utils/createCrudRouter'; -const router = express.Router(); - -// Create a new delivery -router.post('/', async (req, res, next) => { - try { - const repo = await getDeliveriesRepository(); - const newDelivery = await repo.create(req.body as Omit); - res.status(201).json(newDelivery); - } catch (error) { - next(error); - } -}); - -// Get all deliveries -router.get('/', async (req, res, next) => { - try { - const repo = await getDeliveriesRepository(); - const deliveries = await repo.findAll(); - res.json(deliveries); - } catch (error) { - next(error); - } -}); - -// Get a delivery by ID -router.get('/:id', async (req, res, next) => { - try { - const repo = await getDeliveriesRepository(); - const delivery = await repo.findById(parseInt(req.params.id)); - if (delivery) { - res.json(delivery); - } else { - res.status(404).send('Delivery not found'); - } - } catch (error) { - next(error); - } -}); - -// Update the status of a delivery -router.put('/:id/status', async (req, res, next) => { - try { - const { status } = req.body; - const repo = await getDeliveriesRepository(); - const delivery = await repo.findById(parseInt(req.params.id)); - - if (delivery) { - const updatedDelivery = await repo.updateStatus(parseInt(req.params.id), status); - res.json(updatedDelivery); - } else { - res.status(404).send('Delivery not found'); - } - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Delivery not found'); - } else { - next(error); - } - } -}); - -// Update a delivery by ID -router.put('/:id', async (req, res, next) => { - try { - const repo = await getDeliveriesRepository(); - const updatedDelivery = await repo.update(parseInt(req.params.id), req.body); - res.json(updatedDelivery); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Delivery not found'); - } else { - next(error); - } - } -}); - -// Delete a delivery by ID -router.delete('/:id', async (req, res, next) => { - try { - const repo = await getDeliveriesRepository(); - await repo.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Delivery not found'); - } else { - next(error); - } - } +const router = createCrudRouter({ + getRepository: getDeliveriesRepository, + entityName: 'Delivery', + entityNamePlural: 'Deliveries', + schemaName: 'Delivery', }); export default router; diff --git a/api/src/routes/headquarters.ts b/api/src/routes/headquarters.ts index 5ec459b..bbf1843 100644 --- a/api/src/routes/headquarters.ts +++ b/api/src/routes/headquarters.ts @@ -99,78 +99,14 @@ * description: Headquarters not found */ -import express from 'express'; -import { Headquarters } from '../models/headquarters'; import { getHeadquartersRepository } from '../repositories/headquartersRepo'; -import { NotFoundError } from '../utils/errors'; +import { createCrudRouter } from '../utils/createCrudRouter'; -const router = express.Router(); - -// Create a new headquarters -router.post('/', async (req, res, next) => { - try { - const repo = await getHeadquartersRepository(); - const newHeadquarters = await repo.create(req.body as Omit); - res.status(201).json(newHeadquarters); - } catch (error) { - next(error); - } -}); - -// Get all headquarters -router.get('/', async (req, res, next) => { - try { - const repo = await getHeadquartersRepository(); - const headquarters = await repo.findAll(); - res.json(headquarters); - } catch (error) { - next(error); - } -}); - -// Get a headquarters by ID -router.get('/:id', async (req, res, next) => { - try { - const repo = await getHeadquartersRepository(); - const headquarters = await repo.findById(parseInt(req.params.id)); - if (headquarters) { - res.json(headquarters); - } else { - res.status(404).send('Headquarters not found'); - } - } catch (error) { - next(error); - } -}); - -// Update a headquarters by ID -router.put('/:id', async (req, res, next) => { - try { - const repo = await getHeadquartersRepository(); - const updatedHeadquarters = await repo.update(parseInt(req.params.id), req.body); - res.json(updatedHeadquarters); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Headquarters not found'); - } else { - next(error); - } - } -}); - -// Delete a headquarters by ID -router.delete('/:id', async (req, res, next) => { - try { - const repo = await getHeadquartersRepository(); - await repo.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Headquarters not found'); - } else { - next(error); - } - } +const router = createCrudRouter({ + getRepository: getHeadquartersRepository, + entityName: 'Headquarters', + entityNamePlural: 'Headquarters', + schemaName: 'Headquarters', }); export default router; diff --git a/api/src/routes/order.ts b/api/src/routes/order.ts index ef8465b..fed5ad0 100644 --- a/api/src/routes/order.ts +++ b/api/src/routes/order.ts @@ -99,78 +99,14 @@ * description: Order not found */ -import express from 'express'; -import { Order } from '../models/order'; import { getOrdersRepository } from '../repositories/ordersRepo'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { createCrudRouter } from '../utils/createCrudRouter'; -const router = express.Router(); - -// Create a new order -router.post('/', async (req, res, next) => { - try { - const repo = await getOrdersRepository(); - const newOrder = await repo.create(req.body as Omit); - res.status(201).json(newOrder); - } catch (error) { - next(error); - } -}); - -// Get all orders -router.get('/', async (req, res, next) => { - try { - const repo = await getOrdersRepository(); - const orders = await repo.findAll(); - res.json(orders); - } catch (error) { - next(error); - } -}); - -// Get an order by ID -router.get('/:id', async (req, res, next) => { - try { - const repo = await getOrdersRepository(); - const order = await repo.findById(parseInt(req.params.id)); - if (order) { - res.json(order); - } else { - res.status(404).send('Order not found'); - } - } catch (error) { - next(error); - } -}); - -// Update an order by ID -router.put('/:id', async (req, res, next) => { - try { - const repo = await getOrdersRepository(); - const updatedOrder = await repo.update(parseInt(req.params.id), req.body); - res.json(updatedOrder); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Order not found'); - } else { - next(error); - } - } -}); - -// Delete an order by ID -router.delete('/:id', async (req, res, next) => { - try { - const repo = await getOrdersRepository(); - await repo.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Order not found'); - } else { - next(error); - } - } +const router = createCrudRouter({ + getRepository: getOrdersRepository, + entityName: 'Order', + entityNamePlural: 'Orders', + schemaName: 'Order', }); export default router; diff --git a/api/src/routes/orderDetail.ts b/api/src/routes/orderDetail.ts index f12352d..6b66a62 100644 --- a/api/src/routes/orderDetail.ts +++ b/api/src/routes/orderDetail.ts @@ -99,78 +99,14 @@ * description: Order detail not found */ -import express from 'express'; -import { OrderDetail } from '../models/orderDetail'; import { getOrderDetailsRepository } from '../repositories/orderDetailsRepo'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { createCrudRouter } from '../utils/createCrudRouter'; -const router = express.Router(); - -// Create a new order detail -router.post('/', async (req, res, next) => { - try { - const repo = await getOrderDetailsRepository(); - const newOrderDetail = await repo.create(req.body as Omit); - res.status(201).json(newOrderDetail); - } catch (error) { - next(error); - } -}); - -// Get all order details -router.get('/', async (req, res, next) => { - try { - const repo = await getOrderDetailsRepository(); - const orderDetails = await repo.findAll(); - res.json(orderDetails); - } catch (error) { - next(error); - } -}); - -// Get an order detail by ID -router.get('/:id', async (req, res, next) => { - try { - const repo = await getOrderDetailsRepository(); - const orderDetail = await repo.findById(parseInt(req.params.id)); - if (orderDetail) { - res.json(orderDetail); - } else { - res.status(404).send('Order detail not found'); - } - } catch (error) { - next(error); - } -}); - -// Update an order detail by ID -router.put('/:id', async (req, res, next) => { - try { - const repo = await getOrderDetailsRepository(); - const updatedOrderDetail = await repo.update(parseInt(req.params.id), req.body); - res.json(updatedOrderDetail); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Order detail not found'); - } else { - next(error); - } - } -}); - -// Delete an order detail by ID -router.delete('/:id', async (req, res, next) => { - try { - const repo = await getOrderDetailsRepository(); - await repo.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Order detail not found'); - } else { - next(error); - } - } +const router = createCrudRouter({ + getRepository: getOrderDetailsRepository, + entityName: 'OrderDetail', + entityNamePlural: 'OrderDetails', + schemaName: 'OrderDetail', }); export default router; diff --git a/api/src/routes/orderDetailDelivery.ts b/api/src/routes/orderDetailDelivery.ts index 8f910da..50700ce 100644 --- a/api/src/routes/orderDetailDelivery.ts +++ b/api/src/routes/orderDetailDelivery.ts @@ -99,80 +99,14 @@ * description: Order detail delivery not found */ -import express from 'express'; -import { OrderDetailDelivery } from '../models/orderDetailDelivery'; import { getOrderDetailDeliveriesRepository } from '../repositories/orderDetailDeliveriesRepo'; -import { NotFoundError } from '../utils/errors'; +import { createCrudRouter } from '../utils/createCrudRouter'; -const router = express.Router(); - -// Create a new order detail delivery -router.post('/', async (req, res, next) => { - try { - const repo = await getOrderDetailDeliveriesRepository(); - const newOrderDetailDelivery = await repo.create( - req.body as Omit, - ); - res.status(201).json(newOrderDetailDelivery); - } catch (error) { - next(error); - } -}); - -// Get all order detail deliveries -router.get('/', async (req, res, next) => { - try { - const repo = await getOrderDetailDeliveriesRepository(); - const orderDetailDeliveries = await repo.findAll(); - res.json(orderDetailDeliveries); - } catch (error) { - next(error); - } -}); - -// Get an order detail delivery by ID -router.get('/:id', async (req, res, next) => { - try { - const repo = await getOrderDetailDeliveriesRepository(); - const orderDetailDelivery = await repo.findById(parseInt(req.params.id)); - if (orderDetailDelivery) { - res.json(orderDetailDelivery); - } else { - res.status(404).send('Order detail delivery not found'); - } - } catch (error) { - next(error); - } -}); - -// Update an order detail delivery by ID -router.put('/:id', async (req, res, next) => { - try { - const repo = await getOrderDetailDeliveriesRepository(); - const updatedOrderDetailDelivery = await repo.update(parseInt(req.params.id), req.body); - res.json(updatedOrderDetailDelivery); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Order detail delivery not found'); - } else { - next(error); - } - } -}); - -// Delete an order detail delivery by ID -router.delete('/:id', async (req, res, next) => { - try { - const repo = await getOrderDetailDeliveriesRepository(); - await repo.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Order detail delivery not found'); - } else { - next(error); - } - } +const router = createCrudRouter({ + getRepository: getOrderDetailDeliveriesRepository, + entityName: 'OrderDetailDelivery', + entityNamePlural: 'OrderDetailDeliveries', + schemaName: 'OrderDetailDelivery', }); export default router; diff --git a/api/src/routes/product.ts b/api/src/routes/product.ts index 9d12f11..e2c466f 100644 --- a/api/src/routes/product.ts +++ b/api/src/routes/product.ts @@ -100,92 +100,25 @@ */ import express from 'express'; -import { Product } from '../models/product'; import { getProductsRepository } from '../repositories/productsRepo'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { createCrudRouter } from '../utils/createCrudRouter'; -const router = express.Router(); - -// Create a new product -router.post('/', async (req, res, next) => { - try { - const repo = await getProductsRepository(); - const newProduct = await repo.create(req.body as Omit); - res.status(201).json(newProduct); - } catch (error) { - next(error); - } -}); - -// Get all products -router.get('/', async (req, res, next) => { - try { - const repo = await getProductsRepository(); - const products = await repo.findAll(); - res.json(products); - } catch (error) { - next(error); - } -}); - -// Get a product by ID -router.get('/:id', async (req, res, next) => { - try { - const repo = await getProductsRepository(); - const product = await repo.findById(parseInt(req.params.id)); - if (product) { - res.json(product); - } else { - res.status(404).send('Product not found'); - } - } catch (error) { - next(error); - } +const router = createCrudRouter({ + getRepository: getProductsRepository, + entityName: 'Product', + entityNamePlural: 'Products', + schemaName: 'Product', }); -// Get a product by name +// Custom route: Find products by name (partial match) router.get('/name/:name', async (req, res, next) => { try { const repo = await getProductsRepository(); - const product = await repo.findByName(req.params.name); - if (product) { - res.json(product); - } else { - res.status(404).send('Product not found'); - } + const products = await repo.findByName(req.params.name); + res.json(products); } catch (error) { next(error); } }); -// Update a product by ID -router.put('/:id', async (req, res, next) => { - try { - const repo = await getProductsRepository(); - const updatedProduct = await repo.update(parseInt(req.params.id), req.body); - res.json(updatedProduct); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Product not found'); - } else { - next(error); - } - } -}); - -// Delete a product by ID -router.delete('/:id', async (req, res, next) => { - try { - const repo = await getProductsRepository(); - await repo.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Product not found'); - } else { - next(error); - } - } -}); - export default router; diff --git a/api/src/routes/supplier.ts b/api/src/routes/supplier.ts index dd605bd..f7e15ee 100644 --- a/api/src/routes/supplier.ts +++ b/api/src/routes/supplier.ts @@ -99,78 +99,14 @@ * description: Supplier not found */ -import express from 'express'; -import { Supplier } from '../models/supplier'; import { getSuppliersRepository } from '../repositories/suppliersRepo'; -import { handleDatabaseError, NotFoundError } from '../utils/errors'; +import { createCrudRouter } from '../utils/createCrudRouter'; -const router = express.Router(); - -// Create a new supplier -router.post('/', async (req, res, next) => { - try { - const repo = await getSuppliersRepository(); - const newSupplier = await repo.create(req.body as Omit); - res.status(201).json(newSupplier); - } catch (error) { - next(error); - } -}); - -// Get all suppliers -router.get('/', async (req, res, next) => { - try { - const repo = await getSuppliersRepository(); - const suppliers = await repo.findAll(); - res.json(suppliers); - } catch (error) { - next(error); - } -}); - -// Get a supplier by ID -router.get('/:id', async (req, res, next) => { - try { - const repo = await getSuppliersRepository(); - const supplier = await repo.findById(parseInt(req.params.id)); - if (supplier) { - res.json(supplier); - } else { - res.status(404).send('Supplier not found'); - } - } catch (error) { - next(error); - } -}); - -// Update a supplier by ID -router.put('/:id', async (req, res, next) => { - try { - const repo = await getSuppliersRepository(); - const updatedSupplier = await repo.update(parseInt(req.params.id), req.body); - res.json(updatedSupplier); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Supplier not found'); - } else { - next(error); - } - } -}); - -// Delete a supplier by ID -router.delete('/:id', async (req, res, next) => { - try { - const repo = await getSuppliersRepository(); - await repo.delete(parseInt(req.params.id)); - res.status(204).send(); - } catch (error) { - if (error instanceof NotFoundError) { - res.status(404).send('Supplier not found'); - } else { - next(error); - } - } +const router = createCrudRouter({ + getRepository: getSuppliersRepository, + entityName: 'Supplier', + entityNamePlural: 'Suppliers', + schemaName: 'Supplier', }); export default router; diff --git a/api/src/utils/createCrudRouter.ts b/api/src/utils/createCrudRouter.ts new file mode 100644 index 0000000..12d25cd --- /dev/null +++ b/api/src/utils/createCrudRouter.ts @@ -0,0 +1,216 @@ +/** + * Generic CRUD Router Factory + * + * Generates standard CRUD routes for entities with a BaseRepository. + * Reduces boilerplate by ~85% while maintaining full type safety. + */ + +import express, { Router } from 'express'; +import { BaseRepository } from '../repositories/BaseRepository'; +import { NotFoundError } from './errors'; + +export interface CrudRouterConfig { + /** Function that returns the repository instance */ + getRepository: () => Promise>; + /** Entity name for error messages (e.g., 'Product', 'Branch') */ + entityName: string; + /** Plural entity name for Swagger tags (e.g., 'Products', 'Branches') */ + entityNamePlural: string; + /** Schema name for Swagger references (e.g., 'Product', 'Branch') */ + schemaName: string; +} + +/** + * Creates a router with standard CRUD operations + * @param config Configuration for the CRUD router + * @returns Express Router with CRUD endpoints + */ +export function createCrudRouter( + config: CrudRouterConfig +): Router { + const router = express.Router(); + const { getRepository, entityName } = config; + + // Create a new entity + router.post('/', async (req, res, next) => { + try { + const repo = await getRepository(); + const newEntity = await repo.create(req.body); + res.status(201).json(newEntity); + } catch (error) { + next(error); + } + }); + + // Get all entities + router.get('/', async (req, res, next) => { + try { + const repo = await getRepository(); + const entities = await repo.findAll(); + res.json(entities); + } catch (error) { + next(error); + } + }); + + // Get entity by ID + router.get('/:id', async (req, res, next) => { + try { + const repo = await getRepository(); + const entity = await repo.findById(parseInt(req.params.id)); + if (entity) { + res.json(entity); + } else { + res.status(404).send(`${entityName} not found`); + } + } catch (error) { + next(error); + } + }); + + // Update entity by ID + router.put('/:id', async (req, res, next) => { + try { + const repo = await getRepository(); + const updatedEntity = await repo.update(parseInt(req.params.id), req.body); + res.json(updatedEntity); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send(`${entityName} not found`); + } else { + next(error); + } + } + }); + + // Delete entity by ID + router.delete('/:id', async (req, res, next) => { + try { + const repo = await getRepository(); + await repo.delete(parseInt(req.params.id)); + res.status(204).send(); + } catch (error) { + if (error instanceof NotFoundError) { + res.status(404).send(`${entityName} not found`); + } else { + next(error); + } + } + }); + + return router; +} + +/** + * Helper function to generate Swagger documentation comment block for CRUD routes. + * This should be placed at the top of the route file. + * + * @param config Configuration for the CRUD router + * @returns String with Swagger JSDoc comments + */ +export function generateSwaggerDocs(config: CrudRouterConfig): string { + const { entityName, entityNamePlural, schemaName } = config; + const entityLower = entityName.toLowerCase(); + const entityPlural = entityNamePlural.toLowerCase(); + + return `/** + * @swagger + * tags: + * name: ${entityNamePlural} + * description: API endpoints for managing ${entityPlural} + */ + +/** + * @swagger + * /api/${entityPlural}: + * get: + * summary: Returns all ${entityPlural} + * tags: [${entityNamePlural}] + * responses: + * 200: + * description: List of all ${entityPlural} + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/${schemaName}' + * post: + * summary: Create a new ${entityLower} + * tags: [${entityNamePlural}] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/${schemaName}' + * responses: + * 201: + * description: ${entityName} created successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/${schemaName}' + * + * /api/${entityPlural}/{id}: + * get: + * summary: Get a ${entityLower} by ID + * tags: [${entityNamePlural}] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ${entityName} ID + * responses: + * 200: + * description: ${entityName} found + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/${schemaName}' + * 404: + * description: ${entityName} not found + * put: + * summary: Update a ${entityLower} + * tags: [${entityNamePlural}] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ${entityName} ID + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/${schemaName}' + * responses: + * 200: + * description: ${entityName} updated successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/${schemaName}' + * 404: + * description: ${entityName} not found + * delete: + * summary: Delete a ${entityLower} + * tags: [${entityNamePlural}] + * parameters: + * - in: path + * name: id + * required: true + * schema: + * type: integer + * description: ${entityName} ID + * responses: + * 204: + * description: ${entityName} deleted successfully + * 404: + * description: ${entityName} not found + */`; +} diff --git a/frontend/src/components/admin/AdminProducts.tsx b/frontend/src/components/admin/AdminProducts.tsx index 2cfad3f..9008e59 100644 --- a/frontend/src/components/admin/AdminProducts.tsx +++ b/frontend/src/components/admin/AdminProducts.tsx @@ -5,27 +5,10 @@ import { Navigate } from 'react-router-dom'; import ProductForm from '../entity/product/ProductForm'; import axios from 'axios'; import { api } from '../../api/config'; +import { Product, Supplier } from '../../types/models'; -interface Supplier { - supplierId: number; - name: string; - description: string; - contactPerson: string; - email: string; - phone: string; -} - -interface Product { - productId: number; - supplierId: number; - name: string; - description: string; - price: number; - sku: string; - unit: string; - imgName: string; +interface ProductWithSupplier extends Product { supplier?: Supplier; - discount?: number; } type SortField = 'name' | 'price' | 'sku' | 'unit' | 'supplier'; @@ -34,9 +17,9 @@ type SortOrder = 'asc' | 'desc'; export default function AdminProducts() { const { isAdmin } = useAuth(); const { darkMode } = useTheme(); - const [products, setProducts] = useState([]); + const [products, setProducts] = useState([]); const [suppliers, setSuppliers] = useState([]); - const [editingProduct, setEditingProduct] = useState(undefined); + const [editingProduct, setEditingProduct] = useState(undefined); const [showForm, setShowForm] = useState(false); const [sortField, setSortField] = useState('name'); const [sortOrder, setSortOrder] = useState('asc'); diff --git a/frontend/src/components/entity/product/ProductForm.tsx b/frontend/src/components/entity/product/ProductForm.tsx index 5ce6110..f4dcf69 100644 --- a/frontend/src/components/entity/product/ProductForm.tsx +++ b/frontend/src/components/entity/product/ProductForm.tsx @@ -2,23 +2,7 @@ import { useState } from 'react'; import axios from 'axios'; import { api } from '../../../api/config'; import { useTheme } from '../../../context/ThemeContext'; - -interface Supplier { - supplierId: number; - name: string; -} - -interface Product { - productId: number; - supplierId: number; - name: string; - description: string; - price: number; - sku: string; - unit: string; - imgName: string; - discount?: number; -} +import { Product, Supplier } from '../../../types/models'; interface ProductFormProps { product?: Product; diff --git a/frontend/src/components/entity/product/Products.tsx b/frontend/src/components/entity/product/Products.tsx index bb7790e..ea38671 100644 --- a/frontend/src/components/entity/product/Products.tsx +++ b/frontend/src/components/entity/product/Products.tsx @@ -3,18 +3,7 @@ import axios from 'axios'; import { useQuery } from 'react-query'; import { api } from '../../../api/config'; import { useTheme } from '../../../context/ThemeContext'; - -interface Product { - productId: number; - name: string; - description: string; - price: number; - imgName: string; - sku: string; - unit: string; - supplierId: number; - discount?: number; -} +import { Product } from '../../../types/models'; const fetchProducts = async (): Promise => { const { data } = await axios.get(`${api.baseURL}${api.endpoints.products}`); diff --git a/frontend/src/types/models.ts b/frontend/src/types/models.ts new file mode 100644 index 0000000..1e42a88 --- /dev/null +++ b/frontend/src/types/models.ts @@ -0,0 +1,77 @@ +/** + * Shared type definitions for the OctoCAT Supply Chain Management Application + * These types align with the API models in api/src/models/ + */ + +export interface Product { + productId: number; + supplierId: number; + name: string; + description: string; + price: number; + sku: string; + unit: string; + imgName: string; + discount?: number; +} + +export interface Supplier { + supplierId: number; + name: string; + description: string; + contactPerson: string; + email: string; + phone: string; +} + +export interface Branch { + branchId: number; + headquartersId: number; + name: string; + description: string; + address: string; + contactPerson: string; + email: string; + phone: string; +} + +export interface Headquarters { + headquartersId: number; + name: string; + description: string; + address: string; + contactPerson: string; + email: string; + phone: string; +} + +export interface Order { + orderId: number; + branchId: number; + orderDate: string; + status: string; + totalAmount: number; +} + +export interface OrderDetail { + orderDetailId: number; + orderId: number; + productId: number; + quantity: number; + price: number; +} + +export interface Delivery { + deliveryId: number; + orderId: number; + deliveryDate: string; + status: string; + trackingNumber: string; +} + +export interface OrderDetailDelivery { + orderDetailDeliveryId: number; + orderDetailId: number; + deliveryId: number; + quantityDelivered: number; +}