Skip to content

Commit c09b2e5

Browse files
authored
Merge pull request #4 from adobe-rnd/config
fix: configurable paths in config
2 parents 2d6cb5a + ecc5886 commit c09b2e5

File tree

6 files changed

+195
-103
lines changed

6 files changed

+195
-103
lines changed

src/config.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2024 Adobe. All rights reserved.
3+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License. You may obtain a copy
5+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
*
7+
* Unless required by applicable law or agreed to in writing, software distributed under
8+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9+
* OF ANY KIND, either express or implied. See the License for the specific language
10+
* governing permissions and limitations under the License.
11+
*/
12+
13+
/**
14+
* @type {Record<string, Record<string, Config>>}
15+
*/
16+
const TENANT_CONFIGS = {
17+
visualcomfort: {
18+
base: {
19+
apiKey: '59878b5d8af24fe9a354f523f5a0bb62',
20+
magentoEnvironmentId: '97034e45-43a5-48ab-91ab-c9b5a98623a8',
21+
magentoWebsiteCode: 'base',
22+
magentoStoreViewCode: 'default',
23+
coreEndpoint: 'https://www.visualcomfort.com/graphql',
24+
},
25+
'/us/p/{{urlkey}}/{{sku}}': {
26+
pageType: 'product',
27+
apiKey: '59878b5d8af24fe9a354f523f5a0bb62',
28+
magentoEnvironmentId: '97034e45-43a5-48ab-91ab-c9b5a98623a8',
29+
magentoWebsiteCode: 'base',
30+
magentoStoreViewCode: 'default',
31+
coreEndpoint: 'https://www.visualcomfort.com/graphql',
32+
},
33+
},
34+
};
35+
36+
/**
37+
* @param {string[]} patterns
38+
* @param {string} path
39+
*/
40+
function findOrderedMatches(patterns, path) {
41+
return patterns
42+
.map((pattern) => {
43+
const re = new RegExp(pattern.replace(/\{\{([^}]+)\}\}/g, '([^/]+)').replace(/\*/g, '([^/]+)'));
44+
const match = path.match(re);
45+
return match ? pattern : null;
46+
})
47+
.filter(Boolean)
48+
.sort((a, b) => a.length - b.length);
49+
}
50+
51+
function extractPathParams(pattern, path) {
52+
// create a RegExp with named groups from the string contained in '{{}}'
53+
const re = new RegExp(pattern.replace(/\{\{([^}]+)\}\}/g, '(?<$1>[^/]+)'));
54+
const match = path.match(re);
55+
return match ? match.groups : {};
56+
}
57+
58+
/**
59+
* @param {Context} ctx
60+
* @param {string} tenant
61+
* @param {Partial<Config>} [overrides={}]
62+
* @returns {Config|null}
63+
*/
64+
export function resolveConfig(ctx, tenant, overrides, configs = TENANT_CONFIGS) {
65+
const confMap = configs[tenant];
66+
if (!confMap) {
67+
return null;
68+
}
69+
70+
// order paths by preference
71+
const suffix = `/${ctx.url.pathname.split('/').slice(3).join('/')}`;
72+
const paths = findOrderedMatches(
73+
Object.keys(confMap).filter((p) => p !== 'base'),
74+
suffix,
75+
);
76+
77+
// merge configs
78+
return {
79+
...paths.reduce((conf, key) => ({
80+
...conf,
81+
...confMap[key],
82+
params: {
83+
...conf.params,
84+
...extractPathParams(key, suffix),
85+
},
86+
}), {
87+
...(confMap.base ?? {}),
88+
params: {},
89+
}),
90+
...overrides,
91+
};
92+
}

src/index.js

+62-85
Original file line numberDiff line numberDiff line change
@@ -11,44 +11,16 @@
1111
*/
1212
// @ts-check
1313

14-
import { errorResponse, makeContext } from './util.js';
14+
import { errorResponse, errorWithResponse, makeContext } from './util.js';
1515
import getProductQueryCS from './queries/cs-product.js';
1616
import getProductQueryCore from './queries/core-product.js';
1717
import HTML_TEMPLATE from './templates/html.js';
18+
import { resolveConfig } from './config.js';
1819

1920
/**
20-
* @type {Record<string, Config>}
21-
*/
22-
const TENANT_CONFIGS = {
23-
visualcomfort: {
24-
apiKey: '59878b5d8af24fe9a354f523f5a0bb62',
25-
magentoEnvironmentId: '97034e45-43a5-48ab-91ab-c9b5a98623a8',
26-
magentoWebsiteCode: 'base',
27-
magentoStoreViewCode: 'default',
28-
coreEndpoint: 'https://www.visualcomfort.com/graphql',
29-
},
30-
};
31-
32-
/**
33-
* @param {string} tenant
34-
* @param {Partial<Config>} [overrides={}]
35-
* @returns {Config|null}
36-
*/
37-
function lookupConfig(tenant, overrides) {
38-
if (!TENANT_CONFIGS[tenant]) {
39-
return null;
40-
}
41-
// @ts-ignore
42-
return {
43-
...TENANT_CONFIGS[tenant],
44-
...overrides,
45-
};
46-
}
47-
48-
/**
49-
* @param {string} sku
50-
* @param {Config} config
51-
*/
21+
* @param {string} sku
22+
* @param {Config} config
23+
*/
5224
async function fetchProductCS(sku, config) {
5325
const query = getProductQueryCS({ sku });
5426

@@ -62,32 +34,32 @@ async function fetchProductCS(sku, config) {
6234
},
6335
});
6436
if (!resp.ok) {
65-
console.warn('failed to fetch product: ', resp.status);
66-
return resp;
37+
console.warn('failed to fetch product: ', resp.status, resp.statusText);
38+
throw errorWithResponse(resp.status, 'failed to fetch product');
6739
}
6840

6941
const json = await resp.json();
7042
try {
7143
const [product] = json.data.products;
7244
if (!product) {
73-
return errorResponse(404, 'could not find product', json.errors);
45+
throw errorWithResponse(404, 'could not find product', json.errors);
7446
}
7547
return product;
7648
} catch (e) {
7749
console.error('failed to parse product: ', e);
78-
return errorResponse(500, 'failed to parse product response');
50+
throw errorWithResponse(500, 'failed to parse product response');
7951
}
8052
}
8153

8254
/**
83-
* @param {{ urlKey: string } | { sku: string }} opt
55+
* @param {{ urlkey: string } | { sku: string }} opt
8456
* @param {Config} config
8557
*/
8658
// eslint-disable-next-line no-unused-vars
8759
async function fetchProductCore(opt, config) {
8860
const query = getProductQueryCore(opt);
8961
if (!config.coreEndpoint) {
90-
return errorResponse(400, 'coreEndpoint not configured');
62+
throw errorWithResponse(400, 'coreEndpoint not configured');
9163
}
9264

9365
const resp = await fetch(`${config.coreEndpoint}?query=${encodeURIComponent(query)}`, {
@@ -100,47 +72,36 @@ async function fetchProductCore(opt, config) {
10072
},
10173
});
10274
if (!resp.ok) {
103-
console.warn('failed to fetch product: ', resp.status);
104-
return resp;
75+
console.warn('failed to fetch product: ', resp.status, resp.statusText);
76+
throw errorWithResponse(resp.status, 'failed to fetch product');
10577
}
10678

10779
const json = await resp.json();
10880
try {
10981
const [product] = json.data.products;
11082
if (!product) {
111-
return errorResponse(404, 'could not find product', json.errors);
83+
throw errorWithResponse(404, 'could not find product', json.errors);
11284
}
11385
return product;
11486
} catch (e) {
11587
console.error('failed to parse product: ', e);
116-
return errorResponse(500, 'failed to parse product response');
88+
throw errorWithResponse(500, 'failed to parse product response');
11789
}
11890
}
11991

120-
function resolvePDPTemplate(product) {
121-
return HTML_TEMPLATE(product);
122-
}
123-
12492
/**
125-
* @param {Context} ctx
126-
*/
127-
async function handlePDPRequest(ctx) {
128-
// TODO: pull from config
129-
// eslint-disable-next-line no-unused-vars
130-
const [_, tenant, _route, _geo, _pageType, urlKey, sku] = ctx.url.pathname.split('/');
131-
if (!sku) {
132-
return errorResponse(404, 'missing sku');
133-
}
134-
135-
const overrides = Object.fromEntries(ctx.url.searchParams.entries());
136-
const config = lookupConfig(tenant, overrides);
137-
if (!config) {
138-
return errorResponse(404, 'config not found');
93+
* @param {Context} ctx
94+
* @param {Config} config
95+
*/
96+
async function handlePDPRequest(ctx, config) {
97+
const { sku, urlkey } = config.params;
98+
if (!sku && !urlkey) {
99+
return errorResponse(404, 'missing sku or urlkey');
139100
}
140101

141102
// const product = await fetchProductCore({ sku }, config);
142103
const product = await fetchProductCS(sku.toUpperCase(), config);
143-
const html = resolvePDPTemplate(product);
104+
const html = HTML_TEMPLATE(product);
144105
return new Response(html, {
145106
status: 200,
146107
headers: {
@@ -150,38 +111,54 @@ async function handlePDPRequest(ctx) {
150111
}
151112

152113
/**
153-
* @param {Context} ctx
154-
*/
155-
async function handleContentRequest(ctx) {
156-
const [pageType] = ctx.url.pathname.split('/').slice(4);
157-
switch (pageType) {
158-
case 'product':
159-
return handlePDPRequest(ctx);
160-
default:
161-
return errorResponse(404, 'unknown content subroute');
162-
}
163-
}
114+
* @type {Record<string, (ctx: Context, config: Config) => Promise<Response>>}
115+
*/
116+
const handlers = {
117+
content: async (ctx, config) => {
118+
if (config.pageType !== 'product') {
119+
return errorResponse(404, 'page type not supported');
120+
}
121+
return handlePDPRequest(ctx, config);
122+
},
123+
// eslint-disable-next-line no-unused-vars
124+
graphql: async (ctx, config) => errorResponse(501, 'not implemented'),
125+
};
164126

165127
export default {
166128
/**
167-
*
168-
* @param {Request} request
169-
* @param {Record<string, string>} env
170-
* @param {import("@cloudflare/workers-types/experimental").ExecutionContext} pctx
171-
* @returns {Promise<Response>}
172-
*/
129+
* @param {Request} request
130+
* @param {Record<string, string>} env
131+
* @param {import("@cloudflare/workers-types/experimental").ExecutionContext} pctx
132+
* @returns {Promise<Response>}
133+
*/
173134
async fetch(request, env, pctx) {
174135
const ctx = makeContext(pctx, request, env);
136+
if (ctx.info.method !== 'GET') {
137+
return errorResponse(405, 'method not allowed');
138+
}
139+
175140
const [_, tenant, route] = ctx.url.pathname.split('/');
176141
if (!tenant) {
177-
return errorResponse(400, 'missing tenant');
142+
return errorResponse(404, 'missing tenant');
143+
}
144+
if (!route) {
145+
return errorResponse(404, 'missing route');
146+
}
147+
148+
const overrides = Object.fromEntries(ctx.url.searchParams.entries());
149+
const config = resolveConfig(ctx, tenant, overrides);
150+
if (!config) {
151+
return errorResponse(404, 'config not found');
178152
}
179153

180-
switch (route) {
181-
case 'content':
182-
return handleContentRequest(ctx);
183-
default:
184-
return errorResponse(404, 'no route found');
154+
try {
155+
return handlers[route](ctx, config);
156+
} catch (e) {
157+
if (e.response) {
158+
return e.response;
159+
}
160+
ctx.log.error(e);
161+
return errorResponse(500, 'internal server error');
185162
}
186163
},
187164
};

src/queries/core-product.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
import { gql } from '../util.js';
1414

1515
/**
16-
* @param {{ urlKey?: string; sku?: string; }} param0
16+
* @param {{ urlkey?: string; sku?: string; }} param0
1717
*/
18-
export default ({ urlKey, sku }) => gql`{
18+
export default ({ urlkey, sku }) => gql`{
1919
products(
20-
filter: { ${urlKey ? 'url_key' : 'sku'}: { eq: "${urlKey ?? sku}" } }
20+
filter: { ${urlkey ? 'url_key' : 'sku'}: { eq: "${urlkey ?? sku}" } }
2121
) {
2222
items {
2323
sku

src/templates/html.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export default (product) => {
5555
<script src="/scripts/aem.js" type="module"></script>
5656
<script src="/scripts/scripts.js" type="module"></script>
5757
<link rel="stylesheet" href="/styles/styles.css">
58-
<script type="ld+json">
58+
<script type="application/ld+json">
5959
${jsonLd}
6060
</script>
6161
</head>

src/types.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import type { ExecutionContext } from "@cloudflare/workers-types/experimental";
22

33
declare global {
44
export interface Config {
5+
pageType: 'product' | string;
56
apiKey: string;
67
magentoEnvironmentId: string;
78
magentoWebsiteCode: string;
89
magentoStoreViewCode: string;
910
coreEndpoint: string;
11+
params: Record<string, string>;
1012
}
1113

1214
export interface Product {
@@ -24,6 +26,10 @@ declare global {
2426
url: URL;
2527
env: Record<string, string>;
2628
log: Console;
29+
info: {
30+
method: string;
31+
headers: Record<string, string>;
32+
}
2733
}
2834
}
2935

0 commit comments

Comments
 (0)