UNPKG

spapi-listing-builder

Version:

It is a simple npm package for generating compliant Amazon SP-API listing JSON quickly and efficiently.

1,027 lines (980 loc) 25.4 kB
import Draft from 'json-schema-library'; const imageTypeJsonMap = { Main: "main_product_image_locator", Swatch: "swatch_product_image_locator", PT1: "other_product_image_locator_1", PT2: "other_product_image_locator_2", PT3: "other_product_image_locator_3", PT4: "other_product_image_locator_4", PT5: "other_product_image_locator_5", PT6: "other_product_image_locator_6", PT7: "other_product_image_locator_7", PT8: "other_product_image_locator_8", MainOfferImage: "main_offer_image_locator", OfferImage1: "other_offer_image_locator_1", OfferImage2: "other_offer_image_locator_2", OfferImage3: "other_offer_image_locator_3", OfferImage4: "other_offer_image_locator_4", OfferImage5: "other_offer_image_locator_5", EEGL: "image_locator_eegl", PS01: "image_locator_ps01", PS02: "image_locator_ps02", PS03: "image_locator_ps03", PS04: "image_locator_ps04", PS05: "image_locator_ps05", PS06: "image_locator_ps06" }; function getImageType(key) { return imageTypeJsonMap[key]; } class FeedHeader { sellerId; constructor(sellerId) { this.sellerId = sellerId; } main() { return { sellerId: this.sellerId, version: "2.0", issueLocale: "en_US" }; } } class FeedImg { sellerId; list; constructor(sellerId, list) { this.sellerId = sellerId; this.list = list; } main() { return { header: new FeedHeader(this.sellerId).main(), messages: this.genMessage() }; } genMessage() { return this.list.map((item, idx) => { return { messageId: idx + 1, sku: item.sku, operationType: "PATCH", productType: "PRODUCT", patches: this.genPatches(item.imgs) }; }); } genPatches(imgs) { return imgs.filter((item) => { return getImageType(item.type); }).map((item) => { return { op: "replace", path: `/attributes/${getImageType(item.type)}`, value: [ { media_location: item.url } ] }; }); } } class FeedPrice { sellerId; list; constructor(sellerId, list) { this.sellerId = sellerId; this.list = list; } main() { return { header: new FeedHeader(this.sellerId).main(), messages: this.genMessage() }; } genMessage() { return this.list.map((item, idx) => { return { messageId: idx + 1, sku: item.sku, operationType: "PATCH", productType: "PRODUCT", patches: [ { op: "replace", path: "/attributes/purchasable_offer", value: [ { audience: "ALL", our_price: [ { schedule: [ { value_with_tax: item.sell_price } ] } ] } ] } ] }; }); } } function combineObjAttr(bool, obj, key, attr) { if (bool || bool === 0) { obj[key] = attr; } return obj; } function filterUndefinedKeys(obj) { for (const key in obj) { if (obj[key] === void 0) { delete obj[key]; } } return obj; } function renderListingArrValue(value) { if (!value) { return void 0; } if (typeof value === "object") { return [{ ...value }]; } else if (typeof value === "number" || typeof value === "boolean") { return [{ value: value.toString() }]; } return [{ value }]; } class ListingPrice { priceData; constructor(priceData) { this.priceData = priceData; } main() { return { productType: "PRODUCT", patches: [ { op: "replace", path: "/attributes/purchasable_offer", value: [ this.genPatche() ] } ] }; } genPatche() { const sendData = { audience: "ALL" }; combineObjAttr(this.priceData.sell_price, sendData, "our_price", [ { schedule: [ { value_with_tax: this.priceData.sell_price } ] } ]); combineObjAttr(this.priceData.low_price, sendData, "minimum_seller_allowed_price", [ { schedule: [ { value_with_tax: this.priceData.low_price } ] } ]); combineObjAttr(this.priceData.max_price, sendData, "maximum_seller_allowed_price", [ { schedule: [ { value_with_tax: this.priceData.max_price } ] } ]); return sendData; } genValue() { return renderListingArrValue( { our_price: [ { schedule: [ { value_with_tax: this.priceData.sell_price } ] } ] } ); } } class ListingQuantity { quantityData; constructor(quantityData) { this.quantityData = quantityData; } main() { return { productType: "PRODUCT", patches: [ { op: "replace", path: "/attributes/fulfillment_availability", value: [ this.genPatche() ] } ] }; } genPatche() { const sendData = { audience: "ALL", fulfillment_channel_code: this.quantityData.fulfillment_channel_code || "DEFAULT" }; combineObjAttr(this.quantityData.quantity, sendData, "quantity", this.quantityData.quantity); combineObjAttr(Boolean(this.quantityData.deal_time), sendData, "lead_time_to_ship_max_days", this.quantityData.deal_time); return sendData; } genValue() { return renderListingArrValue({ fulfillment_channel_code: "DEFAULT", quantity: this.quantityData.quantity, lead_time_to_ship_max_days: this.quantityData.deal_time }); } } class ListingImg { imgData; constructor(imgData) { this.imgData = imgData.filter((item) => { return getImageType(item.type); }); } main() { return { productType: "PRODUCT", patches: this.genPatches() }; } genPatches() { return this.imgData.map((item) => { return { op: "replace", path: `/attributes/${getImageType(item.type)}`, value: this.genValue(item.url) }; }); } genValuesMap() { const obj = {}; this.imgData.forEach((item) => { obj[getImageType(item.type)] = this.genValue(item.url); }); return obj; } genValue(value) { return [ { media_location: value } ]; } } class BatteriesRequired { batteries_required; constructor(batteries_required = 0) { this.batteries_required = batteries_required; } main() { return renderListingArrValue(Boolean(this.batteries_required)); } } class Brand { brand_name; constructor(brand_name) { this.brand_name = brand_name; } main() { return renderListingArrValue(this.brand_name); } } class BulletPoint { bullet_points; constructor(bullet_points = []) { this.bullet_points = bullet_points; } main() { return this.bullet_points.map((item) => { return { value: item }; }); } } class Condition { condition; constructor(condition) { this.condition = condition || "new_new"; } main() { return renderListingArrValue(this.condition); } } class CountryOfOrigin { country_of_origin; constructor(country_of_origin) { this.country_of_origin = country_of_origin || "CN"; } main() { return renderListingArrValue(this.country_of_origin); } } class Description { product_description; constructor(product_description) { this.product_description = product_description; } main() { return renderListingArrValue(this.product_description); } } class GenericKeyword { search_terms; constructor(search_terms) { this.search_terms = search_terms; } main() { return renderListingArrValue(this.search_terms); } } class GiftOptions { constructor() { } main() { return renderListingArrValue({ can_be_messaged: "false", can_be_wrapped: "false" }); } } class ItemDimensions { height; length; width; constructor(height, length = 1, width = 1) { this.height = height; this.length = length; this.width = width; } main() { return renderListingArrValue({ height: { unit: "centimeters", value: this.height }, length: { unit: "centimeters", value: this.length }, width: { unit: "centimeters", value: this.width } }); } } class ItemName { title; constructor(title) { this.title = title; } main() { return renderListingArrValue(this.title); } } class ItemPackageQuantity { item_package_quantity; constructor(item_package_quantity = 1) { this.item_package_quantity = item_package_quantity; } main() { return renderListingArrValue(this.item_package_quantity); } } class ItemTypeKeyword { item_type_keyword; constructor(item_type_keyword) { this.item_type_keyword = item_type_keyword; } main() { return renderListingArrValue(this.item_type_keyword); } } class ItemWeight { weight; constructor(weight = 0) { this.weight = weight; } main() { return renderListingArrValue({ unit: "kilograms", value: this.weight.toFixed(2) }); } } class ListPrice { list_price; constructor(list_price = 0) { this.list_price = list_price; } main() { return renderListingArrValue(this.list_price); } } class Manufacturer { manufacturer; constructor(manufacturer) { this.manufacturer = manufacturer; } main() { return renderListingArrValue(this.manufacturer); } } class MaxOrderQuantity { max_order_quantity; constructor(max_order_quantity = 100) { this.max_order_quantity = max_order_quantity; } main() { return renderListingArrValue(String(this.max_order_quantity)); } } class NumberOfItems { constructor() { } main() { return renderListingArrValue("1"); } } class PartNumber { manufactuer_id; constructor(manufactuer_id) { this.manufactuer_id = manufactuer_id; } main() { return renderListingArrValue(this.manufactuer_id); } } class ProductIdentifier { product_identifier_type; product_identifier_id; constructor(product_identifier_type, product_identifier_id = "") { this.product_identifier_type = product_identifier_type; this.product_identifier_id = product_identifier_id; } main() { return renderListingArrValue({ value: this.product_identifier_id, type: this.product_identifier_type }); } } class ProductTaxCode { product_tax_code; constructor(product_tax_code) { this.product_tax_code = product_tax_code; } main() { return renderListingArrValue(this.product_tax_code); } } class RecommendedBrowseNodes { recommendedBrowseNodes; constructor(recommendedBrowseNodes) { this.recommendedBrowseNodes = recommendedBrowseNodes; } main() { return this.recommendedBrowseNodes.map((item) => { return { value: item }; }); } } class SupplierDeclaredDgHzRegulation { constructor() { } main() { return renderListingArrValue("not_applicable"); } } class SupplierDeclaredHasProductIdentifierExemption { supplier_declared_has_product_identifier_exemption; constructor(supplier_declared_has_product_identifier_exemption) { this.supplier_declared_has_product_identifier_exemption = supplier_declared_has_product_identifier_exemption; } main() { return renderListingArrValue(Boolean(this.supplier_declared_has_product_identifier_exemption)); } } class ProductBaseInfo { data; marketplace_id; constructor(marketplace_id, data) { this.marketplace_id = marketplace_id; this.data = data; } main() { const data = this.data; return { purchasable_offer: data.sell_price && new ListingPrice({ sell_price: data.sell_price }).genValue(), list_price: data.list_price && new ListPrice(data.list_price).main(), fulfillment_availability: data.quantity && new ListingQuantity({ quantity: data.quantity, deal_time: data.deal_time }).genValue(), item_name: new ItemName(data.title).main(), batteries_required: new BatteriesRequired(data.is_electric).main(), manufacturer: new Manufacturer(data.manufacturer).main(), item_weight: new ItemWeight(data.weight).main(), gift_options: new GiftOptions().main(), product_tax_code: new ProductTaxCode().main(), item_type_keyword: new ItemTypeKeyword(data.item_type_keyword).main(), condition_type: new Condition(data.condition).main(), number_of_items: new NumberOfItems().main(), externally_assigned_product_identifier: data.product_identifier_type && new ProductIdentifier(data.product_identifier_type, data.product_identifier_id).main(), recommended_browse_nodes: data.recommendedBrowseNodes && new RecommendedBrowseNodes(data.recommendedBrowseNodes).main(), bullet_point: new BulletPoint(data.bullet_points).main(), item_package_quantity: new ItemPackageQuantity().main(), item_dimensions: data.height && new ItemDimensions(data.height, data.length, data.width).main(), part_number: new PartNumber(data.manufactuer_id).main(), max_order_quantity: new MaxOrderQuantity(data.max_order_quantity).main(), product_description: new Description(data.product_description).main(), supplier_declared_dg_hz_regulation: new SupplierDeclaredDgHzRegulation().main(), brand: new Brand(data.brand_name).main(), generic_keyword: new GenericKeyword(data.search_terms).main(), country_of_origin: new CountryOfOrigin(data.country_of_origin).main(), supplier_declared_has_product_identifier_exemption: new SupplierDeclaredHasProductIdentifierExemption(data.supplier_declared_has_product_identifier_exemption).main(), ...data.imgs ? new ListingImg(data.imgs).genValuesMap() : [] }; } } class ChildParentSkuRelationship { parent_sku; constructor(parent_sku) { this.parent_sku = parent_sku; } main() { const obj = { child_relationship_type: "variation" }; combineObjAttr(Boolean(this.parent_sku), obj, "parent_sku", this.parent_sku); return renderListingArrValue(obj); } } class Color { color; constructor(color = "normal") { this.color = color; } main() { return renderListingArrValue(this.color); } } class ParentageLevel { parentage; constructor(parentage) { this.parentage = parentage; } main() { return renderListingArrValue(this.parentage); } } class Size { size; constructor(size = "normal") { this.size = size; } main() { return renderListingArrValue(this.size); } } class VariationTheme { variation_theme; constructor(variation_theme = "SIZE_NAME/COLOR_NAME") { this.variation_theme = variation_theme; } main() { return renderListingArrValue({ name: this.variation_theme }); } } class ProductParentage { data; marketplace_id; constructor(marketplace_id, data) { this.marketplace_id = marketplace_id; this.data = data; } main() { const data = this.data; return { variation_theme: new VariationTheme(data.variation_theme).main(), color: new Color(data.color).main(), size: new Size(data.size).main(), parentage_level: new ParentageLevel(data.parentage).main(), child_parent_sku_relationship: new ChildParentSkuRelationship(data.parent_sku).main() }; } } class ListingProduct { data; marketplace_id; type; renderOtherAttributesFn; constructor({ marketplace_id, data, type, renderOtherAttributesFn }) { this.marketplace_id = marketplace_id; this.type = type || "LISTING"; this.data = data; this.renderOtherAttributesFn = renderOtherAttributesFn; } main() { if (this.type === "FOLLOW_ASIN") { return this.renderFollowAsin(); } else if (this.type === "LISTING") { return this.renderListing(); } throw new Error(`Invalid type: ${this.type}`); } renderFollowAsin() { const data = this.data; const attributes = { condition_type: new Condition(data.condition).main(), merchant_suggested_asin: [ { value: data.asin } ], fulfillment_availability: new ListingQuantity({ quantity: data.quantity || 0, deal_time: data.deal_time }).genValue(), purchasable_offer: data.sell_price && new ListingPrice({ sell_price: data.sell_price }).genValue(), max_order_quantity: new MaxOrderQuantity(data.max_order_quantity).main() }; Object.assign(attributes, this.callRenderOtherAttributesFn(attributes)); return { productType: data.product_type, requirements: "LISTING_OFFER_ONLY", attributes: filterUndefinedKeys(attributes) }; } renderListing() { const data = this.data; const attributes = { ...new ProductBaseInfo(this.marketplace_id, data).main() }; if (data.parentage) { Object.assign(attributes, new ProductParentage(this.marketplace_id, data).main()); } Object.assign(attributes, this.callRenderOtherAttributesFn(attributes)); return { productType: data.product_type, requirements: "LISTING", attributes: filterUndefinedKeys(attributes) }; } callRenderOtherAttributesFn(attributes) { if (!this.renderOtherAttributesFn) { return {}; } return this.renderOtherAttributesFn({ attributes, data: this.data, renderListingArrValue: renderListingArrValue }); } } class FeedProduct { sellerId; list; marketplace_id; renderOtherAttributesFn; constructor(sellerId, marketplace_id, list, renderOtherAttributesFn) { this.sellerId = sellerId; this.marketplace_id = marketplace_id; this.list = list; this.renderOtherAttributesFn = renderOtherAttributesFn; } main(type) { return { header: new FeedHeader(this.sellerId).main(), messages: this.genMessage(type) }; } genMessage(type) { return this.list.map((item, idx) => { return { messageId: idx + 1, sku: item.sku, operationType: "UPDATE", ...new ListingProduct({ marketplace_id: this.marketplace_id, data: item, renderOtherAttributesFn: this.renderOtherAttributesFn, type }).main() }; }); } } class FeedQuantity { sellerId; list; constructor(sellerId, list) { this.sellerId = sellerId; this.list = list; } main() { return { header: new FeedHeader(this.sellerId).main(), messages: this.genMessage() }; } genMessage() { return this.list.map((item, idx) => { return { messageId: idx + 1, sku: item.sku, operationType: "PATCH", productType: "PRODUCT", patches: [ { op: "replace", path: "/attributes/fulfillment_availability", value: [ { fulfillment_channel_code: "DEFAULT", quantity: item.quantity, lead_time_to_ship_max_days: item.deal_time || 2 } ] } ] }; }); } } class FeedRelation { sellerId; list; constructor(sellerId, list) { this.sellerId = sellerId; this.list = list; } main() { return { header: new FeedHeader(this.sellerId).main(), messages: this.genMessage() }; } genMessage() { return this.list.map((item, idx) => { return { messageId: idx + 1, sku: item.sku, operationType: "PATCH", productType: "LUGGAGE", patches: [ { op: "replace", path: "/attributes/child_parent_sku_relationship", value: [ { child_relationship_type: "variation", parent_sku: item.parent_sku } ] } ] }; }); } } class ListingRelation { parent_sku; constructor(parent_sku) { this.parent_sku = parent_sku; } main() { return { productType: "LUGGAGE", patches: [ { op: "replace", path: "/attributes/child_parent_sku_relationship", value: [ { child_relationship_type: "variation", parent_sku: this.parent_sku } ] } ] }; } } class ConvertSchemaItem2FormItem { field; schemaItem; required; constructor(field, schemaItem, required) { this.field = field; this.schemaItem = schemaItem; this.required = required; } main() { const key = this.parserSchemaPropertiesKey(); if (Array.isArray(key)) { return key.map((k) => this.convert(k)); } else { return this.convert(key); } } convert(key) { const { field, schemaItem } = this; const properties = schemaItem.items.properties; const { component, componentProps } = this.predictComponentData(properties[key]); return filterUndefinedKeys({ field: ["value", "type"].includes(key) ? field : `${field}_${key}`, label: schemaItem.title, component, componentProps, required: this.required?.includes(field) ? true : void 0 }); } predictComponentData(properties) { const { type, description, enumNames } = properties; if (enumNames) { const options = enumNames.map((label, idx) => { return { label, value: properties.enum[idx] }; }); return { component: type === "boolean" ? "Switch" : "Select", componentProps: { placeholder: description, options } }; } else if (type === "string") { return { component: "Input", componentProps: filterUndefinedKeys({ placeholder: description, max: properties.maxLength, min: properties.minLength }) }; } else if (type === "integer") { return { component: "InputNumber", componentProps: { placeholder: description, max: properties.maximum, min: properties.minimum } }; } else if (["array", "object"].includes(type) && properties?.items?.properties?.schedule?.items?.properties?.value_with_tax) { return { component: "inputNumber", componentProps: { placeholder: description, min: 0 } }; } else if (type === "object" && (properties?.properties?.value?.oneOf && properties?.properties?.value?.oneOf[0].format === "date")) { return { component: "DatePicker", componentProps: { placeholder: description } }; } else if (type === "array") { return this.predictComponentData(properties.items.properties[properties.items.required[0]]); } console.log("properties", properties); throw new Error(`predictComponentData: Unknown type--${type}`); } parserSchemaPropertiesKey() { const properties = this.schemaItem.items.properties; const invalidKeys = ["marketplace_id", "language_tag", "currency"]; const keys = Object.keys(properties).filter((key) => !invalidKeys.includes(key) && typeof properties[key] === "object"); if (keys.length === 1) { return keys[0]; } if (keys.length === 2 && keys.includes("value") && keys.includes("type")) { return "type"; } else if (keys.length >= 2) { return keys; } throw new Error(`parserSchemaPropertiesKey Error: Invalid keys--${keys}`); } } class SchemaCheck { jsonSchema; data; schema; required; constructor(schema, data) { this.schema = schema; this.jsonSchema = new Draft.Draft2019(schema); this.data = data; this.required = this.getRequiredFields(); } validate() { return this.jsonSchema.validate(this.data); } getRequiredFields() { return this.schema.required; } getRequiredSchema() { const requiredFields = this.getRequiredFields(); return requiredFields.map((item) => { return this.jsonSchema.getSchema({ pointer: item }); }); } getProperties(key) { return this.schema.properties[key]; } getAllPropertiesKeys() { return Object.keys(this.schema.properties); } convert2FormItems() { return this.getAllPropertiesKeys().map((field) => { const schema = this.jsonSchema.getSchema({ pointer: field }); try { return new ConvertSchemaItem2FormItem(field, schema, this.required).main(); } catch (e) { console.error(e); return void 0; } }); } convertRequiredSchema2FormItems() { return this.getRequiredFields().map((field) => { const schema = this.jsonSchema.getSchema({ pointer: field }); try { return new ConvertSchemaItem2FormItem(field, schema, this.required).main(); } catch (e) { console.error(e); return void 0; } }); } } export { ConvertSchemaItem2FormItem, FeedImg, FeedPrice, FeedProduct, FeedQuantity, FeedRelation, ListingImg, ListingPrice, ListingProduct, ListingQuantity, ListingRelation, SchemaCheck, combineObjAttr, filterUndefinedKeys, getImageType, imageTypeJsonMap, renderListingArrValue };