@godaddy/gd-commerce-server 0.3.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.
- package/README.md +42 -0
- package/dist/index.js +14 -6
- 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) {
|
|
@@ -1435,6 +1437,9 @@ const skuGroupQuery = `
|
|
|
1435
1437
|
mediaObjects(first: 25) {
|
|
1436
1438
|
edges { node { url type } }
|
|
1437
1439
|
}
|
|
1440
|
+
attributeValues(first: 50) {
|
|
1441
|
+
edges { node { name } }
|
|
1442
|
+
}
|
|
1438
1443
|
inventoryCounts {
|
|
1439
1444
|
edges {
|
|
1440
1445
|
node {
|
|
@@ -1465,8 +1470,9 @@ async function readProduct(req, res) {
|
|
|
1465
1470
|
if (typeof productId !== "string" || !productId) throw new InvalidRequestError("Missing product id");
|
|
1466
1471
|
const config = readCommerceConfigForResponse(res);
|
|
1467
1472
|
assertCommerceCartScope(req, config);
|
|
1468
|
-
const { storeId, clientId, apiBaseUrl } = config;
|
|
1473
|
+
const { storeId, channelId, clientId, apiBaseUrl } = config;
|
|
1469
1474
|
const variables = buildSkuGroupVariables({
|
|
1475
|
+
channelId,
|
|
1470
1476
|
productId,
|
|
1471
1477
|
selectedAttributeValues: asStringArray$1(req.query.attributeValues),
|
|
1472
1478
|
skuGroupFirst: asNumber$1(req.query.skuGroupFirst)
|
|
@@ -1491,8 +1497,9 @@ var GET_default$3 = commerceRoute("Failed to load product", readProduct);
|
|
|
1491
1497
|
//#endregion
|
|
1492
1498
|
//#region src/server/api/commerce/products/GET.ts
|
|
1493
1499
|
const skuGroupsQuery = `
|
|
1494
|
-
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) {
|
|
1495
1501
|
skuGroups(
|
|
1502
|
+
channelId: { eq: $channelId }
|
|
1496
1503
|
first: $first
|
|
1497
1504
|
after: $after
|
|
1498
1505
|
id: $id
|
|
@@ -1590,8 +1597,9 @@ function asNumber(value) {
|
|
|
1590
1597
|
async function listProducts(req, res) {
|
|
1591
1598
|
const config = readCommerceConfigForResponse(res);
|
|
1592
1599
|
assertCommerceCartScope(req, config);
|
|
1593
|
-
const { storeId, clientId, apiBaseUrl } = config;
|
|
1600
|
+
const { storeId, channelId, clientId, apiBaseUrl } = config;
|
|
1594
1601
|
const variables = buildSkuGroupsVariables({
|
|
1602
|
+
channelId,
|
|
1595
1603
|
first: asNumber(req.query.first) ?? 24,
|
|
1596
1604
|
after: typeof req.query.after === "string" ? req.query.after : void 0,
|
|
1597
1605
|
searchQuery: typeof req.query.searchQuery === "string" ? req.query.searchQuery : void 0,
|