Initial commit
This commit is contained in:
11
packages/api/products/README.md
Normal file
11
packages/api/products/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# products
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build products` to build the library.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test products` to execute the unit tests via [Jest](https://jestjs.io).
|
||||
19
packages/api/products/eslint.config.mjs
Normal file
19
packages/api/products/eslint.config.mjs
Normal file
@@ -0,0 +1,19 @@
|
||||
import baseConfig from '../../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
rules: {
|
||||
'@nx/dependency-checks': [
|
||||
'error',
|
||||
{
|
||||
ignoredFiles: ['{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}'],
|
||||
},
|
||||
],
|
||||
},
|
||||
languageOptions: {
|
||||
parser: await import('jsonc-eslint-parser'),
|
||||
},
|
||||
},
|
||||
];
|
||||
11
packages/api/products/package.json
Normal file
11
packages/api/products/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@org/products",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "commonjs",
|
||||
"main": "./src/index.js",
|
||||
"types": "./src/index.d.ts",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
8
packages/api/products/project.json
Normal file
8
packages/api/products/project.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "products",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/api/products/src",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:api"],
|
||||
"targets": {}
|
||||
}
|
||||
1
packages/api/products/src/index.ts
Normal file
1
packages/api/products/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/products.service';
|
||||
107
packages/api/products/src/lib/products.service.spec.ts
Normal file
107
packages/api/products/src/lib/products.service.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { ProductsService } from './products.service';
|
||||
import { ProductFilter } from '@org/models';
|
||||
|
||||
describe('ProductsService', () => {
|
||||
let service: ProductsService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new ProductsService();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getAllProducts', () => {
|
||||
it('should return paginated products with default pagination', () => {
|
||||
const result = service.getAllProducts();
|
||||
|
||||
expect(result).toHaveProperty('items');
|
||||
expect(result).toHaveProperty('total');
|
||||
expect(result).toHaveProperty('page');
|
||||
expect(result).toHaveProperty('pageSize');
|
||||
expect(result.page).toBe(1);
|
||||
expect(result.pageSize).toBe(12);
|
||||
expect(result.items.length).toBeLessThanOrEqual(12);
|
||||
});
|
||||
|
||||
it('should filter products by category', () => {
|
||||
const filter: ProductFilter = { category: 'Electronics' };
|
||||
const result = service.getAllProducts(filter);
|
||||
|
||||
result.items.forEach((product) => {
|
||||
expect(product.category).toBe('Electronics');
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter products by price range', () => {
|
||||
const filter: ProductFilter = { minPrice: 50, maxPrice: 150 };
|
||||
const result = service.getAllProducts(filter);
|
||||
|
||||
result.items.forEach((product) => {
|
||||
expect(product.price).toBeGreaterThanOrEqual(50);
|
||||
expect(product.price).toBeLessThanOrEqual(150);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle search term filtering', () => {
|
||||
const filter: ProductFilter = { searchTerm: 'Product 1' };
|
||||
const result = service.getAllProducts(filter);
|
||||
|
||||
result.items.forEach((product) => {
|
||||
const matchesSearch =
|
||||
product.name.toLowerCase().includes('product 1') ||
|
||||
product.description.toLowerCase().includes('product 1');
|
||||
expect(matchesSearch).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should paginate results correctly', () => {
|
||||
const page1 = service.getAllProducts(undefined, 1, 5);
|
||||
const page2 = service.getAllProducts(undefined, 2, 5);
|
||||
|
||||
expect(page1.items.length).toBeLessThanOrEqual(5);
|
||||
expect(page2.items.length).toBeLessThanOrEqual(5);
|
||||
expect(page1.items[0]?.id).not.toBe(page2.items[0]?.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProductById', () => {
|
||||
it('should return a product by id', () => {
|
||||
const product = service.getProductById('prod-1');
|
||||
|
||||
expect(product).toBeTruthy();
|
||||
expect(product?.id).toBe('prod-1');
|
||||
});
|
||||
|
||||
it('should return null for non-existent id', () => {
|
||||
const product = service.getProductById('non-existent');
|
||||
|
||||
expect(product).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCategories', () => {
|
||||
it('should return unique categories', () => {
|
||||
const categories = service.getCategories();
|
||||
|
||||
expect(categories).toBeTruthy();
|
||||
expect(Array.isArray(categories)).toBe(true);
|
||||
expect(categories.length).toBeGreaterThan(0);
|
||||
|
||||
const uniqueCategories = new Set(categories);
|
||||
expect(uniqueCategories.size).toBe(categories.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPriceRange', () => {
|
||||
it('should return min and max prices', () => {
|
||||
const range = service.getPriceRange();
|
||||
|
||||
expect(range).toHaveProperty('min');
|
||||
expect(range).toHaveProperty('max');
|
||||
expect(range.min).toBeLessThanOrEqual(range.max);
|
||||
expect(range.min).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
99
packages/api/products/src/lib/products.service.ts
Normal file
99
packages/api/products/src/lib/products.service.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { Product, ProductFilter, PaginatedResponse } from '@org/models';
|
||||
|
||||
export class ProductsService {
|
||||
private products: Product[] = this.generateMockProducts();
|
||||
|
||||
private generateMockProducts(): Product[] {
|
||||
const categories = ['Electronics', 'Clothing', 'Books', 'Home & Garden', 'Sports'];
|
||||
const products: Product[] = [];
|
||||
|
||||
for (let i = 1; i <= 50; i++) {
|
||||
const category = categories[Math.floor(Math.random() * categories.length)];
|
||||
products.push({
|
||||
id: `prod-${i}`,
|
||||
name: `Product ${i}`,
|
||||
description: `This is a high-quality ${category.toLowerCase()} product with excellent features and great value for money.`,
|
||||
price: Math.round(Math.random() * 500 * 100) / 100 + 10,
|
||||
category,
|
||||
imageUrl: `https://placehold.co/300x300?text=Product+${i}`,
|
||||
inStock: Math.random() > 0.2,
|
||||
rating: Math.round((Math.random() * 2 + 3) * 10) / 10,
|
||||
reviewCount: Math.floor(Math.random() * 500),
|
||||
});
|
||||
}
|
||||
|
||||
return products;
|
||||
}
|
||||
|
||||
getAllProducts(
|
||||
filter?: ProductFilter,
|
||||
page = 1,
|
||||
pageSize = 12
|
||||
): PaginatedResponse<Product> {
|
||||
let filteredProducts = [...this.products];
|
||||
|
||||
if (filter) {
|
||||
if (filter.category) {
|
||||
filteredProducts = filteredProducts.filter(
|
||||
p => p.category === filter.category
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.minPrice !== undefined) {
|
||||
const minPrice = filter.minPrice;
|
||||
filteredProducts = filteredProducts.filter(p => p.price >= minPrice);
|
||||
}
|
||||
|
||||
if (filter.maxPrice !== undefined) {
|
||||
const maxPrice = filter.maxPrice;
|
||||
filteredProducts = filteredProducts.filter(p => p.price <= maxPrice);
|
||||
}
|
||||
|
||||
if (filter.inStock !== undefined) {
|
||||
filteredProducts = filteredProducts.filter(
|
||||
p => p.inStock === filter.inStock
|
||||
);
|
||||
}
|
||||
|
||||
if (filter.searchTerm) {
|
||||
const searchLower = filter.searchTerm.toLowerCase();
|
||||
filteredProducts = filteredProducts.filter(
|
||||
p =>
|
||||
p.name.toLowerCase().includes(searchLower) ||
|
||||
p.description.toLowerCase().includes(searchLower) ||
|
||||
p.category.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const total = filteredProducts.length;
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const items = filteredProducts.slice(startIndex, endIndex);
|
||||
|
||||
return {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
totalPages,
|
||||
};
|
||||
}
|
||||
|
||||
getProductById(id: string): Product | null {
|
||||
return this.products.find(p => p.id === id) || null;
|
||||
}
|
||||
|
||||
getCategories(): string[] {
|
||||
return [...new Set(this.products.map(p => p.category))];
|
||||
}
|
||||
|
||||
getPriceRange(): { min: number; max: number } {
|
||||
const prices = this.products.map(p => p.price);
|
||||
return {
|
||||
min: Math.min(...prices),
|
||||
max: Math.max(...prices),
|
||||
};
|
||||
}
|
||||
}
|
||||
16
packages/api/products/tsconfig.json
Normal file
16
packages/api/products/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs"
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
packages/api/products/tsconfig.lib.json
Normal file
10
packages/api/products/tsconfig.lib.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
28
packages/api/products/tsconfig.spec.json
Normal file
28
packages/api/products/tsconfig.spec.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
22
packages/api/products/vite.config.ts
Normal file
22
packages/api/products/vite.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../../node_modules/.vite/packages/api/products',
|
||||
plugins: [tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
test: {
|
||||
name: 'products',
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
reporters: ['default'],
|
||||
passWithNoTests: true,
|
||||
coverage: {
|
||||
reportsDirectory: '../../../coverage/packages/api/products',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user