@godaddy/gd-commerce-server 0.4.0 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +42 -0
  2. package/dist/index.js +11 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -50,6 +50,16 @@ Hosts with their own configuration service can implement `CommerceConfiguration`
50
50
 
51
51
  The HTTP checkout route accepts an existing `draftOrderId` or a direct `skuId`; Commerce resolves catalog prices. It rejects `lineItemData`. For non-catalog charges such as fixed-price deposits, calculate the amount on the server and call `createCheckoutSession(params, configuration)` directly. Never forward browser-supplied prices to this trusted helper. Server callers can also use `getOrderStatus(orderId, configuration)` without an HTTP loopback.
52
52
 
53
+ ### Product channel visibility
54
+
55
+ `GET /products` and `GET /products/:id` use the channel from the server configuration and request only `ACTIVE` products. Search, category filters, and explicit product IDs retain that channel filter. Product-detail requests for products unavailable on that channel return 404. Browser query parameters cannot select a different channel. Products configured for all channels remain available when the configured channel is active and registered to the store.
56
+
57
+ If a configured `GODADDY_CHANNEL_ID` is inactive or is not registered to the configured store, the Catalog API returns an empty product list and product-detail requests return 404. These are successful empty results from the Catalog API, not configuration errors. When an expected catalog is empty, verify that the channel is active and registered to `GODADDY_STORE_ID`, then check product status and channel assignments. A missing `GODADDY_CHANNEL_ID` is different: the default configuration reader rejects it with 503 `not_configured`.
58
+
59
+ The Catalog API resolves active store channels through the paginated `/v1/commerce/channels` endpoint when evaluating a channel filter. Those lookups are not currently cached by the Catalog API, so channel-filtered catalog requests can add upstream calls and latency.
60
+
61
+ These checks control product browsing. `GET /skus/:id`, cart creation (`POST /cart`), and item addition (`POST /cart/:id/items`) forward SKU IDs without this package checking whether their products are available on the configured channel. A 404 from product details does not establish that a SKU lookup or cart write will be rejected. Hosts that require channel-based restrictions on these operations must enforce them before forwarding the SKU IDs; browsing filters are not an authorization boundary.
62
+
53
63
  ## Checkout return destinations
54
64
 
55
65
  Configure `checkoutReturnUrls` on the router using trusted deployment settings. Both lists contain complete absolute HTTPS URLs; scheme, origin, port, path, and configured query parameters must match. Success URLs may add a single `orderId` query parameter. Additional query parameters, fragments, credentials, relative URLs, and unlisted destinations are rejected. Configure separate cancel and success destinations as shown above; do not derive the allowlist from request headers or request bodies.
@@ -79,6 +89,38 @@ report `unbound` again only after it is deliberately disconnected. The storefron
79
89
  the generic sample products and local sample cart; this server never creates sample Commerce
80
90
  records or simulates checkout.
81
91
 
92
+ #### A site before its store connects
93
+
94
+ A template that shows the storefront's sample products before a store connects needs the catalog routes and an
95
+ explicit connection state. Here `COMMERCE_CONNECTION_STATE` is a host-owned setting, set to `unbound` when the site
96
+ is created and to `ready` by the host's connection flow. Read it from whatever records the connection; never derive
97
+ it from missing `GODADDY_*` variables.
98
+
99
+ ```ts
100
+ import express from 'express';
101
+ import { createCommerceCatalogRouter, createRuntimeCommerceConfiguration } from '@godaddy/gd-commerce-server';
102
+
103
+ const runtime = createRuntimeCommerceConfiguration();
104
+ const configuration = {
105
+ ...runtime,
106
+ readConnectionState() {
107
+ const state = process.env.COMMERCE_CONNECTION_STATE;
108
+ if (state === 'unbound' || state === 'connecting' || state === 'ready') return state;
109
+ throw new Error('COMMERCE_CONNECTION_STATE must be unbound, connecting, or ready.');
110
+ },
111
+ };
112
+
113
+ const app = express();
114
+ app.use(express.json());
115
+ // Before static files and any single-page app or SSR fallback, which would answer /api/commerce with HTML.
116
+ app.use('/api/commerce', createCommerceCatalogRouter(configuration));
117
+ app.use(express.static('dist'));
118
+ ```
119
+
120
+ `createCommerceCatalogRouter` exposes no checkout routes, so a site without checkout needs no `checkoutReturnUrls`.
121
+ `GET /api/commerce/config` should return `{"state":"unbound"}` as JSON. If it returns the app's HTML page, the
122
+ router is not mounted, or a fallback route runs before it.
123
+
82
124
  ## Errors
83
125
 
84
126
  Every route reports failures the same way. The body is `{ "error": "<customer-facing message>", "code": "<code>", "requestId": "<id>" }` (order-status also includes `success: false`). The same id is passed to the logger. Bodies never contain upstream messages, configuration details, or credentials.
package/dist/index.js CHANGED
@@ -1349,6 +1349,7 @@ function buildSkuGroupsVariables(input) {
1349
1349
  const categoryIds = input.categoryIds?.filter(Boolean) ?? [];
1350
1350
  const hasExplicitFilters = productIds.length > 0 || categoryIds.length > 0;
1351
1351
  return {
1352
+ channelId: input.channelId,
1352
1353
  ...input.first !== void 0 && { first: input.first },
1353
1354
  ...input.after && { after: input.after },
1354
1355
  ...productIds.length > 0 && { id: { in: [...productIds] } },
@@ -1356,9 +1357,10 @@ function buildSkuGroupsVariables(input) {
1356
1357
  ...!hasExplicitFilters && input.searchQuery && { label: { contains: input.searchQuery } }
1357
1358
  };
1358
1359
  }
1359
- function buildSkuGroupVariables({ productId, selectedAttributeValues, skuGroupFirst }) {
1360
+ function buildSkuGroupVariables({ channelId, productId, selectedAttributeValues, skuGroupFirst }) {
1360
1361
  const attributeValues = selectedAttributeValues ? [...selectedAttributeValues] : [];
1361
1362
  return {
1363
+ channelId,
1362
1364
  id: productId,
1363
1365
  attributeValues,
1364
1366
  ...!attributeValues.length && { first: skuGroupFirst ?? 50 }
@@ -1368,8 +1370,8 @@ function buildSkuGroupVariables({ productId, selectedAttributeValues, skuGroupFi
1368
1370
  //#endregion
1369
1371
  //#region src/server/api/commerce/products/[id]/GET.ts
1370
1372
  const skuGroupQuery = `
1371
- query SkuGroup($id: String!, $first: Int, $attributeValues: [String!] = []) {
1372
- activeSkuGroups: skuGroups(id: { in: [$id] }, status: { eq: "ACTIVE" }, first: 1) {
1373
+ query SkuGroup($channelId: String!, $id: String!, $first: Int, $attributeValues: [String!] = []) {
1374
+ activeSkuGroups: skuGroups(id: { in: [$id] }, channelId: { eq: $channelId }, status: { eq: "ACTIVE" }, first: 1) {
1373
1375
  edges { node { id } }
1374
1376
  }
1375
1377
  skuGroup(id: $id) {
@@ -1468,8 +1470,9 @@ async function readProduct(req, res) {
1468
1470
  if (typeof productId !== "string" || !productId) throw new InvalidRequestError("Missing product id");
1469
1471
  const config = readCommerceConfigForResponse(res);
1470
1472
  assertCommerceCartScope(req, config);
1471
- const { storeId, clientId, apiBaseUrl } = config;
1473
+ const { storeId, channelId, clientId, apiBaseUrl } = config;
1472
1474
  const variables = buildSkuGroupVariables({
1475
+ channelId,
1473
1476
  productId,
1474
1477
  selectedAttributeValues: asStringArray$1(req.query.attributeValues),
1475
1478
  skuGroupFirst: asNumber$1(req.query.skuGroupFirst)
@@ -1494,8 +1497,9 @@ var GET_default$3 = commerceRoute("Failed to load product", readProduct);
1494
1497
  //#endregion
1495
1498
  //#region src/server/api/commerce/products/GET.ts
1496
1499
  const skuGroupsQuery = `
1497
- query SkuGroups($first: Int, $after: String, $id: SKUGroupIdsFilter, $listId: ListIdFilter, $label: LabelFilter) {
1500
+ query SkuGroups($channelId: String!, $first: Int, $after: String, $id: SKUGroupIdsFilter, $listId: ListIdFilter, $label: LabelFilter) {
1498
1501
  skuGroups(
1502
+ channelId: { eq: $channelId }
1499
1503
  first: $first
1500
1504
  after: $after
1501
1505
  id: $id
@@ -1593,8 +1597,9 @@ function asNumber(value) {
1593
1597
  async function listProducts(req, res) {
1594
1598
  const config = readCommerceConfigForResponse(res);
1595
1599
  assertCommerceCartScope(req, config);
1596
- const { storeId, clientId, apiBaseUrl } = config;
1600
+ const { storeId, channelId, clientId, apiBaseUrl } = config;
1597
1601
  const variables = buildSkuGroupsVariables({
1602
+ channelId,
1598
1603
  first: asNumber(req.query.first) ?? 24,
1599
1604
  after: typeof req.query.after === "string" ? req.query.after : void 0,
1600
1605
  searchQuery: typeof req.query.searchQuery === "string" ? req.query.searchQuery : void 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@godaddy/gd-commerce-server",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Opinionated Express server integration for GoDaddy Commerce",
5
5
  "type": "module",
6
6
  "license": "MIT",