@tokenoftrust/storefront-runner 2.2.105 → 2.2.106

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.
@@ -18,8 +18,9 @@
18
18
 
19
19
  export const CATALOG_MODEL = {
20
20
  ProductStatus: { alias: '"active" | "draft" | "archived"' },
21
+ ProductVisibility: { alias: '"public" | "unlisted" | "hidden"' },
21
22
  Currency: { alias: "string" },
22
- SourcePlatform: { alias: '"shopify" | "directory" | "csv" | "manual" | "synthesized"' },
23
+ SourcePlatform: { alias: '"shopify" | "bigcommerce" | "directory" | "csv" | "manual" | "synthesized"' },
23
24
  SourceRef: {
24
25
  fields: { platform: "SourcePlatform", source_id: "string", "source_handle?": "string" },
25
26
  },
@@ -81,10 +82,15 @@ export const CATALOG_MODEL = {
81
82
  "compare_at_price?": "number",
82
83
  currency: "Currency",
83
84
  option_values: "Record<string, string>",
85
+ "metafields?": "Metafield[]",
84
86
  inventory: "InventoryState",
85
87
  "barcode?": "string",
88
+ "mpn?": "string",
86
89
  "weight?": "number",
87
90
  "weight_unit?": "string",
91
+ "width?": "number",
92
+ "height?": "number",
93
+ "depth?": "number",
88
94
  position: "number",
89
95
  "image_id?": "string",
90
96
  },
@@ -102,6 +108,7 @@ export const CATALOG_MODEL = {
102
108
  "product_type?": "string",
103
109
  tags: "string[]",
104
110
  status: "ProductStatus",
111
+ "visibility?": "ProductVisibility",
105
112
  seo: "SeoFields",
106
113
  options: "ProductOption[]",
107
114
  images: "CatalogImage[]",
@@ -123,6 +130,7 @@ export const CATALOG_MODEL = {
123
130
  source: "SourceRef",
124
131
  handle: "string",
125
132
  title: "string",
133
+ "visibility?": "ProductVisibility",
126
134
  "description_html?": "string",
127
135
  "description_text?": "string",
128
136
  "image?": "CatalogImage",
@@ -130,6 +138,8 @@ export const CATALOG_MODEL = {
130
138
  product_handles: "string[]",
131
139
  "rule?": "Record<string, unknown>",
132
140
  "position?": "number",
141
+ "parent_id?": "string",
142
+ "tree_id?": "string",
133
143
  created_at: "string",
134
144
  updated_at: "string",
135
145
  migrated_at: "string",
@@ -125,7 +125,7 @@ export async function insertCollection(db: Queryable, c: CatalogCollection): Pro
125
125
  }
126
126
 
127
127
  export async function listCollections(db: Queryable): Promise<CatalogCollection[]> {
128
- const rows = await db.all<{ data: string }>(`SELECT data FROM collections ORDER BY position ASC`);
128
+ const rows = await db.all<{ data: string }>(`SELECT data FROM collections WHERE COALESCE(json_extract(data, '$.visibility'), 'public') = 'public' ORDER BY position ASC`);
129
129
  return rows.map((r) => JSON.parse(r.data) as CatalogCollection);
130
130
  }
131
131
 
@@ -163,6 +163,7 @@ export async function searchProducts(
163
163
  FROM products_fts f
164
164
  JOIN products p ON p.handle = f.handle
165
165
  WHERE products_fts MATCH ? AND p.status = 'active'
166
+ AND COALESCE(json_extract(p.data, '$.visibility'), 'public') = 'public'
166
167
  ORDER BY rank
167
168
  LIMIT ?`,
168
169
  expr, limit,
@@ -189,7 +190,7 @@ export async function getProductByHandle(
189
190
  */
190
191
  export async function listAllActiveProducts(db: Queryable): Promise<CatalogProduct[]> {
191
192
  const rows = await db.all<{ data: string }>(
192
- `SELECT data FROM products WHERE status = 'active' ORDER BY position ASC`,
193
+ `SELECT data FROM products WHERE status = 'active' AND COALESCE(json_extract(data, '$.visibility'), 'public') = 'public' ORDER BY position ASC`,
193
194
  );
194
195
  return rows.map((r) => JSON.parse(r.data) as CatalogProduct);
195
196
  }
@@ -380,7 +381,7 @@ export async function listProducts(
380
381
  ? "JOIN product_collections pc ON pc.handle = p.handle AND pc.collection = ?"
381
382
  : "";
382
383
  const poolParams: unknown[] = query.collection ? [query.collection] : [];
383
- const poolSql = `FROM products p ${poolJoin} WHERE p.status = 'active'`;
384
+ const poolSql = `FROM products p ${poolJoin} WHERE p.status = 'active' AND COALESCE(json_extract(p.data, '$.visibility'), 'public') = 'public'`;
384
385
 
385
386
  // Result filters layered on the pool.
386
387
  const clauses = buildFilterClauses(query);
@@ -8,7 +8,7 @@
8
8
  * merchant gets out of the box (docs/extraction-runbook.md).
9
9
  */
10
10
  import Fuse, { type IFuseOptions } from "fuse.js";
11
- import type { CatalogProduct } from "@tot/public-runtime";
11
+ import { isPublicCatalogProduct, type CatalogProduct } from "@tot/public-runtime";
12
12
 
13
13
  export interface SearchHit {
14
14
  product: CatalogProduct;
@@ -35,7 +35,7 @@ const FUSE_OPTIONS: IFuseOptions<CatalogProduct> = {
35
35
 
36
36
  export function buildSearchIndex(products: CatalogProduct[]): SearchIndex {
37
37
  // Defensive: only ever index active products (the draft canary must never leak).
38
- const active = products.filter((p) => p.status === "active");
38
+ const active = products.filter(isPublicCatalogProduct);
39
39
  const fuse = new Fuse(active, FUSE_OPTIONS);
40
40
  return {
41
41
  search(query: string, limit = 12): SearchHit[] {
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import {
14
14
  normalizeProductSectionSource,
15
+ isPublicCatalogProduct,
15
16
  type CatalogProduct,
16
17
  type ProductSectionSource,
17
18
  } from "@tot/public-runtime";
@@ -29,7 +30,7 @@ export const DEFAULT_SECTION_LIMIT = 12;
29
30
  /** Bestseller signal is tag-based in Phase 1 (no order history at the edge). */
30
31
  const BESTSELLER_TAGS = new Set(["bestseller", "best-seller", "best-sellers"]);
31
32
 
32
- const isActive = (p: CatalogProduct) => p.status === "active";
33
+ const isActive = isPublicCatalogProduct;
33
34
 
34
35
  /**
35
36
  * Resolve a SectionSource to a concrete, active product list via the tenant's
@@ -22,6 +22,7 @@ import type {
22
22
  Review,
23
23
  TenantConfig,
24
24
  } from "@tot/public-runtime";
25
+ import { isPublicCatalogProduct } from "@tot/public-runtime";
25
26
  import { buildFacets, applyQuery, paginate } from "./query.js";
26
27
  import { D1ToTClient } from "./d1Client.js";
27
28
  import { fromD1, type D1Like } from "../d1/catalog.js";
@@ -34,7 +35,7 @@ import { readTenantCatalogFiles, type CatalogArtifactSource } from "./tenantCata
34
35
  // Re-exported so existing `import { ToTClient } from "./ToTClient"` sites work.
35
36
  export type { ToTClient } from "./totClientInterface.js";
36
37
 
37
- const isActive = (p: CatalogProduct) => p.status === "active";
38
+ const isActive = isPublicCatalogProduct;
38
39
 
39
40
  // ---------------------------------------------------------------------------
40
41
  // FixtureToTClient — an in-memory catalog (store files or the in-repo snapshot).
@@ -82,7 +83,7 @@ export class FixtureToTClient implements ToTClient {
82
83
  }
83
84
 
84
85
  async listCollections(): Promise<CatalogCollection[]> {
85
- return [...this.collections].sort(
86
+ return this.collections.filter((collection) => !collection.visibility || collection.visibility === "public").sort(
86
87
  (a, b) => (a.position ?? 99) - (b.position ?? 99),
87
88
  );
88
89
  }
@@ -265,7 +265,7 @@ export function nonEmptyCollections(
265
265
  collections: CatalogCollection[],
266
266
  activeProducts: CatalogProduct[],
267
267
  ): CatalogCollection[] {
268
- return collections.filter((c) => activeMemberCount(c, activeProducts) > 0);
268
+ return collections.filter((c) => (c.visibility ?? "public") === "public" && activeMemberCount(c, activeProducts) > 0);
269
269
  }
270
270
 
271
271
  export function paginate<T>(
@@ -26,7 +26,7 @@ if (!Astro.locals.features.catalog) return new Response(null, { status: 404 });
26
26
  if (!handle) return new Response(null, { status: 404 });
27
27
 
28
28
  const collection = await tot.getCollectionByHandle(handle);
29
- if (!collection) return new Response(null, { status: 404 });
29
+ if (!collection || collection.visibility === "hidden") return new Response(null, { status: 404 });
30
30
 
31
31
  const url = new URL(Astro.request.url);
32
32
  const query = parseQuery(url);
@@ -56,10 +56,10 @@ if (!handle) return Astro.redirect(withBase(basePath, "/404"));
56
56
  const product = await tot.getProductByHandle(handle);
57
57
 
58
58
  // Draft canary: drafts resolve by handle (preview/direct link) but are noindex.
59
- if (!product || product.status === "archived") {
59
+ if (!product || product.status === "archived" || product.visibility === "hidden") {
60
60
  return new Response(null, { status: 404 });
61
61
  }
62
- const isDraft = product.status !== "active";
62
+ const isDraft = product.status !== "active" || product.visibility === "unlisted";
63
63
 
64
64
  // Private-app widgets — product.aside slot, one iframe per live install.
65
65
  // No dark-mode preference is resolved anywhere in the storefront yet; "light"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.2.105",
3
+ "version": "2.2.106",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  export type ProductStatus = "active" | "draft" | "archived";
17
+ export type ProductVisibility = "public" | "unlisted" | "hidden";
17
18
 
18
19
  export type Currency = string; // ISO 4217, e.g. "USD"
19
20
 
@@ -26,6 +27,7 @@ export type Currency = string; // ISO 4217, e.g. "USD"
26
27
  */
27
28
  export type SourcePlatform =
28
29
  | "shopify"
30
+ | "bigcommerce"
29
31
  | "directory"
30
32
  | "csv"
31
33
  | "manual"
@@ -149,10 +151,15 @@ export interface CatalogVariant {
149
151
  currency: Currency;
150
152
  /** Map of optionName -> chosen value, e.g. { "Color": "Black", "Size": "M" }. */
151
153
  option_values: Record<string, string>;
154
+ metafields?: Metafield[];
152
155
  inventory: InventoryState;
153
156
  barcode?: string;
157
+ mpn?: string;
154
158
  weight?: number;
155
159
  weight_unit?: string;
160
+ width?: number;
161
+ height?: number;
162
+ depth?: number;
156
163
  position: number;
157
164
  /** CatalogImage.id of the variant's featured image, if any. */
158
165
  image_id?: string;
@@ -172,6 +179,8 @@ export interface CatalogProduct {
172
179
  product_type?: string;
173
180
  tags: string[];
174
181
  status: ProductStatus;
182
+ /** Omitted on older records; readers treat them as public. */
183
+ visibility?: ProductVisibility;
175
184
  seo: SeoFields;
176
185
  options: ProductOption[];
177
186
  images: CatalogImage[];
@@ -193,6 +202,11 @@ export interface CatalogProduct {
193
202
  source_checksum: string;
194
203
  }
195
204
 
205
+ /** Public listings and search include only visible, active products. */
206
+ export function isPublicCatalogProduct(product: Pick<CatalogProduct, "status" | "visibility">): boolean {
207
+ return product.status === "active" && (product.visibility ?? "public") === "public";
208
+ }
209
+
196
210
  export function normalizePurchaseOptions(
197
211
  product: Pick<CatalogProduct, "purchase_options">,
198
212
  ): ProductPurchaseOption[] {
@@ -222,6 +236,7 @@ export interface CatalogCollection {
222
236
  source: SourceRef;
223
237
  handle: string;
224
238
  title: string;
239
+ visibility?: ProductVisibility;
225
240
  description_html?: string;
226
241
  description_text?: string;
227
242
  image?: CatalogImage;
@@ -231,6 +246,9 @@ export interface CatalogCollection {
231
246
  /** For smart collections, the original rule (kept for re-resolution / audit). */
232
247
  rule?: Record<string, unknown>;
233
248
  position?: number;
249
+ /** Source category parent and tree preserve navigation hierarchy. */
250
+ parent_id?: string;
251
+ tree_id?: string;
234
252
  created_at: string;
235
253
  updated_at: string;
236
254
  migrated_at: string;