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> {
if (data === null || typeof data !== 'object') {
return this;
}
//
// @todo: This is AI garbage, and I need to clean it up. Too messy.
//
// Route every key through its validating setter instead of a blind Object.assign, so values
// can't bypass the per-field checks. `Record<keyof Product, …>` forces this map to stay
// exhaustive — adding a Product field without a handler is a compile error. Keys not present
// here (i.e. not part of Product) are dropped with a warning.
const dispatch: Record<keyof Product, (value: unknown) => void> = {
title: (v) => this.setTitle(v),
supplier: (v) => this.setSupplier(v),
url: (v) => this.setURL(v),
permalink: (v) => this.setPermalink(v),
description: (v) => this.setDescription(v),
shortDescription: (v) => this.setShortDescription(v),
manufacturer: (v) => this.setManufacturer(v),
vendor: (v) => this.setVendor(v),
sku: (v) => this.setSku(v),
id: (v) => this.setID(v),
uuid: (v) => this.setUUID(v),
cacheKey: (v) => this.setCacheKey(v),
cas: (v) => this.setCAS(v),
formula: (v) => this.setFormula(v),
smiles: (v) => this.setSmiles(v),
iupacName: (v) => this.setIupacName(v),
pubchemId: (v) => this.setPubchemId(v),
inchiKey: (v) => this.setInChIKey(v),
inchi: (v) => this.setInChI(v),
moleweight: (v) => this.setMoleweight(v),
purity: (v) => this.setPurity(v),
grade: (v) => this.setGrade(v),
concentration: (v) => this.setConcentration(v),
moles: (v) => this.setMoles(v),
price: (v) => this.setPrice(v),
usdPrice: (v) => this.setUsdPrice(v),
localPrice: (v) => this.setLocalPrice(v),
currencyCode: (v) => this.setCurrencyCode(v),
currencySymbol: (v) => this.setCurrencySymbol(v),
uom: (v) => this.setUOM(v),
baseUom: (v) => this.setBaseUom(v),
baseQuantity: (v) => this.setBaseQuantity(v),
quantity: (v) => this.setQuantityValue(v),
rating: (v) => this.setRating(v),
reviewCount: (v) => this.setReviewCount(v),
status: (v) => this.setStatus(v),
statusTxt: (v) => this.setStatusTxt(v),
shippingInformation: (v) => this.setShippingInformation(v),
purchaseRestriction: (v) => this.setPurchaseRestriction(v),
attributes: (v) => this.setAttributes(v),
availability: (v) => {
// setAvailability is overloaded per type, so a `string | boolean` union won't resolve —
// narrow to a single type in each branch before calling.
if (typeof v === 'string') this.setAvailability(v);
else if (typeof v === 'boolean') this.setAvailability(v);
},
images: (v) => this.addImages(v),
sdsUrl: (v) => this.setSDSUrl(v),
coaUrl: (v) => this.setCoaUrl(v),
specSheetUrl: (v) => this.setSpecSheetUrl(v),
docLinks: (v) => this.setDocLinks(v),
supplierCountry: (v) => this.setSupplierCountry(v),
supplierShipping: (v) => this.setSupplierShipping(v),
paymentMethods: (v) => this.setSupplierPaymentMethods(v),
supplierEbayStoreURL: (v) => this.setSupplierEbayStoreURL(v),
supplierAmazonStoreURL: (v) => this.setSupplierAmazonStoreURL(v),
variants: (v) => {
if (Array.isArray(v)) this.setVariants(v);
},
matchPercentage: (v) => this.setMatchPercentage(v),
_fuzz: (v) => {
if (this.isFuzzMeta(v)) this.product._fuzz = v;
},
// Positional row index (synthetic, like _fuzz) — carried through if present
// but never a real identifier.
_id: (v) => {
if (typeof v === 'number') this.product._id = v;
},
// Display-only back-reference set by the ungrouped table view, never sourced
// from supplier or cached data — dropped here so it can't round-trip in.
parentProduct: () => {},
// Display-only price-history series id stamped by the ungrouped table view;
// like parentProduct, dropped here so it can't round-trip in from cached data.
priceSeriesKey: () => {},
// Display-only trend sort value stamped by the results table; like
// parentProduct, dropped here so it can't round-trip in from cached data.
priceTrendValue: () => {},
};
// `Object.entries` widens each key to `string`; narrow it back to a dispatch
// key via `in` (dispatch is keyed by every `keyof Product`) so no cast is needed.
const isProductKey = (key: string): key is keyof Product => key in dispatch;
for (const [key, value] of Object.entries(data)) {
if (!isProductKey(key)) {
this.logger.warn('setData| dropping unsupported key', { key, value });
continue;
}
dispatch[key](value);
}
return this;
}
Validates and sets the product quantity from an arbitrary value. Unlike setQuantity, this sets only the numeric quantity (the UOM is set separately via its own key), so it can be driven by setData where each field arrives independently.
The quantity to set, or any value (anything that isn't a positive number is ignored)
The builder instance for method chaining
builder.setQuantityValue(500);
setQuantityValue(quantity: unknown): ProductBuilder<T> {
const value = typeof quantity === 'string' ? Number(quantity) : quantity;
if (typeof value === 'number' && !Number.isNaN(value) && value > 0) {
this.product.quantity = value;
}
return this;
}
PrivateisNarrows an unknown value to the { score: number; idx: number } shape of Product._fuzz.
The value to test
True when the value is a valid fuzz-metadata object
this.isFuzzMeta({ score: 0.9, idx: 3 }); // true
this.isFuzzMeta({ score: "high" }); // false
private isFuzzMeta(value: unknown): value is { score: number; idx: number } {
return (
typeof value === 'object' &&
value !== null &&
'score' in value &&
typeof value.score === 'number' &&
'idx' in value &&
typeof value.idx === 'number'
);
}
Sets the basic information for the product by delegating to setTitle, setURL, and setSupplier, each of which validates its own input.
The display name/title of the product, or any value (invalid input is ignored)
The URL where the product can be found (can be relative to baseURL), or any value
The name of the supplier/vendor, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setBasicInfo(
'Hydrochloric Acid',
'/products/hcl-solution',
'ChemSupplier'
);
setBasicInfo(title: unknown, url: unknown, supplier: unknown): ProductBuilder<T> {
return this.setTitle(title).setURL(url).setSupplier(supplier);
}
Sets the product title. A value that isn't a non-empty string is ignored.
The display name/title of the product, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setTitle("Hydrochloric Acid");
setTitle(title: unknown): ProductBuilder<T> {
if (typeof title === 'string' && title.trim().length > 0) {
this.product.title = title;
} else if (title != null && this.showFailedValidation) {
this.logger.warn('setTitle| Invalid title value', { title, builder: this });
}
return this;
}
Sets the supplier name. A value that isn't a non-empty string is ignored.
The name of the supplier/vendor, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setSupplier("ChemSupplier");
setSupplier(supplier: unknown): ProductBuilder<T> {
if (typeof supplier === 'string' && supplier.trim().length > 0) {
this.product.supplier = supplier;
} else if (supplier != null && this.showFailedValidation) {
this.logger.warn('setSupplier| Invalid supplier value', { supplier, builder: this });
}
return this;
}
Sets the URL for the product. A value that isn't a usable URL is ignored.
The URL to set, or any value (non-URLs are ignored)
The builder instance for method chaining
builder.setURL("https://example.com/products/sodium-chloride-500g");
setURL(url: unknown): ProductBuilder<T> {
const href = this.resolveHref(url);
if (href) {
this.product.url = href;
} else if (url != null && this.showFailedValidation) {
this.logger.warn('setURL| Invalid URL', { url, builder: this });
}
return this;
}
Sets the permalink for the product. A value that isn't a usable URL is ignored.
The permalink to set, or any value (non-URLs are ignored)
The builder instance for method chaining
builder.setPermalink("https://example.com/products/sodium-chloride-500g");
setPermalink(permalink: unknown): ProductBuilder<T> {
const href = this.resolveHref(permalink);
if (href) {
this.product.permalink = href;
} else if (permalink != null && this.showFailedValidation) {
this.logger.warn('setPermalink| Invalid permalink', { permalink, builder: this });
}
return this;
}
Sets the SDS URL for the product. Accepts the often-optional result of a parser directly; a value that isn't a usable URL (undefined, empty, wrong type) is ignored.
The SDS URL to set, or any value (non-URLs are ignored)
The builder instance for method chaining
builder.setSDSUrl("https://example.com/sds.pdf");
builder.setSDSUrl(findPdfHref(html)); // no-ops when findPdfHref returns undefined
setSDSUrl(sdsUrl: unknown): ProductBuilder<T> {
const href = this.resolveHref(sdsUrl);
if (href) {
this.product.sdsUrl = href;
} else if (sdsUrl != null && this.showFailedValidation) {
this.logger.warn('setSDSUrl| Invalid SDS URL', { sdsUrl, builder: this });
}
return this;
}
Sets the Certificate of Analysis (COA) URL for the product. Accepts the often-optional result of a parser directly; a value that isn't a usable URL (undefined, empty, wrong type) is ignored.
The COA URL to set, or any value (non-URLs are ignored)
The builder instance for method chaining
builder.setCoaUrl("https://example.com/coa.pdf");
builder.setCoaUrl(findPdfHref(html)); // no-ops when findPdfHref returns undefined
setCoaUrl(coaUrl: unknown): ProductBuilder<T> {
const href = this.resolveHref(coaUrl);
if (href) {
this.product.coaUrl = href;
} else if (coaUrl != null && this.showFailedValidation) {
this.logger.warn('setCoaUrl| Invalid COA URL', { coaUrl, builder: this });
}
return this;
}
Sets the spec sheet URL for the product. A value that isn't a non-empty string is ignored.
The spec sheet URL to set, or any value (non-strings are ignored)
The builder instance for method chaining
builder.setSpecSheetUrl("https://example.com/spec-sheet.pdf");
setSpecSheetUrl(specSheetUrl: unknown): ProductBuilder<T> {
const href = this.resolveHref(specSheetUrl);
if (href) {
this.product.specSheetUrl = href;
} else if (specSheetUrl != null && this.showFailedValidation) {
this.logger.warn('setSpecSheetUrl| Invalid spec sheet URL', { specSheetUrl, builder: this });
}
return this;
}
Sets the product's default (primary) full-size image. Placed first among the image-type entries so it's treated as the default; a previous default set this way is replaced. A value that isn't a usable URL is ignored; alt text is only applied alongside a valid image.
The image URL to set, or any value (non-URLs are ignored)
OptionalimageAltText: unknownThe alt text for the image, or any value (non-strings are ignored)
The builder instance for method chaining
builder.setImage("https://example.com/image.jpg");
builder.setImage("https://example.com/image.jpg", "Sodium Chloride ACS Grade 500g");
setImage(imageURL: unknown, imageAltText?: unknown): ProductBuilder<T> {
const image = this.buildImage(imageURL, 'image', imageAltText);
if (image) {
this.setDefaultImage(image);
} else if (imageURL != null && this.showFailedValidation) {
this.logger.warn('setImage| Invalid image URL', { imageURL, builder: this });
}
return this;
}
Sets the product's default (primary) thumbnail. Placed first among the thumbnail-type entries so it's treated as the default; a previous default set this way is replaced. A supplier's main thumbnail is often distinct from its gallery images, so this is kept as its own entry rather than attached to any image. A value that isn't a usable URL is ignored.
The thumbnail URL to set, or any value (non-URLs are ignored)
The builder instance for method chaining
builder.setThumbnail("https://example.com/thumb.jpg");
setThumbnail(thumbnail: unknown): ProductBuilder<T> {
const image = this.buildImage(thumbnail, 'thumbnail');
if (image) {
this.setDefaultImage(image);
} else if (thumbnail != null && this.showFailedValidation) {
this.logger.warn('setThumbnail| Invalid thumbnail URL', { thumbnail, builder: this });
}
return this;
}
Appends a full-size image to the product's image list. Unlike setImage, this doesn't replace the default — use it for gallery images. A value that isn't a usable URL is ignored.
The image URL to add, or any value (non-URLs are ignored)
OptionalimageAltText: unknownThe alt text for the image, or any value (non-strings are ignored)
The builder instance for method chaining
builder.addImage("https://example.com/a.jpg", "front label");
addImage(imageURL: unknown, imageAltText?: unknown): ProductBuilder<T> {
const image = this.buildImage(imageURL, 'image', imageAltText);
if (image) {
this.pushImage(image);
} else if (imageURL != null && this.showFailedValidation) {
this.logger.warn('addImage| Invalid image URL', { imageURL, builder: this });
}
return this;
}
Appends a thumbnail to the product's image list. Unlike setThumbnail, this doesn't replace the default. A value that isn't a usable URL is ignored.
The thumbnail URL to add, or any value (non-URLs are ignored)
The builder instance for method chaining
builder.addThumbnail("https://example.com/a-t.jpg");
addThumbnail(thumbnail: unknown): ProductBuilder<T> {
const image = this.buildImage(thumbnail, 'thumbnail');
if (image) {
this.pushImage(image);
} else if (thumbnail != null && this.showFailedValidation) {
this.logger.warn('addThumbnail| Invalid thumbnail URL', { thumbnail, builder: this });
}
return this;
}
Appends multiple full-size images to the product's image list. Each entry may be a plain URL
string (added via addImage as an "image") or a pre-built { href, type, altText? }
ProductImage object (whose explicit type is preserved, so mixed image/thumbnail arrays
are supported). Entries that don't resolve are ignored.
An array of URL strings and/or ProductImage values, or any value (non-arrays are ignored)
The builder instance for method chaining
builder.addImages(["https://example.com/a.jpg", "https://example.com/b.jpg"]);
builder.addImages([{ href: "https://example.com/a-t.jpg", type: "thumbnail" }]);
addImages(images: unknown): ProductBuilder<T> {
this.addImageEntries(images, 'image');
return this;
}
Appends multiple thumbnails to the product's image list. Each entry may be a plain URL string
(added via addThumbnail as a "thumbnail") or a pre-built { href, type, altText? }
ProductImage object (whose explicit type is preserved). Entries that don't resolve are
ignored.
An array of URL strings and/or ProductImage values, or any value (non-arrays are ignored)
The builder instance for method chaining
builder.addThumbnails(["https://example.com/a-t.jpg", "https://example.com/b-t.jpg"]);
addThumbnails(thumbnails: unknown): ProductBuilder<T> {
this.addImageEntries(thumbnails, 'thumbnail');
return this;
}
PrivateaddAppends each entry of an array to the product's image list. Pre-built ProductImage
objects are resolved with their explicit type preserved; any other entry is treated as a raw
URL and added with defaultType via addImage/addThumbnail.
The array of URL strings and/or ProductImage values (non-arrays are ignored)
The type to stamp on raw-URL entries
Nothing.
this.addImageEntries(["/a.jpg"], "image"); // adds { href: "https://base/a.jpg", type: "image" }
this.addImageEntries([{ href: "/a-t.jpg", type: "thumbnail" }], "image"); // preserves "thumbnail"
private addImageEntries(entries: unknown, defaultType: ProductImageType): void {
if (!Array.isArray(entries)) {
return;
}
const uniqueEntries = [...new Set(entries)];
for (const entry of uniqueEntries) {
if (isProductImage(entry)) {
const resolved = this.resolveImageEntry(entry);
if (resolved) this.pushImage(resolved);
} else if (defaultType === 'image') {
this.addImage(entry);
} else {
this.addThumbnail(entry);
}
}
}
PrivatebuildBuilds a ProductImage of the given type from a URL, resolving it to an absolute href, or
undefined when the URL isn't usable.
The image URL.
Whether the entry is a full-size image or a thumbnail.
OptionalaltText: unknownOptional alt text; applied only when it's a non-empty string.
The built image entry, or undefined when the URL doesn't resolve.
this.buildImage("/a.jpg", "image", "front"); // => { href: "https://base/a.jpg", type: "image", altText: "front" }
this.buildImage("", "thumbnail"); // => undefined
private buildImage(
url: unknown,
type: ProductImageType,
altText?: unknown,
): ProductImage | undefined {
const href = this.resolveHref(url);
if (!href) {
return undefined;
}
const image: ProductImage = { href, type };
if (typeof altText === 'string' && altText.trim().length > 0) {
image.altText = altText;
}
return image;
}
PrivateresolveNormalizes an unknown { href, type, altText? } value into a ProductImage with an
absolute href, or undefined when it isn't a valid entry or its href doesn't resolve.
The candidate image entry.
The resolved image entry, or undefined when it's not valid.
this.resolveImageEntry({ href: "/a.jpg", type: "image" }); // => { href: "https://base/a.jpg", type: "image" }
this.resolveImageEntry({ href: "/a.jpg", type: "banner" }); // => undefined
private resolveImageEntry(entry: unknown): ProductImage | undefined {
if (!isProductImage(entry)) {
return undefined;
}
return this.buildImage(entry.href, entry.type, entry.altText);
}
PrivatepushAppends an image entry to the product's image list, creating the list on first use.
The already-resolved image entry to append.
Nothing.
this.pushImage({ href: "https://example.com/a.jpg", type: "image" });
private pushImage(image: ProductImage): void {
if (!this.product.images) {
this.product.images = [];
}
this.product.images.push(image);
}
PrivatesetMakes an image entry the default for its type by inserting it at the front of the list, so it's the first entry of that type. Existing entries (gallery images/thumbnails) are kept.
The image entry to set as its type's default.
Nothing.
this.setDefaultImage({ href: "https://example.com/a.jpg", type: "image" });
private setDefaultImage(image: ProductImage): void {
if (!this.product.images) {
this.product.images = [];
}
this.product.images.unshift(image);
}
Sets the formula for the product. A value that isn't a recognizable formula is ignored.
The formula to set, or any value (invalid input is ignored)
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("NaBH4");
// sets this.product.formula to "NaBH4" (clean formula stored as-is)
builder.setFormula("Just some text");
// leaves this.product.formula unset
setFormula(formula: unknown): ProductBuilder<T> {
if (typeof formula !== 'string' || formula.trim().length === 0) {
return this;
}
const trimmed = formula.trim();
// Store as-is when the value is already a finished formula: a clean ASCII formula (incl. ones
// containing "1" like "C12H22O11", which findFormulaInHtml mishandles), or one already
// display-formatted with unicode subscripts / hydrate notation (e.g. "NH₄NaC₄H₄O₆ x 4H₂O"), or
// a polymer repeating unit carrying a variable subscript index (e.g. "(C3H3NaO2)ₙ"). Re-parsing
// those would corrupt them.
if (!trimmed.includes('<sub>') && (isMoleForm(trimmed) || /[₀-₉ₙₘₓ]/.test(trimmed))) {
this.product.formula = trimmed;
return this;
}
// Otherwise extract and subscript-format a formula from HTML/surrounding text.
const parsedResult = findFormulaInHtml(trimmed);
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, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setGrade("ACS Grade");
builder.setGrade("Reagent Grade");
setGrade(grade: unknown): ProductBuilder<T> {
if (typeof grade === 'string' && grade.trim().length > 0) {
this.product.grade = grade;
} else if (grade != null && this.showFailedValidation) {
this.logger.warn('setGrade| Invalid grade value', { grade, builder: this });
}
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 (a number or numeric string), or any value (invalid input is ignored)
The builder instance for method chaining
builder.setPrice(123.34);
setPrice(price: unknown): ProductBuilder<T> {
if (typeof price !== 'number' && typeof price !== 'string') {
// A non-null, non-numeric value is a genuine misuse worth flagging; absent input is a quiet no-op.
if (price != null && this.showFailedValidation) {
this.logger.warn('setPrice| Invalid price', {
price,
builder: this,
});
}
return this;
}
const value = Number(price);
if (!Number.isNaN(value)) {
this.product.price = value;
}
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, or any value (anything that isn't a known symbol is ignored)
The builder instance for method chaining
builder.setCurrencySymbol('$');
setCurrencySymbol(sign: unknown): ProductBuilder<T> {
if (isCurrencySymbol(sign)) {
this.product.currencySymbol = sign;
} else if (sign != null && this.showFailedValidation) {
this.logger.warn('setCurrencySymbol| Invalid currency symbol', {
sign,
builder: this,
});
}
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, or any value (anything that isn't a known code is ignored)
The builder instance for method chaining
builder.setCurrencyCode('USD');
setCurrencyCode(code: unknown): ProductBuilder<T> {
if (isCurrencyCode(code)) {
this.product.currencyCode = code;
} else if (code != null && this.showFailedValidation) {
this.logger.warn('setCurrencyCode| Invalid currency code', {
code,
builder: this,
});
}
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 moles for the product.
The moles to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setMoles(23.5);
setMoles(moles: unknown): ProductBuilder<T> {
if (typeof moles === 'number' && !Number.isNaN(moles) && moles > 0) {
this.product.moles = moles;
} else if (moles != null && this.showFailedValidation) {
this.logger.warn('setMoles| Invalid moles', { moles, builder: this });
}
return this;
}
Sets the unit of measure for the product.
The unit of measure to set, or any value (anything that isn't a known UOM is ignored)
The builder instance for method chaining
builder.setUOM('g');
setUOM(uom: unknown): ProductBuilder<T> {
if (isUOM(uom)) {
this.product.uom = uom;
} else if (uom != null && this.showFailedValidation) {
this.logger.warn('setUOM| Invalid UOM', { uom, builder: this });
}
return this;
}
Sets the country of the supplier.
The country of the supplier, or any value (anything that isn't a known country code is ignored)
The builder instance for method chaining
builder.setSupplierCountry("US");
setSupplierCountry(country: unknown): ProductBuilder<T> {
if (isCountryCode(country)) {
this.product.supplierCountry = country;
} else if (country != null && this.showFailedValidation) {
this.logger.warn('setSupplierCountry| Invalid country value', { country, builder: this });
}
return this;
}
Sets the shipping scope of the supplier.
The shipping scope, or any value (anything that isn't a known shipping range is ignored)
The builder instance for method chaining
builder.setSupplierShipping("worldwide");
setSupplierShipping(shipping: unknown): ProductBuilder<T> {
if (isShippingRange(shipping)) {
this.product.supplierShipping = shipping;
} else if (shipping != null && this.showFailedValidation) {
this.logger.warn('setSupplierShipping| Invalid shipping value', { shipping, builder: this });
}
return this;
}
Sets the payment methods accepted by the supplier.
A payment method or array of them, or any value (non-payment-method entries are dropped)
The builder instance for method chaining
builder.setSupplierPaymentMethods(["visa", "mastercard"]);
setSupplierPaymentMethods(paymentMethods: unknown): ProductBuilder<T> {
const candidates = Array.isArray(paymentMethods) ? paymentMethods : [paymentMethods];
const valid = candidates.filter(isPaymentMethod);
if (valid.length > 0) {
this.product.paymentMethods = valid;
} else if (paymentMethods != null && this.showFailedValidation) {
this.logger.warn('setSupplierPaymentMethods| Invalid payment methods', {
paymentMethods,
builder: this,
});
}
return this;
}
PrivateabsoluteNarrows a value to an absolute http(s) URL string.
Storefront URLs live on a foreign host, so they can't go through ProductBuilder.resolveHref
(which resolves against the supplier's own baseURL), and isFullURL is too permissive — it
accepts any scheme, including javascript:, and these values are rendered into an href.
The candidate URL, or any value
The normalized absolute URL, or undefined when it isn't one
this.absoluteHttpURL("https://www.ebay.com/str/x"); // "https://www.ebay.com/str/x"
this.absoluteHttpURL("javascript:alert(1)"); // undefined
private absoluteHttpURL(value: unknown): string | undefined {
if (typeof value !== 'string' || value.length === 0) return undefined;
try {
const url = new URL(value);
return url.protocol === 'https:' || url.protocol === 'http:' ? url.toString() : undefined;
} catch {
return undefined;
}
}
Sets the supplier's eBay storefront URL, shown in the expanded product row for suppliers that restrict shipping on their own site but ship more freely via eBay.
The storefront URL, or any value (anything that isn't an absolute http(s) URL is ignored)
The builder instance for method chaining
builder.setSupplierEbayStoreURL("https://www.ebay.com/str/dailybiousa");
setSupplierEbayStoreURL(storeURL: unknown): ProductBuilder<T> {
const href = this.absoluteHttpURL(storeURL);
if (href) {
this.product.supplierEbayStoreURL = href;
} else if (storeURL != null && this.showFailedValidation) {
this.logger.warn('setSupplierEbayStoreURL| Invalid eBay store URL', {
storeURL,
builder: this,
});
}
return this;
}
Sets the supplier's Amazon storefront URL. Counterpart to
ProductBuilder.setSupplierEbayStoreURL, for suppliers that accept "amazononly".
The storefront URL, or any value (anything that isn't an absolute http(s) URL is ignored)
The builder instance for method chaining
builder.setSupplierAmazonStoreURL("https://www.amazon.com/s?k=HiMedia");
setSupplierAmazonStoreURL(storeURL: unknown): ProductBuilder<T> {
const href = this.absoluteHttpURL(storeURL);
if (href) {
this.product.supplierAmazonStoreURL = href;
} else if (storeURL != null && this.showFailedValidation) {
this.logger.warn('setSupplierAmazonStoreURL| Invalid Amazon store URL', {
storeURL,
builder: this,
});
}
return this;
}
Sets the product description.
The detailed description of the product, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setDescription(
'High purity sodium chloride, 99.9% pure, suitable for laboratory use'
);
setDescription(description: unknown): ProductBuilder<T> {
if (typeof description === 'string' && description.trim().length > 0) {
this.product.description = htmlToAscii(description);
} else if (description != null && this.showFailedValidation) {
this.logger.warn('setDescription| Invalid description', { description, builder: this });
}
return this;
}
Sets the short description for the product.
The short description to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setShortDescription("Sodium Chloride 500g");
setShortDescription(shortDescription: unknown): ProductBuilder<T> {
if (typeof shortDescription === 'string' && shortDescription.trim().length > 0) {
this.product.shortDescription = htmlToAscii(shortDescription);
} else if (shortDescription != null && this.showFailedValidation) {
this.logger.warn('setShortDescription| Invalid short description', {
shortDescription,
builder: this,
});
}
return this;
}
Sets the rating for the product.
The rating to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setRating(4.5);
setRating(rating: unknown): ProductBuilder<T> {
if (typeof rating === 'number' || typeof rating === 'string') {
const value = Number(rating);
if (!Number.isNaN(value)) {
this.product.rating = value;
}
} else if (rating != null && this.showFailedValidation) {
this.logger.warn('setRating| Invalid rating', { rating, builder: this });
}
return this;
}
Sets the review count for the product.
The review count to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setReviewCount(100);
setReviewCount(reviewCount: unknown): ProductBuilder<T> {
if (typeof reviewCount === 'number' || typeof reviewCount === 'string') {
const value = Number(reviewCount);
if (!Number.isNaN(value)) {
this.product.reviewCount = value;
}
} else if (reviewCount != null) {
this.logger.warn('setReviewCount| Invalid review count', { reviewCount, builder: this });
}
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", or any value (invalid input is ignored)
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: unknown): ProductBuilder<T> {
if (typeof cas !== 'string') {
// Only flag a genuine misuse (a non-string value); an absent value is a quiet no-op.
if (cas != null) {
this.logger.warn(`setCAS| Invalid CAS number`, {
cas,
builder: this,
});
}
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.
The unique identifier for the product, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setID(12345);
setID(id: unknown): ProductBuilder<T> {
if (typeof id === 'number' || typeof id === 'string') {
this.product.id = id;
} else if (id != null && this.showFailedValidation) {
this.logger.warn('setID| Invalid ID value', { id, builder: this });
}
return this;
}
Sets the UUID for the product.
The UUID string for the product, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setUUID('550e8400-e29b-41d4-a716-446655440000');
setUUID(uuid: unknown): ProductBuilder<T> {
if (typeof uuid === 'string' && uuid.trim().length > 0) {
this.product.uuid = uuid;
} else if (uuid != null && this.showFailedValidation) {
this.logger.warn('setUUID| Invalid UUID value', { uuid, builder: this });
}
return this;
}
Sets the SKU (Stock Keeping Unit) for the product.
The SKU string for the product, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setSku('CHEM-NaCl-500G');
setSku(sku: unknown): ProductBuilder<T> {
if (typeof sku === 'string' && sku.trim().length > 0) {
this.product.sku = sku;
} else if (sku != null && this.showFailedValidation) {
this.logger.warn('setSku| Invalid SKU value', { sku, builder: this });
}
return this;
}
Sets the stable per-product cache/exclusion identity (see Product.cacheKey). Accepts a non-empty string or a number (coerced to string, since some suppliers key on a numeric id). Invalid input is ignored.
The identity string/number, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setCacheKey("FAM_889460");
builder.setCacheKey(12345); // stored as "12345"
setCacheKey(cacheKey: unknown): ProductBuilder<T> {
if (typeof cacheKey === 'string' && cacheKey.trim().length > 0) {
this.product.cacheKey = cacheKey;
} else if (typeof cacheKey === 'number' && Number.isFinite(cacheKey)) {
this.product.cacheKey = String(cacheKey);
} else if (cacheKey != null && this.showFailedValidation) {
this.logger.warn('setCacheKey| Invalid cache key value', { cacheKey, builder: this });
}
return this;
}
Sets the SMILES for the product. An absent value (undefined/null/empty) is ignored silently; a value that is present but fails SMILES validation is ignored and logged as a warning.
The SMILES to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setSmiles("C1=CC=CC=C1");
builder.setSmiles(undefined); // no-op, no warning
setSmiles(smiles: unknown): ProductBuilder<T> {
if (isSmiles(smiles)) {
this.product.smiles = smiles;
} else if (
smiles !== undefined &&
smiles !== null &&
smiles !== '' &&
this.showFailedValidation
) {
this.logger.warn('setSmiles| Invalid SMILES', {
smiles,
builder: this,
});
}
return this;
}
Sets the IUPAC name for the product. An absent value (undefined/null/empty) is ignored silently; a value that is present but not a usable name is ignored and logged as a warning.
The IUPAC name to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setIupacName("dipotassium;oxalate");
builder.setIupacName(undefined); // no-op, no warning
setIupacName(iupacName: unknown): ProductBuilder<T> {
if (isIupacName(iupacName)) {
this.product.iupacName = iupacName;
} else if (
iupacName !== undefined &&
iupacName !== null &&
iupacName !== '' &&
this.showFailedValidation
) {
this.logger.warn('setIupacName| Invalid IUPAC name', {
iupacName,
builder: this,
});
}
return this;
}
Sets the PubChem Compound ID (CID) for the product. Numeric strings are coerced first; an absent value (undefined/null/empty) is ignored silently; a value that is present but not a positive integer is ignored and logged as a warning.
The PubChem CID to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setPubchemId(11413);
builder.setPubchemId("11413"); // coerced to 11413
setPubchemId(pubchemId: unknown): ProductBuilder<T> {
if (isPubChemCID(pubchemId)) {
this.product.pubchemId = toPubChemCID(pubchemId);
} else if (
pubchemId !== undefined &&
pubchemId !== null &&
pubchemId !== '' &&
this.showFailedValidation
) {
this.logger.warn('setPubchemId| Invalid PubChem CID', {
pubchemId,
builder: this,
});
}
return this;
}
Sets the InChIKey for the product. An absent value (undefined/null/empty) is ignored silently; a value that is present but fails InChIKey validation is ignored and logged as a warning.
The InChIKey to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setInChIKey("IRXRGVFLQOSHOH-UHFFFAOYSA-L");
builder.setInChIKey(undefined); // no-op, no warning
setInChIKey(inchiKey: unknown): ProductBuilder<T> {
if (isInChIKey(inchiKey)) {
this.product.inchiKey = inchiKey;
} else if (
inchiKey !== undefined &&
inchiKey !== null &&
inchiKey !== '' &&
this.showFailedValidation
) {
this.logger.warn('setInChIKey| Invalid InChIKey', {
inchiKey,
builder: this,
});
}
return this;
}
Sets the InChI string for the product. An absent value (undefined/null/empty) is ignored silently; a value that is present but fails InChI validation is ignored and logged as a warning.
The InChI string to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setInChI("1S/C2H2O4.2K/c3-1(4)2(5)6;;/h(H,3,4)(H,5,6);;/q;2*+1/p-2");
builder.setInChI(undefined); // no-op, no warning
setInChI(inchi: unknown): ProductBuilder<T> {
if (isInChI(inchi)) {
this.product.inchi = inchi;
} else if (inchi !== undefined && inchi !== null && inchi !== '' && this.showFailedValidation) {
this.logger.warn('setInChI| Invalid InChI', {
inchi,
builder: this,
});
}
return this;
}
Sets the vendor for the product.
The vendor name, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setVendor('Vendor Name');
setVendor(vendor: unknown): ProductBuilder<T> {
if (typeof vendor === 'string' && vendor.trim().length > 0) {
this.product.vendor = vendor;
} else if (vendor != null && this.showFailedValidation) {
this.logger.warn('setVendor| Invalid vendor value', { vendor, builder: this });
}
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)) {
// isAvailability accepts strings case-insensitively, so normalize to the
// canonical lowercase enum member rather than storing the raw input
// (e.g. schema.org's "PreOrder" -> "preorder").
if (typeof availability === 'string') {
const normalized = availability.toLowerCase();
return isAvailability(normalized) ? normalized : 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 'limitedstock':
case 'limitedavailability':
return AVAILABILITY.LIMITED_STOCK;
case 'unavailable':
return AVAILABILITY.UNAVAILABLE;
case 'outofstock':
return AVAILABILITY.OUT_OF_STOCK;
case 'soldout':
return AVAILABILITY.SOLD_OUT;
case 'preorder':
return AVAILABILITY.PRE_ORDER;
case 'presale':
return AVAILABILITY.PRE_SALE;
case 'madetoorder':
return AVAILABILITY.MADE_TO_ORDER;
case 'backorder':
return AVAILABILITY.BACKORDER;
case 'reserved':
return AVAILABILITY.RESERVED;
case 'onlineonly':
return AVAILABILITY.ONLINE_ONLY;
case 'instoreonly':
return AVAILABILITY.IN_STORE_ONLY;
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>;
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;
}
Sets the purity for the product, stored as a string. A number in the (0, 100] range is
normalized to "<n>%". A string is resolved via findPurity: a percentage token (with an
optional comparator <, >, ≤, ≥, ≈) is kept when present, otherwise a recognized
chemical grade ("ACS", "HPLC", …) is used — so purity may hold either kind of value. Input
that yields neither is ignored.
The purity to set (e.g. 99, "≥99%", "ACS reagent"), or any value (invalid input is ignored)
The builder instance for method chaining
builder.setPurity(98); // stored as "98%"
builder.setPurity("≥99.995% metals basis"); // stored as "≥99.995%"
builder.setPurity("≥98%(HPLC)"); // stored as "≥98%" (percentage wins)
builder.setPurity("60% in Water"); // stored as "60%"
builder.setPurity("ACS reagent"); // stored as "ACS" (grade fallback)
builder.setPurity("Ships in 3 days"); // ignored
setPurity(purity: unknown): ProductBuilder<T> {
let value: string | undefined;
if (typeof purity === 'number') {
if (!Number.isNaN(purity) && purity > 0 && purity <= 100) {
value = `${purity}%`;
}
} else if (typeof purity === 'string') {
value = findPurity(purity);
}
if (value !== undefined) {
this.product.purity = value;
}
return this;
}
Sets the concentration for the product.
The concentration to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setConcentration("98%");
builder.setConcentration("98.5%");
builder.setConcentration("99%");
builder.setConcentration("99.5%");
builder.setConcentration("100%");
builder.setConcentration("1M");
setConcentration(concentration: unknown): ProductBuilder<T> {
if (typeof concentration === 'string' && concentration.trim().length > 0) {
this.product.concentration = concentration;
}
return this;
}
Sets the molecular weight for the product. A value that doesn't resolve to a positive number is ignored.
The molecular weight to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setMoleweight(100.00);
setMoleweight(moleweight: unknown): ProductBuilder<T> {
if (typeof moleweight === 'string') {
moleweight = Number(moleweight);
}
if (typeof moleweight === 'number' && !Number.isNaN(moleweight) && moleweight > 0) {
this.product.moleweight = moleweight;
}
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];
}
return;
}
Sets the match percentage (Levenshtein result) for the product title compared to the search string.
The match percentage to set, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setMatchPercentage(95);
setMatchPercentage(matchPercentage: unknown): ProductBuilder<T> {
if (typeof matchPercentage === 'number' && !Number.isNaN(matchPercentage)) {
this.product.matchPercentage = matchPercentage;
}
return this;
}
Sets the manufacturer name. A value that isn't a non-empty string is ignored.
The manufacturer name, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setManufacturer("Sigma-Aldrich");
setManufacturer(manufacturer: unknown): ProductBuilder<T> {
if (typeof manufacturer === 'string' && manufacturer.trim().length > 0) {
this.product.manufacturer = manufacturer;
}
return this;
}
Sets the status code of the product. A value that isn't a non-empty string is ignored.
The status code, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setStatus("IN_STOCK");
setStatus(status: unknown): ProductBuilder<T> {
if (typeof status === 'string' && status.trim().length > 0) {
this.product.status = status;
}
return this;
}
Sets the human-readable status text of the product. A non-string value is ignored.
The status description, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setStatusTxt("In Stock");
setStatusTxt(statusTxt: unknown): ProductBuilder<T> {
if (typeof statusTxt === 'string' && statusTxt.trim().length > 0) {
this.product.statusTxt = statusTxt;
}
return this;
}
Sets special shipping information for the product. A non-string value is ignored.
The shipping note, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setShippingInformation("Hazardous material - special shipping required");
setShippingInformation(shippingInformation: unknown): ProductBuilder<T> {
if (typeof shippingInformation === 'string' && shippingInformation.trim().length > 0) {
this.product.shippingInformation = shippingInformation;
}
return this;
}
Sets the parsed purchase restrictions for the product. Accepts an object shaped like
PurchaseRestriction and keeps only the recognized, well-typed fields
(excludedCountries is filtered to valid country codes); anything else is dropped.
The restriction is stored only when at least one meaningful field survives.
The purchase restriction object, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setPurchaseRestriction({ excludedCountries: ["US", "DE"], buyerRestricted: true });
setPurchaseRestriction(restriction: unknown): ProductBuilder<T> {
if (typeof restriction !== 'object' || restriction === null) {
return this;
}
const result: PurchaseRestriction = {};
if ('excludedCountries' in restriction && Array.isArray(restriction.excludedCountries)) {
const codes = restriction.excludedCountries.filter(isCountryCode);
if (codes.length > 0) {
result.excludedCountries = codes;
}
}
if ('euOnly' in restriction && restriction.euOnly === true) result.euOnly = true;
if ('restrictedDelivery' in restriction && restriction.restrictedDelivery === true) {
result.restrictedDelivery = true;
}
if ('buyerRestricted' in restriction && restriction.buyerRestricted === true) {
result.buyerRestricted = true;
}
if (
'declarationOfUseRequired' in restriction &&
restriction.declarationOfUseRequired === true
) {
result.declarationOfUseRequired = true;
}
if ('note' in restriction && typeof restriction.note === 'string' && restriction.note.trim()) {
result.note = restriction.note.trim();
}
if (Object.keys(result).length > 0) {
this.product.purchaseRestriction = result;
}
return this;
}
Sets the product attributes. Accepts an array and keeps only entries shaped like
{ name: string; value: string }; anything else is dropped.
The attributes array, or any value (invalid entries are dropped)
The builder instance for method chaining
builder.setAttributes([{ name: "Size", value: "500g" }]);
setAttributes(attributes: unknown): ProductBuilder<T> {
if (!Array.isArray(attributes)) {
return this;
}
const valid = attributes.filter(
(entry): entry is { name: string; value: string } =>
typeof entry === 'object' &&
entry !== null &&
'name' in entry &&
typeof entry.name === 'string' &&
'value' in entry &&
typeof entry.value === 'string',
);
if (valid.length > 0) {
this.product.attributes = valid;
}
return this;
}
Sets the reference (base) quantity used for unit conversions. A value that doesn't resolve to a positive number is ignored.
The base quantity, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setBaseQuantity(500);
setBaseQuantity(baseQuantity: unknown): ProductBuilder<T> {
const value = typeof baseQuantity === 'string' ? Number(baseQuantity) : baseQuantity;
if (typeof value === 'number' && !Number.isNaN(value) && value > 0) {
this.product.baseQuantity = value;
}
return this;
}
Sets the reference (base) unit of measure used for conversions. Anything that isn't a known UOM is ignored.
The base unit of measure, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setBaseUom("g");
setBaseUom(baseUom: unknown): ProductBuilder<T> {
if (isUOM(baseUom)) {
this.product.baseUom = baseUom;
} else if (baseUom != null) {
this.logger.warn('Invalid base UOM', { baseUom, builder: this });
}
return this;
}
Sets the price converted to USD. A value that doesn't resolve to a non-negative number is ignored. Note: build recomputes this from price/currency, so it is normally derived.
The USD price, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setUsdPrice(19.99);
setUsdPrice(usdPrice: unknown): ProductBuilder<T> {
const value = typeof usdPrice === 'string' ? Number(usdPrice) : usdPrice;
if (typeof value === 'number' && !Number.isNaN(value) && value >= 0) {
this.product.usdPrice = value;
} else if (usdPrice != null) {
this.logger.warn('Invalid USD price', { usdPrice, builder: this });
}
return this;
}
Sets the price converted to the user's local currency. A value that doesn't resolve to a non-negative number is ignored.
The local price, or any value (invalid input is ignored)
The builder instance for method chaining
builder.setLocalPrice(18.5);
setLocalPrice(localPrice: unknown): ProductBuilder<T> {
const value = typeof localPrice === 'string' ? Number(localPrice) : localPrice;
if (typeof value === 'number' && !Number.isNaN(value) && value >= 0) {
this.product.localPrice = value;
} else if (localPrice != null) {
this.logger.warn('Invalid local price', { localPrice, builder: this });
}
return this;
}
Sets related documentation links (MSDS, SDS, etc.). Accepts an array and keeps only the non-empty string entries; anything else is dropped.
The documentation URLs, or any value (invalid entries are dropped)
The builder instance for method chaining
builder.setDocLinks(["https://supplier.com/msds/nacl.pdf"]);
setDocLinks(docLinks: unknown): ProductBuilder<T> {
if (!Array.isArray(docLinks)) {
return this;
}
const valid = docLinks.filter(
(link): link is string => typeof link === 'string' && link.trim().length > 0,
);
if (valid.length > 0) {
this.product.docLinks = valid;
}
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 {
if (URL.canParse(path)) {
return String(path);
}
const urlObj = new URL(path, this.baseURL);
return String(urlObj);
}
PrivateresolveResolves an unknown value to an absolute URL string, or undefined when it isn't URL-like.
Lets the URL setters accept the often-optional output of a parser (e.g. findPdfHref)
directly, without each caller having to null-check first.
The candidate URL (string or URL), or anything else
The absolute URL as a string, or undefined if the value isn't a usable URL
this.resolveHref("/sds.pdf"); // "https://example.com/sds.pdf"
this.resolveHref(undefined); // undefined
private resolveHref(value: unknown): string | undefined {
if (value instanceof URL) {
return this.href(value);
}
if (typeof value === 'string' && value.trim().length > 0) {
return this.href(value);
}
return undefined;
}
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);
}
// Default the variant's human-facing permalink to its own permalink,
// then its processing URL, then the parent product's permalink/URL.
const variantPermalink =
variant.permalink ?? variant.url ?? this.product.permalink ?? this.product.url;
if (variantPermalink) {
variant.permalink = this.href(variantPermalink);
}
// Re-populate the variant using the parent product properties as defaults and the current
// values as overrides.
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);
// Human-facing permalink defaults to the processing URL when a supplier
// didn't set one (the common case for scraped suppliers).
this.product.permalink = this.href(this.product.permalink ?? this.product.url);
this.logger.debug('ProductBuilder| Built product:', { product: this.product, builder: this });
// isProduct() above narrows to the base Product; T is the caller's concrete subtype of Product.
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);
// Cached .dump() output is opaque unknown[]; no runtime shape exists for the generic Partial<T>,
// and build() re-validates the result via isProduct() before use.
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