@asteby/storefront 0.17.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -124,9 +124,14 @@ function appendParams(search, params) {
124
124
  return search;
125
125
  }
126
126
  function productListQuery(params = {}) {
127
- const { filters, handles, ...rest } = params;
127
+ const { filters, handles, tags_any, ...rest } = params;
128
128
  const search = appendParams(new URLSearchParams(), rest);
129
129
  if (handles && handles.length > 0) search.set("handles", handles.join(","));
130
+ if (tags_any && tags_any.length > 0) {
131
+ if (tags_any.length > 5) throw new RangeError("tags_any takes at most 5 tags");
132
+ if (tags_any.some((t) => t.includes(",") || t.length > 120)) throw new TypeError("tags_any: a tag has at most 120 characters and no comma");
133
+ search.set("tags_any", tags_any.join(","));
134
+ }
130
135
  if (filters) {
131
136
  for (const [key, raw] of Object.entries(filters)) {
132
137
  const name = key.startsWith("filter.") ? key : `filter.${key}`;
@@ -573,6 +578,19 @@ function createStorefrontClient(config) {
573
578
  /** Physical delete (a profile leaves nothing behind). A saved default passes to another profile. */
574
579
  delete: (id, options) => write("DELETE", `/storefront/customer/fiscal-profiles/${enc(id)}`, void 0, options)
575
580
  },
581
+ /**
582
+ * Saved items of the buyer («My items», «My garage»; contract 1.9, `capabilities().subjects`). Personal data:
583
+ * private, never cached, never logged. `422` with `field: 'attributes.<field>'` when the kind refuses a value
584
+ * (it never echoes it); `409` for an item already saved or past the limit; `429` for too many saves; `404` for
585
+ * an item that is not the buyer's. The server builds `label` and `fit_key`; private fields come back masked.
586
+ */
587
+ subjects: {
588
+ list: async (params, options) => (await get("/storefront/customer/subjects", params, { ...options, cache: "no-store" })).nodes,
589
+ create: (input, options) => write("POST", "/storefront/customer/subjects", input, options),
590
+ update: (id, patch, options) => write("PATCH", `/storefront/customer/subjects/${enc(id)}`, patch, options),
591
+ /** Physical delete (the item leaves nothing behind). The default of its kind passes to another item. */
592
+ delete: (id, options) => write("DELETE", `/storefront/customer/subjects/${enc(id)}`, void 0, options)
593
+ },
576
594
  addresses: {
577
595
  list: (options) => get("/storefront/customer/addresses", void 0, options),
578
596
  create: (address, options) => write("POST", "/storefront/customer/addresses", address, options),
@@ -581,6 +599,11 @@ function createStorefrontClient(config) {
581
599
  }
582
600
  },
583
601
  content: {
602
+ /**
603
+ * The kinds of buyer item the organization declares («My garage») and their catalogs (contract 1.9; public,
604
+ * cacheable). `[]` when it declares none or the server predates the route: the store has no such section.
605
+ */
606
+ subjectKinds: async (options) => (await getOrNull("/storefront/subject-kinds", void 0, options))?.nodes ?? [],
584
607
  theme: (options) => get("/storefront/content/theme", void 0, options),
585
608
  /** `null` when the page does not exist. */
586
609
  page: (handle, options) => getOrNull(`/storefront/content/pages/${enc(handle)}`, previewQuery(options), options),
@@ -1538,6 +1561,199 @@ function safeInvoiceFilename(name, fallback = "factura") {
1538
1561
  return clean || fallback;
1539
1562
  }
1540
1563
 
1564
+ // src/subjects.ts
1565
+ var MIN_SUBJECT_YEAR = 1900;
1566
+ var MAX_SUBJECT_YEAR = 2100;
1567
+ var MAX_VALUE_LENGTH = 80;
1568
+ var MAX_LABEL_LENGTH = 120;
1569
+ var MAX_FIT_KEY_LENGTH = 100;
1570
+ var isPresent = (v) => v !== null && v !== void 0 && v !== "";
1571
+ var asYear = (v) => {
1572
+ const n = typeof v === "number" ? v : typeof v === "string" && /^\s*\d+\s*$/.test(v) ? Number(v) : NaN;
1573
+ return Number.isInteger(n) && n >= MIN_SUBJECT_YEAR && n <= MAX_SUBJECT_YEAR ? n : null;
1574
+ };
1575
+ var cleanText = (v, max) => {
1576
+ if (typeof v !== "string") return null;
1577
+ const s = v.split(/\s+/).filter(Boolean).join(" ");
1578
+ return [...s].length > max || /[\u0000-\u001f\u007f-\u009f]/.test(s) ? null : s;
1579
+ };
1580
+ var slug = (s) => s.toLowerCase().replace(/[^a-z0-9._+-]+/g, "-").replace(/^-+|-+$/g, "");
1581
+ function coversYear(node, year) {
1582
+ if (node.ranges && node.ranges.length > 0) return node.ranges.some(([a, b]) => year >= a && year <= b);
1583
+ if (node.from !== void 0 && year < node.from) return false;
1584
+ if (node.to !== void 0 && year > node.to) return false;
1585
+ return true;
1586
+ }
1587
+ var declaresYears = (n) => n.from !== void 0 || n.to !== void 0 || (n.ranges?.length ?? 0) > 0;
1588
+ function subjectLevels(kind) {
1589
+ return kind.fields.filter((f) => f.source === "catalog" && f.type !== "year");
1590
+ }
1591
+ function subjectFields(kind) {
1592
+ return kind.fields;
1593
+ }
1594
+ function walk(kind, values) {
1595
+ const path = [];
1596
+ const ids = {};
1597
+ let tree = kind.catalog?.tree;
1598
+ for (const f of subjectLevels(kind)) {
1599
+ const raw = values[f.key];
1600
+ const s = isPresent(raw) ? cleanText(raw, MAX_VALUE_LENGTH) : null;
1601
+ const node = s && tree ? tree[s] ?? tree[s.toLowerCase()] : void 0;
1602
+ if (!s || !node) return { path, ids, next: tree };
1603
+ ids[f.key] = tree && tree[s] ? s : s.toLowerCase();
1604
+ path.push(node);
1605
+ tree = node.children;
1606
+ if (!tree) return { path, ids, next: void 0 };
1607
+ }
1608
+ return { path, ids, next: tree };
1609
+ }
1610
+ var byLabel = (a, b) => a.label.localeCompare(b.label, void 0, { sensitivity: "base", numeric: true });
1611
+ function subjectOptions(kind, fieldKey, values) {
1612
+ const field = kind.fields.find((f) => f.key === fieldKey);
1613
+ if (!field || field.source !== "catalog" || !kind.catalog) return [];
1614
+ if (field.type === "year") {
1615
+ const upto = kind.fields.slice(0, kind.fields.indexOf(field));
1616
+ const { path: path2 } = walk({ ...kind, fields: upto }, values);
1617
+ const declared = path2.filter(declaresYears);
1618
+ if (declared.length === 0) return [];
1619
+ let lo = Infinity;
1620
+ let hi = -Infinity;
1621
+ for (const n of declared) {
1622
+ for (const [a, b] of n.ranges?.length ? n.ranges : [[n.from ?? MIN_SUBJECT_YEAR, n.to ?? MAX_SUBJECT_YEAR]]) {
1623
+ lo = Math.min(lo, a);
1624
+ hi = Math.max(hi, b);
1625
+ }
1626
+ }
1627
+ const out = [];
1628
+ for (let y = Math.min(hi, MAX_SUBJECT_YEAR); y >= Math.max(lo, MIN_SUBJECT_YEAR); y--) if (declared.every((n) => coversYear(n, y))) out.push({ value: String(y), label: String(y) });
1629
+ return out;
1630
+ }
1631
+ const levels = subjectLevels(kind);
1632
+ const at = levels.findIndex((f) => f.key === fieldKey);
1633
+ if (at < 0) return [];
1634
+ const before = Object.fromEntries(levels.slice(0, at).map((f) => [f.key, values[f.key]]));
1635
+ const { path, next } = walk({ ...kind, fields: kind.fields.filter((f) => levels.slice(0, at).includes(f) || f.type === "year") }, before);
1636
+ if (path.length < at || !next) return [];
1637
+ const year = asYear(values[kind.fields.find((f) => f.type === "year" && f.source === "catalog")?.key ?? ""]);
1638
+ return Object.entries(next).filter(([, n]) => year === null || coversYear(n, year)).map(([value, n]) => ({ value, label: n.label })).sort(byLabel);
1639
+ }
1640
+ function reconcileSubjectValues(kind, values) {
1641
+ const out = { ...values };
1642
+ for (const f of kind.fields) {
1643
+ if (f.source !== "catalog" || !isPresent(out[f.key])) continue;
1644
+ const options = subjectOptions(kind, f.key, out);
1645
+ const v = String(out[f.key]);
1646
+ const ok = f.type === "year" ? options.length === 0 || options.some((o) => o.value === v) : options.some((o) => o.value === v || o.value === v.toLowerCase());
1647
+ if (!ok) out[f.key] = "";
1648
+ }
1649
+ return out;
1650
+ }
1651
+ function resolveSubject(kind, values) {
1652
+ const problems = {};
1653
+ const known = new Set(kind.fields.map((f) => f.key));
1654
+ for (const key2 of Object.keys(values)) if (isPresent(values[key2]) && !known.has(key2)) problems[key2] = "unknown";
1655
+ const attributes = {};
1656
+ const priv = {};
1657
+ const display = {};
1658
+ const ids = {};
1659
+ const path = [];
1660
+ let tree = kind.catalog?.tree;
1661
+ const years = [];
1662
+ const put = (f, v) => {
1663
+ if (f.private) priv[f.key] = String(v);
1664
+ else attributes[f.key] = v;
1665
+ };
1666
+ for (const f of kind.fields) {
1667
+ const raw = values[f.key];
1668
+ if (!isPresent(raw)) {
1669
+ if (f.required) problems[f.key] = "required";
1670
+ if (f.source === "catalog" && f.type !== "year") tree = void 0;
1671
+ continue;
1672
+ }
1673
+ const max = f.max ?? MAX_VALUE_LENGTH;
1674
+ if (f.type === "year") {
1675
+ const y = asYear(raw);
1676
+ if (y === null) {
1677
+ problems[f.key] = "invalid";
1678
+ continue;
1679
+ }
1680
+ put(f, y);
1681
+ display[f.key] = ids[f.key] = String(y);
1682
+ if (f.source === "catalog") years.push(f.key);
1683
+ } else if (f.source === "catalog") {
1684
+ if (!kind.catalog) {
1685
+ problems[f.key] = "catalog_unavailable";
1686
+ continue;
1687
+ }
1688
+ const s = cleanText(raw, max);
1689
+ if (s === null) {
1690
+ problems[f.key] = "invalid";
1691
+ continue;
1692
+ }
1693
+ const node = tree?.[s] ?? tree?.[s.toLowerCase()];
1694
+ if (!node) {
1695
+ problems[f.key] = "not_in_catalog";
1696
+ continue;
1697
+ }
1698
+ const id = tree?.[s] ? s : s.toLowerCase();
1699
+ path.push(node);
1700
+ tree = node.children;
1701
+ put(f, id);
1702
+ display[f.key] = node.label;
1703
+ ids[f.key] = id;
1704
+ } else {
1705
+ const s = cleanText(raw, max);
1706
+ if (s === null) {
1707
+ problems[f.key] = "invalid";
1708
+ continue;
1709
+ }
1710
+ put(f, s);
1711
+ display[f.key] = s;
1712
+ ids[f.key] = slug(s);
1713
+ }
1714
+ }
1715
+ for (const key2 of years) {
1716
+ const y = attributes[key2];
1717
+ if (y !== void 0 && !problems[key2] && !path.every((n) => coversYear(n, y))) problems[key2] = "year_range";
1718
+ }
1719
+ if (Object.keys(problems).length > 0) return { ok: false, problems };
1720
+ const label = [...kind.fields.filter((f) => !f.private).map((f) => display[f.key]).filter(Boolean).join(" ")].slice(0, MAX_LABEL_LENGTH).join("").trim();
1721
+ if (!label) return { ok: false, problems: { _: "required" } };
1722
+ let complete = true;
1723
+ const key = kind.fit_key_template.replace(/\{([a-z][a-z0-9_]*)\}/g, (_m, name) => {
1724
+ const id = ids[name];
1725
+ if (!id) complete = false;
1726
+ return id ?? "";
1727
+ });
1728
+ return { ok: true, subject: { label, fit_key: complete && key.length <= MAX_FIT_KEY_LENGTH ? key.toLowerCase() : "", attributes, private: priv } };
1729
+ }
1730
+ var PRIVATE_MASK = "\u2022\u2022";
1731
+ var isMasked = (v) => typeof v === "string" && v.startsWith(PRIVATE_MASK);
1732
+ var DEFAULT_FIT_TAG_PREFIX = "fit:";
1733
+ var fitTag = (fitKey, prefix = DEFAULT_FIT_TAG_PREFIX) => `${prefix}${fitKey}`;
1734
+ function fitTagsAny(fitKey, opts = {}) {
1735
+ if (!fitKey) return [];
1736
+ const prefix = opts.prefix ?? DEFAULT_FIT_TAG_PREFIX;
1737
+ return opts.universal === false ? [fitTag(fitKey, prefix)] : [fitTag(fitKey, prefix), `${prefix}universal`];
1738
+ }
1739
+ var SUBJECT_CATALOG_PAGE_BYTES = 28 * 1024;
1740
+ var utf8Bytes = (s) => new TextEncoder().encode(s).length;
1741
+ function splitSubjectTree(tree, maxBytes = SUBJECT_CATALOG_PAGE_BYTES) {
1742
+ const size = (part) => utf8Bytes(JSON.stringify(JSON.stringify(part)));
1743
+ const parts = [];
1744
+ let current = {};
1745
+ for (const [id, node] of Object.entries(tree)) {
1746
+ if (size({ [id]: node }) > maxBytes) throw new RangeError(`splitSubjectTree: option ${id} alone exceeds ${maxBytes} bytes`);
1747
+ const next = { ...current, [id]: node };
1748
+ if (Object.keys(current).length > 0 && size(next) > maxBytes) {
1749
+ parts.push(current);
1750
+ current = { [id]: node };
1751
+ } else current = next;
1752
+ }
1753
+ if (Object.keys(current).length > 0) parts.push(current);
1754
+ return parts;
1755
+ }
1756
+
1541
1757
  // src/delivery.ts
1542
1758
  function parseDelivery(value) {
1543
1759
  const raw = (value ?? "").trim();
@@ -1566,11 +1782,16 @@ function deliveryNotices(delivery) {
1566
1782
 
1567
1783
  exports.CLIENT_IP_HEADER = CLIENT_IP_HEADER;
1568
1784
  exports.COUNTRY_HEADERS = COUNTRY_HEADERS;
1785
+ exports.DEFAULT_FIT_TAG_PREFIX = DEFAULT_FIT_TAG_PREFIX;
1786
+ exports.MAX_SUBJECT_YEAR = MAX_SUBJECT_YEAR;
1787
+ exports.MIN_SUBJECT_YEAR = MIN_SUBJECT_YEAR;
1569
1788
  exports.MISSING_WEIGHT = MISSING_WEIGHT;
1570
1789
  exports.NOTE_MAX_LENGTH = NOTE_MAX_LENGTH;
1571
1790
  exports.PAYMENT_CHOICE_REFUSED_FIELD = PAYMENT_CHOICE_REFUSED_FIELD;
1572
1791
  exports.PICKUP_STEPS = PICKUP_STEPS;
1792
+ exports.PRIVATE_MASK = PRIVATE_MASK;
1573
1793
  exports.SHIPPING_STEPS = SHIPPING_STEPS;
1794
+ exports.SUBJECT_CATALOG_PAGE_BYTES = SUBJECT_CATALOG_PAGE_BYTES;
1574
1795
  exports.StorefrontError = StorefrontError;
1575
1796
  exports.appendParams = appendParams;
1576
1797
  exports.availabilityLocale = availabilityLocale;
@@ -1579,6 +1800,7 @@ exports.availabilityView = availabilityView;
1579
1800
  exports.cartLineProblemView = cartLineProblemView;
1580
1801
  exports.clampPercent = clampPercent;
1581
1802
  exports.classifyPaymentError = classifyPaymentError;
1803
+ exports.coversYear = coversYear;
1582
1804
  exports.createStorefrontClient = createStorefrontClient;
1583
1805
  exports.dataExportBytes = dataExportBytes;
1584
1806
  exports.declineMessage = declineMessage;
@@ -1586,6 +1808,8 @@ exports.declineMessageKey = declineMessageKey;
1586
1808
  exports.deliveryInput = deliveryInput;
1587
1809
  exports.deliveryNotices = deliveryNotices;
1588
1810
  exports.diagnose = diagnose;
1811
+ exports.fitTag = fitTag;
1812
+ exports.fitTagsAny = fitTagsAny;
1589
1813
  exports.formatAvailabilityMessage = formatAvailabilityMessage;
1590
1814
  exports.formatMoney = formatMoney;
1591
1815
  exports.formatVoucherExpiry = formatVoucherExpiry;
@@ -1594,6 +1818,7 @@ exports.groupInvoiceDocs = groupInvoiceDocs;
1594
1818
  exports.hreflangLinks = hreflangLinks;
1595
1819
  exports.invoiceFileBytes = invoiceFileBytes;
1596
1820
  exports.isAccountDeleteBlocked = isAccountDeleteBlocked;
1821
+ exports.isMasked = isMasked;
1597
1822
  exports.isReauthRequired = isReauthRequired;
1598
1823
  exports.isStorefrontError = isStorefrontError;
1599
1824
  exports.isTestInvoice = isTestInvoice;
@@ -1619,8 +1844,10 @@ exports.productAvailability = productAvailability;
1619
1844
  exports.productListQuery = productListQuery;
1620
1845
  exports.promoProgressView = promoProgressView;
1621
1846
  exports.promotionMessages = promotionMessages;
1847
+ exports.reconcileSubjectValues = reconcileSubjectValues;
1622
1848
  exports.repairPath = repairPath;
1623
1849
  exports.resolveAvailability = resolveAvailability;
1850
+ exports.resolveSubject = resolveSubject;
1624
1851
  exports.rewardLabel = rewardLabel;
1625
1852
  exports.safeExportFilename = safeExportFilename;
1626
1853
  exports.safeHttpsUrl = safeHttpsUrl;
@@ -1629,7 +1856,11 @@ exports.safeTrackingUrl = safeTrackingUrl;
1629
1856
  exports.schemaAvailability = schemaAvailability;
1630
1857
  exports.shipmentsStatus = shipmentsStatus;
1631
1858
  exports.sortTrackingEvents = sortTrackingEvents;
1859
+ exports.splitSubjectTree = splitSubjectTree;
1632
1860
  exports.storefrontDomainHeader = storefrontDomainHeader;
1861
+ exports.subjectFields = subjectFields;
1862
+ exports.subjectLevels = subjectLevels;
1863
+ exports.subjectOptions = subjectOptions;
1633
1864
  exports.suggestMarket = suggestMarket;
1634
1865
  exports.toMoney = toMoney;
1635
1866
  exports.visitorCountry = visitorCountry;