@tokenoftrust/storefront-runner 1.3.2
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/LICENSE +58 -0
- package/README.md +42 -0
- package/apps/storefront/astro.config.mjs +66 -0
- package/apps/storefront/dev-plugins/dev-loop-state.mjs +147 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +238 -0
- package/apps/storefront/env.d.ts +162 -0
- package/apps/storefront/migrations/0000_baseline.sql +54 -0
- package/apps/storefront/migrations/0001_products_fts.sql +9 -0
- package/apps/storefront/migrations/README.md +54 -0
- package/apps/storefront/migrations/meta/0000_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/0001_snapshot.json +347 -0
- package/apps/storefront/migrations/meta/_journal.json +20 -0
- package/apps/storefront/package.json +42 -0
- package/apps/storefront/public/brand/northwind-logo-dark.svg +4 -0
- package/apps/storefront/public/brand/northwind-logo-light.svg +4 -0
- package/apps/storefront/public/brand/northwind-og.svg +13 -0
- package/apps/storefront/public/favicon.svg +5 -0
- package/apps/storefront/public/js/dashboard-team.js +147 -0
- package/apps/storefront/public/js/dashboard-vendor-search.js +115 -0
- package/apps/storefront/public/shared/commerce-chrome.css +369 -0
- package/apps/storefront/src/components/Badge.astro +34 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +45 -0
- package/apps/storefront/src/components/CollectionCard.astro +56 -0
- package/apps/storefront/src/components/EmptyState.astro +55 -0
- package/apps/storefront/src/components/Img.astro +68 -0
- package/apps/storefront/src/components/ProductCard.astro +228 -0
- package/apps/storefront/src/components/Section.astro +39 -0
- package/apps/storefront/src/components/Seo.astro +70 -0
- package/apps/storefront/src/components/WidgetFrame.astro +34 -0
- package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +531 -0
- package/apps/storefront/src/components/admin/AdminCustomersTab.astro +597 -0
- package/apps/storefront/src/components/admin/AdminFoxyCartSettingsPanel.astro +227 -0
- package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +465 -0
- package/apps/storefront/src/components/admin/AdminProductsTab.astro +901 -0
- package/apps/storefront/src/components/admin/AdminReportingTab.astro +435 -0
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +782 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +1223 -0
- package/apps/storefront/src/components/auth/AuthBadge.astro +44 -0
- package/apps/storefront/src/components/chrome/AnnouncementBar.astro +108 -0
- package/apps/storefront/src/components/chrome/MegaMenu.astro +26 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +201 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +201 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +234 -0
- package/apps/storefront/src/components/chrome/UtilityNav.astro +79 -0
- package/apps/storefront/src/components/commerce/ArticleCard.astro +242 -0
- package/apps/storefront/src/components/commerce/BrandGrid.astro +175 -0
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +23 -0
- package/apps/storefront/src/components/commerce/FoxySidecartStyles.astro +61 -0
- package/apps/storefront/src/components/commerce/PriceDisplay.astro +91 -0
- package/apps/storefront/src/components/commerce/ProductCarousel.astro +197 -0
- package/apps/storefront/src/components/commerce/ProductGrid.astro +75 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +58 -0
- package/apps/storefront/src/components/commerce/ReviewList.astro +54 -0
- package/apps/storefront/src/components/commerce/ReviewSummary.astro +44 -0
- package/apps/storefront/src/components/commerce/TagFilterCloud.astro +149 -0
- package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +37 -0
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +99 -0
- package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +45 -0
- package/apps/storefront/src/components/compliance/NicotineWarning.astro +33 -0
- package/apps/storefront/src/components/compliance/PactActNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/Prop65Warning.astro +80 -0
- package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +38 -0
- package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +35 -0
- package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +53 -0
- package/apps/storefront/src/components/home/Hero.astro +164 -0
- package/apps/storefront/src/components/islands/AgeGate.tsx +109 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +237 -0
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +73 -0
- package/apps/storefront/src/components/islands/MobileNav.tsx +239 -0
- package/apps/storefront/src/components/islands/QuickView.tsx +138 -0
- package/apps/storefront/src/components/islands/RecentlyViewed.tsx +122 -0
- package/apps/storefront/src/components/islands/SavedList.tsx +99 -0
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +248 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +375 -0
- package/apps/storefront/src/components/marketing/CardGrid.astro +72 -0
- package/apps/storefront/src/components/marketing/CodeSample.astro +20 -0
- package/apps/storefront/src/components/marketing/CtaBand.astro +24 -0
- package/apps/storefront/src/components/marketing/Faq.astro +37 -0
- package/apps/storefront/src/components/marketing/Integrations.astro +63 -0
- package/apps/storefront/src/components/marketing/MarketingBlocks.astro +53 -0
- package/apps/storefront/src/components/marketing/MarketingCtas.astro +48 -0
- package/apps/storefront/src/components/marketing/MarketingHero.astro +72 -0
- package/apps/storefront/src/components/marketing/ProofStrip.astro +37 -0
- package/apps/storefront/src/components/marketing/SplitCompare.astro +53 -0
- package/apps/storefront/src/components/marketing/Steps.astro +36 -0
- package/apps/storefront/src/components/marketing/Testimonials.astro +38 -0
- package/apps/storefront/src/components/marketing/TrustBar.astro +22 -0
- package/apps/storefront/src/components/membership/IllustrativeLabel.astro +56 -0
- package/apps/storefront/src/components/membership/TierCards.astro +164 -0
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +148 -0
- package/apps/storefront/src/components/plp/ActiveFilters.astro +79 -0
- package/apps/storefront/src/components/plp/FacetSidebar.astro +199 -0
- package/apps/storefront/src/components/plp/Pagination.astro +99 -0
- package/apps/storefront/src/components/plp/SortSelect.astro +64 -0
- package/apps/storefront/src/components/style-guide/StyleGuideNav.astro +95 -0
- package/apps/storefront/src/components/subscription/HowItWorksSteps.astro +67 -0
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +67 -0
- package/apps/storefront/src/components/subscription/SavingsExplainer.astro +101 -0
- package/apps/storefront/src/config/adminTargetTenant.ts +28 -0
- package/apps/storefront/src/config/devTenantSeed.ts +78 -0
- package/apps/storefront/src/config/devTenants.ts +16 -0
- package/apps/storefront/src/config/kvTenantResolver.ts +87 -0
- package/apps/storefront/src/config/resolver.ts +102 -0
- package/apps/storefront/src/config/tenants.ts +87 -0
- package/apps/storefront/src/layouts/Layout.astro +368 -0
- package/apps/storefront/src/lib/admin/README.md +122 -0
- package/apps/storefront/src/lib/admin/ai/approvalMode.ts +21 -0
- package/apps/storefront/src/lib/admin/ai/askEnvelope.ts +48 -0
- package/apps/storefront/src/lib/admin/ai/audit.ts +14 -0
- package/apps/storefront/src/lib/admin/ai/index.ts +6 -0
- package/apps/storefront/src/lib/admin/ai/operationalRuns.ts +155 -0
- package/apps/storefront/src/lib/admin/ai/recommendation.ts +20 -0
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +172 -0
- package/apps/storefront/src/lib/admin/checkoutStyleTarget.ts +65 -0
- package/apps/storefront/src/lib/admin/envelope.ts +10 -0
- package/apps/storefront/src/lib/admin/evidence/evidenceReference.ts +32 -0
- package/apps/storefront/src/lib/admin/memoizeByEnvelope.ts +27 -0
- package/apps/storefront/src/lib/admin/orderClient.ts +146 -0
- package/apps/storefront/src/lib/admin/seed.ts +54 -0
- package/apps/storefront/src/lib/admin/services/mock/aiWorkflows.ts +59 -0
- package/apps/storefront/src/lib/admin/services/mock/customers.ts +86 -0
- package/apps/storefront/src/lib/admin/services/mock/evidence.ts +99 -0
- package/apps/storefront/src/lib/admin/services/mock/foxyCartSettings.ts +78 -0
- package/apps/storefront/src/lib/admin/services/mock/fulfillment.ts +62 -0
- package/apps/storefront/src/lib/admin/services/mock/index.ts +12 -0
- package/apps/storefront/src/lib/admin/services/mock/orders.ts +184 -0
- package/apps/storefront/src/lib/admin/services/mock/products.ts +105 -0
- package/apps/storefront/src/lib/admin/services/mock/reporting.ts +51 -0
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +70 -0
- package/apps/storefront/src/lib/admin/services/mock/tenants.ts +29 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +300 -0
- package/apps/storefront/src/lib/admin/viewmodels/aiWorkflows.ts +67 -0
- package/apps/storefront/src/lib/admin/viewmodels/customers.ts +56 -0
- package/apps/storefront/src/lib/admin/viewmodels/fulfillment.ts +49 -0
- package/apps/storefront/src/lib/admin/viewmodels/orders.ts +92 -0
- package/apps/storefront/src/lib/admin/viewmodels/products.ts +74 -0
- package/apps/storefront/src/lib/admin/viewmodels/reporting.ts +34 -0
- package/apps/storefront/src/lib/admin/viewmodels/settings.ts +64 -0
- package/apps/storefront/src/lib/adminTenantEnvelope.ts +90 -0
- package/apps/storefront/src/lib/analytics/index.ts +47 -0
- package/apps/storefront/src/lib/auth/brokerAssertion.ts +136 -0
- package/apps/storefront/src/lib/auth/devAuth.ts +90 -0
- package/apps/storefront/src/lib/auth/gatePage.ts +81 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +341 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +119 -0
- package/apps/storefront/src/lib/auth/route.ts +146 -0
- package/apps/storefront/src/lib/auth/session.ts +203 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +503 -0
- package/apps/storefront/src/lib/basePath.ts +41 -0
- package/apps/storefront/src/lib/cfImage.ts +55 -0
- package/apps/storefront/src/lib/chrome/model.ts +244 -0
- package/apps/storefront/src/lib/chrome/parseConfig.ts +34 -0
- package/apps/storefront/src/lib/colors.ts +97 -0
- package/apps/storefront/src/lib/commerce/foxyCartSettings.ts +253 -0
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +91 -0
- package/apps/storefront/src/lib/content-edit/client.ts +230 -0
- package/apps/storefront/src/lib/content-edit/index.ts +14 -0
- package/apps/storefront/src/lib/content-edit/route.ts +25 -0
- package/apps/storefront/src/lib/content-edit/types.ts +19 -0
- package/apps/storefront/src/lib/cspStaticHashes.ts +24 -0
- package/apps/storefront/src/lib/d1/catalog.ts +366 -0
- package/apps/storefront/src/lib/dashboard/staffAdmission.ts +43 -0
- package/apps/storefront/src/lib/dashboard/staffVendorAccess.ts +449 -0
- package/apps/storefront/src/lib/dashboard/tenantSelection.ts +136 -0
- package/apps/storefront/src/lib/dashboard/vendorQuery.ts +61 -0
- package/apps/storefront/src/lib/demoGate.ts +113 -0
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +78 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +72 -0
- package/apps/storefront/src/lib/dev/ai-widget-client.js +308 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +14 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +155 -0
- package/apps/storefront/src/lib/dev/devDraft.ts +133 -0
- package/apps/storefront/src/lib/dev/hostedCockpit.ts +169 -0
- package/apps/storefront/src/lib/dev/livePreview.ts +77 -0
- package/apps/storefront/src/lib/dev/runtimeStore.ts +120 -0
- package/apps/storefront/src/lib/dev/storeStatus.ts +142 -0
- package/apps/storefront/src/lib/diag/redact.ts +20 -0
- package/apps/storefront/src/lib/edgeCache.ts +128 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +62 -0
- package/apps/storefront/src/lib/env.ts +60 -0
- package/apps/storefront/src/lib/fixtures/sampleProducts.ts +130 -0
- package/apps/storefront/src/lib/format.ts +38 -0
- package/apps/storefront/src/lib/foxyCommerce.ts +49 -0
- package/apps/storefront/src/lib/imageAttrs.ts +55 -0
- package/apps/storefront/src/lib/jsonld.ts +166 -0
- package/apps/storefront/src/lib/maintenance.ts +71 -0
- package/apps/storefront/src/lib/membership/model.ts +97 -0
- package/apps/storefront/src/lib/memoryKv.ts +32 -0
- package/apps/storefront/src/lib/messaging/messagesClient.ts +143 -0
- package/apps/storefront/src/lib/placeholder.ts +99 -0
- package/apps/storefront/src/lib/previewReload.ts +125 -0
- package/apps/storefront/src/lib/priceBuckets.ts +33 -0
- package/apps/storefront/src/lib/rawChrome.ts +505 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +136 -0
- package/apps/storefront/src/lib/search/index.ts +74 -0
- package/apps/storefront/src/lib/sections.ts +129 -0
- package/apps/storefront/src/lib/securityHeaders.ts +31 -0
- package/apps/storefront/src/lib/storyblok/content-model.ts +289 -0
- package/apps/storefront/src/lib/storyblok/provider.ts +607 -0
- package/apps/storefront/src/lib/subscription/model.ts +68 -0
- package/apps/storefront/src/lib/tenantRouting.ts +91 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +177 -0
- package/apps/storefront/src/lib/tot/d1Client.ts +62 -0
- package/apps/storefront/src/lib/tot/query.ts +249 -0
- package/apps/storefront/src/lib/tot/totClientInterface.ts +30 -0
- package/apps/storefront/src/lib/url.ts +142 -0
- package/apps/storefront/src/lib/wishlist.ts +63 -0
- package/apps/storefront/src/middleware/index.ts +455 -0
- package/apps/storefront/src/pages/404.astro +61 -0
- package/apps/storefront/src/pages/[...slug].astro +90 -0
- package/apps/storefront/src/pages/admin.astro +1675 -0
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +53 -0
- package/apps/storefront/src/pages/api/admin/orders.ts +71 -0
- package/apps/storefront/src/pages/api/auth/logout.ts +21 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +107 -0
- package/apps/storefront/src/pages/api/auth/send.ts +85 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +135 -0
- package/apps/storefront/src/pages/api/cache-purge.ts +44 -0
- package/apps/storefront/src/pages/api/checkout-style.css.ts +43 -0
- package/apps/storefront/src/pages/api/cli/policy.ts +70 -0
- package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +164 -0
- package/apps/storefront/src/pages/api/csp-report.ts +25 -0
- package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +124 -0
- package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +120 -0
- package/apps/storefront/src/pages/api/dev/activity.ts +124 -0
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +73 -0
- package/apps/storefront/src/pages/api/dev/diff.ts +68 -0
- package/apps/storefront/src/pages/api/dev/file.ts +57 -0
- package/apps/storefront/src/pages/api/dev/locate.ts +125 -0
- package/apps/storefront/src/pages/api/dev/publish.ts +128 -0
- package/apps/storefront/src/pages/api/dev/tree.ts +70 -0
- package/apps/storefront/src/pages/api/dev/write.ts +116 -0
- package/apps/storefront/src/pages/api/newsletter.ts +104 -0
- package/apps/storefront/src/pages/api/request-access.ts +172 -0
- package/apps/storefront/src/pages/auth/login.astro +149 -0
- package/apps/storefront/src/pages/auth/magic.astro +105 -0
- package/apps/storefront/src/pages/capabilities.astro +292 -0
- package/apps/storefront/src/pages/collections/[handle].astro +177 -0
- package/apps/storefront/src/pages/collections/index.astro +59 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +100 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +154 -0
- package/apps/storefront/src/pages/dashboard/index.astro +160 -0
- package/apps/storefront/src/pages/index.astro +290 -0
- package/apps/storefront/src/pages/llms.txt.ts +92 -0
- package/apps/storefront/src/pages/products/[handle].astro +402 -0
- package/apps/storefront/src/pages/robots.txt.ts +24 -0
- package/apps/storefront/src/pages/saved.astro +35 -0
- package/apps/storefront/src/pages/search.astro +150 -0
- package/apps/storefront/src/pages/sitemap.xml.ts +78 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +1280 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +229 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +144 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +116 -0
- package/apps/storefront/src/pages/style-guide/index.astro +98 -0
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +57 -0
- package/apps/storefront/src/styles/global.css +292 -0
- package/apps/storefront/src/themes/reference.ts +68 -0
- package/apps/storefront/src/themes/schema.ts +111 -0
- package/apps/storefront/src/themes/tenants/index.ts +51 -0
- package/apps/storefront/tsconfig.json +16 -0
- package/apps/storefront/vitest.config.ts +22 -0
- package/package.json +26 -0
- package/packages/public-runtime/LICENSE +58 -0
- package/packages/public-runtime/package.json +22 -0
- package/packages/public-runtime/src/binary-path.ts +68 -0
- package/packages/public-runtime/src/capabilities.ts +89 -0
- package/packages/public-runtime/src/catalog-api.ts +81 -0
- package/packages/public-runtime/src/catalog-d1.ts +192 -0
- package/packages/public-runtime/src/checkout-style-publish.ts +200 -0
- package/packages/public-runtime/src/checkout-style.ts +375 -0
- package/packages/public-runtime/src/csp.ts +109 -0
- package/packages/public-runtime/src/customization-preview.ts +197 -0
- package/packages/public-runtime/src/customization-reconcile.ts +63 -0
- package/packages/public-runtime/src/customization-runtime.ts +157 -0
- package/packages/public-runtime/src/customization-scripts.ts +64 -0
- package/packages/public-runtime/src/customization-versioning.ts +95 -0
- package/packages/public-runtime/src/foxy-orders.ts +136 -0
- package/packages/public-runtime/src/foxy.ts +196 -0
- package/packages/public-runtime/src/hot-standby-readiness.ts +140 -0
- package/packages/public-runtime/src/index.ts +34 -0
- package/packages/public-runtime/src/product.ts +159 -0
- package/packages/public-runtime/src/search.ts +45 -0
- package/packages/public-runtime/src/tenant-assets.ts +444 -0
- package/packages/public-runtime/src/tenant.ts +384 -0
- package/packages/public-runtime/tsconfig.json +8 -0
- package/packages/public-runtime/vitest.config.ts +8 -0
- package/pnpm-workspace.yaml +7 -0
- package/scripts/build/copy-tenant-assets.mjs +69 -0
- package/scripts/dev/ai-edit.mjs +183 -0
- package/scripts/dev/checkout-watch.mjs +116 -0
- package/scripts/dev/file-browser.mjs +159 -0
- package/scripts/dev/file-writer.mjs +141 -0
- package/scripts/dev/git-status.mjs +171 -0
- package/scripts/dev/locate-handler.mjs +163 -0
- package/scripts/dev/locate.mjs +112 -0
- package/scripts/dev/port-check.mjs +48 -0
- package/scripts/dev/safe-path.mjs +109 -0
- package/scripts/dev/shot.mjs +46 -0
- package/scripts/dev/tenant-source-root.mjs +76 -0
- package/scripts/dev/unified-diff.mjs +81 -0
- package/scripts/tot-dev.mjs +376 -0
- package/tenants/README.md +14 -0
- package/tenants/home/content/chrome.json +7 -0
- package/tenants/home/content/home.html +58 -0
- package/tenants/home/content/home.json +11 -0
- package/tenants/home/public/js/storefront.js +210 -0
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +212 -0
- package/tenants/home/theme.json +6 -0
- package/tsconfig.base.json +20 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1 catalog read-model queries (the one-store target — epics/edge-read-replica.md).
|
|
3
|
+
*
|
|
4
|
+
* Queries run against a small `Queryable` seam so the SAME code serves D1 in the
|
|
5
|
+
* Worker (via `fromD1`) and an in-memory `node:sqlite` in tests. Covers FTS5
|
|
6
|
+
* (trigram) search, getProductByHandle, collections, and faceted `listProducts`
|
|
7
|
+
* (filter + sort + paginate + facet counts in SQL GROUP BY) — the full read path,
|
|
8
|
+
* verified end-to-end against the real D1 engine (workerd).
|
|
9
|
+
*/
|
|
10
|
+
import {
|
|
11
|
+
CATALOG_D1_SCHEMA,
|
|
12
|
+
toProductRow,
|
|
13
|
+
toCollectionRow,
|
|
14
|
+
facetRows,
|
|
15
|
+
collectionRows,
|
|
16
|
+
type CatalogCollection,
|
|
17
|
+
type CatalogProduct,
|
|
18
|
+
type Facet,
|
|
19
|
+
type ListProductsQuery,
|
|
20
|
+
type ListProductsResponse,
|
|
21
|
+
} from "@tot/public-runtime";
|
|
22
|
+
import { PRICE_BUCKETS } from "../priceBuckets.js";
|
|
23
|
+
|
|
24
|
+
/** Minimal async query seam — implemented by D1 and by the node:sqlite test shim. */
|
|
25
|
+
export interface Queryable {
|
|
26
|
+
all<T = Record<string, unknown>>(sql: string, ...params: unknown[]): Promise<T[]>;
|
|
27
|
+
first<T = Record<string, unknown>>(sql: string, ...params: unknown[]): Promise<T | null>;
|
|
28
|
+
run(sql: string, ...params: unknown[]): Promise<void>;
|
|
29
|
+
exec(sql: string): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The slice of the Cloudflare D1Database we use (kept local to avoid a types dep). */
|
|
33
|
+
export interface D1Like {
|
|
34
|
+
prepare(sql: string): {
|
|
35
|
+
bind(...vals: unknown[]): {
|
|
36
|
+
all(): Promise<{ results: unknown[] }>;
|
|
37
|
+
first(): Promise<unknown>;
|
|
38
|
+
run(): Promise<unknown>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
exec(sql: string): Promise<unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Adapt a Cloudflare D1Database to the Queryable seam. */
|
|
45
|
+
export function fromD1(db: D1Like): Queryable {
|
|
46
|
+
return {
|
|
47
|
+
all: async <T>(sql: string, ...params: unknown[]) =>
|
|
48
|
+
(await db.prepare(sql).bind(...params).all()).results as T[],
|
|
49
|
+
first: async <T>(sql: string, ...params: unknown[]) =>
|
|
50
|
+
((await db.prepare(sql).bind(...params).first()) ?? null) as T | null,
|
|
51
|
+
run: async (sql: string, ...params: unknown[]) => {
|
|
52
|
+
await db.prepare(sql).bind(...params).run();
|
|
53
|
+
},
|
|
54
|
+
exec: async (sql: string) => {
|
|
55
|
+
await db.exec(sql);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Create the catalog schema (idempotent). Used for local/test seeding. */
|
|
61
|
+
export async function initSchema(db: Queryable): Promise<void> {
|
|
62
|
+
await db.exec(CATALOG_D1_SCHEMA);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Insert one product into all read-model tables (products, fts, facets, collections). */
|
|
66
|
+
export async function insertProduct(db: Queryable, p: CatalogProduct, position = 0): Promise<void> {
|
|
67
|
+
const r = toProductRow(p, position);
|
|
68
|
+
await db.run(
|
|
69
|
+
`INSERT OR REPLACE INTO products
|
|
70
|
+
(handle,title,vendor,product_type,tags,description_text,price_min,price_max,currency,image,available,status,position,created_at,data)
|
|
71
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
72
|
+
r.handle, r.title, r.vendor, r.product_type, r.tags, r.description_text,
|
|
73
|
+
r.price_min, r.price_max, r.currency, r.image, r.available, r.status,
|
|
74
|
+
r.position, r.created_at, r.data,
|
|
75
|
+
);
|
|
76
|
+
await db.run(
|
|
77
|
+
`INSERT INTO products_fts (handle,title,vendor,product_type,tags,description_text)
|
|
78
|
+
VALUES (?,?,?,?,?,?)`,
|
|
79
|
+
p.handle, p.title, p.vendor ?? "", p.product_type ?? "", p.tags.join(" "), p.description_text,
|
|
80
|
+
);
|
|
81
|
+
for (const f of facetRows(p)) {
|
|
82
|
+
await db.run(`INSERT INTO product_facets (handle,name,value) VALUES (?,?,?)`, p.handle, f.name, f.value);
|
|
83
|
+
}
|
|
84
|
+
for (const c of collectionRows(p)) {
|
|
85
|
+
await db.run(`INSERT INTO product_collections (handle,collection) VALUES (?,?)`, p.handle, c.collection);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Insert one collection row. */
|
|
90
|
+
export async function insertCollection(db: Queryable, c: CatalogCollection): Promise<void> {
|
|
91
|
+
const r = toCollectionRow(c);
|
|
92
|
+
await db.run(
|
|
93
|
+
`INSERT OR REPLACE INTO collections (handle,title,position,data) VALUES (?,?,?,?)`,
|
|
94
|
+
r.handle, r.title, r.position, r.data,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function listCollections(db: Queryable): Promise<CatalogCollection[]> {
|
|
99
|
+
const rows = await db.all<{ data: string }>(`SELECT data FROM collections ORDER BY position ASC`);
|
|
100
|
+
return rows.map((r) => JSON.parse(r.data) as CatalogCollection);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getCollectionByHandle(
|
|
104
|
+
db: Queryable,
|
|
105
|
+
handle: string,
|
|
106
|
+
): Promise<CatalogCollection | null> {
|
|
107
|
+
const row = await db.first<{ data: string }>(
|
|
108
|
+
`SELECT data FROM collections WHERE handle = ?`,
|
|
109
|
+
handle,
|
|
110
|
+
);
|
|
111
|
+
return row ? (JSON.parse(row.data) as CatalogCollection) : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Build an FTS5 query expression: implicit-AND of quoted terms (trigram substring). */
|
|
115
|
+
function ftsExpr(q: string): string | null {
|
|
116
|
+
const terms = q
|
|
117
|
+
.trim()
|
|
118
|
+
.split(/\s+/)
|
|
119
|
+
.filter((t) => t.length >= 3) // FTS5 trigram tokens are 3 chars; shorter can't match
|
|
120
|
+
.map((t) => `"${t.replace(/"/g, '""')}"`);
|
|
121
|
+
return terms.length ? terms.join(" ") : null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Typo-tolerant search over the active catalog, ranked by FTS5 relevance. */
|
|
125
|
+
export async function searchProducts(
|
|
126
|
+
db: Queryable,
|
|
127
|
+
q: string,
|
|
128
|
+
limit = 24,
|
|
129
|
+
): Promise<CatalogProduct[]> {
|
|
130
|
+
const expr = ftsExpr(q);
|
|
131
|
+
if (!expr) return [];
|
|
132
|
+
const rows = await db.all<{ data: string }>(
|
|
133
|
+
`SELECT p.data AS data
|
|
134
|
+
FROM products_fts f
|
|
135
|
+
JOIN products p ON p.handle = f.handle
|
|
136
|
+
WHERE products_fts MATCH ? AND p.status = 'active'
|
|
137
|
+
ORDER BY rank
|
|
138
|
+
LIMIT ?`,
|
|
139
|
+
expr, limit,
|
|
140
|
+
);
|
|
141
|
+
return rows.map((r) => JSON.parse(r.data) as CatalogProduct);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Full product by handle (resolves drafts too — callers gate visibility). */
|
|
145
|
+
export async function getProductByHandle(
|
|
146
|
+
db: Queryable,
|
|
147
|
+
handle: string,
|
|
148
|
+
): Promise<CatalogProduct | null> {
|
|
149
|
+
const row = await db.first<{ data: string }>(
|
|
150
|
+
`SELECT data FROM products WHERE handle = ?`,
|
|
151
|
+
handle,
|
|
152
|
+
);
|
|
153
|
+
return row ? (JSON.parse(row.data) as CatalogProduct) : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sortClause(sort?: ListProductsQuery["sort"]): string {
|
|
157
|
+
switch (sort) {
|
|
158
|
+
case "price-asc":
|
|
159
|
+
return "ORDER BY p.price_min ASC, p.title COLLATE NOCASE ASC";
|
|
160
|
+
case "price-desc":
|
|
161
|
+
return "ORDER BY p.price_min DESC, p.title COLLATE NOCASE ASC";
|
|
162
|
+
case "newest":
|
|
163
|
+
return "ORDER BY p.created_at DESC";
|
|
164
|
+
case "title-asc":
|
|
165
|
+
return "ORDER BY p.title COLLATE NOCASE ASC";
|
|
166
|
+
case "featured":
|
|
167
|
+
default:
|
|
168
|
+
// Bestsellers float up, then the load order (position) — mirrors the
|
|
169
|
+
// in-Worker featured sort.
|
|
170
|
+
return `ORDER BY (CASE WHEN EXISTS (
|
|
171
|
+
SELECT 1 FROM product_facets f
|
|
172
|
+
WHERE f.handle = p.handle AND f.name = 'tags' AND f.value = 'bestseller'
|
|
173
|
+
) THEN 0 ELSE 1 END), p.position ASC`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** One filter clause, tagged with the facet group it belongs to (so a
|
|
178
|
+
* per-group facet-count query can exclude just its own clause). "__q__" is
|
|
179
|
+
* the free-text search — never excluded, since it narrows every facet. */
|
|
180
|
+
interface FilterClause {
|
|
181
|
+
name: string;
|
|
182
|
+
sql: string;
|
|
183
|
+
params: unknown[];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildFilterClauses(query: ListProductsQuery): FilterClause[] {
|
|
187
|
+
const clauses: FilterClause[] = [];
|
|
188
|
+
for (const [name, values] of Object.entries(query.filters ?? {})) {
|
|
189
|
+
if (name === "availability" || name === "price" || !values.length) continue;
|
|
190
|
+
const placeholders = values.map(() => "?").join(",");
|
|
191
|
+
clauses.push({
|
|
192
|
+
name,
|
|
193
|
+
sql: `p.handle IN (SELECT handle FROM product_facets WHERE name = ? AND value IN (${placeholders}))`,
|
|
194
|
+
params: [name, ...values],
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (query.tags?.length) {
|
|
198
|
+
const placeholders = query.tags.map(() => "?").join(",");
|
|
199
|
+
clauses.push({
|
|
200
|
+
name: "tags",
|
|
201
|
+
sql: `p.handle IN (SELECT handle FROM product_facets WHERE name = 'tags' AND value IN (${placeholders}))`,
|
|
202
|
+
params: [...query.tags],
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (query.availability === "in-stock") {
|
|
206
|
+
clauses.push({ name: "availability", sql: "p.available = 1", params: [] });
|
|
207
|
+
}
|
|
208
|
+
if (query.priceMin != null) {
|
|
209
|
+
clauses.push({ name: "price", sql: "p.price_max >= ?", params: [query.priceMin] });
|
|
210
|
+
}
|
|
211
|
+
if (query.priceMax != null) {
|
|
212
|
+
clauses.push({ name: "price", sql: "p.price_min <= ?", params: [query.priceMax] });
|
|
213
|
+
}
|
|
214
|
+
if (query.q?.trim()) {
|
|
215
|
+
const like = `%${query.q.trim().toLowerCase()}%`;
|
|
216
|
+
clauses.push({
|
|
217
|
+
name: "__q__",
|
|
218
|
+
sql: "(lower(p.title) LIKE ? OR lower(p.vendor) LIKE ? OR lower(p.product_type) LIKE ? OR lower(p.description_text) LIKE ? OR lower(p.tags) LIKE ?)",
|
|
219
|
+
params: [like, like, like, like, like],
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return clauses;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Render clauses (minus one excluded group, if given) as an " AND ..." suffix. */
|
|
226
|
+
function clauseSqlExcept(
|
|
227
|
+
clauses: FilterClause[],
|
|
228
|
+
except?: string,
|
|
229
|
+
): { sql: string; params: unknown[] } {
|
|
230
|
+
const kept = clauses.filter((c) => c.name !== except);
|
|
231
|
+
return {
|
|
232
|
+
sql: kept.length ? ` AND ${kept.map((c) => c.sql).join(" AND ")}` : "",
|
|
233
|
+
params: kept.flatMap((c) => c.params),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Facet counts with LIVE narrowing: each group's value counts apply every
|
|
239
|
+
* OTHER active filter (all groups but its own, plus q/availability/price),
|
|
240
|
+
* matching the storefront's multi-select facet UI. A value known to the pool
|
|
241
|
+
* but narrowed to zero by other filters still appears (count: 0) instead of
|
|
242
|
+
* vanishing, so the facet list stays stable while filtering.
|
|
243
|
+
*/
|
|
244
|
+
async function buildFacetsSql(
|
|
245
|
+
db: Queryable,
|
|
246
|
+
poolSql: string,
|
|
247
|
+
poolParams: unknown[],
|
|
248
|
+
clauses: FilterClause[],
|
|
249
|
+
): Promise<Facet[]> {
|
|
250
|
+
// Discover every (name, value) pair present anywhere in the pool
|
|
251
|
+
// (unfiltered), so facet groups/values don't disappear when narrowed to 0.
|
|
252
|
+
const allRows = await db.all<{ name: string; value: string }>(
|
|
253
|
+
`SELECT DISTINCT pf.name AS name, pf.value AS value
|
|
254
|
+
FROM product_facets pf
|
|
255
|
+
WHERE pf.handle IN (SELECT p.handle ${poolSql})`,
|
|
256
|
+
...poolParams,
|
|
257
|
+
);
|
|
258
|
+
const namesInOrder: string[] = [];
|
|
259
|
+
const valuesByName = new Map<string, string[]>();
|
|
260
|
+
for (const r of allRows) {
|
|
261
|
+
if (!valuesByName.has(r.name)) {
|
|
262
|
+
namesInOrder.push(r.name);
|
|
263
|
+
valuesByName.set(r.name, []);
|
|
264
|
+
}
|
|
265
|
+
valuesByName.get(r.name)!.push(r.value);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const facets: Facet[] = [];
|
|
269
|
+
for (const name of namesInOrder) {
|
|
270
|
+
const { sql: exceptSql, params: exceptParams } = clauseSqlExcept(clauses, name);
|
|
271
|
+
const rows = await db.all<{ value: string; n: number }>(
|
|
272
|
+
`SELECT pf.value AS value, COUNT(DISTINCT pf.handle) AS n
|
|
273
|
+
FROM product_facets pf
|
|
274
|
+
WHERE pf.name = ?
|
|
275
|
+
AND pf.handle IN (SELECT p.handle ${poolSql}${exceptSql})
|
|
276
|
+
GROUP BY pf.value`,
|
|
277
|
+
name, ...poolParams, ...exceptParams,
|
|
278
|
+
);
|
|
279
|
+
const counts = new Map<string, number>(valuesByName.get(name)!.map((v) => [v, 0]));
|
|
280
|
+
for (const r of rows) counts.set(r.value, r.n);
|
|
281
|
+
facets.push({
|
|
282
|
+
name,
|
|
283
|
+
values: [...counts.entries()]
|
|
284
|
+
.map(([value, count]) => ({ value, count }))
|
|
285
|
+
.sort((a, b) => b.count - a.count || a.value.localeCompare(b.value)),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const { sql: availExceptSql, params: availExceptParams } = clauseSqlExcept(
|
|
290
|
+
clauses,
|
|
291
|
+
"availability",
|
|
292
|
+
);
|
|
293
|
+
const avail = await db.first<{ instock: number | null; total: number }>(
|
|
294
|
+
`SELECT SUM(CASE WHEN p.available = 1 THEN 1 ELSE 0 END) AS instock, COUNT(*) AS total ${poolSql}${availExceptSql}`,
|
|
295
|
+
...poolParams, ...availExceptParams,
|
|
296
|
+
);
|
|
297
|
+
facets.push({
|
|
298
|
+
name: "availability",
|
|
299
|
+
values: [
|
|
300
|
+
{ value: "in-stock", count: avail?.instock ?? 0 },
|
|
301
|
+
{ value: "all", count: avail?.total ?? 0 },
|
|
302
|
+
],
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const { sql: priceExceptSql, params: priceExceptParams } = clauseSqlExcept(clauses, "price");
|
|
306
|
+
// Bucket boundaries are fixed numeric constants (PRICE_BUCKETS), not user
|
|
307
|
+
// input, so inlining them into the CASE expression is safe.
|
|
308
|
+
const bucketCase = PRICE_BUCKETS.map(
|
|
309
|
+
(b) =>
|
|
310
|
+
`SUM(CASE WHEN p.price_min >= ${b.min}${b.max != null ? ` AND p.price_min < ${b.max}` : ""} THEN 1 ELSE 0 END) AS "${b.key}"`,
|
|
311
|
+
).join(", ");
|
|
312
|
+
const priceRow = await db.first<Record<string, number>>(
|
|
313
|
+
`SELECT ${bucketCase} ${poolSql}${priceExceptSql}`,
|
|
314
|
+
...poolParams, ...priceExceptParams,
|
|
315
|
+
);
|
|
316
|
+
facets.push({
|
|
317
|
+
name: "price",
|
|
318
|
+
values: PRICE_BUCKETS.map((b) => ({ value: b.key, count: priceRow?.[b.key] ?? 0 })),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return facets;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Faceted listing — filter, sort, paginate, and count facets in SQL, replacing
|
|
326
|
+
* the in-Worker O(products×options×variants) scan. Option/tag filters are
|
|
327
|
+
* intra-group OR, inter-group AND (a product matches a group if it has any
|
|
328
|
+
* variant value in it). Facet counts are LIVE: each group's counts reflect
|
|
329
|
+
* every OTHER active filter (see buildFacetsSql), not just the collection pool.
|
|
330
|
+
*/
|
|
331
|
+
export async function listProducts(
|
|
332
|
+
db: Queryable,
|
|
333
|
+
query: ListProductsQuery = {},
|
|
334
|
+
): Promise<ListProductsResponse> {
|
|
335
|
+
const pageSize = Math.max(1, query.pageSize ?? 12);
|
|
336
|
+
|
|
337
|
+
// Pool: active products, optionally scoped to a collection.
|
|
338
|
+
const poolJoin = query.collection
|
|
339
|
+
? "JOIN product_collections pc ON pc.handle = p.handle AND pc.collection = ?"
|
|
340
|
+
: "";
|
|
341
|
+
const poolParams: unknown[] = query.collection ? [query.collection] : [];
|
|
342
|
+
const poolSql = `FROM products p ${poolJoin} WHERE p.status = 'active'`;
|
|
343
|
+
|
|
344
|
+
// Result filters layered on the pool.
|
|
345
|
+
const clauses = buildFilterClauses(query);
|
|
346
|
+
const { sql: filterClause, params: filterParams } = clauseSqlExcept(clauses);
|
|
347
|
+
const resultParams = [...poolParams, ...filterParams];
|
|
348
|
+
|
|
349
|
+
const totalRow = await db.first<{ n: number }>(
|
|
350
|
+
`SELECT COUNT(*) AS n ${poolSql}${filterClause}`,
|
|
351
|
+
...resultParams,
|
|
352
|
+
);
|
|
353
|
+
const total = totalRow?.n ?? 0;
|
|
354
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
355
|
+
const page = Math.min(Math.max(1, query.page ?? 1), pageCount);
|
|
356
|
+
const offset = (page - 1) * pageSize;
|
|
357
|
+
|
|
358
|
+
const rows = await db.all<{ data: string }>(
|
|
359
|
+
`SELECT p.data AS data ${poolSql}${filterClause} ${sortClause(query.sort)} LIMIT ? OFFSET ?`,
|
|
360
|
+
...resultParams, pageSize, offset,
|
|
361
|
+
);
|
|
362
|
+
const products = rows.map((r) => JSON.parse(r.data) as CatalogProduct);
|
|
363
|
+
|
|
364
|
+
const facets = await buildFacetsSql(db, poolSql, poolParams, clauses);
|
|
365
|
+
return { products, page, pageSize, total, facets };
|
|
366
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the ToT-staff two-phase admission (#8425 staff access), kept
|
|
3
|
+
* out of the Astro route files so the gate + selection-builder are unit-testable.
|
|
4
|
+
*
|
|
5
|
+
* PHASE 1 (admission gate): only a session that actually carries ToT-staff scope
|
|
6
|
+
* (`staff[]` non-empty) may search vendors or enter one. This is the near-side
|
|
7
|
+
* gate on the storefront's staff endpoints; tot20 re-checks staff scope + writes
|
|
8
|
+
* the audit row on the far side (`staffVendorAccess`), so this is defence-in-depth,
|
|
9
|
+
* not the authority.
|
|
10
|
+
*
|
|
11
|
+
* PHASE 2 (scope): after tot20 admits, we build a short-lived {@link StaffSelection}
|
|
12
|
+
* scoped to the returned staffRoles capability. It is stamped on the session as a
|
|
13
|
+
* DISTINCT marker from `tenants[]` — never a synthesized membership.
|
|
14
|
+
*/
|
|
15
|
+
import type { SessionRecord, StaffSelection } from "@/lib/auth/session";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Staff selection lifetime. Deliberately short: staff cross-vendor access is a
|
|
19
|
+
* deliberate, audited act, not a standing grant — re-entering re-audits.
|
|
20
|
+
*/
|
|
21
|
+
export const STAFF_SELECTION_TTL_SECONDS = 30 * 60;
|
|
22
|
+
|
|
23
|
+
/** True when the session carries ToT-staff scope (the admission-gate predicate). */
|
|
24
|
+
export function isStaffSession(
|
|
25
|
+
session: Pick<SessionRecord, "staff"> | null | undefined,
|
|
26
|
+
): boolean {
|
|
27
|
+
return !!session && Array.isArray(session.staff) && session.staff.length > 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Build the short-lived staff selection marker to stamp on the session. */
|
|
31
|
+
export function buildStaffSelection(input: {
|
|
32
|
+
resource: string;
|
|
33
|
+
capability: string;
|
|
34
|
+
nowSeconds: number;
|
|
35
|
+
ttlSeconds?: number;
|
|
36
|
+
}): StaffSelection {
|
|
37
|
+
const ttl = input.ttlSeconds && input.ttlSeconds > 0 ? input.ttlSeconds : STAFF_SELECTION_TTL_SECONDS;
|
|
38
|
+
return {
|
|
39
|
+
resource: input.resource.trim(),
|
|
40
|
+
capability: input.capability,
|
|
41
|
+
exp: input.nowSeconds + ttl,
|
|
42
|
+
};
|
|
43
|
+
}
|