add api category brand and fix product
This commit is contained in:
@@ -55,6 +55,7 @@ Query params:
|
|||||||
- `attributes` as JSON string
|
- `attributes` as JSON string
|
||||||
- `tags` as JSON string array
|
- `tags` as JSON string array
|
||||||
- `featured`
|
- `featured`
|
||||||
|
- `sort`: `newest | oldest | price_asc | price_desc | popular | top_rated`
|
||||||
- `page`
|
- `page`
|
||||||
- `limit`
|
- `limit`
|
||||||
|
|
||||||
@@ -65,12 +66,76 @@ Optional request header:
|
|||||||
Example:
|
Example:
|
||||||
|
|
||||||
```http
|
```http
|
||||||
GET /api/products?search=skf&featured=true&page=1&limit=20
|
GET /api/products?search=skf&featured=true&sort=price_asc&page=1&limit=20
|
||||||
x-product-type: Industrial
|
x-product-type: Industrial
|
||||||
```
|
```
|
||||||
|
|
||||||
List response items are intentionally lightweight and include only fields needed for product cards / list pages. The `attributes` array is limited to the first 2 visible attributes, sorted by `displayOrder`.
|
List response items are intentionally lightweight and include only fields needed for product cards / list pages. The `attributes` array is limited to the first 2 visible attributes, sorted by `displayOrder`.
|
||||||
|
|
||||||
|
### `GET /api/products/brands/:brandSlug`
|
||||||
|
|
||||||
|
Lists published products for one brand by brand slug.
|
||||||
|
|
||||||
|
Supports the same query params as `GET /api/products`:
|
||||||
|
|
||||||
|
- `search`
|
||||||
|
- `featured`
|
||||||
|
- `sort`
|
||||||
|
- `page`
|
||||||
|
- `limit`
|
||||||
|
|
||||||
|
Optional request header:
|
||||||
|
|
||||||
|
- `x-product-type`: `Industrial | Automotive`
|
||||||
|
|
||||||
|
### `GET /api/products/brands/:brandSlug/filters`
|
||||||
|
|
||||||
|
Returns available brand page filters relative to the selected brand and current query filters.
|
||||||
|
|
||||||
|
Response shape:
|
||||||
|
|
||||||
|
- `categories`: available categories with count
|
||||||
|
- `attributes`: available filterable attributes and their values with count
|
||||||
|
- `stock`: `inStock` and `outOfStock`
|
||||||
|
|
||||||
|
Optional request header:
|
||||||
|
|
||||||
|
- `x-product-type`: `Industrial | Automotive`
|
||||||
|
|
||||||
|
### `GET /api/products/categories/:categorySlug`
|
||||||
|
|
||||||
|
Lists published products for one category by category slug.
|
||||||
|
|
||||||
|
Supports the same query params as `GET /api/products`:
|
||||||
|
|
||||||
|
- `search`
|
||||||
|
- `featured`
|
||||||
|
- `sort`
|
||||||
|
- `page`
|
||||||
|
- `limit`
|
||||||
|
|
||||||
|
Optional request header:
|
||||||
|
|
||||||
|
- `x-product-type`: `Industrial | Automotive`
|
||||||
|
|
||||||
|
### `GET /api/products/categories/:categorySlug/filters`
|
||||||
|
|
||||||
|
Returns available category page filters relative to the selected category and current query filters.
|
||||||
|
|
||||||
|
Response shape:
|
||||||
|
|
||||||
|
- `brands`: available brands with count
|
||||||
|
- `attributes`: available filterable attributes and their values with count
|
||||||
|
- `stock`: `inStock` and `outOfStock`
|
||||||
|
|
||||||
|
Optional request header:
|
||||||
|
|
||||||
|
- `x-product-type`: `Industrial | Automotive`
|
||||||
|
|
||||||
|
### `GET /api/product/:slug`
|
||||||
|
|
||||||
|
Returns one published product by slug with category and up to 10 approved reviews.
|
||||||
|
|
||||||
### `GET /api/products/:id`
|
### `GET /api/products/:id`
|
||||||
|
|
||||||
Returns one published product with category and up to 10 approved reviews.
|
Returns one published product with category and up to 10 approved reviews.
|
||||||
@@ -117,6 +182,7 @@ Query params:
|
|||||||
- `attributes` as JSON string
|
- `attributes` as JSON string
|
||||||
- `tags` as JSON string array
|
- `tags` as JSON string array
|
||||||
- `featured`
|
- `featured`
|
||||||
|
- `sort`: `newest | oldest | price_asc | price_desc | popular | top_rated`
|
||||||
- `page`
|
- `page`
|
||||||
- `limit`
|
- `limit`
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"start": "nest start",
|
"start": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
|
"seed:industrial-products": "ts-node -r tsconfig-paths/register src/scripts/seed-industrial-products.ts",
|
||||||
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
|
"lint": "eslint \"{src,test}/**/*.ts\" --fix",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"typeorm": "typeorm-ts-node-commonjs -d src/config/typeorm.config.ts",
|
"typeorm": "typeorm-ts-node-commonjs -d src/config/typeorm.config.ts",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { BrandController } from './brand.controller';
|
|||||||
import { BrandService } from './brand.service';
|
import { BrandService } from './brand.service';
|
||||||
import { CategoryController } from './category.controller';
|
import { CategoryController } from './category.controller';
|
||||||
import { CategoryService } from './category.service';
|
import { CategoryService } from './category.service';
|
||||||
|
import { ProductDetailsController } from './product-details.controller';
|
||||||
import { AttributeDefinition } from './entities/attribute-definition.entity';
|
import { AttributeDefinition } from './entities/attribute-definition.entity';
|
||||||
import { Brand } from './entities/brand.entity';
|
import { Brand } from './entities/brand.entity';
|
||||||
import { Category } from './entities/category.entity';
|
import { Category } from './entities/category.entity';
|
||||||
@@ -34,6 +35,7 @@ import { ProductsService } from './products.service';
|
|||||||
CategoryController,
|
CategoryController,
|
||||||
BrandController,
|
BrandController,
|
||||||
ProductsController,
|
ProductsController,
|
||||||
|
ProductDetailsController,
|
||||||
AdminProductsController,
|
AdminProductsController,
|
||||||
AttributeDefinitionsController,
|
AttributeDefinitionsController,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Transform, Type } from 'class-transformer';
|
import { plainToInstance, Transform, Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
ArrayMaxSize,
|
ArrayMaxSize,
|
||||||
ArrayUnique,
|
ArrayUnique,
|
||||||
@@ -123,14 +123,26 @@ export class CreateProductDto {
|
|||||||
|
|
||||||
@ApiPropertyOptional({ type: ProductMetaDto })
|
@ApiPropertyOptional({ type: ProductMetaDto })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(parseJsonValue)
|
@Transform(({ value }) => {
|
||||||
|
const parsedValue = parseJsonValue({ value } as never);
|
||||||
|
return parsedValue ? plainToInstance(ProductMetaDto, parsedValue) : undefined;
|
||||||
|
})
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => ProductMetaDto)
|
@Type(() => ProductMetaDto)
|
||||||
meta?: ProductMetaDto;
|
meta?: ProductMetaDto;
|
||||||
|
|
||||||
@ApiPropertyOptional({ type: [ProductAttributeInputDto] })
|
@ApiPropertyOptional({ type: [ProductAttributeInputDto] })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(parseJsonValue)
|
@Transform(({ value }) => {
|
||||||
|
const parsedValue = parseJsonValue({ value } as never);
|
||||||
|
if (!Array.isArray(parsedValue)) {
|
||||||
|
return parsedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedValue.map((item) =>
|
||||||
|
plainToInstance(ProductAttributeInputDto, item),
|
||||||
|
);
|
||||||
|
})
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@Type(() => ProductAttributeInputDto)
|
@Type(() => ProductAttributeInputDto)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
IsArray,
|
IsArray,
|
||||||
@@ -11,45 +12,55 @@ import {
|
|||||||
Min,
|
Min,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { parseJsonValue } from '../../../common/utils/json-transform.util';
|
import { parseJsonValue } from '../../../common/utils/json-transform.util';
|
||||||
|
import { ProductListSort } from '../enums/product-list-sort.enum';
|
||||||
import { ProductStatus } from '../enums/product-status.enum';
|
import { ProductStatus } from '../enums/product-status.enum';
|
||||||
import { ProductType } from '../enums/product-type.enum';
|
import { ProductType } from '../enums/product-type.enum';
|
||||||
|
|
||||||
export class FilterProductsDto {
|
export class FilterProductsDto {
|
||||||
|
@ApiPropertyOptional()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
search?: string;
|
search?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ProductType })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(ProductType)
|
@IsEnum(ProductType)
|
||||||
type?: ProductType;
|
type?: ProductType;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ProductStatus })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(ProductStatus)
|
@IsEnum(ProductStatus)
|
||||||
status?: ProductStatus;
|
status?: ProductStatus;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
categoryId?: string;
|
categoryId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsUUID()
|
@IsUUID()
|
||||||
brandId?: string;
|
brandId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
brand?: string;
|
brand?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Object })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(parseJsonValue)
|
@Transform(parseJsonValue)
|
||||||
@IsObject()
|
@IsObject()
|
||||||
attributes?: Record<string, unknown>;
|
attributes?: Record<string, unknown>;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: [String] })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(parseJsonValue)
|
@Transform(parseJsonValue)
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsString({ each: true })
|
@IsString({ each: true })
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) =>
|
@Transform(({ value }) =>
|
||||||
typeof value === 'boolean' ? value : String(value).toLowerCase() === 'true',
|
typeof value === 'boolean' ? value : String(value).toLowerCase() === 'true',
|
||||||
@@ -57,12 +68,23 @@ export class FilterProductsDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
enum: ProductListSort,
|
||||||
|
description:
|
||||||
|
'Sort list by newest, oldest, cheapest, most expensive, most popular, or top rated.',
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(ProductListSort)
|
||||||
|
sort?: ProductListSort;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 1 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => Number(value))
|
@Transform(({ value }) => Number(value))
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
page?: number = 1;
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ default: 20 })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => Number(value))
|
@Transform(({ value }) => Number(value))
|
||||||
@IsInt()
|
@IsInt()
|
||||||
|
|||||||
8
src/modules/catalog/enums/product-list-sort.enum.ts
Normal file
8
src/modules/catalog/enums/product-list-sort.enum.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export enum ProductListSort {
|
||||||
|
NEWEST = 'newest',
|
||||||
|
OLDEST = 'oldest',
|
||||||
|
PRICE_ASC = 'price_asc',
|
||||||
|
PRICE_DESC = 'price_desc',
|
||||||
|
POPULAR = 'popular',
|
||||||
|
TOP_RATED = 'top_rated',
|
||||||
|
}
|
||||||
24
src/modules/catalog/product-details.controller.ts
Normal file
24
src/modules/catalog/product-details.controller.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Controller, Get, Headers, Param } from '@nestjs/common';
|
||||||
|
import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
|
||||||
|
@ApiTags('Products')
|
||||||
|
@ApiHeader({
|
||||||
|
name: 'x-product-type',
|
||||||
|
required: false,
|
||||||
|
description: 'Optional product type filter header. Falls back to query param `type` if omitted.',
|
||||||
|
enum: ['Industrial', 'Automotive'],
|
||||||
|
})
|
||||||
|
@Controller('product')
|
||||||
|
export class ProductDetailsController {
|
||||||
|
constructor(private readonly productsService: ProductsService) {}
|
||||||
|
|
||||||
|
@Get(':slug')
|
||||||
|
@ApiOperation({ summary: 'Get one published product by slug' })
|
||||||
|
findBySlug(
|
||||||
|
@Param('slug') slug: string,
|
||||||
|
@Headers('x-product-type') productType?: string,
|
||||||
|
) {
|
||||||
|
return this.productsService.findPublicOneBySlug(slug, productType);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,13 +24,60 @@ export class ProductsController {
|
|||||||
return this.productsService.findPublic(filters, productType);
|
return this.productsService.findPublic(filters, productType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('slug/:slug')
|
@Get('brands/:brandSlug')
|
||||||
@ApiOperation({ summary: 'Get one published product by slug' })
|
@ApiOperation({ summary: 'List published products by brand slug' })
|
||||||
findBySlug(
|
findByBrandSlug(
|
||||||
@Param('slug') slug: string,
|
@Param('brandSlug') brandSlug: string,
|
||||||
|
@Query() filters: FilterProductsDto,
|
||||||
@Headers('x-product-type') productType?: string,
|
@Headers('x-product-type') productType?: string,
|
||||||
) {
|
) {
|
||||||
return this.productsService.findPublicOneBySlug(slug, productType);
|
return this.productsService.findPublicByBrandSlug(
|
||||||
|
brandSlug,
|
||||||
|
filters,
|
||||||
|
productType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('brands/:brandSlug/filters')
|
||||||
|
@ApiOperation({ summary: 'Get available brand page filters by brand slug' })
|
||||||
|
findBrandFilters(
|
||||||
|
@Param('brandSlug') brandSlug: string,
|
||||||
|
@Query() filters: FilterProductsDto,
|
||||||
|
@Headers('x-product-type') productType?: string,
|
||||||
|
) {
|
||||||
|
return this.productsService.findPublicBrandFilters(
|
||||||
|
brandSlug,
|
||||||
|
filters,
|
||||||
|
productType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('categories/:categorySlug')
|
||||||
|
@ApiOperation({ summary: 'List published products by category slug' })
|
||||||
|
findByCategorySlug(
|
||||||
|
@Param('categorySlug') categorySlug: string,
|
||||||
|
@Query() filters: FilterProductsDto,
|
||||||
|
@Headers('x-product-type') productType?: string,
|
||||||
|
) {
|
||||||
|
return this.productsService.findPublicByCategorySlug(
|
||||||
|
categorySlug,
|
||||||
|
filters,
|
||||||
|
productType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('categories/:categorySlug/filters')
|
||||||
|
@ApiOperation({ summary: 'Get available category page filters by category slug' })
|
||||||
|
findCategoryFilters(
|
||||||
|
@Param('categorySlug') categorySlug: string,
|
||||||
|
@Query() filters: FilterProductsDto,
|
||||||
|
@Headers('x-product-type') productType?: string,
|
||||||
|
) {
|
||||||
|
return this.productsService.findPublicCategoryFilters(
|
||||||
|
categorySlug,
|
||||||
|
filters,
|
||||||
|
productType,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/reviews')
|
@Get(':id/reviews')
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { ProductMeta } from './entities/product-meta.entity';
|
|||||||
import { Product } from './entities/product.entity';
|
import { Product } from './entities/product.entity';
|
||||||
import { ProductReview } from './entities/product-review.entity';
|
import { ProductReview } from './entities/product-review.entity';
|
||||||
import { AttributeDataType } from './enums/attribute-data-type.enum';
|
import { AttributeDataType } from './enums/attribute-data-type.enum';
|
||||||
|
import { ProductListSort } from './enums/product-list-sort.enum';
|
||||||
import { ProductStatus } from './enums/product-status.enum';
|
import { ProductStatus } from './enums/product-status.enum';
|
||||||
import { ProductType } from './enums/product-type.enum';
|
import { ProductType } from './enums/product-type.enum';
|
||||||
|
|
||||||
@@ -118,6 +119,68 @@ export class ProductsService {
|
|||||||
return this.findAll(this.mergeProductTypeFilter(filters, headerProductType), false);
|
return this.findAll(this.mergeProductTypeFilter(filters, headerProductType), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findPublicByBrandSlug(
|
||||||
|
brandSlug: string,
|
||||||
|
filters: FilterProductsDto,
|
||||||
|
headerProductType?: string,
|
||||||
|
) {
|
||||||
|
const mergedFilters = this.mergeProductTypeFilter(filters, headerProductType);
|
||||||
|
const brand = await this.resolveBrandBySlug(brandSlug, mergedFilters.type);
|
||||||
|
|
||||||
|
return this.findAll(
|
||||||
|
{
|
||||||
|
...mergedFilters,
|
||||||
|
brandId: brand.id,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findPublicByCategorySlug(
|
||||||
|
categorySlug: string,
|
||||||
|
filters: FilterProductsDto,
|
||||||
|
headerProductType?: string,
|
||||||
|
) {
|
||||||
|
const mergedFilters = this.mergeProductTypeFilter(filters, headerProductType);
|
||||||
|
const category = await this.resolveCategoryBySlug(categorySlug, mergedFilters.type);
|
||||||
|
|
||||||
|
return this.findAll(
|
||||||
|
{
|
||||||
|
...mergedFilters,
|
||||||
|
categoryId: category.id,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findPublicBrandFilters(
|
||||||
|
brandSlug: string,
|
||||||
|
filters: FilterProductsDto,
|
||||||
|
headerProductType?: string,
|
||||||
|
) {
|
||||||
|
const mergedFilters = this.mergeProductTypeFilter(filters, headerProductType);
|
||||||
|
const brand = await this.resolveBrandBySlug(brandSlug, mergedFilters.type);
|
||||||
|
|
||||||
|
return this.buildProductFacets({
|
||||||
|
...mergedFilters,
|
||||||
|
brandId: brand.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async findPublicCategoryFilters(
|
||||||
|
categorySlug: string,
|
||||||
|
filters: FilterProductsDto,
|
||||||
|
headerProductType?: string,
|
||||||
|
) {
|
||||||
|
const mergedFilters = this.mergeProductTypeFilter(filters, headerProductType);
|
||||||
|
const category = await this.resolveCategoryBySlug(categorySlug, mergedFilters.type);
|
||||||
|
|
||||||
|
return this.buildProductFacets({
|
||||||
|
...mergedFilters,
|
||||||
|
categoryId: category.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async findAdmin(filters: FilterProductsDto, headerProductType?: string) {
|
async findAdmin(filters: FilterProductsDto, headerProductType?: string) {
|
||||||
return this.findAll(this.mergeProductTypeFilter(filters, headerProductType), true);
|
return this.findAll(this.mergeProductTypeFilter(filters, headerProductType), true);
|
||||||
}
|
}
|
||||||
@@ -467,16 +530,36 @@ export class ProductsService {
|
|||||||
const page = filters.page ?? 1;
|
const page = filters.page ?? 1;
|
||||||
const limit = filters.limit ?? 20;
|
const limit = filters.limit ?? 20;
|
||||||
|
|
||||||
|
const query = this.createFilteredProductsQuery(filters, includeUnpublished)
|
||||||
|
.skip((page - 1) * limit)
|
||||||
|
.take(limit);
|
||||||
|
|
||||||
|
const [items, total] = await query.getManyAndCount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: items.map((item) =>
|
||||||
|
includeUnpublished
|
||||||
|
? this.serializeAdminProductListItem(item)
|
||||||
|
: this.serializePublicProductListItem(item),
|
||||||
|
),
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createFilteredProductsQuery(
|
||||||
|
filters: FilterProductsDto,
|
||||||
|
includeUnpublished: boolean,
|
||||||
|
) {
|
||||||
const query = this.productsRepository
|
const query = this.productsRepository
|
||||||
.createQueryBuilder('product')
|
.createQueryBuilder('product')
|
||||||
.leftJoinAndSelect('product.primaryCategory', 'primaryCategory')
|
.leftJoinAndSelect('product.primaryCategory', 'primaryCategory')
|
||||||
.leftJoinAndSelect('product.categories', 'categories')
|
.leftJoinAndSelect('product.categories', 'categories')
|
||||||
.leftJoinAndSelect('product.brandEntity', 'brandEntity')
|
.leftJoinAndSelect('product.brandEntity', 'brandEntity')
|
||||||
.leftJoinAndSelect('product.meta', 'meta')
|
.leftJoinAndSelect('product.meta', 'meta');
|
||||||
.orderBy('product.featured', 'DESC')
|
|
||||||
.addOrderBy('product.createdAt', 'DESC')
|
|
||||||
.skip((page - 1) * limit)
|
|
||||||
.take(limit);
|
|
||||||
|
|
||||||
if (!includeUnpublished) {
|
if (!includeUnpublished) {
|
||||||
query.andWhere('product.status = :publishedStatus', {
|
query.andWhere('product.status = :publishedStatus', {
|
||||||
@@ -525,17 +608,9 @@ export class ProductsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.applyAttributeFilters(query, filters.attributes);
|
this.applyAttributeFilters(query, filters.attributes);
|
||||||
|
this.applyProductSort(query, filters.sort);
|
||||||
|
|
||||||
const [items, total] = await query.getManyAndCount();
|
return query;
|
||||||
|
|
||||||
return {
|
|
||||||
items: items.map((item) => this.serializeProductListItem(item, includeUnpublished)),
|
|
||||||
meta: {
|
|
||||||
total,
|
|
||||||
page,
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyAttributeFilters(
|
private applyAttributeFilters(
|
||||||
@@ -572,6 +647,210 @@ export class ProductsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyProductSort(
|
||||||
|
query: SelectQueryBuilder<Product>,
|
||||||
|
sort?: ProductListSort,
|
||||||
|
) {
|
||||||
|
switch (sort) {
|
||||||
|
case ProductListSort.OLDEST:
|
||||||
|
query.orderBy('product.createdAt', 'ASC').addOrderBy('product.id', 'ASC');
|
||||||
|
return;
|
||||||
|
case ProductListSort.PRICE_ASC:
|
||||||
|
query
|
||||||
|
.orderBy('COALESCE(product.sale_price_usd, product.base_price_usd)', 'ASC')
|
||||||
|
.addOrderBy('product.createdAt', 'DESC');
|
||||||
|
return;
|
||||||
|
case ProductListSort.PRICE_DESC:
|
||||||
|
query
|
||||||
|
.orderBy('COALESCE(product.sale_price_usd, product.base_price_usd)', 'DESC')
|
||||||
|
.addOrderBy('product.createdAt', 'DESC');
|
||||||
|
return;
|
||||||
|
case ProductListSort.POPULAR:
|
||||||
|
query
|
||||||
|
.orderBy('product.reviewsCount', 'DESC')
|
||||||
|
.addOrderBy('product.averageRating', 'DESC')
|
||||||
|
.addOrderBy('product.createdAt', 'DESC');
|
||||||
|
return;
|
||||||
|
case ProductListSort.TOP_RATED:
|
||||||
|
query
|
||||||
|
.orderBy('product.averageRating', 'DESC')
|
||||||
|
.addOrderBy('product.reviewsCount', 'DESC')
|
||||||
|
.addOrderBy('product.createdAt', 'DESC');
|
||||||
|
return;
|
||||||
|
case ProductListSort.NEWEST:
|
||||||
|
default:
|
||||||
|
query
|
||||||
|
.orderBy('product.featured', 'DESC')
|
||||||
|
.addOrderBy('product.createdAt', 'DESC')
|
||||||
|
.addOrderBy('product.id', 'DESC');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildProductFacets(filters: FilterProductsDto) {
|
||||||
|
const products = await this.createFilteredProductsQuery(filters, false)
|
||||||
|
.leftJoinAndSelect('product.attributeValues', 'attributeValues')
|
||||||
|
.leftJoinAndSelect('attributeValues.attribute', 'attributeDefinition')
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
const categoryMap = new Map<
|
||||||
|
string,
|
||||||
|
{ id: string; name: string; slug: string; imageUrl: string | null; count: number }
|
||||||
|
>();
|
||||||
|
const brandMap = new Map<
|
||||||
|
string,
|
||||||
|
{ id: string; name: string; slug: string; imageUrl: string | null; count: number }
|
||||||
|
>();
|
||||||
|
const attributeMap = new Map<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
values: Map<string, { value: string; count: number }>;
|
||||||
|
}
|
||||||
|
>();
|
||||||
|
|
||||||
|
let inStock = 0;
|
||||||
|
let outOfStock = 0;
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
if ((product.stock ?? 0) > 0) {
|
||||||
|
inStock += 1;
|
||||||
|
} else {
|
||||||
|
outOfStock += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product.brandEntity) {
|
||||||
|
const currentBrand = brandMap.get(product.brandEntity.id);
|
||||||
|
if (currentBrand) {
|
||||||
|
currentBrand.count += 1;
|
||||||
|
} else {
|
||||||
|
brandMap.set(product.brandEntity.id, {
|
||||||
|
id: product.brandEntity.id,
|
||||||
|
name: product.brandEntity.name,
|
||||||
|
slug: product.brandEntity.slug,
|
||||||
|
imageUrl: product.brandEntity.imageUrl ?? null,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueCategories = new Map<string, Product['primaryCategory']>();
|
||||||
|
if (product.primaryCategory) {
|
||||||
|
uniqueCategories.set(product.primaryCategory.id, product.primaryCategory);
|
||||||
|
}
|
||||||
|
for (const category of product.categories ?? []) {
|
||||||
|
uniqueCategories.set(category.id, category);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const category of uniqueCategories.values()) {
|
||||||
|
if (!category) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentCategory = categoryMap.get(category.id);
|
||||||
|
if (currentCategory) {
|
||||||
|
currentCategory.count += 1;
|
||||||
|
} else {
|
||||||
|
categoryMap.set(category.id, {
|
||||||
|
id: category.id,
|
||||||
|
name: category.name,
|
||||||
|
slug: category.slug,
|
||||||
|
imageUrl: category.imageUrl ?? null,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attributeValue of this.sortAttributes(product.attributeValues)) {
|
||||||
|
if (!attributeValue.attribute?.isVisible || !attributeValue.attribute?.isFilterable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedValue = this.normalizeAttributeFacetValue(attributeValue);
|
||||||
|
if (!normalizedValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributeSlug = attributeValue.attribute.slug;
|
||||||
|
let attributeEntry = attributeMap.get(attributeSlug);
|
||||||
|
if (!attributeEntry) {
|
||||||
|
attributeEntry = {
|
||||||
|
slug: attributeSlug,
|
||||||
|
name: attributeValue.attribute.name,
|
||||||
|
values: new Map<string, { value: string; count: number }>(),
|
||||||
|
};
|
||||||
|
attributeMap.set(attributeSlug, attributeEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentValue = attributeEntry.values.get(normalizedValue);
|
||||||
|
if (currentValue) {
|
||||||
|
currentValue.count += 1;
|
||||||
|
} else {
|
||||||
|
attributeEntry.values.set(normalizedValue, {
|
||||||
|
value: normalizedValue,
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
categories: [...categoryMap.values()].sort((left, right) =>
|
||||||
|
left.name.localeCompare(right.name),
|
||||||
|
),
|
||||||
|
brands: [...brandMap.values()].sort((left, right) =>
|
||||||
|
left.name.localeCompare(right.name),
|
||||||
|
),
|
||||||
|
attributes: [...attributeMap.values()]
|
||||||
|
.map((attribute) => ({
|
||||||
|
slug: attribute.slug,
|
||||||
|
name: attribute.name,
|
||||||
|
values: [...attribute.values.values()].sort((left, right) =>
|
||||||
|
left.value.localeCompare(right.value),
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
.sort((left, right) => left.name.localeCompare(right.name)),
|
||||||
|
stock: {
|
||||||
|
inStock,
|
||||||
|
outOfStock,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeAttributeFacetValue(attributeValue: ProductAttributeValue) {
|
||||||
|
const valueText = attributeValue.valueText ?? attributeValue.attribute?.defaultValueText;
|
||||||
|
if (valueText) {
|
||||||
|
return valueText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueNumber =
|
||||||
|
attributeValue.valueNumber ??
|
||||||
|
(attributeValue.attribute?.defaultValueNumber !== null &&
|
||||||
|
attributeValue.attribute?.defaultValueNumber !== undefined
|
||||||
|
? Number(attributeValue.attribute.defaultValueNumber)
|
||||||
|
: null);
|
||||||
|
if (valueNumber !== null && valueNumber !== undefined) {
|
||||||
|
return String(valueNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueBoolean =
|
||||||
|
attributeValue.valueBoolean ?? attributeValue.attribute?.defaultValueBoolean;
|
||||||
|
if (valueBoolean !== null && valueBoolean !== undefined) {
|
||||||
|
return valueBoolean ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueJson = attributeValue.valueJson ?? attributeValue.attribute?.defaultValueJson;
|
||||||
|
if (Array.isArray(valueJson)) {
|
||||||
|
return valueJson.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueJson && typeof valueJson === 'object') {
|
||||||
|
return JSON.stringify(valueJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private async findOneById(id: string) {
|
private async findOneById(id: string) {
|
||||||
const product = await this.productsRepository.findOne({
|
const product = await this.productsRepository.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
@@ -656,6 +935,36 @@ export class ProductsService {
|
|||||||
return brand;
|
return brand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveBrandBySlug(slug: string, type?: Product['type']) {
|
||||||
|
const brand = await this.brandsRepository.findOne({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
...(type ? { type } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!brand) {
|
||||||
|
throw new NotFoundException('Brand not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async resolveCategoryBySlug(slug: string, type?: Product['type']) {
|
||||||
|
const category = await this.categoriesRepository.findOne({
|
||||||
|
where: {
|
||||||
|
slug,
|
||||||
|
...(type ? { type } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
throw new NotFoundException('Category not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
private async ensurePublishedProduct(productId: string) {
|
private async ensurePublishedProduct(productId: string) {
|
||||||
const product = await this.productsRepository.findOne({
|
const product = await this.productsRepository.findOne({
|
||||||
where: { id: productId, status: ProductStatus.PUBLISHED },
|
where: { id: productId, status: ProductStatus.PUBLISHED },
|
||||||
@@ -882,7 +1191,55 @@ export class ProductsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private serializeProductListItem(product: Product, includeAdminFields: boolean) {
|
private serializePublicProductListItem(product: Product) {
|
||||||
|
const meta = product.meta ?? null;
|
||||||
|
const orderedAttributes = this.sortAttributes(product.attributeValues).filter(
|
||||||
|
(item) => item.attribute?.isVisible ?? true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: product.id,
|
||||||
|
title: product.title,
|
||||||
|
slug: product.slug,
|
||||||
|
brand: product.brand,
|
||||||
|
brandslug: product.brandEntity?.slug ?? null,
|
||||||
|
basePriceUSD: product.basePriceUSD,
|
||||||
|
salePriceUSD: product.salePriceUSD ?? null,
|
||||||
|
stock: product.stock,
|
||||||
|
averageRating: product.averageRating,
|
||||||
|
reviewsCount: product.reviewsCount,
|
||||||
|
mainImageUrl: product.mainImageUrl ?? null,
|
||||||
|
primaryCategory: product.primaryCategory
|
||||||
|
? {
|
||||||
|
id: product.primaryCategory.id,
|
||||||
|
name: product.primaryCategory.name,
|
||||||
|
slug: product.primaryCategory.slug,
|
||||||
|
imageUrl: product.primaryCategory.imageUrl,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
attributes: orderedAttributes.slice(0, 2).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
displayOrder: item.displayOrder ?? 0,
|
||||||
|
attributeId: item.attribute?.id,
|
||||||
|
name: item.attribute?.name,
|
||||||
|
slug: item.attribute?.slug,
|
||||||
|
dataType: item.attribute?.dataType,
|
||||||
|
unit: item.overrideUnit ?? item.attribute?.unit ?? null,
|
||||||
|
valueText: item.valueText ?? item.attribute?.defaultValueText ?? null,
|
||||||
|
valueNumber:
|
||||||
|
item.valueNumber ??
|
||||||
|
(item.attribute?.defaultValueNumber !== null &&
|
||||||
|
item.attribute?.defaultValueNumber !== undefined
|
||||||
|
? Number(item.attribute.defaultValueNumber)
|
||||||
|
: null),
|
||||||
|
valueBoolean:
|
||||||
|
item.valueBoolean ?? item.attribute?.defaultValueBoolean ?? null,
|
||||||
|
valueJson: item.valueJson ?? item.attribute?.defaultValueJson ?? null,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private serializeAdminProductListItem(product: Product) {
|
||||||
const meta = product.meta ?? null;
|
const meta = product.meta ?? null;
|
||||||
const orderedAttributes = this.sortAttributes(product.attributeValues).filter(
|
const orderedAttributes = this.sortAttributes(product.attributeValues).filter(
|
||||||
(item) => item.attribute?.isVisible ?? true,
|
(item) => item.attribute?.isVisible ?? true,
|
||||||
@@ -941,13 +1298,9 @@ export class ProductsService {
|
|||||||
item.valueBoolean ?? item.attribute?.defaultValueBoolean ?? null,
|
item.valueBoolean ?? item.attribute?.defaultValueBoolean ?? null,
|
||||||
valueJson: item.valueJson ?? item.attribute?.defaultValueJson ?? null,
|
valueJson: item.valueJson ?? item.attribute?.defaultValueJson ?? null,
|
||||||
})),
|
})),
|
||||||
...(includeAdminFields
|
status: product.status,
|
||||||
? {
|
createdAt: product.createdAt,
|
||||||
status: product.status,
|
updatedAt: product.updatedAt,
|
||||||
createdAt: product.createdAt,
|
|
||||||
updatedAt: product.updatedAt,
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
883
src/scripts/seed-industrial-products.ts
Normal file
883
src/scripts/seed-industrial-products.ts
Normal file
@@ -0,0 +1,883 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { AppModule } from '../app.module';
|
||||||
|
import { CreateProductDto } from '../modules/catalog/dto/create-product.dto';
|
||||||
|
import { AttributeDefinition } from '../modules/catalog/entities/attribute-definition.entity';
|
||||||
|
import { Brand } from '../modules/catalog/entities/brand.entity';
|
||||||
|
import { Category } from '../modules/catalog/entities/category.entity';
|
||||||
|
import { ProductAttributeValue } from '../modules/catalog/entities/product-attribute-value.entity';
|
||||||
|
import { ProductMeta } from '../modules/catalog/entities/product-meta.entity';
|
||||||
|
import { Product } from '../modules/catalog/entities/product.entity';
|
||||||
|
import { AttributeDataType } from '../modules/catalog/enums/attribute-data-type.enum';
|
||||||
|
import { ProductStatus } from '../modules/catalog/enums/product-status.enum';
|
||||||
|
import { ProductType } from '../modules/catalog/enums/product-type.enum';
|
||||||
|
|
||||||
|
type SeedAttribute = {
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
valueText: string;
|
||||||
|
displayOrder: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SeedProductInput = {
|
||||||
|
brandId: string;
|
||||||
|
brandName: string;
|
||||||
|
categoryId: string;
|
||||||
|
categoryName: string;
|
||||||
|
model: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
basePriceUSD: number;
|
||||||
|
salePriceUSD: number;
|
||||||
|
stock: number;
|
||||||
|
featured?: boolean;
|
||||||
|
tags: string[];
|
||||||
|
attributes: SeedAttribute[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryIds = {
|
||||||
|
deepGroove: '23b15696-fac5-4237-a544-0c3caee8a9f7',
|
||||||
|
selfAligning: '4e617026-3128-440b-9ef9-fcfe8292f76b',
|
||||||
|
tapered: '1e5b26fa-4c85-4456-bd1f-934cad3c2e33',
|
||||||
|
spherical: '5e3203a5-003b-4232-9825-b4232682cf5f',
|
||||||
|
thrust: 'f0cf3ff5-fb2e-4475-90b9-236551e38b90',
|
||||||
|
angularContact: '7ee944d8-dbe2-47d2-80e7-b722d1a9c4ab',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const categoryNames = {
|
||||||
|
[categoryIds.deepGroove]: 'بلبرینگهای شیار عمیق',
|
||||||
|
[categoryIds.selfAligning]: 'بلبرینگهای خودتنظیم',
|
||||||
|
[categoryIds.tapered]: 'رولبرینگهای مخروطی',
|
||||||
|
[categoryIds.spherical]: 'رولبرینگهای بشکهای',
|
||||||
|
[categoryIds.thrust]: 'بلبرینگهای کفگرد',
|
||||||
|
[categoryIds.angularContact]: 'بلبرینگهای تماس زاویهای',
|
||||||
|
} satisfies Record<string, string>;
|
||||||
|
|
||||||
|
const brandIds = {
|
||||||
|
skf: 'd3466c35-f9cd-4ed5-8dba-1b19217d3498',
|
||||||
|
fag: '44bd3e53-c1f5-447d-9e2a-ee5175996ef4',
|
||||||
|
nsk: '89952168-20ee-4209-aff0-cbf8a915fd54',
|
||||||
|
timken: 'af81bbef-8ce9-4001-a193-cc472c7ccf15',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const brandNames = {
|
||||||
|
[brandIds.skf]: 'SKF',
|
||||||
|
[brandIds.fag]: 'FAG',
|
||||||
|
[brandIds.nsk]: 'NSK',
|
||||||
|
[brandIds.timken]: 'TIMKEN',
|
||||||
|
} satisfies Record<string, string>;
|
||||||
|
|
||||||
|
const seedProducts: SeedProductInput[] = [
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.deepGroove,
|
||||||
|
categoryName: categoryNames[categoryIds.deepGroove],
|
||||||
|
model: '6204-2RS',
|
||||||
|
title: 'بلبرینگ SKF مدل 6204-2RS',
|
||||||
|
description:
|
||||||
|
'بلبرینگ شیار عمیق تک ردیفه، دارای آببند پلاستیکی در دو طرف (2RS) برای جلوگیری از نفوذ گرد و غبار. مناسب برای الکتروموتورها و پمپهای کوچک.',
|
||||||
|
basePriceUSD: 15,
|
||||||
|
salePriceUSD: 13,
|
||||||
|
stock: 36,
|
||||||
|
featured: true,
|
||||||
|
tags: ['industrial', 'deep-groove', 'skf', '2rs'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'شیار عمیق تک ردیفه', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'آببند پلاستیکی دوطرفه 2RS', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'الکتروموتورها و پمپهای کوچک', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.deepGroove,
|
||||||
|
categoryName: categoryNames[categoryIds.deepGroove],
|
||||||
|
model: '6305-ZZ',
|
||||||
|
title: 'بلبرینگ FAG مدل 6305-ZZ',
|
||||||
|
description:
|
||||||
|
'بلبرینگ با واشر فلزی دوطرفه (ZZ). مقاوم در برابر حرارت بالا و مناسب برای کاربردهایی با سرعت چرخش زیاد و محیطهای خشک.',
|
||||||
|
basePriceUSD: 18,
|
||||||
|
salePriceUSD: 16,
|
||||||
|
stock: 28,
|
||||||
|
tags: ['industrial', 'deep-groove', 'fag', 'zz'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'شیار عمیق تک ردیفه', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'واشر فلزی دوطرفه ZZ', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سرعت چرخش بالا در محیطهای خشک', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.nsk,
|
||||||
|
brandName: 'NSK',
|
||||||
|
categoryId: categoryIds.deepGroove,
|
||||||
|
categoryName: categoryNames[categoryIds.deepGroove],
|
||||||
|
model: '6008-DDU',
|
||||||
|
title: 'بلبرینگ NSK مدل 6008-DDU',
|
||||||
|
description:
|
||||||
|
'طراحی بهینهسازی شده برای کاهش اصطکاک. دارای کاسه نمد لاستیکی با تماس کامل، ایدهآل برای تجهیزات کشاورزی و محیطهای آلوده.',
|
||||||
|
basePriceUSD: 21,
|
||||||
|
salePriceUSD: 18,
|
||||||
|
stock: 24,
|
||||||
|
tags: ['industrial', 'deep-groove', 'nsk', 'ddu'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'شیار عمیق با اصطکاک پایین', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'کاسه نمد لاستیکی DDU با تماس کامل', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'تجهیزات کشاورزی و محیطهای آلوده', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.deepGroove,
|
||||||
|
categoryName: categoryNames[categoryIds.deepGroove],
|
||||||
|
model: '6210-C3',
|
||||||
|
title: 'بلبرینگ SKF مدل 6210-C3',
|
||||||
|
description:
|
||||||
|
'دارای لقی داخلی بیشتر از حد استاندارد (C3) برای جبران انبساط حرارتی. پرکاربرد در گیربکسها و شفتهای دور بالا.',
|
||||||
|
basePriceUSD: 24,
|
||||||
|
salePriceUSD: 22,
|
||||||
|
stock: 18,
|
||||||
|
tags: ['industrial', 'deep-groove', 'skf', 'c3'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'شیار عمیق تک ردیفه', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'لقی داخلی بیشتر C3', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'گیربکسها و شفتهای دور بالا', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.deepGroove,
|
||||||
|
categoryName: categoryNames[categoryIds.deepGroove],
|
||||||
|
model: '6004',
|
||||||
|
title: 'بلبرینگ FAG مدل 6004',
|
||||||
|
description:
|
||||||
|
'بلبرینگ بدون آببند (باز)، مناسب برای کار در محیطهای غوطهور در روغن با قابلیت تحمل بارهای شعاعی متوسط.',
|
||||||
|
basePriceUSD: 11,
|
||||||
|
salePriceUSD: 9,
|
||||||
|
stock: 44,
|
||||||
|
tags: ['industrial', 'deep-groove', 'fag', 'open'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'بلبرینگ باز بدون آببند', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'مناسب کارکرد در روغن', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'بارهای شعاعی متوسط در سیستمهای روغنکاریشده', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.angularContact,
|
||||||
|
categoryName: categoryNames[categoryIds.angularContact],
|
||||||
|
model: '7205 BEP',
|
||||||
|
title: 'بلبرینگ SKF مدل 7205 BEP',
|
||||||
|
description:
|
||||||
|
'بلبرینگ تماس زاویهای تک ردیفه با زاویه تماس ۴۰ درجه و قفسه پلیآمیدی. مناسب برای تحمل همزمان بارهای شعاعی و محوری یکطرفه.',
|
||||||
|
basePriceUSD: 29,
|
||||||
|
salePriceUSD: 26,
|
||||||
|
stock: 17,
|
||||||
|
featured: true,
|
||||||
|
tags: ['industrial', 'angular-contact', 'skf', 'bep'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویهای تک ردیفه', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'زاویه تماس 40 درجه با قفسه پلیآمیدی', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'تحمل همزمان بار شعاعی و محوری یکطرفه', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.angularContact,
|
||||||
|
categoryName: categoryNames[categoryIds.angularContact],
|
||||||
|
model: '7308 B.TVP',
|
||||||
|
title: 'بلبرینگ FAG مدل 7308 B.TVP',
|
||||||
|
description:
|
||||||
|
'طراحی شده برای کارکردهای دقیق و سرعت بالا. دارای قفسه پلاستیکی تقویت شده با الیاف شیشه، پرکاربرد در ماشینابزارها.',
|
||||||
|
basePriceUSD: 34,
|
||||||
|
salePriceUSD: 31,
|
||||||
|
stock: 13,
|
||||||
|
tags: ['industrial', 'angular-contact', 'fag', 'tvp'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویهای دقیق برای سرعت بالا', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه پلاستیکی تقویتشده با الیاف شیشه', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'ماشینابزارها و تجهیزات دقیق', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.nsk,
|
||||||
|
brandName: 'NSK',
|
||||||
|
categoryId: categoryIds.angularContact,
|
||||||
|
categoryName: categoryNames[categoryIds.angularContact],
|
||||||
|
model: '7210 BW',
|
||||||
|
title: 'بلبرینگ NSK مدل 7210 BW',
|
||||||
|
description:
|
||||||
|
'زاویه تماس استاندارد، مجهز به قفسه فولادی پرسکاری شده. دوام عالی در پمپهای گریز از مرکز.',
|
||||||
|
basePriceUSD: 32,
|
||||||
|
salePriceUSD: 29,
|
||||||
|
stock: 15,
|
||||||
|
tags: ['industrial', 'angular-contact', 'nsk', 'bw'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویهای با زاویه استاندارد', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی پرسکاری شده', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'پمپهای گریز از مرکز', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.angularContact,
|
||||||
|
categoryName: categoryNames[categoryIds.angularContact],
|
||||||
|
model: '3206 A-2RS1',
|
||||||
|
title: 'بلبرینگ SKF مدل 3206 A-2RS1',
|
||||||
|
description:
|
||||||
|
'بلبرینگ تماس زاویهای دو ردیفه مجهز به آببند لاستیکی. عملکردی مشابه دو بلبرینگ تک ردیفه پشت به پشت با اشغال فضای کمتر.',
|
||||||
|
basePriceUSD: 38,
|
||||||
|
salePriceUSD: 35,
|
||||||
|
stock: 11,
|
||||||
|
tags: ['industrial', 'angular-contact', 'skf', '2rs1'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویهای دو ردیفه', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'آببند لاستیکی 2RS1 با فضای نصب کمتر', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'جایگزین فشرده برای آرایش پشت به پشت', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.angularContact,
|
||||||
|
categoryName: categoryNames[categoryIds.angularContact],
|
||||||
|
model: '7312 B.MP',
|
||||||
|
title: 'بلبرینگ FAG مدل 7312 B.MP',
|
||||||
|
description:
|
||||||
|
'دارای قفسه برنجی ماشینکاری شده (MP) برای مقاومت حداکثری در برابر بارهای شوکدار و ارتعاشات صنعتی.',
|
||||||
|
basePriceUSD: 42,
|
||||||
|
salePriceUSD: 39,
|
||||||
|
stock: 9,
|
||||||
|
tags: ['industrial', 'angular-contact', 'fag', 'mp'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'تماس زاویهای تک ردیفه صنعتی', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه برنجی ماشینکاریشده MP', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'بارهای شوکدار و ارتعاشات صنعتی', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.selfAligning,
|
||||||
|
categoryName: categoryNames[categoryIds.selfAligning],
|
||||||
|
model: '1205 EKTN9',
|
||||||
|
title: 'بلبرینگ SKF مدل 1205 EKTN9',
|
||||||
|
description:
|
||||||
|
'سوراخ مخروطی (Tapered Bore) با قفسه پلیآمیدی. توانایی جبران انحرافات شفت تا ۲.۵ درجه، ایدهآل برای شفتهای بلند.',
|
||||||
|
basePriceUSD: 23,
|
||||||
|
salePriceUSD: 20,
|
||||||
|
stock: 21,
|
||||||
|
tags: ['industrial', 'self-aligning', 'skf', 'ektn9'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم با سوراخ مخروطی', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'جبران انحراف شفت تا 2.5 درجه', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'شفتهای بلند و ناهمراستا', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.selfAligning,
|
||||||
|
categoryName: categoryNames[categoryIds.selfAligning],
|
||||||
|
model: '1308 TVH',
|
||||||
|
title: 'بلبرینگ FAG مدل 1308 TVH',
|
||||||
|
description:
|
||||||
|
'بلبرینگ خودتنظیم با طراحی داخلی بهینهشده. مناسب برای تجهیزات نساجی و ماشینآلات کاغذسازی با انحرافات زاویهای مکرر.',
|
||||||
|
basePriceUSD: 27,
|
||||||
|
salePriceUSD: 24,
|
||||||
|
stock: 16,
|
||||||
|
tags: ['industrial', 'self-aligning', 'fag', 'tvh'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم با طراحی داخلی بهینه', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'عملکرد پایدار در انحرافات زاویهای مکرر', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'تجهیزات نساجی و کاغذسازی', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.nsk,
|
||||||
|
brandName: 'NSK',
|
||||||
|
categoryId: categoryIds.selfAligning,
|
||||||
|
categoryName: categoryNames[categoryIds.selfAligning],
|
||||||
|
model: '2210 K',
|
||||||
|
title: 'بلبرینگ NSK مدل 2210 K',
|
||||||
|
description:
|
||||||
|
'دارای سوراخ مخروطی، نصب آسان با استفاده از غلاف کلاچ (Adapter Sleeve). اصطکاک بسیار پایین و کارکرد خنک.',
|
||||||
|
basePriceUSD: 31,
|
||||||
|
salePriceUSD: 28,
|
||||||
|
stock: 14,
|
||||||
|
tags: ['industrial', 'self-aligning', 'nsk', 'k'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم با سوراخ مخروطی K', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'نصب آسان با Adapter Sleeve', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سیستمهای نیازمند اصطکاک پایین و کارکرد خنک', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.selfAligning,
|
||||||
|
categoryName: categoryNames[categoryIds.selfAligning],
|
||||||
|
model: '2306-2RS1TN9',
|
||||||
|
title: 'بلبرینگ SKF مدل 2306-2RS1TN9',
|
||||||
|
description:
|
||||||
|
'نسخه آببندی شده بلبرینگ خودتنظیم. بدون نیاز به روانکاری مجدد (Maintenance-free)، مناسب برای محیطهای کثیف.',
|
||||||
|
basePriceUSD: 35,
|
||||||
|
salePriceUSD: 32,
|
||||||
|
stock: 12,
|
||||||
|
tags: ['industrial', 'self-aligning', 'skf', '2rs1tn9'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم آببندیشده', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'بدون نیاز به روانکاری مجدد', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'محیطهای کثیف و پرگردوغبار', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.selfAligning,
|
||||||
|
categoryName: categoryNames[categoryIds.selfAligning],
|
||||||
|
model: '1212',
|
||||||
|
title: 'بلبرینگ FAG مدل 1212',
|
||||||
|
description:
|
||||||
|
'مدل استاندارد سوراخ استوانهای با قفسه فولادی. تحمل بارهای شعاعی ملایم و سرعتهای دورانی بالا.',
|
||||||
|
basePriceUSD: 22,
|
||||||
|
salePriceUSD: 19,
|
||||||
|
stock: 22,
|
||||||
|
tags: ['industrial', 'self-aligning', 'fag', 'standard'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'خودتنظیم با سوراخ استوانهای', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی استاندارد', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'بارهای شعاعی ملایم با سرعت دورانی بالا', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.timken,
|
||||||
|
brandName: 'TIMKEN',
|
||||||
|
categoryId: categoryIds.tapered,
|
||||||
|
categoryName: categoryNames[categoryIds.tapered],
|
||||||
|
model: '32210',
|
||||||
|
title: 'رولبرینگ TIMKEN مدل 32210',
|
||||||
|
description:
|
||||||
|
'طراحی اختصاصی تیمکن برای تحمل بارهای ترکیبی (شعاعی و محوری) سنگین. کاربرد وسیع در توپی چرخ خودروهای سنگین و گیربکسها.',
|
||||||
|
basePriceUSD: 48,
|
||||||
|
salePriceUSD: 44,
|
||||||
|
stock: 19,
|
||||||
|
featured: true,
|
||||||
|
tags: ['industrial', 'tapered-roller', 'timken', 'heavy-duty'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی برای بار ترکیبی', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بارهای شعاعی و محوری سنگین', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'توپی چرخ خودروهای سنگین و گیربکسها', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.tapered,
|
||||||
|
categoryName: categoryNames[categoryIds.tapered],
|
||||||
|
model: '30205 J2/Q',
|
||||||
|
title: 'رولبرینگ SKF مدل 30205 J2/Q',
|
||||||
|
description:
|
||||||
|
'کیفیت سطح هندسی فوقالعاده بالا برای توزیع یکنواخت بار. دارای قفسه فولادی استاندارد، مناسب برای پینیون دیفرانسیل.',
|
||||||
|
basePriceUSD: 37,
|
||||||
|
salePriceUSD: 34,
|
||||||
|
stock: 20,
|
||||||
|
tags: ['industrial', 'tapered-roller', 'skf', 'j2-q'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی متریک', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'توزیع یکنواخت بار با کیفیت سطح هندسی بالا', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'پینیون دیفرانسیل و انتقال قدرت', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.tapered,
|
||||||
|
categoryName: categoryNames[categoryIds.tapered],
|
||||||
|
model: '32309-A',
|
||||||
|
title: 'رولبرینگ FAG مدل 32309-A',
|
||||||
|
description:
|
||||||
|
'زاویه تماس بزرگتر برای تحمل بارهای محوری شدیدتر. استفاده گسترده در گیربکسهای صنعتی و تجهیزات معدنی.',
|
||||||
|
basePriceUSD: 41,
|
||||||
|
salePriceUSD: 38,
|
||||||
|
stock: 17,
|
||||||
|
tags: ['industrial', 'tapered-roller', 'fag', 'axial-load'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی با زاویه تماس بزرگ', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بالاتر بارهای محوری', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'گیربکسهای صنعتی و تجهیزات معدنی', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.timken,
|
||||||
|
brandName: 'TIMKEN',
|
||||||
|
categoryId: categoryIds.tapered,
|
||||||
|
categoryName: categoryNames[categoryIds.tapered],
|
||||||
|
model: '30306 M',
|
||||||
|
title: 'رولبرینگ TIMKEN مدل 30306 M',
|
||||||
|
description:
|
||||||
|
'طراحی متریک استاندارد با مقاومت سایش بالا. ایدهآل برای محورهای دوار در ماشینآلات راهسازی.',
|
||||||
|
basePriceUSD: 39,
|
||||||
|
salePriceUSD: 36,
|
||||||
|
stock: 16,
|
||||||
|
tags: ['industrial', 'tapered-roller', 'timken', 'metric'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی متریک استاندارد', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'مقاومت سایش بالا', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'محورهای دوار در ماشینآلات راهسازی', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.nsk,
|
||||||
|
brandName: 'NSK',
|
||||||
|
categoryId: categoryIds.tapered,
|
||||||
|
categoryName: categoryNames[categoryIds.tapered],
|
||||||
|
model: '32015 X',
|
||||||
|
title: 'رولبرینگ NSK مدل 32015 X',
|
||||||
|
description:
|
||||||
|
'طراحی فشرده با مقطع باریک. توانایی تحمل بارهای سنگین در فضاهای محدود سیستمهای انتقال قدرت.',
|
||||||
|
basePriceUSD: 46,
|
||||||
|
salePriceUSD: 42,
|
||||||
|
stock: 12,
|
||||||
|
tags: ['industrial', 'tapered-roller', 'nsk', 'compact'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ مخروطی فشرده', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بار سنگین در فضای محدود', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سیستمهای انتقال قدرت با فضای نصب محدود', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.spherical,
|
||||||
|
categoryName: categoryNames[categoryIds.spherical],
|
||||||
|
model: '22215 EK',
|
||||||
|
title: 'رولبرینگ SKF مدل 22215 EK',
|
||||||
|
description:
|
||||||
|
'سوراخ مخروطی، تحمل بارهای فوقسنگین شعاعی و جبران ناهمراستایی شفت. قلب تپنده سنگشکنها و آسیابهای صنعتی.',
|
||||||
|
basePriceUSD: 67,
|
||||||
|
salePriceUSD: 61,
|
||||||
|
stock: 10,
|
||||||
|
featured: true,
|
||||||
|
tags: ['industrial', 'spherical-roller', 'skf', 'heavy-duty'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکهای با سوراخ مخروطی', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بار فوقسنگین و جبران ناهمراستایی', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سنگشکنها و آسیابهای صنعتی', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.spherical,
|
||||||
|
categoryName: categoryNames[categoryIds.spherical],
|
||||||
|
model: '22318 E1.CC',
|
||||||
|
title: 'رولبرینگ FAG مدل 22318 E1.CC',
|
||||||
|
description:
|
||||||
|
'طراحی سری E1 با ظرفیت تحمل بار ارتقا یافته. دارای قفسه فولادی دوتکه، مناسب برای سرندهای ارتعاشی و نوار نقالهها.',
|
||||||
|
basePriceUSD: 72,
|
||||||
|
salePriceUSD: 66,
|
||||||
|
stock: 8,
|
||||||
|
tags: ['industrial', 'spherical-roller', 'fag', 'e1-cc'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکهای سری E1', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی دوتکه با ظرفیت بار ارتقایافته', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سرندهای ارتعاشی و نوار نقالهها', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.timken,
|
||||||
|
brandName: 'TIMKEN',
|
||||||
|
categoryId: categoryIds.spherical,
|
||||||
|
categoryName: categoryNames[categoryIds.spherical],
|
||||||
|
model: '22220 EJ W33',
|
||||||
|
title: 'رولبرینگ TIMKEN مدل 22220 EJ W33',
|
||||||
|
description:
|
||||||
|
'مجهز به شیار و سه سوراخ روانکاری (W33) روی حلقه خارجی. طول عمر بالا در شرایط کاری خشن صنایع فولاد.',
|
||||||
|
basePriceUSD: 78,
|
||||||
|
salePriceUSD: 71,
|
||||||
|
stock: 7,
|
||||||
|
tags: ['industrial', 'spherical-roller', 'timken', 'w33'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکهای صنعتی سنگین', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'شیار و سه سوراخ روانکاری W33', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'شرایط کاری خشن صنایع فولاد', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.spherical,
|
||||||
|
categoryName: categoryNames[categoryIds.spherical],
|
||||||
|
model: '22312 E',
|
||||||
|
title: 'رولبرینگ SKF مدل 22312 E',
|
||||||
|
description:
|
||||||
|
'سوراخ استوانهای با طراحی داخلی بهینهشده برای کاهش دمای کاری. قابل اطمینان در توربینهای بادی و صنایع دریایی.',
|
||||||
|
basePriceUSD: 63,
|
||||||
|
salePriceUSD: 58,
|
||||||
|
stock: 9,
|
||||||
|
tags: ['industrial', 'spherical-roller', 'skf', 'low-temperature'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکهای با سوراخ استوانهای', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'طراحی داخلی بهینه برای کاهش دمای کاری', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'توربینهای بادی و صنایع دریایی', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.nsk,
|
||||||
|
brandName: 'NSK',
|
||||||
|
categoryId: categoryIds.spherical,
|
||||||
|
categoryName: categoryNames[categoryIds.spherical],
|
||||||
|
model: '22211 EA',
|
||||||
|
title: 'رولبرینگ NSK مدل 22211 EA',
|
||||||
|
description:
|
||||||
|
'قفسه فولادی پرسکاری شده مقاوم. عملکرد عالی در سرعتهای بالا با حداقل لرزش و صدای تولیدی.',
|
||||||
|
basePriceUSD: 59,
|
||||||
|
salePriceUSD: 54,
|
||||||
|
stock: 11,
|
||||||
|
tags: ['industrial', 'spherical-roller', 'nsk', 'ea'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'رولبرینگ بشکهای برای سرعت بالا', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی پرسکاریشده مقاوم', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'سیستمهای سرعت بالا با لرزش و صدای کم', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.thrust,
|
||||||
|
categoryName: categoryNames[categoryIds.thrust],
|
||||||
|
model: '51105',
|
||||||
|
title: 'بلبرینگ SKF مدل 51105',
|
||||||
|
description:
|
||||||
|
'بلبرینگ کفگرد تکجهته، تحمل منحصراً بارهای محوری. طراحی شده با واشر شفت و واشر محفظه با دقت بالا.',
|
||||||
|
basePriceUSD: 17,
|
||||||
|
salePriceUSD: 15,
|
||||||
|
stock: 26,
|
||||||
|
tags: ['industrial', 'thrust-bearing', 'skf', 'single-direction'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کفگرد تکجهته', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل انحصاری بار محوری', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'موقعیتهای نیازمند دقت بالای واشر شفت و محفظه', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.thrust,
|
||||||
|
categoryName: categoryNames[categoryIds.thrust],
|
||||||
|
model: '51208',
|
||||||
|
title: 'بلبرینگ FAG مدل 51208',
|
||||||
|
description:
|
||||||
|
'مناسب برای بارهای محوری متوسط تا سنگین. قطعات کاملاً قابل تفکیک برای نصب و نگهداری آسان در جکهای صنعتی.',
|
||||||
|
basePriceUSD: 22,
|
||||||
|
salePriceUSD: 20,
|
||||||
|
stock: 18,
|
||||||
|
tags: ['industrial', 'thrust-bearing', 'fag', 'serviceable'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کفگرد تکجهته قابل تفکیک', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'نصب و نگهداری آسان', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'جکهای صنعتی و بارهای محوری متوسط تا سنگین', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.nsk,
|
||||||
|
brandName: 'NSK',
|
||||||
|
categoryId: categoryIds.thrust,
|
||||||
|
categoryName: categoryNames[categoryIds.thrust],
|
||||||
|
model: '51310',
|
||||||
|
title: 'بلبرینگ NSK مدل 51310',
|
||||||
|
description:
|
||||||
|
'مقطع ضخیمتر برای کاربردهای سنگینتر مانند دستگاههای پرس و اکسترودرها. دارای قفسه فولادی ماشینکاری شده.',
|
||||||
|
basePriceUSD: 28,
|
||||||
|
salePriceUSD: 25,
|
||||||
|
stock: 14,
|
||||||
|
tags: ['industrial', 'thrust-bearing', 'nsk', 'heavy-section'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کفگرد با مقطع ضخیمتر', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'قفسه فولادی ماشینکاریشده', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'دستگاههای پرس و اکسترودرها', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.skf,
|
||||||
|
brandName: 'SKF',
|
||||||
|
categoryId: categoryIds.thrust,
|
||||||
|
categoryName: categoryNames[categoryIds.thrust],
|
||||||
|
model: '51112',
|
||||||
|
title: 'بلبرینگ SKF مدل 51112',
|
||||||
|
description:
|
||||||
|
'ایدهآل برای موقعیتدهی محوری شفتها. کیفیت ساخت بالا برای جلوگیری از لغزش ساچمهها در سرعتهای دورانی متغیر.',
|
||||||
|
basePriceUSD: 24,
|
||||||
|
salePriceUSD: 22,
|
||||||
|
stock: 16,
|
||||||
|
tags: ['industrial', 'thrust-bearing', 'skf', 'shaft-positioning'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کفگرد تکجهته برای موقعیتدهی محوری', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'جلوگیری از لغزش ساچمهها در سرعت متغیر', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'موقعیتدهی محوری شفتها', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
brandId: brandIds.fag,
|
||||||
|
brandName: 'FAG',
|
||||||
|
categoryId: categoryIds.thrust,
|
||||||
|
categoryName: categoryNames[categoryIds.thrust],
|
||||||
|
model: '52205',
|
||||||
|
title: 'بلبرینگ FAG مدل 52205',
|
||||||
|
description:
|
||||||
|
'بلبرینگ کفگرد دوجهته. قابلیت تحمل بارهای محوری در هر دو جهت و تثبیت کامل موقعیت شفت، مناسب برای ماشینهای فرز و تراش.',
|
||||||
|
basePriceUSD: 31,
|
||||||
|
salePriceUSD: 28,
|
||||||
|
stock: 12,
|
||||||
|
tags: ['industrial', 'thrust-bearing', 'fag', 'double-direction'],
|
||||||
|
attributes: [
|
||||||
|
{ name: 'نوع طراحی', slug: 'design-type', valueText: 'کفگرد دوجهته', displayOrder: 0 },
|
||||||
|
{ name: 'ویژگی شاخص', slug: 'key-feature', valueText: 'تحمل بار محوری در هر دو جهت', displayOrder: 1 },
|
||||||
|
{ name: 'کاربرد پیشنهادی', slug: 'recommended-application', valueText: 'ماشینهای فرز و تراش', displayOrder: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function slugify(value: string) {
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[./]/g, '-')
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.replace(/[^a-z0-9-]/g, '')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-|-$/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPayload(input: SeedProductInput): CreateProductDto {
|
||||||
|
const slug = slugify(`${input.brandName}-${input.model}`);
|
||||||
|
const summary = input.description.length > 160
|
||||||
|
? `${input.description.slice(0, 157)}...`
|
||||||
|
: input.description;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sku: `${input.brandName}-${input.model}`.replace(/\s+/g, '-'),
|
||||||
|
title: input.title,
|
||||||
|
slug,
|
||||||
|
technicalCode: input.model,
|
||||||
|
brandId: input.brandId,
|
||||||
|
basePriceUSD: input.basePriceUSD,
|
||||||
|
salePriceUSD: input.salePriceUSD,
|
||||||
|
stock: input.stock,
|
||||||
|
type: ProductType.INDUSTRIAL,
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
featured: input.featured ?? false,
|
||||||
|
primaryCategoryId: input.categoryId,
|
||||||
|
categoryIds: [input.categoryId],
|
||||||
|
tags: input.tags,
|
||||||
|
meta: {
|
||||||
|
shortDescription: summary,
|
||||||
|
description: [
|
||||||
|
input.description,
|
||||||
|
`برند: ${input.brandName}`,
|
||||||
|
`دستهبندی: ${input.categoryName}`,
|
||||||
|
`کد فنی: ${input.model}`,
|
||||||
|
].join('\n'),
|
||||||
|
metaTitle: input.title,
|
||||||
|
metaDescription: summary,
|
||||||
|
shareTitle: input.title,
|
||||||
|
shareDescription: summary,
|
||||||
|
},
|
||||||
|
attributes: input.attributes.map((attribute) => ({
|
||||||
|
name: attribute.name,
|
||||||
|
slug: attribute.slug,
|
||||||
|
dataType: AttributeDataType.TEXT,
|
||||||
|
valueText: attribute.valueText,
|
||||||
|
displayOrder: attribute.displayOrder,
|
||||||
|
isVisible: true,
|
||||||
|
isFilterable: attribute.displayOrder < 2,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||||
|
logger: ['error', 'warn', 'log'],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const productRepository = app.get<Repository<Product>>(getRepositoryToken(Product));
|
||||||
|
const brandRepository = app.get<Repository<Brand>>(getRepositoryToken(Brand));
|
||||||
|
const categoryRepository = app.get<Repository<Category>>(getRepositoryToken(Category));
|
||||||
|
const productMetaRepository = app.get<Repository<ProductMeta>>(getRepositoryToken(ProductMeta));
|
||||||
|
const productAttributeValueRepository = app.get<Repository<ProductAttributeValue>>(
|
||||||
|
getRepositoryToken(ProductAttributeValue),
|
||||||
|
);
|
||||||
|
const attributeDefinitionRepository = app.get<Repository<AttributeDefinition>>(
|
||||||
|
getRepositoryToken(AttributeDefinition),
|
||||||
|
);
|
||||||
|
|
||||||
|
const brands = await brandRepository.findBy(
|
||||||
|
Object.values(brandIds).map((id) => ({ id })),
|
||||||
|
);
|
||||||
|
const categories = await categoryRepository.findBy(
|
||||||
|
Object.values(categoryIds).map((id) => ({ id })),
|
||||||
|
);
|
||||||
|
const brandById = new Map(brands.map((item) => [item.id, item]));
|
||||||
|
const categoryById = new Map(categories.map((item) => [item.id, item]));
|
||||||
|
|
||||||
|
const requiredAttributeDefinitions = [
|
||||||
|
{
|
||||||
|
slug: 'design-type',
|
||||||
|
name: 'نوع طراحی',
|
||||||
|
isFilterable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'key-feature',
|
||||||
|
name: 'ویژگی شاخص',
|
||||||
|
isFilterable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'recommended-application',
|
||||||
|
name: 'کاربرد پیشنهادی',
|
||||||
|
isFilterable: false,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const attributeDefinitionBySlug = new Map<string, AttributeDefinition>();
|
||||||
|
for (const definition of requiredAttributeDefinitions) {
|
||||||
|
let attributeDefinition = await attributeDefinitionRepository.findOne({
|
||||||
|
where: { slug: definition.slug },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!attributeDefinition) {
|
||||||
|
attributeDefinition = await attributeDefinitionRepository.save(
|
||||||
|
attributeDefinitionRepository.create({
|
||||||
|
name: definition.name,
|
||||||
|
slug: definition.slug,
|
||||||
|
dataType: AttributeDataType.TEXT,
|
||||||
|
unit: null,
|
||||||
|
options: [],
|
||||||
|
isFilterable: definition.isFilterable,
|
||||||
|
isVisible: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeDefinitionBySlug.set(definition.slug, attributeDefinition);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Seeding ${seedProducts.length} industrial products...`);
|
||||||
|
|
||||||
|
for (const item of seedProducts) {
|
||||||
|
const payload = buildPayload(item);
|
||||||
|
const brand = brandById.get(item.brandId);
|
||||||
|
const category = categoryById.get(item.categoryId);
|
||||||
|
|
||||||
|
if (!brand) {
|
||||||
|
throw new Error(`Brand not found for seed input: ${item.brandName} (${item.brandId})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!category) {
|
||||||
|
throw new Error(
|
||||||
|
`Category not found for seed input: ${item.categoryName} (${item.categoryId})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await productRepository
|
||||||
|
.createQueryBuilder('product')
|
||||||
|
.select('product.id', 'id')
|
||||||
|
.where('product.slug = :slug', { slug: payload.slug })
|
||||||
|
.getRawOne<{ id: string }>();
|
||||||
|
|
||||||
|
const product = await productRepository.save(
|
||||||
|
productRepository.create({
|
||||||
|
id: existing?.id,
|
||||||
|
sku: payload.sku,
|
||||||
|
title: payload.title,
|
||||||
|
slug: payload.slug,
|
||||||
|
technicalCode: payload.technicalCode,
|
||||||
|
brand: item.brandName,
|
||||||
|
brandEntity: brand,
|
||||||
|
basePriceUSD: payload.basePriceUSD,
|
||||||
|
salePriceUSD: payload.salePriceUSD ?? null,
|
||||||
|
stock: payload.stock,
|
||||||
|
featured: payload.featured ?? false,
|
||||||
|
type: ProductType.INDUSTRIAL,
|
||||||
|
status: ProductStatus.PUBLISHED,
|
||||||
|
primaryCategory: category,
|
||||||
|
categories: [category],
|
||||||
|
tags: payload.tags ?? [],
|
||||||
|
mainImageUrl: null,
|
||||||
|
threeDModelUrl: null,
|
||||||
|
imageGalleryUrls: [],
|
||||||
|
averageRating: existing?.id ? undefined : 0,
|
||||||
|
reviewsCount: existing?.id ? undefined : 0,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const existingMeta = await productMetaRepository
|
||||||
|
.createQueryBuilder('meta')
|
||||||
|
.select('meta.id', 'id')
|
||||||
|
.where('meta.product_id = :productId', { productId: product.id })
|
||||||
|
.getRawOne<{ id: string }>();
|
||||||
|
|
||||||
|
await productMetaRepository.save(
|
||||||
|
productMetaRepository.create({
|
||||||
|
id: existingMeta?.id,
|
||||||
|
product,
|
||||||
|
shortDescription: payload.meta?.shortDescription ?? null,
|
||||||
|
description: payload.meta?.description ?? null,
|
||||||
|
metaTitle: payload.meta?.metaTitle ?? null,
|
||||||
|
metaDescription: payload.meta?.metaDescription ?? null,
|
||||||
|
shareTitle: payload.meta?.shareTitle ?? null,
|
||||||
|
shareDescription: payload.meta?.shareDescription ?? null,
|
||||||
|
shareImageUrl: payload.meta?.shareImageUrl ?? null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await productAttributeValueRepository
|
||||||
|
.createQueryBuilder()
|
||||||
|
.delete()
|
||||||
|
.from(ProductAttributeValue)
|
||||||
|
.where('productId = :productId', { productId: product.id })
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
const attributeValues = (payload.attributes ?? []).map((attribute) => {
|
||||||
|
const definition = attributeDefinitionBySlug.get(attribute.slug ?? '');
|
||||||
|
if (!definition) {
|
||||||
|
throw new Error(`Attribute definition not found for slug: ${attribute.slug}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return productAttributeValueRepository.create({
|
||||||
|
product,
|
||||||
|
attribute: definition,
|
||||||
|
displayOrder: attribute.displayOrder ?? 0,
|
||||||
|
valueText: attribute.valueText ?? null,
|
||||||
|
valueNumber: attribute.valueNumber ?? null,
|
||||||
|
valueBoolean: attribute.valueBoolean ?? null,
|
||||||
|
valueJson: attribute.valueJson ?? null,
|
||||||
|
overrideUnit: attribute.overrideUnit ?? null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (attributeValues.length > 0) {
|
||||||
|
await productAttributeValueRepository.save(attributeValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${existing?.id ? 'Updated' : 'Created'}: ${payload.title}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Industrial products seed completed successfully.');
|
||||||
|
} finally {
|
||||||
|
await app.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main().catch((error: unknown) => {
|
||||||
|
console.error('Industrial products seed failed.');
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user