Skip to content

Commit ddc1541

Browse files
authored
feat: productbus (#81)
* feat: add productbus entry schema * fix: schema changes * move cs support under its route * fix: handle 404 on unknown routes * allow json extension, schema tweaks * fix: formatting tweaks * fix: allow json extension in catalog get * handle images * chore: fix tests * add logs * more logs * more logs * test * get media filename from request * test * dont mutate execution context * cleanup * chore: fix postdeploy tests * chore: cleanup * cleanup * fix postdeploy test * chore: address comments * chore: fix postdeploy test
1 parent 7673c54 commit ddc1541

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+966
-1432
lines changed

package-lock.json

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,8 @@
6767
},
6868
"lint-staged": {
6969
"*.(js|cjs|mjs)": "eslint"
70+
},
71+
"dependencies": {
72+
"@adobe/helix-shared-process-queue": "^3.1.1"
7073
}
7174
}

src/index.js

+20-11
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { resolveConfig } from './utils/config.js';
1515
import handlers from './routes/index.js';
1616

1717
/**
18-
* @param {Request} req
18+
* @param {import("@cloudflare/workers-types/experimental").Request} req
1919
*/
2020
async function parseData(req) {
2121
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
@@ -40,22 +40,27 @@ async function parseData(req) {
4040
}
4141

4242
/**
43-
* @param {import("@cloudflare/workers-types/experimental").ExecutionContext} pctx
44-
* @param {Request} req
43+
* @param {import("@cloudflare/workers-types/experimental").ExecutionContext} eCtx
44+
* @param {import("@cloudflare/workers-types/experimental").Request} req
4545
* @param {Env} env
4646
* @returns {Promise<Context>}
4747
*/
48-
export async function makeContext(pctx, req, env) {
48+
export async function makeContext(eCtx, req, env) {
4949
/** @type {Context} */
5050
// @ts-ignore
51-
const ctx = pctx;
51+
const ctx = {
52+
executionContext: eCtx,
53+
};
5254
// @ts-ignore
5355
ctx.attributes = {};
5456
ctx.env = env;
5557
ctx.url = new URL(req.url);
5658
ctx.log = console;
59+
const filename = ctx.url.pathname.split('/').pop() ?? '';
5760
ctx.info = {
61+
filename,
5862
method: req.method.toUpperCase(),
63+
extension: filename.split('.').pop(),
5964
headers: Object.fromEntries(
6065
[...req.headers.entries()]
6166
.map(([k, v]) => [k.toLowerCase(), v]),
@@ -67,24 +72,28 @@ export async function makeContext(pctx, req, env) {
6772

6873
export default {
6974
/**
70-
* @param {Request} request
75+
* @param {import("@cloudflare/workers-types/experimental").Request} request
7176
* @param {Env} env
72-
* @param {import("@cloudflare/workers-types/experimental").ExecutionContext} pctx
77+
* @param {import("@cloudflare/workers-types/experimental").ExecutionContext} eCtx
7378
* @returns {Promise<Response>}
7479
*/
75-
async fetch(request, env, pctx) {
76-
const ctx = await makeContext(pctx, request, env);
80+
async fetch(request, env, eCtx) {
81+
const ctx = await makeContext(eCtx, request, env);
7782

7883
try {
7984
const overrides = Object.fromEntries(ctx.url.searchParams.entries());
8085
ctx.config = await resolveConfig(ctx, overrides);
8186

82-
console.debug('resolved config: ', JSON.stringify(ctx.config));
87+
console.debug('resolved config: ', JSON.stringify(ctx.config, null, 2));
8388
if (!ctx.config) {
8489
return errorResponse(404, 'config not found');
8590
}
8691

87-
return await handlers[ctx.config.route](ctx, request);
92+
const fn = handlers[ctx.config.route];
93+
if (!fn) {
94+
return errorResponse(404, 'route not found');
95+
}
96+
return await fn(ctx, request);
8897
} catch (e) {
8998
if (e.response) {
9099
return e.response;

src/routes/auth/handler.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@ import update from './update.js';
1616
import rotate from './rotate.js';
1717

1818
/**
19-
* @type {Record<string, Record<string, (ctx: Context, req: Request) => Promise<Response>>>}
19+
* @type {Record<
20+
* string,
21+
* Record<
22+
* string,
23+
* (
24+
* ctx: Context,
25+
* req: import("@cloudflare/workers-types/experimental").Request
26+
* ) => Promise<Response>
27+
* >
28+
* >}
2029
*/
2130
const handlers = {
2231
token: {
@@ -28,7 +37,7 @@ const handlers = {
2837

2938
/**
3039
* @param {Context} ctx
31-
* @param {Request} req
40+
* @param {import("@cloudflare/workers-types/experimental").Request} req
3241
* @returns {Promise<Response>}
3342
*/
3443
export default async function handler(ctx, req) {

src/routes/catalog/StorageClient.js

+8-14
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default class StorageClient {
4444
/**
4545
* Load product by SKU.
4646
* @param {string} sku - The SKU of the product.
47-
* @returns {Promise<Product>} - A promise that resolves to the product.
47+
* @returns {Promise<ProductBusEntry>} - A promise that resolves to the product.
4848
*/
4949
async fetchProduct(sku) {
5050
const {
@@ -68,19 +68,13 @@ export default class StorageClient {
6868
}
6969

7070
const productData = await object.json();
71-
productData.attributeMap = Object.fromEntries((productData.attributes ?? [])
72-
.map(({ name, value }) => [name, value]));
73-
(productData.variants ?? []).forEach((variant) => {
74-
variant.attributeMap = Object.fromEntries((variant.attributes ?? [])
75-
.map(({ name, value }) => [name, value]));
76-
});
77-
7871
return productData;
7972
}
8073

8174
/**
8275
* Save products in batches
83-
* @param {Product[]} products - The products to save.
76+
*
77+
* @param {ProductBusEntry[]} products - The products to save.
8478
* @returns {Promise<Partial<BatchResult>[]>} - Resolves with an array of save results.
8579
*/
8680
async saveProducts(products) {
@@ -94,7 +88,7 @@ export default class StorageClient {
9488

9589
/**
9690
* Handler function to process a batch of products.
97-
* @param {Product[]} batch - An array of products to save.
91+
* @param {ProductBusEntry[]} batch - An array of products to save.
9892
* @returns {Promise<Partial<BatchResult>[]>} - Resolves with an array of save results.
9993
*/
10094
async storeProductsBatch(batch) {
@@ -110,12 +104,12 @@ export default class StorageClient {
110104
} = this.ctx;
111105

112106
const storePromises = batch.map(async (product) => {
113-
const { sku, name, urlKey } = product;
107+
const { sku, title, urlKey } = product;
114108
const key = `${org}/${site}/${storeCode}/${storeViewCode}/products/${sku}.json`;
115109
const body = JSON.stringify(product);
116110

117111
try {
118-
const customMetadata = { sku, name };
112+
const customMetadata = { sku, title };
119113
if (urlKey) {
120114
customMetadata.urlKey = urlKey;
121115
}
@@ -130,7 +124,7 @@ export default class StorageClient {
130124
if (urlKey) {
131125
const metadataKey = `${org}/${site}/${storeCode}/${storeViewCode}/urlkeys/${urlKey}`;
132126
await env.CATALOG_BUCKET.put(metadataKey, '', {
133-
httpMetadata: { contentType: 'application/octet-stream' },
127+
httpMetadata: { contentType: 'text/plain' },
134128
customMetadata,
135129
});
136130
}
@@ -305,7 +299,7 @@ export default class StorageClient {
305299
/**
306300
* List all products from R2.
307301
* TODO: Setup pagination
308-
* @returns {Promise<Product[]>} - A promise that resolves to the products.
302+
* @returns {Promise<ProductBusEntry[]>} - A promise that resolves to the products.
309303
*/
310304
async listAllProducts() {
311305
const {

src/routes/catalog/fetch.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import StorageClient from './StorageClient.js';
1818
* @returns {Promise<Response>} - A promise that resolves to the product response.
1919
*/
2020
export default async function fetch(ctx) {
21+
const { sku } = ctx.config;
22+
2123
const storage = StorageClient.fromContext(ctx);
22-
const sku = ctx.url.pathname.split('/').pop();
2324
const product = await storage.fetchProduct(sku);
2425

25-
// TODO: use long ttl, add cache keys
2626
return new Response(JSON.stringify(product), {
2727
headers: { 'Content-Type': 'application/json' },
2828
});

src/routes/catalog/handler.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ import update from './update.js';
1717
import remove from './remove.js';
1818

1919
/**
20-
* @type {Record<string, Record<string, (ctx: Context, req: Request) => Promise<Response>>>}
20+
* @type {Record<
21+
* string,
22+
* Record<
23+
* string,
24+
* (
25+
* ctx: Context,
26+
* req: import("@cloudflare/workers-types/experimental").Request
27+
* ) => Promise<Response>
28+
* >
29+
* >
30+
* }
2131
*/
2232
const handlers = {
2333
lookup: {
@@ -39,7 +49,7 @@ const handlers = {
3949
/**
4050
* Handles productbus requests.
4151
* @param {Context} ctx - The context object containing request information and utilities.
42-
* @param {Request} request - The request object.
52+
* @param {import("@cloudflare/workers-types/experimental").Request} request - The request object.
4353
* @returns {Promise<Response>} - A promise that resolves to the catalog response.
4454
*/
4555
export default async function handler(ctx, request) {
@@ -60,7 +70,7 @@ export default async function handler(ctx, request) {
6070
storeCode,
6171
storeViewCode,
6272
subRoute,
63-
sku,
73+
sku: sku && sku.endsWith('.json') ? sku.slice(0, -5) : sku,
6474
});
6575

6676
const fn = handlers[subRoute]?.[method];

src/routes/catalog/update.js

+7-15
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,31 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import { assertValidProduct, hasUppercase } from '../../utils/product.js';
13+
import { assertValidProduct } from '../../utils/product.js';
1414
import { errorResponse } from '../../utils/http.js';
1515
import StorageClient from './StorageClient.js';
1616
import { assertAuthorization } from '../../utils/auth.js';
17-
17+
import { extractAndReplaceImages } from '../../utils/media.js';
1818
/**
1919
* Handles a PUT request to update a product.
2020
* @param {Context} ctx - The context object containing request information and utilities.
2121
* @returns {Promise<Response>} - A promise that resolves to the product response.
2222
*/
2323
export default async function update(ctx) {
24-
const { config, log, data: product } = ctx;
25-
24+
const { config, log, data } = ctx;
2625
if (config.sku === '*') {
2726
return errorResponse(501, 'not implemented');
2827
}
2928

30-
if (!product || typeof product !== 'object') {
31-
return errorResponse(400, 'invalid product data');
32-
}
33-
34-
if (hasUppercase(config.sku)) {
35-
return errorResponse(400, 'sku must be lowercase');
36-
}
29+
assertValidProduct(data);
3730

38-
if (config.sku !== product.sku) {
31+
if (config.sku !== data.sku) {
3932
return errorResponse(400, 'sku must match the product data');
4033
}
4134

42-
assertValidProduct(product);
43-
4435
await assertAuthorization(ctx);
4536

37+
const product = await extractAndReplaceImages(ctx, data);
4638
const storage = StorageClient.fromContext(ctx);
4739
const saveResults = await storage.saveProducts([product]);
4840

@@ -52,5 +44,5 @@ export default async function update(ctx) {
5244
timestamp: new Date().toISOString(),
5345
});
5446

55-
return new Response(JSON.stringify(saveResults), { status: 201 });
47+
return new Response(JSON.stringify({ ...saveResults, product }), { status: 201 });
5648
}

src/routes/content/adobe-commerce.js src/routes/content/adobe-commerce/index.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import { errorResponse, errorWithResponse, ffetch } from '../../utils/http.js';
13+
import { errorResponse, errorWithResponse, ffetch } from '../../../utils/http.js';
1414
import getProductQuery, { adapter as productAdapter } from './queries/cs-product.js';
1515
import getVariantsQuery, { adapter as variantsAdapter } from './queries/cs-variants.js';
1616
import getProductSKUQueryCore from './queries/core-product-sku.js';
1717
import getProductSKUQueryCS from './queries/cs-product-sku.js';
18-
import htmlTemplateFromContext from '../../templates/html/index.js';
18+
import htmlTemplateFromContext from './templates/html/index.js';
1919

2020
/**
2121
* @param {string} sku

src/routes/content/queries/core-product-sku.js src/routes/content/adobe-commerce/queries/core-product-sku.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import { gql } from '../../../utils/product.js';
13+
import { gql } from '../util.js';
1414

1515
/**
1616
* @param {{ urlkey: string; }} param0

src/routes/content/queries/core-product.js src/routes/content/adobe-commerce/queries/core-product.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import { gql } from '../../../utils/product.js';
13+
import { gql } from '../util.js';
1414

1515
/**
1616
* @param {{ urlkey?: string; sku?: string; }} param0

src/routes/content/queries/cs-product-sku.js src/routes/content/adobe-commerce/queries/cs-product-sku.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import { gql } from '../../../utils/product.js';
13+
import { gql } from '../util.js';
1414

1515
/**
1616
* @param {{ urlkey: string; }} param0

src/routes/content/queries/cs-product.js src/routes/content/adobe-commerce/queries/cs-product.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import { forceImagesHTTPS } from '../../../utils/http.js';
1413
import {
1514
gql,
1615
parseRating,
1716
parseSpecialToDate,
1817
sortImagesByRole,
19-
} from '../../../utils/product.js';
18+
forceImagesHTTPS,
19+
} from '../util.js';
2020

2121
function extractMinMaxPrice(data) {
2222
let minPrice = data.priceRange?.minimum ?? data.price;

src/routes/content/queries/cs-variants.js src/routes/content/adobe-commerce/queries/cs-variants.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13-
import { forceImagesHTTPS } from '../../../utils/http.js';
1413
import {
1514
gql,
1615
parseRating,
1716
parseSpecialToDate,
1817
sortImagesByRole,
19-
} from '../../../utils/product.js';
18+
forceImagesHTTPS,
19+
} from '../util.js';
2020

2121
/**
2222
* @param {Config} config

0 commit comments

Comments
 (0)