Creates a new ProductBuilder instance.
The base URL of the supplier's website, used for resolving relative URLs
const builder = new ProductBuilder('https://example.com');
Sets the data for the product by merging the provided data object.
The builder instance for method chaining
builder.setData({
title: "Test Chemical",
price: 29.99,
quantity: 500,
uom: "g"
});
setData(data: Partial<T>): ProductBuilder<T> {
Object.assign(this.product, data);
return this;
}
Sets the basic information for the product including title, URL, and supplier name.
The display name/title of the product
The URL where the product can be found (can be relative to baseURL)
The name of the supplier/vendor
The builder instance for method chaining
builder.setBasicInfo(
'Hydrochloric Acid',
'/products/hcl-solution',
'ChemSupplier'
);
setBasicInfo(title: string, url: string, supplier: string): ProductBuilder<T> {
this.product.title = title;
this.product.url = this.href(url);
this.product.supplier = supplier;
return this;
}
Sets the formula for the product.
Optionalformula: stringThe formula to set
The builder instance for method chaining
builder.setFormula('foobar K<sub>2</sub>Cr<sub>2</sub>O<sub>7</sub> baz');
// sets this.product.formula to "K₂Cr₂O₇"
builder.setFormula("H<sub>2</sub>SO<sub>4</sub>");
// sets this.product.formula to "H₂SO ₄"
builder.setFormula("Just some text");
// sets this.product.formula to undefined
setFormula(formula?: string): ProductBuilder<T> {
if (formula && typeof formula === "string" && formula.trim().length > 0) {
const parsedResult = findFormulaInHtml(formula);
if (parsedResult) {
this.product.formula = parsedResult;
}
}
return this;
}
Sets the grade/purity level of the product. Only sets the grade if a non-empty string is provided.
The grade or purity level of the product
The builder instance for method chaining
builder.setGrade("ACS Grade");
builder.setGrade("Reagent Grade");
setGrade(grade: string): ProductBuilder<T> {
if (grade && grade?.trim()?.length > 0) {
this.product.grade = grade;
}
return this;
}
Sets the price for the product. This is useful for if the price and currency are easier to add separately (eg: getting the currency code is done in a different request handler)
The price to set
The builder instance for method chaining
builder.setPrice(123.34);
setPrice(price: number | string): ProductBuilder<T> {
if (typeof price !== "number" && typeof price !== "string") {
this.logger.warn(`setPrice| Invalid price: ${price}`, {
price,
builder: this,
product: this.product,
});
return this;
}
this.product.price = Number(price);
return this;
}
Sets the currency symbol for the product. This is useful for if the price and currency are easier to add separately (eg: getting the currency code is done in a different request handler). If no currency symbol is set, then it will be inferred from the currencyCode
The currency symbol to set
The builder instance for method chaining
builder.setCurrencySymbol('$');
setCurrencySymbol(sign: CurrencySymbol): ProductBuilder<T> {
if (typeof sign !== "string") {
this.logger.warn(`setCurrencySymbol| Invalid currency symbol: ${sign}`, {
sign,
builder: this,
product: this.product,
});
return this;
}
this.product.currencySymbol = sign;
return this;
}
Sets the currency code for the product. This is useful for if the price and currency are easier to add separately (eg: getting the currency code is done in a different request handler).
The currency code to set
The builder instance for method chaining
builder.setCurrencyCode('USD');
setCurrencyCode(code: CurrencyCode): ProductBuilder<T> {
if (typeof code !== "string") {
this.logger.warn(`setCurrencyCode| Invalid currency code: ${code}`, {
code,
builder: this,
product: this.product,
});
return this;
}
this.product.currencyCode = code;
return this;
}
OverloadSets the pricing information for the product including price and currency details when given a parsedPrice object
ParsedPrice instance
The builder instance for method chaining
builder.setPricing(parsePrice('$123.34'));
// Sets this.product.price to 123.34
// Sets this.product.currencyCode to 'USD'
// Sets this.product.currencySymbol to '$'
setPricing(price: ParsedPrice): ProductBuilder<T>;
OverloadSets the pricing information for the product including price and currency details when given a price
Price in string format
The builder instance for method chaining
builder.setPricing('$123.34');
// Sets this.product.price to 123.34
// Sets this.product.currencyCode to 'USD'
// Sets this.product.currencySymbol to '$'
setPricing(price: string): ProductBuilder<T>;
OverloadSets the pricing information for the product including price and currency details when given a price
Price in number format
The ISO currency code (e.g., 'USD', 'EUR')
The currency symbol (e.g., '$', '€')
The builder instance for method chaining
builder.setPricing(123.34, 'USD', '$');
// Sets this.product.price to 123.34
// Sets this.product.currencyCode to 'USD'
// Sets this.product.currencySymbol to '$'
setPricing(
price: number | string,
currencyCode: string,
currencySymbol: string,
): ProductBuilder<T>;
OverloadSets the quantity information for the product.
QuantityObject format
The builder instance for method chaining
// For 500 grams
builder.setQuantity(parseQuantity('500g'));
// Sets this.product.quantity to 500
// Sets this.product.uom to 'g'
setQuantity(quantity: QuantityObject): ProductBuilder<T>;
OverloadSets the quantity information for the product.
Quantity in string format
The builder instance for method chaining
// For 500 grams
builder.setQuantity('500g');
// Sets this.product.quantity to 500
// Sets this.product.uom to 'g'
setQuantity(quantity: string): ProductBuilder<T>;
OverloadSets the quantity information for the product.
Quantity in number format
The unit of measure (e.g., 'g', 'ml', 'kg')
The builder instance for method chaining
// For 500 grams
builder.setQuantity(500, 'g');
// Sets this.product.quantity to 500
// Sets this.product.uom to 'g'
setQuantity(quantity: number, uom: string): ProductBuilder<T>;
Sets the unit of measure for the product.
The unit of measure for the product
The builder instance for method chaining
builder.setUOM('g');
setUOM(uom: string): ProductBuilder<T> {
if (typeof uom === "string" && uom.trim().length > 0) {
this.product.uom = uom;
return this;
}
this.logger.warn(`Unknown UOM: ${uom}`);
return this;
}
Sets the country of the supplier.
The country of the supplier
The builder instance for method chaining
builder.setSupplierCountry("US");
setSupplierCountry(country: CountryCode): ProductBuilder<T> {
this.product.supplierCountry = country;
return this;
}
Sets the shipping scope of the supplier.
The shipping scope of the supplier
The builder instance for method chaining
builder.setSupplierShipping("worldwide");
setSupplierShipping(shipping: ShippingRange): ProductBuilder<T> {
this.product.supplierShipping = shipping;
return this;
}
Sets the payment methods accepted by the supplier.
The payment methods accepted by the supplier
The builder instance for method chaining
builder.setSupplierPaymentMethods(["visa", "mastercard"]);
setSupplierPaymentMethods(paymentMethods: PaymentMethod[]): ProductBuilder<T> {
if (Array.isArray(paymentMethods)) {
this.product.paymentMethods = paymentMethods;
} else if (typeof paymentMethods === "string") {
this.product.paymentMethods = [paymentMethods];
}
return this;
}
Sets the product description.
The detailed description of the product
The builder instance for method chaining
builder.setDescription(
'High purity sodium chloride, 99.9% pure, suitable for laboratory use'
);
setDescription(description: string): ProductBuilder<T> {
this.product.description = description;
return this;
}
Sets the CAS (Chemical Abstracts Service) registry number for the product. Validates the CAS number format before setting.
The CAS registry number in format "XXXXX-XX-X"
The builder instance for method chaining
// For sodium chloride
builder.setCAS('7647-14-5');
// For invalid CAS number (will not set)
builder.setCAS('invalid-cas');
setCAS(cas: string): ProductBuilder<T> {
if (typeof cas !== "string") {
this.logger.warn(`setCAS| Invalid CAS number`, {
cas,
builder: this,
product: this.product,
});
return this;
}
if (isCAS(cas)) {
this.product.cas = cas;
} else {
const parsedACAS = findCAS(cas);
if (parsedACAS) {
this.product.cas = parsedACAS;
}
}
return this;
}
Sets the ID for the product.
Optionalid: string | numberThe unique identifier for the product
The builder instance for method chaining
builder.setID(12345);
setID(id?: number | string): ProductBuilder<T> {
if (id) {
this.product.id = id as T["id"];
}
return this;
}
Sets the UUID for the product.
The UUID string for the product
The builder instance for method chaining
builder.setUUID('550e8400-e29b-41d4-a716-446655440000');
setUUID(uuid: string): ProductBuilder<T> {
if (uuid && uuid.trim().length > 0) {
this.product.uuid = uuid;
}
return this;
}
Sets the SKU (Stock Keeping Unit) for the product.
The SKU string for the product
The builder instance for method chaining
builder.setSku('CHEM-NaCl-500G');
setSku(sku: string): ProductBuilder<T> {
if (sku && sku.trim().length > 0) {
this.product.sku = sku;
}
return this;
}
Sets the vendor for the product.
Optionalvendor: stringThe vendor name
The builder instance for method chaining
builder.setVendor('Vendor Name');
setVendor(vendor?: string): ProductBuilder<T> {
if (vendor) {
this.product.vendor = vendor;
}
return this;
}
Tries to determine the availability of the product based on variable input.
Optionalavailability: string | booleanThe availability of the product
The availability of the product
// In stock
builder.determineAvailability("instock");
builder.determineAvailability(true);
builder.determineAvailability("outofstock");
builder.determineAvailability("unavailable");
builder.determineAvailability(false);
builder.determineAvailability("preorder");
builder.determineAvailability("backorder");
builder.determineAvailability("discontinued");
determineAvailability(availability?: AVAILABILITY | boolean | string): Maybe<AVAILABILITY> {
if (typeof availability === "undefined") return;
if (isAvailability(availability)) return availability;
if (typeof availability === "boolean")
return availability ? AVAILABILITY.IN_STOCK : AVAILABILITY.OUT_OF_STOCK;
if (typeof availability === "string") {
// converting to lower and removing all non-alpha characters just to standardize the values for easier processing.
switch (availability.toLowerCase().replaceAll(/[^a-z]/g, "")) {
case "instock":
case "available":
return AVAILABILITY.IN_STOCK;
case "unavailable":
case "outofstock":
return AVAILABILITY.OUT_OF_STOCK;
case "preorder":
return AVAILABILITY.PRE_ORDER;
case "backorder":
return AVAILABILITY.BACKORDER;
case "discontinued":
return AVAILABILITY.DISCONTINUED;
default:
return;
}
}
}
Sets the availability of the product.
The availability of the product
The builder instance for method chaining
// In stock
builder.setAvailability("IN_STOCK");
// Set as in stock
builder.setAvailability(false);
// Out of stock
// etc
setAvailability(availability: AVAILABILITY): ProductBuilder<T>;
Sets the availability of the product.
The availability of the product
The builder instance for method chaining
// In stock
builder.setAvailability("IN_STOCK");
// Set as in stock
builder.setAvailability(false);
// Out of stock
// etc
setAvailability(availability: boolean): ProductBuilder<T>;
Sets the availability of the product.
The availability of the product
The builder instance for method chaining
// In stock
builder.setAvailability("IN_STOCK");
// Set as in stock
builder.setAvailability(false);
// Out of stock
// etc
setAvailability(availability: string): ProductBuilder<T>;
Just a place to hold the products original response object.
Optionaldata: Record<string, unknown>The raw data to add the raw data.
The builder instance for method chaining
builder.addRawData({
title: 'Sodium Chloride',
price: 29.99,
});
addRawData(data?: Record<string, unknown>): ProductBuilder<T> {
Object.assign(this.rawData, data);
return this;
}
Adds a single variant to the product.
The builder instance for method chaining
builder.addVariant({
title: '500g Package',
price: 49.99,
quantity: 500,
uom: 'g',
sku: 'CHEM-500G'
});
addVariant(variant: Partial<Variant>): ProductBuilder<T> {
if (!this.product.variants) {
this.product.variants = [];
}
this.product.variants.push(variant);
return this;
}
Adds multiple variants to the product at once.
The builder instance for method chaining
builder.addVariants([
{
title: '500g Package',
price: 49.99,
quantity: 500,
uom: 'g'
},
{
title: '1kg Package',
price: 89.99,
quantity: 1000,
uom: 'g'
}
]);
addVariants(variants: Partial<Variant>[]): ProductBuilder<T> {
for (const variant of variants) {
this.addVariant(variant);
}
return this;
}
Sets the variants for the product. Slightly different from addVariants in that it will replace the existing variants with the new ones.
The builder instance for method chaining
builder.setVariants([{ id: 1, title: '500g Package', price: 49.99, quantity: 500, uom: 'g' }]);
setVariants(variants: Partial<Variant>[]): ProductBuilder<T> {
this.product.variants = variants;
return this;
}
Get a specific property from the product.
The key of the property to get
The value of the property
const title = builder.get("title");
console.log(title); // "Sodium Chloride"
get(key: keyof T): T[keyof T] | Maybe<T[keyof T]> {
if (key in this.product && typeof this.product[key] !== "undefined") {
return this.product[key] as T[keyof T];
}
return;
}
Sets the match percentage (Levenshtein result) for the product title compared to the search string.
The match percentage to set
The builder instance for method chaining
builder.setMatchPercentage(95);
setMatchPercentage(matchPercentage: number): ProductBuilder<T> {
if (typeof matchPercentage === "number") {
this.product.matchPercentage = matchPercentage;
}
return this;
}
Gets a specific variant from the product.
The index of the variant to get
The variant object
const variant = builder.getVariant(0);
console.log(variant); // { id: 1, title: '500g Package', price: 49.99, quantity: 500, uom: 'g' }
getVariant(index: number): Variant | undefined {
return this.product.variants?.[index];
}
PrivatehrefConverts a relative or partial URL to an absolute URL using the base URL.
The URL or path to convert
The absolute URL as a string
const url = this.href('/products/123');
// Returns: 'https://example.com/products/123'
private href(path: string | URL): string {
const urlObj = new URL(path, this.baseURL);
return urlObj.toString();
}
Builds and validates the final Product object. Performs the following steps:
const product = await builder
.setBasicInfo('Test Chemical', '/products/test', 'Supplier')
.setPricing(29.99, 'USD', '$')
.setQuantity(100, 'g')
.addVariant({
title: '500g Package',
price: 49.99,
quantity: 500,
uom: 'g'
})
.build();
async build(): Promise<Maybe<T>> {
if (!isMinimalProduct(this.product)) {
return;
}
this.product.usdPrice = this.product.price;
const baseQuantity = toBaseQuantity(this.product.quantity, this.product.uom);
if (baseQuantity) {
this.product.baseQuantity = baseQuantity;
}
if (this.product.currencyCode !== "USD") {
this.product.usdPrice = await toUSD(this.product.price, this.product.currencyCode);
}
// Process variants if present
if (this.product.variants?.length) {
// Filter out invalid variants
this.product.variants = this.product.variants.filter((variant) => isValidVariant(variant));
// Process each variant
for (const variant of this.product.variants ?? []) {
if ("quantity" in variant === false || !variant.quantity) {
this.logger.warn("Skipping variant, no quantity found", {
variant,
product: this.product,
builder: this,
});
continue;
}
if ("price" in variant === false || !variant.price) {
this.logger.warn("Skipping variant, no price found", {
variant,
product: this.product,
builder: this,
});
continue;
}
if ("usdPrice" in variant === false || !variant.usdPrice) {
variant.usdPrice = await toUSD(variant.price, this.product.currencyCode);
}
if ("uom" in variant === false || !variant.uom) {
variant.uom = this.product.uom;
}
// Sometimes variants don't have their own titles, they're just a dropdown on
// the same page, so if that's the case then we should append the quantity to
// the title to differentiate them.
if (
"title" in variant === false ||
!variant.title?.trim()?.length ||
variant.title === this.product.title
) {
variant.title = `${this.product.title} - ${variant.quantity}${variant.uom}`;
}
if (variant.url) {
variant.url = this.href(variant.url);
}
// Re-populate the variant using the parent product properties as defaults and the current
// values as overrides.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { variants: _, ...defaults } = this.product;
Object.assign(variant, defaults, { ...variant });
}
}
if (this.product._fuzz) {
this.product.matchPercentage = this.product._fuzz.score;
}
if (!isProduct(this.product)) {
this.logger.error(`ProductBuilder| Invalid product:`, {
product: this.product,
builder: this,
});
return;
}
this.product.url = this.href(this.product.url);
this.logger.debug("ProductBuilder| Built product:", { product: this.product, builder: this });
return this.product as T;
}
Returns the current state of the product being built. Useful for debugging or inspecting the build progress.
The current partial product object
const partialProduct = builder
.setBasicInfo('Test', '/test', 'Supplier')
.dump();
console.log(partialProduct);
dump(): Partial<T> {
return this.product;
}
StaticcreateCreates an array of ProductBuilder instances from cached product data. This is used to restore builders from cache storage.
The base URL of the supplier's website
Array of cached product data (from .dump())
Array of ProductBuilder instances
const cachedData = await chrome.storage.local.get('cached_products');
const builders = ProductBuilder.createFromCache('https://example.com', cachedData);
for (const builder of builders) {
const product = await builder.build();
console.log(product.title);
}
public static createFromCache<T extends Product>(
baseURL: string,
data: unknown[],
): ProductBuilder<T>[] {
return data.map((d) => {
const builder = new ProductBuilder<T>(baseURL);
builder.setData(d as Partial<T>);
return builder;
});
}
Builder class for constructing Product objects with a fluent interface. Implements the Builder pattern to handle complex product construction with optional fields and data validation.
Remarks
This is a utility class for building product data up over different requests. It is used to build the product data up over different requests, and then return a complete product object.
Example
Source