Skip to content

Commit

Permalink
fix: use env in catalog route
Browse files Browse the repository at this point in the history
  • Loading branch information
maxakuru committed Oct 21, 2024
1 parent b733899 commit 0959827
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 97 deletions.
8 changes: 4 additions & 4 deletions src/catalog/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ export default async function catalogHandler(ctx, config, request) {
return errorResponse(400, 'Invalid URL: Missing "catalog" segment');
}

if (catalogIndex === -1 || pathSegments.length < catalogIndex + 5) {
return errorResponse(400, 'Invalid URL structure: Expected format: /catalog/{env}/{store}/{storeView}/product/{sku}');
if (pathSegments.length < catalogIndex + 4) {
return errorResponse(400, 'Invalid URL structure: Expected format: /{org}/{site}/{env}/catalog/{store}/{storeView}/product/{sku}');
}

const [env, storeCode, storeViewCode, subRoute, sku] = pathSegments.slice(catalogIndex + 1);
const [storeCode, storeViewCode, subRoute, sku] = pathSegments.slice(catalogIndex + 1);

Object.assign(config, {
env, storeCode, storeViewCode, subRoute, sku,
storeCode, storeViewCode, subRoute, sku,
});

if (subRoute === 'lookup') {
Expand Down
5 changes: 3 additions & 2 deletions src/catalog/lookup.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ export async function handleProductLookupRequest(ctx, config) {
const { search } = ctx.url;
const params = new URLSearchParams(search);

if (params.has('urlKey')) {
const sku = await lookupSku(ctx, config, params.get('urlKey'));
if (params.has('urlKey') || params.has('urlkey')) {
const urlkey = params.get('urlKey') || params.get('urlkey');
const sku = await lookupSku(ctx, config, urlkey);
const product = await fetchProduct(ctx, config, sku);
return new Response(JSON.stringify(product), {
headers: { 'Content-Type': 'application/json' },
Expand Down
21 changes: 13 additions & 8 deletions src/catalog/update.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,26 @@ export async function handleProductSaveRequest(ctx, config, request) {
const product = await putProduct(ctx, config, requestBody);
const products = [product];

const matchedKeys = Object.keys(config.confMap)
.filter((key) => config.confMap[key].env === config.env);
const matchedPathPatterns = Object.entries(config.confEnvMap)
.reduce((acc, [env, confMap]) => {
if (env === config.env) {
acc.push(...Object.keys(confMap));
}
return acc;
}, []);

for (const purgeProduct of products) {
for (const key of matchedKeys) {
let path = key.replace('{{sku}}', purgeProduct.sku);
for (const pattern of matchedPathPatterns) {
let path = pattern.replace('{{sku}}', purgeProduct.sku);

if (key.includes('{{urlkey}}') && purgeProduct.urlKey) {
if (path.includes('{{urlkey}}') && purgeProduct.urlKey) {
path = path.replace('{{urlkey}}', purgeProduct.urlKey);
}

for (const env of ['preview', 'live']) {
const response = await callAdmin(config, env, path, { method: 'post' });
for (const op of ['preview', 'live']) {
const response = await callAdmin(config, op, path, { method: 'post' });
if (!response.ok) {
return errorResponse(400, `failed to ${env} product`);
return errorResponse(400, `failed to ${op} product`);
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,10 @@ export async function resolveConfig(ctx, overrides = {}) {
headers: confMap.base?.headers ?? {},
params: {},
}),
confMap,
confEnvMap,
org,
site,
env,
route,
...overrides,
};
Expand Down
2 changes: 1 addition & 1 deletion src/content/adobe-commerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ async function lookupProductSKU(urlkey, config) {
/**
* @param {Context} ctx
* @param {Config} config
* @returns {Promise<Response>}
*/
// @ts-ignore
export async function handle(ctx, config) {
const { urlkey } = config.params;
let { sku } = config.params;
Expand Down
5 changes: 5 additions & 0 deletions src/content/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ import { handle as handleHelixCommerce } from './helix-commerce.js';

const ALLOWED_METHODS = ['GET'];

/**
* @param {Context} ctx
* @param {Config} config
* @returns {Promise<Response>}
*/
export default async function contentHandler(ctx, config) {
if (!ALLOWED_METHODS.includes(ctx.info.method)) {
return errorResponse(405, 'method not allowed');
Expand Down
5 changes: 5 additions & 0 deletions src/content/helix-commerce.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import { errorResponse } from '../utils/http.js';
import { fetchProduct } from '../utils/r2.js';
import HTML_TEMPLATE from '../templates/html.js';

/**
* @param {Context} ctx
* @param {Config} config
* @returns {Promise<Response>}
*/
export async function handle(ctx, config) {
const { urlkey } = config.params;
const { sku } = config.params;
Expand Down
8 changes: 4 additions & 4 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@

import { errorResponse } from './utils/http.js';
import { resolveConfig } from './config.js';
import contentHandler from './content/handler.js';
import catalogHandler from './catalog/handler.js';
import content from './content/handler.js';
import catalog from './catalog/handler.js';

/**
* @type {Record<string, (ctx: Context, config: Config, request: Request) => Promise<Response>>}
*/
const handlers = {
content: async (ctx, config) => contentHandler(ctx, config),
catalog: async (ctx, config, request) => catalogHandler(ctx, config, request),
content,
catalog,
// eslint-disable-next-line no-unused-vars
graphql: async (ctx, config) => errorResponse(501, 'not implemented'),
};
Expand Down
2 changes: 1 addition & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ declare global {
catalogSource: string
catalogEndpoint?: string;
sku?: string;
confMap: ConfigMap;
confEnvMap: ConfigEnvMap;
params: Record<string, string>;
headers: Record<string, string>;
}
Expand Down
48 changes: 22 additions & 26 deletions src/utils/r2.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,12 @@ export async function lookupSku(ctx, config, urlKey) {
export async function listAllProducts(ctx, config) {
const bucket = ctx.env.CATALOG_BUCKET;

const listResponse = await bucket.list({ prefix: `${config.org}/${config.site}/${config.env}/${config.storeCode}/${config.storeViewCode}/` });
const listResponse = await bucket.list({ prefix: `${config.org}/${config.site}/${config.env}/${config.storeCode}/${config.storeViewCode}/products/` });
const files = listResponse.objects;

const batchSize = 50; // Define the batch size
const customMetadataArray = [];

const excludeDirectory = `${config.org}/${config.site}/${config.env}/${config.storeCode}/${config.storeViewCode}/urlkeys/`;

// Helper function to split the array into chunks of a specific size
function chunkArray(array, size) {
const result = [];
Expand All @@ -139,29 +137,27 @@ export async function listAllProducts(ctx, config) {
for (const chunk of fileChunks) {
// Run the requests for this chunk in parallel
const chunkResults = await Promise.all(
chunk
.filter((file) => !file.key.startsWith(excludeDirectory))
.map(async (file) => {
const objectKey = file.key;

// Fetch the head response for each file
const headResponse = await bucket.head(objectKey);

if (headResponse) {
const { customMetadata } = headResponse;
const { sku } = customMetadata;
return {
...customMetadata,
links: {
product: `${ctx.url.origin}/${config.org}/${config.site}/catalog/${config.env}/${config.storeCode}/${config.storeViewCode}/product/${sku}`,
},
};
} else {
return {
fileName: objectKey,
};
}
}),
chunk.map(async (file) => {
const objectKey = file.key;

// Fetch the head response for each file
const headResponse = await bucket.head(objectKey);

if (headResponse) {
const { customMetadata } = headResponse;
const { sku } = customMetadata;
return {
...customMetadata,
links: {
product: `${ctx.url.origin}/${config.org}/${config.site}/${config.env}/catalog/${config.storeCode}/${config.storeViewCode}/product/${sku}`,
},
};
} else {
return {
fileName: objectKey,
};
}
}),
);

// Append the results of this chunk to the overall results array
Expand Down
16 changes: 8 additions & 8 deletions test/catalog/handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('catalogHandler Tests', () => {
it('should return 405 when method is not allowed', async () => {
const ctx = {
info: { method: 'DELETE' },
url: { pathname: '/catalog/stage/store/view/product/sku' },
url: { pathname: '/org/site/env/catalog/store/view/product/sku' },
};
const config = {};
const request = {};
Expand All @@ -59,7 +59,7 @@ describe('catalogHandler Tests', () => {
it('should return 400 when URL is missing "catalog" segment', async () => {
const ctx = {
info: { method: 'GET' },
url: { pathname: '/store/view/product/sku' },
url: { pathname: '/org/site/env/store/view/product/sku' },
};
const config = {};
const request = {};
Expand All @@ -76,7 +76,7 @@ describe('catalogHandler Tests', () => {
it('should return 400 when URL structure is incorrect', async () => {
const ctx = {
info: { method: 'GET' },
url: { pathname: '/catalog/stage/store/view' },
url: { pathname: '/org/site/env/catalog/store/view' },
};
const config = {};
const request = {};
Expand All @@ -87,13 +87,13 @@ describe('catalogHandler Tests', () => {
const response = await catalogHandler(ctx, config, request);

assert.equal(response.status, 400);
assert(errorResponseStub.calledWith(400, 'Invalid URL structure: Expected format: /catalog/{env}/{store}/{storeView}/product/{sku}'));
assert(errorResponseStub.calledWith(400, 'Invalid URL structure: Expected format: /{org}/{site}/{env}/catalog/{store}/{storeView}/product/{sku}'));
});

it('should call handleProductLookupRequest when method is GET and subRoute is "lookup"', async () => {
const ctx = {
info: { method: 'GET' },
url: { pathname: '/catalog/stage/store/view/lookup/sku' },
url: { pathname: '/org/site/env/catalog/store/view/lookup/sku' },
};
const config = {};
const request = {};
Expand All @@ -110,7 +110,7 @@ describe('catalogHandler Tests', () => {
it('should return 405 if subRoute is "lookup" but method is not GET', async () => {
const ctx = {
info: { method: 'PUT' },
url: { pathname: '/catalog/stage/store/view/lookup/sku' },
url: { pathname: '/org/site/env/catalog/store/view/lookup/sku' },
};
const config = {};
const request = {};
Expand All @@ -127,7 +127,7 @@ describe('catalogHandler Tests', () => {
it('should call handleProductSaveRequest when method is PUT and subRoute is not "lookup"', async () => {
const ctx = {
info: { method: 'PUT' },
url: { pathname: '/catalog/stage/store/view/product/sku' },
url: { pathname: '/org/site/stage/catalog/store/view/product/sku' },
};
const config = {};
const request = {};
Expand All @@ -144,7 +144,7 @@ describe('catalogHandler Tests', () => {
it('should call handleProductFetchRequest when method is GET and subRoute is not "lookup"', async () => {
const ctx = {
info: { method: 'GET' },
url: { pathname: '/catalog/stage/store/view/product/sku' },
url: { pathname: '/org/site/stage/catalog/store/view/product/sku' },
};
const config = {};
const request = {};
Expand Down
20 changes: 20 additions & 0 deletions test/catalog/lookup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,26 @@ describe('handleProductLookupRequest Tests', () => {
assert(fetchProductStub.calledOnceWith(ctx, config, '1234'));
});

it('should return a product when urlkey is provided', async () => {
const ctx = {
url: { search: '?urlkey=some-url-key' },
log: { error: sinon.stub() },
};
const config = {};

lookupSkuStub.resolves('1234');
fetchProductStub.resolves({ sku: '1234', name: 'Test Product' });

const response = await handleProductLookupRequest(ctx, config);

assert.equal(response.status, 200);
const responseBody = await response.json();
assert.deepEqual(responseBody, { sku: '1234', name: 'Test Product' });

assert(lookupSkuStub.calledOnceWith(ctx, config, 'some-url-key'));
assert(fetchProductStub.calledOnceWith(ctx, config, '1234'));
});

it('should return a list of all products when no urlKey is provided', async () => {
const ctx = {
url: { search: '' },
Expand Down
22 changes: 18 additions & 4 deletions test/catalog/update.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import { strict as assert } from 'assert';
import sinon from 'sinon';
import esmock from 'esmock';
Expand Down Expand Up @@ -84,7 +85,12 @@ describe('Product Save Tests', () => {
it('should return 201 when product is successfully saved and paths are purged', async () => {
const config = {
sku: '1234',
confMap: { '/path/to/{{sku}}': { env: 'test' }, '/path/to/{{urlkey}}/{{sku}}': { env: 'test' } },
confEnvMap: {
test: {
'/path/to/{{sku}}': { env: 'test' },
'/path/to/{{urlkey}}/{{sku}}': { env: 'test' },
},
},
env: 'test',
};
const ctx = { log: { error: sinon.stub() } };
Expand All @@ -103,7 +109,11 @@ describe('Product Save Tests', () => {
it('should skip calling callAdmin when confMap has no matching keys', async () => {
const config = {
sku: '1234',
confMap: { '/path/to/{{sku}}': { env: 'other-env' } },
confEnvMap: {
'other-env': {
'/path/to/{{sku}}': { env: 'other-env' },
},
},
env: 'test',
};
const ctx = { log: { error: sinon.stub() } };
Expand All @@ -119,7 +129,11 @@ describe('Product Save Tests', () => {
it('should return error when callAdmin fails', async () => {
const config = {
sku: '1234',
confMap: { '/path/to/{{sku}}': { env: 'test' } },
confEnvMap: {
test: {
'/path/to/{{sku}}': { env: 'test' },
},
},
env: 'test',
};
const ctx = { log: { error: sinon.stub() } };
Expand All @@ -140,7 +154,7 @@ describe('Product Save Tests', () => {
});

it('should return 400 if request.json throws a JSON parsing error', async () => {
const config = { sku: '1234', confMap: {} };
const config = { sku: '1234', confEnvMap: {} };
const ctx = { log: { error: sinon.stub() } };
const request = { json: sinon.stub().rejects(new Error('Unexpected token < in JSON at position 0')) };

Expand Down
Loading

0 comments on commit 0959827

Please sign in to comment.