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

[BK] Create ProductType, ProductVariant, and Product Entities and APIs #20

Merged
merged 2 commits into from
Dec 17, 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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ DATABASE_URL=""
PORT=3000 # Port to run the server

# Other Configuration
NODE_ENV=development # Node environment (development, production)
NODE_ENV=test # Node environment (development, production)
JWT_SECRET=your-secret-key # Secret key for JWT authentication
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ["<rootDir>/src/tests/setup.ts"],
moduleFileExtensions: ['js', 'json', 'ts'],
rootDir: './',
testRegex: '.*\\.spec\\.ts$',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"license": "ISC",
"description": "",
"dependencies": {
"dotenv": "^16.4.7",
"axios": "^1.7.9",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
Expand Down
35 changes: 24 additions & 11 deletions src/config/ormconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,29 @@ import dotenv from "dotenv";

dotenv.config();

const AppDataSource = new DataSource({
type: 'postgres',
url: process.env.DATABASE_URL,
entities: [__dirname + '/../entities/*.ts'],
migrations: [__dirname + '/../migrations/*.ts'],
// ssl: {
// rejectUnauthorized: false,
// },
ssl: false,
synchronize: false, // Set to true only in development; false in production
});
const isTestEnv = process.env.NODE_ENV === "test";

const AppDataSource = new DataSource(
isTestEnv
? {
type: "sqlite",
database: ":memory:",
entities: [__dirname + '/../entities/*.ts'],
migrations: [__dirname + '/../migrations/*.ts'],
synchronize: true,
logging: false,
}
: {
type: "postgres",
url: process.env.DATABASE_URL,
entities: [__dirname + '/../entities/*.ts'],
migrations: [__dirname + '/../migrations/*.ts'],
ssl: {
rejectUnauthorized: false,
},
synchronize: false,
logging: false,
}
);

export default AppDataSource;
25 changes: 25 additions & 0 deletions src/entities/Product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, OneToMany, JoinColumn } from 'typeorm';
import { ProductType } from './ProductType';
import { ProductVariant } from './ProductVariant';

@Entity('products')
export class Product {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column({ type: 'text', nullable: true })
description: string;

@ManyToOne(() => ProductType, (productType) => productType.products, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'productTypeId' })
productType: ProductType;

@OneToMany(() => ProductVariant, (variant) => variant.product)
variants: ProductVariant[];

@CreateDateColumn()
createdAt: Date;
}
20 changes: 20 additions & 0 deletions src/entities/ProductType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, OneToMany } from 'typeorm';
import { Product } from './Product';

@Entity('product_types')
export class ProductType {
@PrimaryGeneratedColumn()
id: number;

@Column()
name: string;

@Column({ type: 'text', nullable: true })
description: string;

@CreateDateColumn()
createdAt: Date;

@OneToMany(() => Product, (product) => product.productType)
products: Product[];
}
25 changes: 25 additions & 0 deletions src/entities/ProductVariant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, CreateDateColumn, JoinColumn } from 'typeorm';
import { Product } from './Product';

@Entity('product_variants')
export class ProductVariant {
@PrimaryGeneratedColumn()
id: number;

@ManyToOne(() => Product, (product) => product.variants, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'productId' })
product: Product;


@Column()
sku: string;

@Column('decimal')
price: number;

@Column('int')
stock: number;

@CreateDateColumn()
createdAt: Date;
}
43 changes: 43 additions & 0 deletions src/services/product.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Repository } from 'typeorm';
import { Product } from '../entities/Product';
import { ProductType } from '../entities/ProductType';
import AppDataSource from '../config/ormconfig';

export class ProductService {
private repository: Repository<Product>;

constructor() {
this.repository = AppDataSource.getRepository(Product);
}

async create(data: Partial<Product>, productTypeId: number): Promise<Product | null> {
const productTypeRepo = AppDataSource.getRepository(ProductType);
const productType = await productTypeRepo.findOne({ where: { id: productTypeId } });

if (!productType) return null;

const product = this.repository.create({ ...data, productType });
return await this.repository.save(product);
}

async getAll(): Promise<Product[]> {
return await this.repository.find({ relations: ['productType'] });
}

async getById(id: number): Promise<Product | null> {
return await this.repository.findOne({ where: { id }, relations: ['productType'] });
}

async update(id: number, data: Partial<Product>): Promise<Product | null> {
const product = await this.getById(id);
if (!product) return null;

Object.assign(product, data);
return await this.repository.save(product);
}

async delete(id: number): Promise<boolean> {
const result = await this.repository.delete(id);
return result.affected === 1;
}
}
37 changes: 37 additions & 0 deletions src/services/productType.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Repository } from 'typeorm';
import { ProductType } from '../entities/ProductType';
import AppDataSource from '../config/ormconfig';

export class ProductTypeService {
private repository: Repository<ProductType>;

constructor() {
this.repository = AppDataSource.getRepository(ProductType);
}

async create(data: Partial<ProductType>): Promise<ProductType> {
const productType = this.repository.create(data);
return await this.repository.save(productType);
}

async getAll(): Promise<ProductType[]> {
return await this.repository.find();
}

async getById(id: number): Promise<ProductType | null> {
return await this.repository.findOne({ where: { id } });
}

async update(id: number, data: Partial<ProductType>): Promise<ProductType | null> {
const productType = await this.getById(id);
if (!productType) return null;

Object.assign(productType, data);
return await this.repository.save(productType);
}

async delete(id: number): Promise<boolean> {
const result = await this.repository.delete(id);
return result.affected === 1;
}
}
43 changes: 43 additions & 0 deletions src/services/productVariant.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Repository } from 'typeorm';
import { ProductVariant } from '../entities/ProductVariant';
import { Product } from '../entities/Product';
import AppDataSource from '../config/ormconfig';

export class ProductVariantService {
private repository: Repository<ProductVariant>;

constructor() {
this.repository = AppDataSource.getRepository(ProductVariant);
}

async create(data: Partial<ProductVariant>, productId: number): Promise<ProductVariant | null> {
const productRepo = AppDataSource.getRepository(Product);
const product = await productRepo.findOne({ where: { id: productId } });

if (!product) return null;

const productVariant = this.repository.create({ ...data, product });
return await this.repository.save(productVariant);
}

async getAll(): Promise<ProductVariant[]> {
return await this.repository.find({ relations: ['product'] });
}

async getById(id: number): Promise<ProductVariant | null> {
return await this.repository.findOne({ where: { id }, relations: ['product'] });
}

async update(id: number, data: Partial<ProductVariant>): Promise<ProductVariant | null> {
const variant = await this.getById(id);
if (!variant) return null;

Object.assign(variant, data);
return await this.repository.save(variant);
}

async delete(id: number): Promise<boolean> {
const result = await this.repository.delete(id);
return result.affected === 1;
}
}
80 changes: 80 additions & 0 deletions src/tests/integration/services/productType.integration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import AppDataSource from "../../../config/ormconfig";
import { Product } from "../../../entities/Product";
import { ProductType } from "../../../entities/ProductType";
import { ProductService } from "../../../services/product.service";

describe("ProductService Integration Tests", () => {
let service: ProductService;

beforeAll(async () => {
if (!AppDataSource.isInitialized) {
await AppDataSource.initialize();
}
await AppDataSource.synchronize(true);

service = new ProductService();
});

afterEach(async () => {
const entities = AppDataSource.entityMetadatas;
for (const entity of entities) {
const repository = AppDataSource.getRepository(entity.name);
await repository.clear();
}
});

afterAll(async () => {
if (AppDataSource.isInitialized) {
await AppDataSource.destroy();
}
});

it("should create a Product with ProductType", async () => {
const productTypeRepo = AppDataSource.getRepository(ProductType);

const productType = productTypeRepo.create({
name: "Electronics",
description: "Category for electronics",
});
await productTypeRepo.save(productType);

const productData = { name: "Laptop", description: "Gaming Laptop" };
const product = await service.create(productData, productType.id);

expect(product).toBeDefined();
expect(product.id).toBeDefined();
expect(product.productType).toEqual(
expect.objectContaining({
id: productType.id,
name: "Electronics",
})
);
});

it("should fetch Products associated with a ProductType", async () => {
const productTypeRepo = AppDataSource.getRepository(ProductType);
const productRepo = AppDataSource.getRepository(Product);

const productType = productTypeRepo.create({ name: "Electronics" });
const savedProductType = await productTypeRepo.save(productType);

const product1 = productRepo.create({ name: "Laptop", productType: savedProductType });
const product2 = productRepo.create({ name: "Phone", productType: savedProductType });

await productRepo.save([product1, product2]);

const fetchedProductType = await productTypeRepo.findOne({
where: { id: savedProductType.id },
relations: ["products"],
});

expect(fetchedProductType).toBeDefined();
expect(fetchedProductType.products).toHaveLength(2);
expect(fetchedProductType.products).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: "Laptop" }),
expect.objectContaining({ name: "Phone" }),
])
);
});
});
Loading
Loading