From 61196f166b4dbb6a20d7b0444205f8970b4fba57 Mon Sep 17 00:00:00 2001 From: Max Edell Date: Mon, 9 Sep 2024 17:01:21 -0700 Subject: [PATCH] fix: moving to core query --- src/index.js | 293 +++++++----------------------------- src/queries/core-product.js | 56 +++++++ src/queries/cs-product.js | 140 +++++++++++++++++ src/templates/html.js | 106 +++++++++++++ src/templates/json-ld.js | 62 ++++++++ src/types.d.ts | 5 +- src/util.js | 56 +++++++ 7 files changed, 476 insertions(+), 242 deletions(-) create mode 100644 src/queries/core-product.js create mode 100644 src/queries/cs-product.js create mode 100644 src/templates/html.js create mode 100644 src/templates/json-ld.js create mode 100644 src/util.js diff --git a/src/index.js b/src/index.js index 1f4ce9a..d24606b 100644 --- a/src/index.js +++ b/src/index.js @@ -9,9 +9,13 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ - // @ts-check +import { errorResponse, makeContext } from './util.js'; +import getProductQueryCS from './queries/cs-product.js'; +import getProductQueryCore from './queries/core-product.js'; +import HTML_TEMPLATE from './templates/html.js'; + /** * @type {Record} */ @@ -21,54 +25,10 @@ const TENANT_CONFIGS = { magentoEnvironmentId: '97034e45-43a5-48ab-91ab-c9b5a98623a8', magentoWebsiteCode: 'base', magentoStoreViewCode: 'default', + coreEndpoint: 'https://www.visualcomfort.com/graphql', }, }; -/** -* @param {TemplateStringsArray} strs -* @param {...any} params -* @returns {string} -*/ -export function gql(strs, ...params) { - let res = ''; - strs.forEach((s, i) => { - res += s; - if (i < params.length) { - res += params[i]; - } - }); - return res.replace(/(\\r\\n|\\n|\\r)/gm, ' ').replace(/\s+/g, ' ').trim(); -} - -/** -* @param {number} status -* @param {string} xError -* @param {string|Record} [body=''] -* @returns -*/ -function errorResponse(status, xError, body = '') { - return new Response(typeof body === 'object' ? JSON.stringify(body) : body, { - status, - headers: { 'x-error': xError }, - }); -} - -/** -* @param {import("@cloudflare/workers-types/experimental").ExecutionContext} pctx -* @param {Request} req -* @param {Record} env -* @returns {Context} -*/ -function makeContext(pctx, req, env) { - /** @type {Context} */ - // @ts-ignore - const ctx = pctx; - ctx.env = env; - ctx.url = new URL(req.url); - ctx.log = console; - return ctx; -} - /** * @param {string} tenant * @param {Partial} [overrides={}] @@ -89,140 +49,50 @@ function lookupConfig(tenant, overrides) { * @param {string} sku * @param {Config} config */ -async function fetchProduct(sku, config) { - const query = gql`{ - products( - skus: ["${sku}"] - ) { - __typename - id - sku - name - metaTitle - metaDescription - metaKeyword - description - url - urlKey - shortDescription - url - addToCartAllowed - inStock - images(roles: []) { - url - label - roles - __typename - } - attributes(roles: []) { - name - label - value - roles - __typename - } - ... on SimpleProductView { - price { - final { - amount { - value - currency - __typename - } - __typename - } - regular { - amount { - value - currency - __typename - } - __typename - } - roles - __typename - } - __typename - } - ... on ComplexProductView { - options { - id - title - required - values { - id - title - ... on ProductViewOptionValueProduct { - product { - sku - name - __typename - } - __typename - } - ... on ProductViewOptionValueSwatch { - type - value - __typename - } - __typename - } - __typename - } - priceRange { - maximum { - final { - amount { - value - currency - __typename - } - __typename - } - regular { - amount { - value - currency - __typename - } - __typename - } - roles - __typename - } - minimum { - final { - amount { - value - currency - __typename - } - __typename - } - regular { - amount { - value - currency - __typename - } - __typename - } - roles - __typename - } - __typename - } - __typename - } - } - }`; +async function fetchProductCS(sku, config) { + const query = getProductQueryCS({ sku }); const resp = await fetch(`https://catalog-service.adobe.io/graphql?query=${encodeURIComponent(query)}`, { - // method: 'POST', - // body: query, headers: { - origin: 'https://adobecommerce.live', - // 'content-type':'application/json', + origin: 'https://api.adobecommerce.live', + 'x-api-key': config.apiKey, + 'Magento-Environment-Id': config.magentoEnvironmentId, + 'Magento-Website-Code': config.magentoWebsiteCode, + 'Magento-Store-View-Code': config.magentoStoreViewCode, + }, + }); + if (!resp.ok) { + console.warn('failed to fetch product: ', resp.status); + return resp; + } + + const json = await resp.json(); + try { + const [product] = json.data.products; + if (!product) { + return errorResponse(404, 'could not find product', json.errors); + } + return product; + } catch (e) { + console.error('failed to parse product: ', e); + return errorResponse(500, 'failed to parse product response'); + } +} + +/** + * @param {{ urlKey: string } | { sku: string }} opt + * @param {Config} config + */ +// eslint-disable-next-line no-unused-vars +async function fetchProductCore(opt, config) { + const query = getProductQueryCore(opt); + if (!config.coreEndpoint) { + return errorResponse(400, 'coreEndpoint not configured'); + } + + const resp = await fetch(`${config.coreEndpoint}?query=${encodeURIComponent(query)}`, { + headers: { + origin: 'https://api.adobecommerce.live', 'x-api-key': config.apiKey, 'Magento-Environment-Id': config.magentoEnvironmentId, 'Magento-Website-Code': config.magentoWebsiteCode, @@ -248,68 +118,7 @@ async function fetchProduct(sku, config) { } function resolvePDPTemplate(product) { - return /* html */` - - - - ${product.metaTitle || product.name} - - - - - - - - - - - - - -
-
-
-

${product.name}

- -
- ${product.attributes.map((attr) => ` -
-
${attr.name}
-
${attr.label}
-
${attr.value}
-
`).join('\n')} -
-
- ${product.options.map((opt) => ` -
-
${opt.id}
-
${opt.title}
-
${opt.required === true ? 'required' : ''}
-
- ${opt.values.map((val) => ` -
-
${val.id}
-
${val.title}
-
`).join('\n')}`).join('\n')} -
-
-
-
- - - `; + return HTML_TEMPLATE(product); } /** @@ -322,12 +131,14 @@ async function handlePDPRequest(ctx) { return errorResponse(404, 'missing sku'); } - const config = lookupConfig(tenant, {}); // TODO: allow config overrides from query params + const overrides = Object.fromEntries(ctx.url.searchParams.entries()); + const config = lookupConfig(tenant, overrides); if (!config) { return errorResponse(404, 'config not found'); } - const product = await fetchProduct(sku, config); + // const product = await fetchProductCore({ sku }, config); + const product = await fetchProductCS(sku, config); const html = resolvePDPTemplate(product); return new Response(html, { status: 200, diff --git a/src/queries/core-product.js b/src/queries/core-product.js new file mode 100644 index 0000000..89fc047 --- /dev/null +++ b/src/queries/core-product.js @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { gql } from '../util.js'; + +/** + * @param {{ urlKey?: string; sku?: string; }} param0 + */ +export default ({ urlKey, sku }) => gql`{ + products( + filter: { ${urlKey ? 'url_key' : 'sku'}: { eq: "${urlKey ?? sku}" } } + ) { + items { + sku + name + meta_title + meta_keyword + meta_description + short_description { + html + } + description { + html + } + image { + url + label + disabled + } + thumbnail { + url + label + } + media_gallery { + url + label + } + categories { + category_seo_name + breadcrumbs { + category_name + category_level + } + } + } + } +}`; diff --git a/src/queries/cs-product.js b/src/queries/cs-product.js new file mode 100644 index 0000000..d0ae1d7 --- /dev/null +++ b/src/queries/cs-product.js @@ -0,0 +1,140 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { gql } from '../util.js'; + +export default ({ sku }) => gql`{ + products( + skus: ["${sku}"] + ) { + __typename + id + sku + name + metaTitle + metaDescription + metaKeyword + description + url + urlKey + shortDescription + url + addToCartAllowed + inStock + images(roles: []) { + url + label + roles + __typename + } + attributes(roles: []) { + name + label + value + roles + __typename + } + ... on SimpleProductView { + price { + final { + amount { + value + currency + __typename + } + __typename + } + regular { + amount { + value + currency + __typename + } + __typename + } + roles + __typename + } + __typename + } + ... on ComplexProductView { + options { + id + title + required + values { + id + title + ... on ProductViewOptionValueProduct { + product { + sku + name + __typename + } + __typename + } + ... on ProductViewOptionValueSwatch { + type + value + __typename + } + __typename + } + __typename + } + priceRange { + maximum { + final { + amount { + value + currency + __typename + } + __typename + } + regular { + amount { + value + currency + __typename + } + __typename + } + roles + __typename + } + minimum { + final { + amount { + value + currency + __typename + } + __typename + } + regular { + amount { + value + currency + __typename + } + __typename + } + roles + __typename + } + __typename + } + __typename + } + } + }`; diff --git a/src/templates/html.js b/src/templates/html.js new file mode 100644 index 0000000..ac3fa98 --- /dev/null +++ b/src/templates/html.js @@ -0,0 +1,106 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +// @ts-check + +import JSON_LD_TEMPLATE from './json-ld.js'; + +export default (product) => { + const { + sku, + name, + metaTitle, + metaDescription, + description, + images, + attributes, + options, + } = product; + + const jsonLd = JSON_LD_TEMPLATE({ + sku, + description: description ?? metaDescription, + image: images[0].url, + name, + // TODO: add following... + url: '', + brandName: '', + reviewCount: 0, + ratingValue: 0, + }); + + return /* html */`\ + + + + ${metaTitle || name} + + + + + + + + + + + + + + + + +
+
+
+

${name}

+ +
+ ${attributes.map((attr) => ` +
+
${attr.name}
+
${attr.label}
+
${attr.value}
+
`).join('\n')} +
+
+ ${options.map((opt) => ` +
+
${opt.id}
+
${opt.title}
+
${opt.required === true ? 'required' : ''}
+
+ ${opt.values.map((val) => ` +
+
${val.id}
+
${val.title}
+
`).join('\n')}`).join('\n')} +
+
+
+
+ + `; +}; diff --git a/src/templates/json-ld.js b/src/templates/json-ld.js new file mode 100644 index 0000000..d0fc5a8 --- /dev/null +++ b/src/templates/json-ld.js @@ -0,0 +1,62 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// @ts-check + +/** + * @param {{ +* sku: string; +* url: string; +* description: string; +* image: string; +* name: string; +* brandName: string; +* reviewCount: number; +* ratingValue: number; +* }} param0 +* @returns {string} +*/ +export default ({ + sku, + url, + name, + description, + image, + brandName, + reviewCount, + ratingValue, +}) => JSON.stringify({ + '@context': 'http://schema.org', + '@type': 'Product', + '@id': url, + name, + sku, + description, + image, + productID: sku, + brand: { + '@type': 'Brand', + name: brandName, + }, + offers: [], + ...(typeof reviewCount === 'number' + && typeof ratingValue === 'number' + && reviewCount > 0 + ? { + aggregateRating: { + '@type': 'AggregateRating', + ratingValue, + reviewCount, + }, + } + : {}), +}); diff --git a/src/types.d.ts b/src/types.d.ts index 1a97904..aed002a 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -6,6 +6,7 @@ declare global { magentoEnvironmentId: string; magentoWebsiteCode: string; magentoStoreViewCode: string; + coreEndpoint: string; } export interface Product { @@ -24,4 +25,6 @@ declare global { env: Record; log: Console; } -} \ No newline at end of file +} + +export { }; \ No newline at end of file diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..7974fc7 --- /dev/null +++ b/src/util.js @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** +* @param {TemplateStringsArray} strs +* @param {...any} params +* @returns {string} +*/ +export function gql(strs, ...params) { + let res = ''; + strs.forEach((s, i) => { + res += s; + if (i < params.length) { + res += params[i]; + } + }); + return res.replace(/(\\r\\n|\\n|\\r)/gm, ' ').replace(/\s+/g, ' ').trim(); +} + +/** +* @param {number} status +* @param {string} xError +* @param {string|Record} [body=''] +* @returns +*/ +export function errorResponse(status, xError, body = '') { + return new Response(typeof body === 'object' ? JSON.stringify(body) : body, { + status, + headers: { 'x-error': xError }, + }); +} + +/** +* @param {import("@cloudflare/workers-types/experimental").ExecutionContext} pctx +* @param {Request} req +* @param {Record} env +* @returns {Context} +*/ +export function makeContext(pctx, req, env) { + /** @type {Context} */ + // @ts-ignore + const ctx = pctx; + ctx.env = env; + ctx.url = new URL(req.url); + ctx.log = console; + return ctx; +}