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
JavaScript
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 };