@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.
- package/agency-kit/tools/lib/catalog-model/model.mjs +11 -1
- package/apps/storefront/src/lib/d1/catalog.ts +4 -3
- package/apps/storefront/src/lib/search/index.ts +2 -2
- package/apps/storefront/src/lib/sections.ts +2 -1
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -2
- package/apps/storefront/src/lib/tot/query.ts +1 -1
- package/apps/storefront/src/pages/collections/[handle].astro +1 -1
- package/apps/storefront/src/pages/products/[handle].astro +2 -2
- package/package.json +1 -1
- package/packages/public-runtime/src/product.ts +18 -0
|
@@ -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
|
|
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(
|
|
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 =
|
|
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 =
|
|
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
|
|
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.
|
|
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;
|