Initial commit

This commit is contained in:
2026-07-27 18:01:07 +02:00
commit 0758b00d54
192 changed files with 45091 additions and 0 deletions

View 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).

View 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'),
},
},
];

View 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"
}
}

View 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": {}
}

View File

@@ -0,0 +1 @@
export * from './lib/products.service';

View 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);
});
});
});

View 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),
};
}
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs"
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View 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"]
}

View 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"
]
}

View 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,
},
},
}));