UNPKG

goods-exporter

Version:

A versatile JavaScript library for exporting goods data to various formats such as YML, CSV, and Excel. Simplify data export tasks with ease. Supports streams.

1 lines 115 kB
{"version":3,"file":"index.cjs","sources":["../../src/utils/buildCategoryPath.ts","../../src/utils/writeWithDrain.ts","../../src/utils/delay.ts","../../src/utils/getRFC3339Date.ts","../../src/utils/urlQueryEncode.ts","../../src/streams/CSVStream.ts","../../src/formatter/formater.types.ts","../../src/formatter/CSV.formatter.ts","../../src/formatter/Excel.formatter.ts","../../src/formatter/Insales.formatter.ts","../../src/formatter/JSON.formatter.ts","../../src/formatter/Price.formatter.ts","../../src/formatter/SimpleJSON.formatter.ts","../../src/formatter/TgShop.formatter.ts","../../src/formatter/Tilda.formatter.ts","../../src/formatter/WooCommerce.formatter.ts","../../src/formatter/YML.formatter.ts","../../src/formatter/XML.formatter.ts","../../src/formatter/index.ts","../../src/exporter/goodsExporter.ts","../../src/types/Product.types.ts"],"sourcesContent":["import { type Category } from \"../types\";\r\n\r\nexport const buildCategoryPaths = (\r\n categories: Category[],\r\n): Map<number, Category[]> => {\r\n const idToCategory = new Map<number, Category>();\r\n\r\n categories.forEach((category) => {\r\n idToCategory.set(category.id, category);\r\n });\r\n\r\n const categoryPaths = new Map<number, Category[]>();\r\n\r\n categories.forEach((category) => {\r\n const path: Category[] = [];\r\n\r\n let currentCategory: Category | undefined = category;\r\n\r\n while (currentCategory) {\r\n path.unshift(currentCategory);\r\n\r\n if (currentCategory.parentId !== undefined) {\r\n currentCategory = idToCategory.get(currentCategory.parentId);\r\n } else {\r\n currentCategory = undefined;\r\n }\r\n }\r\n\r\n categoryPaths.set(category.id, path);\r\n });\r\n\r\n return categoryPaths;\r\n};\r\n","import { once } from \"events\";\r\nimport { type Writable } from \"stream\";\r\n\r\nexport const writeWithDrain = (stream: Writable) => {\r\n return async (chunk: any) => {\r\n const canWrite = stream.write(chunk);\r\n if (!canWrite) {\r\n await once(stream, \"drain\");\r\n }\r\n };\r\n};\r\n","export const delay = async (ms: number) =>\r\n await new Promise((resolve) => setTimeout(resolve, ms));\r\n","export function getRFC3339Date(date: Date): string {\r\n const pad = (n: number) => n.toString().padStart(2, \"0\");\r\n const year = date.getFullYear();\r\n const month = pad(date.getMonth() + 1);\r\n const day = pad(date.getDate());\r\n const hour = pad(date.getHours());\r\n const min = pad(date.getMinutes());\r\n\r\n // Смещение в минутах\r\n const tzOffset = -date.getTimezoneOffset();\r\n const sign = tzOffset >= 0 ? \"+\" : \"-\";\r\n const absOffset = Math.abs(tzOffset);\r\n const offsetHour = pad(Math.floor(absOffset / 60));\r\n const offsetMin = pad(absOffset % 60);\r\n\r\n return `${year}-${month}-${day}T${hour}:${min}${sign}${offsetHour}:${offsetMin}`;\r\n}","export const urlQueryEncode = (inputUrl: string): string => {\r\n try {\r\n const url = new URL(inputUrl);\r\n url.search = url.search.replace(/^\\?/, \"\").replace(/,/g, \"%2C\");\r\n return url.toString();\r\n } catch (error) {\r\n console.error(\"Invalid URL:\", error);\r\n return \"\";\r\n }\r\n};\r\n","import { writeWithDrain } from \"../utils\";\r\n\r\nimport { PassThrough } from \"stream\";\r\n\r\nexport interface CSVStreamOptions {\r\n delimiter?: string;\r\n emptyFieldValue?: string;\r\n lineSeparator?: string;\r\n}\r\n\r\nexport class CSVStream {\r\n private readonly stream: PassThrough = new PassThrough();\r\n private readonly delimiter: string = \";\";\r\n private readonly lineSeparator: string = \"\\n\";\r\n private readonly emptyFieldValue: string = \"\";\r\n private columns = new Set<string>();\r\n private readonly writer = writeWithDrain(this.stream);\r\n\r\n constructor({ delimiter, lineSeparator, emptyFieldValue }: CSVStreamOptions) {\r\n if (delimiter !== undefined) this.delimiter = delimiter;\r\n if (lineSeparator !== undefined) this.lineSeparator = lineSeparator;\r\n if (emptyFieldValue !== undefined) this.emptyFieldValue = emptyFieldValue;\r\n }\r\n\r\n public get writableStream() {\r\n return this.stream;\r\n }\r\n\r\n setColumns(columns: Set<string>) {\r\n this.columns = columns;\r\n this.stream.write(\r\n Array.from(this.columns).join(this.delimiter) + this.lineSeparator,\r\n );\r\n }\r\n\r\n async addRow(items: Record<string, any>) {\r\n const data =\r\n Array.from(this.columns)\r\n .map((key) =>\r\n items[key] === undefined ? this.emptyFieldValue : items[key] + \"\",\r\n )\r\n .join(this.delimiter) + this.lineSeparator;\r\n\r\n await this.writer(data);\r\n }\r\n}\r\n","import { type Brand, type Category, type Product } from \"../types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport abstract class FormatterAbstract {\r\n public abstract formatterName: string;\r\n public abstract fileExtension: Extension;\r\n\r\n public abstract format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n option?: FormatterOptions,\r\n ): Promise<void>;\r\n}\r\n\r\nexport interface FormatterOptions {\r\n shopName?: string;\r\n\r\n companyName?: string;\r\n\r\n splitParams?: boolean;\r\n}\r\n\r\nexport enum Extension {\r\n CSV = \"csv\",\r\n YML = \"yml\",\r\n XML = \"xml\",\r\n XLSX = \"xlsx\",\r\n JSON = \"json\",\r\n}\r\n","import { CSVStream } from \"../streams/CSVStream\";\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport class CSVFormatter implements FormatterAbstract {\r\n public formatterName = \"CSV\";\r\n public fileExtension = Extension.CSV;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const mappedCategories: Record<number, string> = {};\r\n categories?.forEach(({ id, name }) => (mappedCategories[id] = name));\r\n\r\n const csvStream = new CSVStream({\r\n delimiter: \";\",\r\n emptyFieldValue: \"\",\r\n lineSeparator: \"\\n\",\r\n });\r\n csvStream.writableStream.pipe(writableStream);\r\n const columns = new Set<string>([\r\n \"url\",\r\n \"productId\",\r\n \"parentId\",\r\n \"variantId\",\r\n \"title\",\r\n \"description\",\r\n \"vendor\",\r\n \"vendorCode\",\r\n \"category\",\r\n \"images\",\r\n \"videos\",\r\n \"timeDeliveryMin\",\r\n \"timeDeliveryMax\",\r\n \"price\",\r\n \"oldPrice\",\r\n \"purchasePrice\",\r\n \"currency\",\r\n \"saleDate\",\r\n \"countryOfOrigin\",\r\n \"tags\",\r\n \"codesTN\",\r\n \"params\",\r\n \"properties\",\r\n \"sizes\",\r\n \"keywords\",\r\n \"relatedProducts\",\r\n ]);\r\n products.forEach((product) => {\r\n Object.entries(product).forEach(([key, value]) => {\r\n if (value) columns.add(key);\r\n });\r\n });\r\n csvStream.setColumns(columns);\r\n for (const product of products) {\r\n const row: Record<string, any> = {\r\n ...product,\r\n category: mappedCategories[product.categoryId],\r\n images: product.images?.join(\",\"),\r\n videos: product.videos?.join(\",\"),\r\n tags: product.tags?.join(\",\"),\r\n codesTN: product.codesTN?.join(\", \"),\r\n params: product.params\r\n ?.map(({ key, value }) => `${key}=${value}`)\r\n .join(\", \"),\r\n properties: product.properties\r\n ?.map(({ key, value }) => `${key}=${value}`)\r\n .join(\", \"),\r\n sizes: product.sizes\r\n ?.map(({ name, value }) => `${name}=${value}`)\r\n .join(\", \"),\r\n keywords: product.keywords?.join(\",\"),\r\n relatedProducts: product.relatedProducts?.join(\",\"),\r\n timeDeliveryMin: product.timeDelivery?.min,\r\n timeDeliveryMax: product.timeDelivery?.max,\r\n };\r\n await csvStream.addRow(row);\r\n }\r\n\r\n // Закрываем поток\r\n csvStream.writableStream.end();\r\n }\r\n}\r\n","import pkg from \"exceljs\";\r\n\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\nconst { stream } = pkg;\r\n\r\nexport class ExcelFormatter implements FormatterAbstract {\r\n public formatterName = \"Excel\";\r\n public fileExtension = Extension.XLSX;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const mappedCategories: Record<number, string> = {};\r\n categories?.forEach(({ id, name }) => (mappedCategories[id] = name));\r\n const columns = new Set<string>([\r\n \"url\",\r\n \"productId\",\r\n \"parentId\",\r\n \"variantId\",\r\n \"title\",\r\n \"description\",\r\n \"vendor\",\r\n \"vendorCode\",\r\n \"category\",\r\n \"images\",\r\n \"videos\",\r\n \"timeDeliveryMin\",\r\n \"timeDeliveryMax\",\r\n \"price\",\r\n \"oldPrice\",\r\n \"purchasePrice\",\r\n \"currency\",\r\n \"saleDate\",\r\n \"countryOfOrigin\",\r\n \"tags\",\r\n \"codesTN\",\r\n \"params\",\r\n \"properties\",\r\n \"sizes\",\r\n \"keywords\",\r\n \"relatedProducts\",\r\n ]);\r\n products.forEach((product) => {\r\n Object.entries(product).forEach(([key, value]) => {\r\n if (value) columns.add(key);\r\n });\r\n });\r\n\r\n const workbook = new stream.xlsx.WorkbookWriter({\r\n stream: writableStream,\r\n });\r\n const worksheet = workbook.addWorksheet(\"products\");\r\n worksheet.columns = Array.from(columns).map((column) => ({\r\n key: column,\r\n header: column,\r\n }));\r\n\r\n products.forEach((product) => {\r\n const row = {\r\n ...product,\r\n category: mappedCategories[product.categoryId],\r\n images: product.images?.join(\",\"),\r\n videos: product.videos?.join(\",\"),\r\n tags: product.tags?.join(\",\"),\r\n keywords: product.keywords?.join(\",\"),\r\n relatedProducts: product.relatedProducts?.join(\",\"),\r\n codesTN: product.codesTN?.join(\", \"),\r\n params: product.params\r\n ?.map(({ key, value }) => `${key}=${value}`)\r\n .join(\", \"),\r\n properties: product.properties\r\n ?.map(({ key, value }) => `${key}=${value}`)\r\n .join(\", \"),\r\n sizes: product.sizes\r\n ?.map(({ name, value }) => `${name}=${value}`)\r\n .join(\", \"),\r\n timeDeliveryMin: product.timeDelivery?.min,\r\n timeDeliveryMax: product.timeDelivery?.max,\r\n };\r\n worksheet.addRow(row).commit();\r\n });\r\n worksheet.commit();\r\n await workbook.commit();\r\n }\r\n}\r\n","import pkg from \"exceljs\";\n\nimport { type Brand, type Category, type Product } from \"../types\";\nimport {\n Extension,\n type FormatterAbstract,\n type FormatterOptions,\n} from \"./formater.types\";\n\nimport { type Writable } from \"stream\";\n\nconst { stream } = pkg;\n\nexport class InsalesFormatter implements FormatterAbstract {\n public formatterName = \"Insales\";\n public fileExtension = Extension.XLSX;\n\n public async format(\n writableStream: Writable,\n products: Product[],\n categories?: Category[],\n brands?: Brand[],\n __?: FormatterOptions,\n ): Promise<void> {\n const mappedCategories: Record<number, Category> = {};\n categories?.forEach(\n (category) => (mappedCategories[category.id] = category),\n );\n\n const mappedBrands: Record<number, Brand> = {};\n brands?.forEach((brand) => (mappedBrands[brand.id] = brand));\n\n const getParams = (product: Product): Record<string, string> => {\n const properties: Record<string, string> = {};\n\n product.params?.forEach(\n (p) => (properties[`Свойство: ${p.key}`] = p.value),\n );\n\n return properties;\n };\n const getProperties = (product: Product): Record<string, string> => {\n const properties: Record<string, string> = {};\n\n product.properties?.forEach(\n (p) => (properties[`Параметр: ${p.key}`] = p.value),\n );\n\n return properties;\n };\n\n const getCategories = (product: Product) => {\n const categories: Record<string, string> = {};\n const categoryList = new Array<string>();\n\n function addCategory(categoryId: number | undefined) {\n if (categoryId === undefined) return;\n\n const category = mappedCategories[categoryId];\n if (category) {\n categoryList.push(category.name);\n addCategory(category.parentId);\n }\n }\n\n addCategory(product.categoryId);\n\n categoryList.forEach((name, i) => {\n const index = categoryList.length - 1 - i;\n const key = index === 0 ? \"Корневая\" : `Подкатегория ${index}`;\n categories[key] = name;\n });\n\n return categories;\n };\n const workbook = new stream.xlsx.WorkbookWriter({\n stream: writableStream,\n });\n const worksheet = workbook.addWorksheet(\"products\");\n const columns = new Set<string>([\n \"Внешний ID\",\n \"Ссылка на товар\",\n \"Артикул\",\n \"Корневая\",\n \"Подкатегория 1\",\n \"Подкатегория 2\",\n \"Название товара или услуги\",\n \"Время доставки: Минимальное\",\n \"Время доставки: Максимальное\",\n \"Старая цена\",\n \"Цена продажи\",\n \"Cебестоимость\",\n \"Категории\",\n \"Остаток\",\n \"Штрих-код\",\n \"Краткое описание\",\n \"Полное описание\",\n \"Габариты варианта\",\n \"Вес\",\n \"Размещение на сайте\",\n \"НДС\",\n \"Валюта склада\",\n \"Изображения варианта\",\n \"Изображения\",\n \"Ссылка на видео\",\n \"Параметр: Артикул\",\n \"Параметр: Пол (Системный)\",\n \"Параметр: Бренд (Системный)\",\n \"Параметр: Логотип бренда (Системный)\",\n \"Параметр: Серия (Системный)\",\n \"Параметр: Дата релиза (Системный)\",\n \"Параметры\",\n \"Свойства\",\n \"Размерная сетка\",\n \"Связанные товары\",\n \"Ключевые слова\",\n ]);\n products.forEach((product) => {\n Object.keys({\n ...getParams(product),\n ...getProperties(product),\n }).forEach((key) => {\n columns.add(key);\n });\n });\n\n worksheet.columns = Array.from(columns).map((column) => ({\n header: column,\n key: column,\n }));\n\n for (const product of products) {\n const externalId = `${product.productId}-${product.variantId}`;\n const row = {\n \"Внешний ID\": externalId,\n \"Ссылка на товар\": product.url,\n Артикул: externalId, // TODO: product.vendorCode,\n \"Название товара или услуги\": product.title,\n \"Время доставки: Минимальное\": product.timeDelivery?.min,\n \"Время доставки: Максимальное\": product.timeDelivery?.max,\n \"Старая цена\": product.oldPrice,\n \"Цена продажи\": product.price,\n Cебестоимость: product.purchasePrice,\n ...getCategories(product),\n Остаток: product.count,\n \"Штрих-код\": product.barcode,\n \"Краткое описание\": undefined,\n \"Полное описание\": product.description,\n \"Габариты варианта\": product.dimensions,\n Вес: product.weight,\n \"Размещение на сайте\": product.available,\n НДС: product.vat?.toString(),\n \"Валюта склада\": product.currency.toString(),\n \"Изображения варианта\":\n product.parentId === undefined\n ? product.images?.join(\" \")\n : undefined,\n Изображения:\n product.parentId === undefined\n ? undefined\n : product.images?.join(\" \"),\n \"Ссылка на видео\": product.videos ? product.videos[0] : undefined,\n \"Параметр: Артикул\": product.vendorCode, // TODO: брать из обычных параметров,\n \"Параметр: Пол (Системный)\": product.gender,\n \"Параметр: Бренд (Системный)\": product.vendor,\n \"Параметр: Логотип бренда (Системный)\":\n product.vendorId === undefined\n ? undefined\n : mappedBrands[product.vendorId]?.logoUrl,\n \"Параметр: Серия (Системный)\": product.seriesName,\n \"Параметр: Дата релиза (Системный)\": product.saleDate,\n ...getParams(product),\n ...getProperties(product),\n \"Размерная сетка\": JSON.stringify(product.sizes),\n \"Связанные товары\": product.relatedProducts?.join(\",\"),\n \"Ключевые слова\": product.keywords?.join(\",\"),\n };\n // todo(delay)\n worksheet.addRow(row).commit();\n }\n\n worksheet.commit();\n await workbook.commit();\n }\n}\n","import { JsonStreamStringify } from \"json-stream-stringify\";\r\n\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport class JSONFormatter implements FormatterAbstract {\r\n public formatterName = \"JSON\";\r\n public fileExtension = Extension.JSON;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n _?: FormatterOptions,\r\n ): Promise<void> {\r\n const stream = new JsonStreamStringify({\r\n categories,\r\n brands,\r\n products,\r\n });\r\n stream.pipe(writableStream);\r\n }\r\n}\r\n","import { JsonStreamStringify } from \"json-stream-stringify\";\r\n\r\nimport type { Brand, Category, Currency, IParam, Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\ninterface PriceSku {\r\n skuId: string;\r\n price: number;\r\n currency: Currency;\r\n timeDelivery?: {\r\n min: number;\r\n max: number;\r\n };\r\n params?: IParam[];\r\n}\r\n\r\ninterface PriceProduct {\r\n productId: number;\r\n skus: PriceSku[];\r\n}\r\n\r\nexport class PriceFormatter implements FormatterAbstract {\r\n public formatterName = \"Price\";\r\n public fileExtension = Extension.JSON;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n _categories?: Category[],\r\n _brands?: Brand[],\r\n _?: FormatterOptions,\r\n ): Promise<void> {\r\n const priceMap = new Map<number, PriceProduct>();\r\n\r\n products.forEach((product) => {\r\n if (!priceMap.has(product.productId)) {\r\n priceMap.set(product.productId, {\r\n productId: product.productId,\r\n skus: [],\r\n });\r\n }\r\n\r\n const productPrice = priceMap.get(product.productId);\r\n\r\n if (!productPrice) {\r\n console.error(`Product ${product.productId} not found in price map`);\r\n\r\n return;\r\n }\r\n\r\n if (!product.price) {\r\n // NOTE: Если у продукта нет цены, то пропускаем его\r\n return;\r\n }\r\n\r\n if (product.variantId === productPrice.productId) {\r\n // NOTE: Если это родитель, то он не является SKU\r\n return;\r\n }\r\n\r\n const sku: PriceSku = {\r\n skuId: String(product.variantId),\r\n price: product.price,\r\n currency: product.currency,\r\n params: product.params,\r\n };\r\n\r\n if (\r\n product.timeDelivery?.min !== undefined ||\r\n product.timeDelivery?.max !== undefined\r\n ) {\r\n sku.timeDelivery = {\r\n min: product.timeDelivery.min ?? 0,\r\n max: product.timeDelivery.max ?? 0,\r\n };\r\n }\r\n\r\n productPrice.skus.push(sku);\r\n });\r\n\r\n const result: PriceProduct[] = Array.from(priceMap.values());\r\n const stream = new JsonStreamStringify(result);\r\n stream.pipe(writableStream);\r\n }\r\n}\r\n","import { JsonStreamStringify } from \"json-stream-stringify\";\r\n\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\ninterface SimpleProduct extends Product {\r\n children: Product[];\r\n}\r\n\r\nexport class SimpleJSONFormatter implements FormatterAbstract {\r\n public formatterName = \"JSON\";\r\n public fileExtension = Extension.JSON;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n _?: FormatterOptions,\r\n ): Promise<void> {\r\n const groupedProduct = new Map<number, SimpleProduct>();\r\n products.forEach((product) => {\r\n if (product.parentId !== undefined) return;\r\n groupedProduct.set(product.variantId, {\r\n ...product,\r\n children: [],\r\n });\r\n });\r\n products.forEach((product) => {\r\n if (product.parentId === undefined) return;\r\n const parent = groupedProduct.get(product.parentId);\r\n if (!parent) return;\r\n parent.children.push(product);\r\n });\r\n const stream = new JsonStreamStringify({\r\n categories,\r\n brands,\r\n products: Array.from(groupedProduct.values()),\r\n });\r\n stream.pipe(writableStream);\r\n }\r\n}\r\n","import pkg from \"exceljs\";\r\n\r\nimport { type Brand, type Category, type IParam, type Product } from \"../types\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\nconst { stream } = pkg;\r\n\r\nexport class TgShopFormatter implements FormatterAbstract {\r\n public formatterName = \"TgShop\";\r\n public fileExtension = Extension.XLSX;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const getParameter = (product: Product, key: string): IParam | undefined =>\r\n product.params?.find((value) => value.key === key);\r\n\r\n const convertProduct = (product: Product) => ({\r\n \"category id\": product.categoryId,\r\n \"group id\": product.parentId,\r\n \"id product\": product.variantId,\r\n \"name product\": product.title,\r\n price: product.price,\r\n picture: product.images?.join(\", \"),\r\n vendorCode: product.vendorCode,\r\n oldprice: product.oldPrice,\r\n description: product.description,\r\n shortDescription: \"\",\r\n quantityInStock: product.count,\r\n color: getParameter(product, \"color\")?.value,\r\n size: getParameter(product, \"size\")?.value,\r\n priority: undefined,\r\n });\r\n const workbook = new stream.xlsx.WorkbookWriter({\r\n stream: writableStream,\r\n });\r\n const categoryWorksheet = workbook.addWorksheet(\"categories\");\r\n const productsWorksheet = workbook.addWorksheet(\"offers\");\r\n categoryWorksheet.columns = [\r\n {\r\n header: \"id\",\r\n key: \"id\",\r\n },\r\n {\r\n header: \"parentId\",\r\n key: \"parentId\",\r\n },\r\n {\r\n header: \"name\",\r\n key: \"name\",\r\n },\r\n ];\r\n const columns = [\r\n \"category id\",\r\n \"group id\",\r\n \"id product\",\r\n \"name product\",\r\n \"price\",\r\n \"picture\",\r\n \"vendorCode\",\r\n \"oldprice\",\r\n \"description\",\r\n \"shortDescription\",\r\n \"quantityInStock\",\r\n \"color\",\r\n \"size\",\r\n \"priority\",\r\n ];\r\n\r\n productsWorksheet.columns = columns.map((column) => ({\r\n header: column,\r\n key: column,\r\n }));\r\n\r\n categories?.forEach((category) => {\r\n categoryWorksheet.addRow(category).commit();\r\n });\r\n\r\n products.forEach((product) => {\r\n productsWorksheet.addRow(convertProduct(product)).commit();\r\n });\r\n categoryWorksheet.commit();\r\n productsWorksheet.commit();\r\n\r\n await workbook.commit();\r\n }\r\n}\r\n","import { CSVStream } from \"../streams/CSVStream\";\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport { urlQueryEncode } from \"../utils\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { type Writable } from \"stream\";\r\n\r\nexport class TildaFormatter implements FormatterAbstract {\r\n public formatterName = \"Tilda\";\r\n public fileExtension = Extension.CSV;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n _?: Brand[],\r\n __?: FormatterOptions,\r\n ): Promise<void> {\r\n const mappedCategories: Record<number, string> = {};\r\n categories?.forEach(({ id, name }) => (mappedCategories[id] = name));\r\n\r\n const csvStream = new CSVStream({\r\n delimiter: \"\\t\",\r\n emptyFieldValue: \"\",\r\n lineSeparator: \"\\n\",\r\n });\r\n csvStream.writableStream.pipe(writableStream);\r\n const columns = new Set<string>([\r\n \"SKU\",\r\n \"Brand\",\r\n \"Category\",\r\n \"Title\",\r\n \"Text\",\r\n \"Photo\",\r\n \"Price\",\r\n \"Price Old\",\r\n \"Quantity\",\r\n \"Editions\",\r\n \"External ID\",\r\n \"Parent UID\",\r\n ]);\r\n\r\n const characteristics = new Set<string>();\r\n\r\n products.forEach((product) => {\r\n product.properties?.forEach(({ key }) => {\r\n characteristics.add(key);\r\n });\r\n });\r\n\r\n characteristics.forEach((charKey) => {\r\n columns.add(`Characteristics:${charKey}`);\r\n });\r\n\r\n csvStream.setColumns(columns);\r\n for (const product of products) {\r\n const row: Record<string, string | number | undefined> = {\r\n SKU: product.vendorCode,\r\n Brand: product.vendor,\r\n Category: mappedCategories[product.categoryId],\r\n Title: product.title,\r\n Text: product.description,\r\n Photo: product.images?.map(urlQueryEncode).join(\",\"),\r\n Price: product.price,\r\n \"Price Old\": product.oldPrice,\r\n Quantity: product.count,\r\n Editions: product.params\r\n ?.map(({ key, value }) => `${key}:${value}`)\r\n .join(\";\"),\r\n \"External ID\": product.variantId,\r\n \"Parent UID\": product.parentId,\r\n };\r\n\r\n product.properties?.forEach(({ key, value }) => {\r\n row[`Characteristics:${key}`] = value;\r\n });\r\n\r\n await csvStream.addRow(row);\r\n }\r\n\r\n csvStream.writableStream.end();\r\n }\r\n}\r\n","import { CSVStream } from \"../streams/CSVStream\";\nimport { type IParam, type Brand, type Category, type Product } from \"../types\";\nimport { buildCategoryPaths, urlQueryEncode } from \"../utils\";\nimport {\n Extension,\n type FormatterAbstract,\n type FormatterOptions,\n} from \"./formater.types\";\n\nimport { type Writable } from \"stream\";\n\nexport interface CreateAttributeProps {\n name?: string;\n id?: number;\n values?: string | number;\n visible?: number;\n global?: number;\n}\n\nexport class WooCommerceFormatter implements FormatterAbstract {\n public formatterName = \"WooCommerce\";\n public fileExtension = Extension.CSV;\n\n private readonly DEFAULT_COLUMNS = [\n \"ID\",\n \"Type\",\n \"SKU\",\n \"Name\",\n \"Parent\",\n \"Short description\",\n \"Description\",\n \"Stock\",\n \"Regular price\",\n \"Position\",\n \"Categories\",\n \"Tags\",\n \"Images\",\n \"SizeGrid\",\n ];\n\n private readonly CUSTOM_ATTRIBUTES: Partial<Record<keyof Product, string>> = {\n gender: \"Пол\",\n vendor: \"Производитель\",\n vendorCode: \"Артикул\",\n seriesName: \"Серия\",\n };\n\n private formatKeyAttribute(key: string) {\n const keyWithoutInvalidCharacters = key\n .replaceAll(\",\", \"\")\n .replaceAll(\";\", \"\");\n\n return keyWithoutInvalidCharacters.length >= 28\n ? keyWithoutInvalidCharacters.slice(0, 24) + \"...\"\n : keyWithoutInvalidCharacters;\n }\n\n private formatValueAttribute(value: string) {\n return value.replaceAll(\",\", \"\").replaceAll(\";\", \"\");\n }\n\n private formatProducts(products: Product[]) {\n const formatParams = (params: IParam[] | undefined) => {\n return params?.map(({ key, value }) => {\n const formatedKey = this.formatKeyAttribute(key);\n const formatedValue = this.formatValueAttribute(value);\n return { key: formatedKey, value: formatedValue };\n });\n };\n\n return products.map((product) => {\n const params = formatParams(product.params);\n const properties = formatParams(product.properties);\n return { ...product, params, properties };\n });\n }\n\n private createAttribute(data: CreateAttributeProps) {\n if (!data?.name || data.id === undefined) return;\n\n const attributeStartName = \"Attribute\";\n\n const attribute: Record<string, string | number> = {};\n\n attribute[`${attributeStartName} ${data.id} name`] = data.name;\n\n if (data.values !== undefined)\n attribute[`${attributeStartName} ${data.id} value(s)`] = data.values;\n if (data.visible !== undefined)\n attribute[`${attributeStartName} ${data.id} visible`] = data.visible;\n if (data.global !== undefined)\n attribute[`${attributeStartName} ${data.id} global`] = data.global;\n\n return attribute;\n }\n\n private extractAttributes(products: Product[]) {\n const formatedProducts = this.formatProducts(products);\n const paramsMap = new Map<number, Record<string, string | number>>();\n const propertiesMap = new Map<number, Record<string, string | number>>();\n const uniqueAttributes = new Map<string, number>();\n\n Object.values(this.CUSTOM_ATTRIBUTES).forEach((attrName) => {\n if (!uniqueAttributes.has(attrName)) {\n uniqueAttributes.set(attrName, uniqueAttributes.size);\n }\n });\n\n formatedProducts.forEach((product) => {\n product.params?.forEach(({ key }) => {\n if (!uniqueAttributes.has(key)) {\n uniqueAttributes.set(key, uniqueAttributes.size);\n }\n });\n\n product.properties?.forEach(({ key }) => {\n if (!uniqueAttributes.has(key)) {\n uniqueAttributes.set(key, uniqueAttributes.size);\n }\n });\n });\n\n formatedProducts.forEach((product) => {\n const paramAttributes = paramsMap.get(product.variantId) ?? {};\n const propertyAttributes = propertiesMap.get(product.variantId) ?? {};\n\n product.params?.forEach(({ key, value }) => {\n const index = uniqueAttributes.get(key);\n\n if (index === undefined) {\n console.error(`Не нашлось уникального ключа для параметра - ${key}`);\n return;\n }\n\n const attribute = this.createAttribute({\n name: key,\n id: index,\n values: value,\n visible: 0,\n global: 0,\n });\n\n if (!attribute) return;\n\n Object.entries(attribute).forEach(\n ([key, value]) => (paramAttributes[key] = value),\n );\n });\n\n Object.entries(this.CUSTOM_ATTRIBUTES).forEach(([field, attrName]) => {\n const value = product[field as keyof Product];\n if (value) {\n const index = uniqueAttributes.get(attrName);\n\n if (index === undefined) {\n console.error(\n `Не нашлось уникального ключа для кастомного атрибута - ${attrName}`,\n );\n return;\n }\n\n if (!paramAttributes[`Attribute ${index} name`]) {\n const attribute = this.createAttribute({\n name: attrName,\n id: index,\n values: value as string | number | undefined,\n global: 0,\n });\n\n if (!attribute) return;\n\n Object.entries(attribute).forEach(\n ([key, val]) => (propertyAttributes[key] = val),\n );\n }\n }\n });\n\n product.properties?.forEach(({ key, value }) => {\n const index = uniqueAttributes.get(key);\n\n if (index === undefined) {\n console.error(`Не нашлось уникального ключа для параметра - ${key}`);\n return;\n }\n\n if (paramAttributes[`Attribute ${index} name`]) {\n console.warn(`Данное свойство уже существует в параметрах - ${key}`);\n return;\n }\n\n const attribute = this.createAttribute({\n name: key,\n id: index,\n values: value,\n global: 0,\n });\n\n if (!attribute) return;\n\n Object.entries(attribute).forEach(\n ([key, value]) => (propertyAttributes[key] = value),\n );\n });\n\n paramsMap.set(product.variantId, paramAttributes);\n propertiesMap.set(product.variantId, propertyAttributes);\n });\n\n return { params: paramsMap, properties: propertiesMap };\n }\n\n private removeVisibleFromAttributes(params: Record<string, string | number>) {\n Object.entries(params).forEach(([key]) => {\n if (key.includes(\"visible\")) params[key] = \"\";\n });\n }\n\n public async format(\n writableStream: Writable,\n products: Product[],\n categories?: Category[],\n _?: Brand[],\n __?: FormatterOptions,\n ): Promise<void> {\n const categoryPaths = buildCategoryPaths(categories ?? []);\n\n const csvStream = new CSVStream({\n delimiter: \";\",\n emptyFieldValue: \"\",\n lineSeparator: \"\\n\",\n });\n csvStream.writableStream.pipe(writableStream);\n const columns = new Set<string>(this.DEFAULT_COLUMNS);\n\n const attributes = this.extractAttributes(products);\n\n const variationsByParentId = new Map<\n number,\n Array<Record<string, string | number | undefined>>\n >();\n\n const imagesByParentId = new Map<number, string | undefined>();\n const sizesByParentId = new Map<number, string | undefined>();\n\n const variations = products.map((product, index) => {\n const pathsArray = categoryPaths\n .get(product.categoryId)\n ?.map((category) => category.name);\n\n const price = product.price ? product.price : \"\";\n const images = product.images?.map(urlQueryEncode).join(\",\");\n\n let row = {\n ID: product.variantId,\n Type: \"variation\",\n SKU: product.variantId,\n Name: product.title,\n Parent: product.parentId ?? 0,\n \"Short description\": \"\",\n Description: product.description,\n Stock: product.count ?? 0,\n \"Regular price\": price,\n Position: index + 1,\n Categories: pathsArray?.join(\" > \"),\n Tags: product.keywords?.join(\",\"),\n Images: images,\n SizeGrid: \"\",\n };\n\n const productParams = attributes.params.get(product.variantId) ?? {};\n\n if (!imagesByParentId.has(row.Parent)) {\n imagesByParentId.set(row.Parent, images);\n }\n\n if (!sizesByParentId.has(row.Parent)) {\n sizesByParentId.set(\n row.Parent,\n product.sizes ? JSON.stringify(product.sizes) : \"\",\n );\n }\n\n this.removeVisibleFromAttributes(productParams);\n\n row = { ...row, ...productParams };\n\n if (variationsByParentId.has(row.Parent)) {\n variationsByParentId.get(row.Parent)?.push(row);\n } else {\n variationsByParentId.set(row.Parent, [row]);\n }\n\n return row;\n });\n\n const parentProducts = new Map<number, any>();\n\n variations.forEach((product) => {\n const currentParent = parentProducts.get(product.Parent);\n\n let row = {\n ...product,\n Type: \"variable\",\n ID: product.Parent,\n SKU: product.Parent,\n Position: 0,\n Parent: \"\",\n \"Regular price\": \"\",\n Images: imagesByParentId.get(product.Parent) ?? \"\",\n SizeGrid: sizesByParentId.get(product.Parent) ?? \"\",\n };\n\n row.Stock = (currentParent?.Stock || 0) + (product?.Stock || 0);\n\n const productParams = attributes.params.get(product.SKU) ?? {};\n const productProperties = attributes.properties.get(product.SKU) ?? {};\n\n Object.entries(productParams).forEach(([key]) => {\n if (key.includes(\"visible\")) productParams[key] = 0;\n });\n\n if (currentParent) {\n Object.entries(productParams).forEach(([key, value]) => {\n if (key.includes(\"value(s)\")) {\n productParams[key] = currentParent[key] + `, ${value}`;\n }\n });\n }\n\n Object.keys({ ...row, ...productParams, ...productProperties }).forEach(\n (item) => columns.add(item),\n );\n\n row = { ...row, ...productParams, ...productProperties };\n\n parentProducts.set(product.Parent, row);\n });\n\n const variableProducts = Array.from(parentProducts.values());\n\n csvStream.setColumns(columns);\n for (const parentProduct of variableProducts) {\n await csvStream.addRow(parentProduct);\n\n for (const variationProduct of variationsByParentId.get(\n parentProduct.ID,\n ) ?? []) {\n await csvStream.addRow(variationProduct);\n }\n }\n\n csvStream.writableStream.end();\n }\n}\n","import { XMLBuilder } from \"fast-xml-parser\";\r\n\r\nimport { type Product, type Category, type Brand } from \"../types\";\r\nimport { getRFC3339Date, writeWithDrain } from \"../utils\";\r\nimport {\r\n Extension,\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n} from \"./formater.types\";\r\n\r\nimport { PassThrough, type Writable } from \"stream\";\r\n\r\nexport class YMLFormatter implements FormatterAbstract {\r\n public formatterName = \"YMl\";\r\n public fileExtension = Extension.YML;\r\n\r\n public async format(\r\n writableStream: Writable,\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n options?: FormatterOptions,\r\n ): Promise<void> {\r\n const result = new PassThrough();\r\n result.pipe(writableStream);\r\n\r\n const builder = new XMLBuilder({\r\n ignoreAttributes: false,\r\n cdataPropName: \"__cdata\",\r\n format: true,\r\n indentBy: \" \",\r\n });\r\n\r\n const date = getRFC3339Date(new Date());\r\n\r\n // Начинаем формирование XML\r\n result.write('<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\\n');\r\n result.write('<yml_catalog date=\"' + date + '\">\\n');\r\n\r\n // Открываем тег <shop>\r\n result.write(\"<shop>\\n\");\r\n\r\n const resultWriter = writeWithDrain(result);\r\n // Добавляем информацию о магазине\r\n if (options?.shopName) {\r\n await resultWriter(builder.build({ name: options.shopName }));\r\n await resultWriter(\"\\n\");\r\n }\r\n if (options?.companyName) {\r\n await resultWriter(builder.build({ company: options.companyName }));\r\n await resultWriter(\"\\n\");\r\n }\r\n\r\n // Добавляем категории и бренды\r\n if (categories) {\r\n await resultWriter(\r\n builder.build({\r\n // tagname: \"categories\",\r\n categories: { category: this.getCategories(categories) },\r\n }),\r\n );\r\n await resultWriter(\"\\n\");\r\n }\r\n if (brands) {\r\n await resultWriter(\r\n builder.build({ brands: { brand: this.getBrands(brands) } }),\r\n );\r\n await resultWriter(\"\\n\");\r\n }\r\n\r\n // Открываем секцию <offers>\r\n await resultWriter(\"<offers>\\n\");\r\n\r\n // Создаем поток для обработки offer элементов\r\n const offerStream = new PassThrough();\r\n const offerWriter = writeWithDrain(offerStream);\r\n\r\n // Пайпим поток offer элементов в основной итоговый поток\r\n offerStream.pipe(result, { end: false });\r\n\r\n // Записываем каждый продукт в поток\r\n for (const product of products) {\r\n if (product.price === 0) continue;\r\n const offer = builder.build({ offer: this.getOffer(product) });\r\n await offerWriter(offer + \"\\n\");\r\n }\r\n\r\n // Завершаем поток offer\r\n offerStream.end();\r\n\r\n offerStream.on(\"end\", () => {\r\n // Закрываем секцию <offers>\r\n result.write(\"</offers>\\n\");\r\n\r\n // Закрываем тег <shop>\r\n result.write(\"</shop>\\n\");\r\n\r\n // Закрываем тег <yml_catalog>\r\n result.write(\"</yml_catalog>\\n\");\r\n\r\n // Завершаем итоговый поток\r\n result.end();\r\n });\r\n }\r\n\r\n private getBrands(brands?: Brand[]) {\r\n if (!brands) return [];\r\n\r\n return brands.map((brand) => ({\r\n \"@_id\": brand.id,\r\n \"@_url\": brand.coverURL ?? \"\",\r\n \"#text\": brand.name,\r\n }));\r\n }\r\n\r\n private getCategories(categories?: Category[]) {\r\n if (!categories) return [];\r\n\r\n return categories.map((cat) => ({\r\n \"@_id\": cat.id,\r\n \"@_parentId\": cat.parentId ?? \"\",\r\n \"#text\": cat.name || `Категория #${cat.id}`,\r\n }));\r\n }\r\n\r\n private getOffer(product: Product): any {\r\n const result = {\r\n \"@_id\": product.variantId,\r\n name: product.title,\r\n price: product.price,\r\n oldprice: product.oldPrice,\r\n purchase_price: product.purchasePrice,\r\n additional_expenses: product.additionalExpenses,\r\n cofinance_price: product.cofinancePrice,\r\n currencyId: product.currency,\r\n categoryId: product.categoryId,\r\n vendorId: product.vendorId,\r\n vendor: product.vendor,\r\n vendorCode: product.vendorCode,\r\n picture: product.images,\r\n video: product.videos,\r\n available: product.available,\r\n \"time-delivery\": product.timeDelivery\r\n ? {\r\n \"@_min\": product.timeDelivery.min,\r\n \"@_max\": product.timeDelivery.max,\r\n \"#text\": `${product.timeDelivery.min}-${product.timeDelivery.max}`,\r\n }\r\n : undefined,\r\n series: product.seriesName,\r\n \"min-quantity\": product.minQuantity,\r\n \"step-quantity\": product.stepQuantity,\r\n size: product.sizes?.map((size) => ({\r\n \"#text\": size.value,\r\n \"@_name\": size.name,\r\n \"@_delimiter\": size.delimiter,\r\n })),\r\n keyword: product.keywords,\r\n saleDate: product.saleDate,\r\n property: product.properties?.map((property) => ({\r\n \"#text\": property.value,\r\n \"@_name\": property.key,\r\n })),\r\n param: product.params?.map((param) => ({\r\n \"#text\": param.value,\r\n \"@_name\": param.key,\r\n })),\r\n description: {\r\n __cdata: product.description,\r\n },\r\n country_of_origin: product.countryOfOrigin,\r\n barcode: product.barcode,\r\n vat: product.vat,\r\n count: product.count,\r\n \"set-ids\": product.tags?.join(\", \"),\r\n adult: product.adult,\r\n downloadable: product.downloadable,\r\n \"period-of-validity-days\": product.validityPeriod,\r\n \"comment-validity-days\": product.validityComment,\r\n \"service-life-days\": product.serviceLifePeriod,\r\n \"comment-life-days\": product.serviceLifeComment,\r\n \"warranty-days\": product.warrantyPeriod,\r\n \"comment-warranty\": product.warrantyComment,\r\n manufacturer_warranty: product.manufacturerWarranty,\r\n certificate: product.certificate,\r\n url: product.url,\r\n weight: product.weight,\r\n dimensions: product.dimensions,\r\n boxCount: product.boxCount,\r\n disabled: product.disabled,\r\n age: product.age\r\n ? {\r\n \"@_unit\": product.age.unit,\r\n \"#text\": product.age.value,\r\n }\r\n : undefined,\r\n \"tn-ved-codes\": product.codesTN?.length\r\n ? {\r\n \"tn-ved-code\": product.codesTN,\r\n }\r\n : undefined,\r\n relatedProduct: product.relatedProducts,\r\n gender: product.gender,\r\n };\r\n if (product.parentId !== undefined) {\r\n return {\r\n ...result,\r\n \"@_group_id\": product.parentId,\r\n };\r\n }\r\n return result;\r\n }\r\n}\r\n","import { Extension } from \"./formater.types\";\r\nimport { YMLFormatter } from \"./YML.formatter\";\r\n\r\nexport class XMLFormatter extends YMLFormatter {\r\n public formatterName = \"XML\";\r\n public fileExtension = Extension.XML;\r\n}\r\n","import { CSVFormatter } from \"./CSV.formatter\";\r\nimport { ExcelFormatter } from \"./Excel.formatter\";\r\nimport { InsalesFormatter } from \"./Insales.formatter\";\r\nimport { JSONFormatter } from \"./JSON.formatter\";\r\nimport { PriceFormatter } from \"./Price.formatter\";\r\nimport { SimpleJSONFormatter } from \"./SimpleJSON.formatter\";\r\nimport { TgShopFormatter } from \"./TgShop.formatter\";\r\nimport { TildaFormatter } from \"./Tilda.formatter\";\r\nimport { WooCommerceFormatter } from \"./WooCommerce.formatter\";\r\nimport { XMLFormatter } from \"./XML.formatter\";\r\nimport { YMLFormatter } from \"./YML.formatter\";\r\n\r\nexport * from \"./formater.types\";\r\n\r\nexport const Formatters = {\r\n TildaFormatter,\r\n CSVFormatter,\r\n InsalesFormatter,\r\n YMLFormatter,\r\n TgShopFormatter,\r\n ExcelFormatter,\r\n JSONFormatter,\r\n SimpleJSONFormatter,\r\n XMLFormatter,\r\n WooCommerceFormatter,\r\n PriceFormatter,\r\n};\r\n","import {\r\n type FormatterAbstract,\r\n type FormatterOptions,\r\n Formatters,\r\n} from \"../formatter\";\r\nimport { type Brand, type Category, type Product } from \"../types\";\r\nimport { type Exporter, type Transformer } from \"./exporter.types\";\r\n\r\nimport fs from \"fs\";\r\n\r\nexport class GoodsExporter<Context extends object | undefined> {\r\n private _context: Context;\r\n\r\n constructor(readonly context: Context) {\r\n this._context = context;\r\n }\r\n\r\n public setContext(context: Context): void {\r\n this._context = context;\r\n }\r\n\r\n private formatter: FormatterAbstract = new Formatters.YMLFormatter();\r\n private exporter: Exporter = () => {\r\n return fs.createWriteStream(\r\n `${this.formatter.formatterName}.output.${this.formatter.fileExtension}`,\r\n );\r\n };\r\n\r\n private transformers = new Array<Transformer<Context>>();\r\n\r\n public setTransformers(transformers: Array<Transformer<Context>>): void {\r\n this.transformers = transformers;\r\n }\r\n\r\n public setFormatter(formatter: FormatterAbstract): void {\r\n this.formatter = formatter;\r\n }\r\n\r\n public setExporter(exporter: Exporter): void {\r\n this.exporter = exporter;\r\n }\r\n\r\n async export(\r\n products: Product[],\r\n categories?: Category[],\r\n brands?: Brand[],\r\n option?: FormatterOptions,\r\n ): Promise<void> {\r\n let transformedProducts: Product[] = products;\r\n\r\n for (const transformer of this.transformers)\r\n transformedProducts = await transformer(\r\n transformedProducts,\r\n this._context,\r\n );\r\n\r\n const writableStream = this.exporter();\r\n\r\n await this.formatter.format(\r\n writableStream,\r\n transformedProducts,\r\n categories,\r\n brands,\r\n option,\r\n );\r\n }\r\n}\r\n","export interface Product {\r\n /**\r\n * **ID товара**\r\n *\r\n * Любая последовательность длиной до 80 знаков. В нее могут входить английские и русские (кроме ё) буквы, цифры и символы . , / \\ ( ) [ ] - = _\r\n *\r\n * Пример: belaya-kofta-12345\r\n */\r\n productId: number;\r\n /**\r\n * **Родительскй SKU**\r\n *\r\n * Любая последовательность длиной до 80 знаков. В нее могут входить английские и русские (кроме ё) буквы, цифры и символы . , / \\ ( ) [ ] - = _\r\n *\r\n * Пример: belaya-kofta-12345\r\n */\r\n parentId?: number;\r\n /**\r\n * **SKU**\r\n *\r\n * Любая последовательность длиной до 80 знаков. В нее могут входить английские и русские (кроме ё) буквы, цифры и символы . , / \\ ( ) [ ] - = _\r\n *\r\n * Пример: belaya-kofta-12345\r\n */\r\n variantId: number;\r\n /**\r\n * **Название**\r\n *\r\n * Составляйте название по схеме: тип + бренд или производитель + модель + особенности, если есть (например, цвет, размер или вес) и количество в упаковке.\r\n *\r\n * Не включайте в название условия продажи (например, «скидка», «бесплатная доставка» и т. д.), эмоциональные характеристики («хит», «супер» и т. д.). Не пишите слова большими буквами — кроме устоявшихся названий брендов и моделей.\r\n *\r\n * Оптимальная длина — 50–60 символов, максимальная — 150.\r\n *\r\n * Составлять хорошие названия помогут [рекомендации](https://yandex.ru/support/marketplace/assortment/fields/title.html