From a816a70599ae9d6cae044cb3622b1595584a195c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morel=20Se=CC=81bastien?= Date: Fri, 3 Jun 2022 13:13:51 -0700 Subject: [PATCH] :sparkles: js-storefrontaware-utils --- .../.github/PULL_REQUEST_TEMPLATE.md | 11 ++ .../.github/issue_template.md | 9 + .../.github/workflows/auto-close-issue.yaml | 21 ++ .../.github/workflows/auto-close-pr.yaml | 23 +++ .../.github/workflows/release.yaml | 44 +++++ .../js-storefrontaware-utils/.gitignore | 5 + .../js-storefrontaware-utils/.npmignore | 0 components/js-storefrontaware-utils/LICENSE | 21 ++ components/js-storefrontaware-utils/README.md | 11 ++ .../js-storefrontaware-utils/package.json | 30 +++ .../src/adapters/filesystem.server.ts | 11 ++ .../src/adapters/memory.server.ts | 7 + .../src/adapters/superfast.server.ts | 187 ++++++++++++++++++ .../src/storefront.server.ts | 17 ++ .../js-storefrontaware-utils/src/types.ts | 20 ++ .../tests/dummy.test.js | 3 + .../js-storefrontaware-utils/tsconfig.json | 11 ++ components/manifest.json | 4 + package.json | 3 +- 19 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 components/js-storefrontaware-utils/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 components/js-storefrontaware-utils/.github/issue_template.md create mode 100644 components/js-storefrontaware-utils/.github/workflows/auto-close-issue.yaml create mode 100644 components/js-storefrontaware-utils/.github/workflows/auto-close-pr.yaml create mode 100644 components/js-storefrontaware-utils/.github/workflows/release.yaml create mode 100644 components/js-storefrontaware-utils/.gitignore create mode 100644 components/js-storefrontaware-utils/.npmignore create mode 100644 components/js-storefrontaware-utils/LICENSE create mode 100644 components/js-storefrontaware-utils/README.md create mode 100644 components/js-storefrontaware-utils/package.json create mode 100644 components/js-storefrontaware-utils/src/adapters/filesystem.server.ts create mode 100644 components/js-storefrontaware-utils/src/adapters/memory.server.ts create mode 100644 components/js-storefrontaware-utils/src/adapters/superfast.server.ts create mode 100644 components/js-storefrontaware-utils/src/storefront.server.ts create mode 100644 components/js-storefrontaware-utils/src/types.ts create mode 100644 components/js-storefrontaware-utils/tests/dummy.test.js create mode 100644 components/js-storefrontaware-utils/tsconfig.json diff --git a/components/js-storefrontaware-utils/.github/PULL_REQUEST_TEMPLATE.md b/components/js-storefrontaware-utils/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..c722b50a --- /dev/null +++ b/components/js-storefrontaware-utils/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,11 @@ +Thanks for your pull request! We love contributions. + +However, this repository is what we call a "subtree split": a read-only copy of one directory of the main repository. It enables developers to depend on specific repository. + +If you want to contribute, you should instead open a pull request on the main repository: + +https://github.com/CrystallizeAPI/libraries + +Thank you for your contribution! + +PS: if you haven't already, please add tests. diff --git a/components/js-storefrontaware-utils/.github/issue_template.md b/components/js-storefrontaware-utils/.github/issue_template.md new file mode 100644 index 00000000..59300a07 --- /dev/null +++ b/components/js-storefrontaware-utils/.github/issue_template.md @@ -0,0 +1,9 @@ +Thanks for reporting an issue! We love feedback. + +However, this repository is what we call a "subtree split": a read-only copy of one directory of the main repository. It enables developers to depend on specific repository. + +If you want to report or contribute, you should instead open your issue on the main repository: + +https://github.com/CrystallizeAPI/libraries + +Thank you for your contribution! diff --git a/components/js-storefrontaware-utils/.github/workflows/auto-close-issue.yaml b/components/js-storefrontaware-utils/.github/workflows/auto-close-issue.yaml new file mode 100644 index 00000000..e7bee79b --- /dev/null +++ b/components/js-storefrontaware-utils/.github/workflows/auto-close-issue.yaml @@ -0,0 +1,21 @@ +on: + issues: + types: [opened, edited] + +jobs: + autoclose: + runs-on: ubuntu-latest + steps: + - name: Close Issue + uses: peter-evans/close-issue@v1 + with: + comment: | + Thanks for reporting an issue! We love feedback. + + However, this repository is what we call a "subtree split": a read-only copy of one directory of the main repository. It enables developers to depend on specific repository. + + If you want to report or contribute, you should instead open your issue on the main repository: + + https://github.com/CrystallizeAPI/libraries + + Thank you for your contribution! diff --git a/components/js-storefrontaware-utils/.github/workflows/auto-close-pr.yaml b/components/js-storefrontaware-utils/.github/workflows/auto-close-pr.yaml new file mode 100644 index 00000000..2df56416 --- /dev/null +++ b/components/js-storefrontaware-utils/.github/workflows/auto-close-pr.yaml @@ -0,0 +1,23 @@ +on: + pull_request: + types: [opened, edited, reopened] + +jobs: + autoclose: + runs-on: ubuntu-latest + steps: + - name: Close Pull Request + uses: peter-evans/close-pull@v1 + with: + comment: | + Thanks for your pull request! We love contributions. + + However, this repository is what we call a "subtree split": a read-only copy of one directory of the main repository. It enables developers to depend on specific repository. + + If you want to contribute, you should instead open a pull request on the main repository: + + https://github.com/CrystallizeAPI/libraries + + Thank you for your contribution! + + PS: if you haven't already, please add tests. diff --git a/components/js-storefrontaware-utils/.github/workflows/release.yaml b/components/js-storefrontaware-utils/.github/workflows/release.yaml new file mode 100644 index 00000000..2c744d22 --- /dev/null +++ b/components/js-storefrontaware-utils/.github/workflows/release.yaml @@ -0,0 +1,44 @@ +on: + push: + tags: + - '*' + +name: Release a New Version + +jobs: + releaseandpublish: + name: Release on Github and Publish on NPM + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout repo + uses: actions/checkout@v3 + + - name: ⎔ Setup node + uses: actions/setup-node@v3 + with: + node-version: 16 + always-auth: true + registry-url: 'https://registry.npmjs.org' + scope: '@crystallize' + + - name: 📥 Download deps + run: yarn install + + - name: 🏄 Run the tests + run: yarn build && yarn test + + - name: 🏷 Create Release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: false + prerelease: false + + - name: 📢 Publish to NPM + run: yarn publish --new-version ${GITHUB_REF#"refs/tags/"} --no-git-tag-version + env: + NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/components/js-storefrontaware-utils/.gitignore b/components/js-storefrontaware-utils/.gitignore new file mode 100644 index 00000000..70e72faa --- /dev/null +++ b/components/js-storefrontaware-utils/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ + +yarn.lock +package-lock.json diff --git a/components/js-storefrontaware-utils/.npmignore b/components/js-storefrontaware-utils/.npmignore new file mode 100644 index 00000000..e69de29b diff --git a/components/js-storefrontaware-utils/LICENSE b/components/js-storefrontaware-utils/LICENSE new file mode 100644 index 00000000..7c4d60fb --- /dev/null +++ b/components/js-storefrontaware-utils/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2022 Crystallize, https://crystallize.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/components/js-storefrontaware-utils/README.md b/components/js-storefrontaware-utils/README.md new file mode 100644 index 00000000..9e8728cb --- /dev/null +++ b/components/js-storefrontaware-utils/README.md @@ -0,0 +1,11 @@ +# Crystallize JS StoreFront Aware + +--- + +This repository is what we call a "subtree split": a read-only copy of one directory of the main repository. + +If you want to report or contribute, you should do it on the main repository: https://github.com/CrystallizeAPI/libraries + +--- + +Utils for MultiSiteFactory diff --git a/components/js-storefrontaware-utils/package.json b/components/js-storefrontaware-utils/package.json new file mode 100644 index 00000000..3dd98cae --- /dev/null +++ b/components/js-storefrontaware-utils/package.json @@ -0,0 +1,30 @@ +{ + "name": "@crystallize/js-storefrontaware-utils", + "license": "MIT", + "version": "0.1.0", + "author": "Crystallize (https://crystallize.com)", + "contributors": [ + "Sébastien Morel " + ], + "scripts": { + "watch": "yarn tsc -W", + "build": "yarn tsc", + "test": "jest", + "bump": "yarn tsc && yarn version --no-git-tag-version --new-version" + }, + "repository": { + "type": "git", + "url": "https://github.com/CrystallizeAPI/js-storefrontaware-utils.git" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "devDependencies": { + "@tsconfig/node16": "^1.0.2", + "@types/node": "^17.0.23", + "jest": "^27.5.1" + }, + "dependencies": { + "typescript": "^4.6.3", + "@crystallize/js-api-client": "*" + } +} diff --git a/components/js-storefrontaware-utils/src/adapters/filesystem.server.ts b/components/js-storefrontaware-utils/src/adapters/filesystem.server.ts new file mode 100644 index 00000000..7e955858 --- /dev/null +++ b/components/js-storefrontaware-utils/src/adapters/filesystem.server.ts @@ -0,0 +1,11 @@ +import { TStoreFrontAdapter, TStoreFrontConfig } from '../types'; +import fs from 'fs/promises'; + +export const createFilesystemAdapter = (filename: string): TStoreFrontAdapter => { + return { + config: async (withSecrets: boolean): Promise => { + const data = await fs.readFile(filename, { encoding: 'utf8' }); + return JSON.parse(data); + }, + }; +}; diff --git a/components/js-storefrontaware-utils/src/adapters/memory.server.ts b/components/js-storefrontaware-utils/src/adapters/memory.server.ts new file mode 100644 index 00000000..514b9e63 --- /dev/null +++ b/components/js-storefrontaware-utils/src/adapters/memory.server.ts @@ -0,0 +1,7 @@ +import { TStoreFrontAdapter, TStoreFrontConfig } from '../types'; + +export const createMemoryAdapter = (config: TStoreFrontConfig): TStoreFrontAdapter => { + return { + config: async (withSecrets: boolean): Promise => config, + }; +}; diff --git a/components/js-storefrontaware-utils/src/adapters/superfast.server.ts b/components/js-storefrontaware-utils/src/adapters/superfast.server.ts new file mode 100644 index 00000000..3578230c --- /dev/null +++ b/components/js-storefrontaware-utils/src/adapters/superfast.server.ts @@ -0,0 +1,187 @@ +import { ClientConfiguration, createClient } from '@crystallize/js-api-client'; +import crypto from 'crypto'; +import { TStoreFrontAdapter, TStoreFrontConfig } from '../types'; + +type TStorage = { + get: (key: string) => Promise; + set: (key: string, value: any, ttl: number) => Promise; +}; + +export const createSuperFastAdapter = ( + hostname: string, + credentials: ClientConfiguration, + storageClient: TStorage, + ttl: number, +): TStoreFrontAdapter => { + const memoryCache: Record< + string, + { + expiresAt: number; + value: any; + } + > = {}; + + return { + config: async (withSecrets: boolean): Promise => { + const domainkey = hostname.split('.')[0]; + if (memoryCache[domainkey]) { + if (memoryCache[domainkey].expiresAt > Date.now() / 1000) { + return memoryCache[domainkey].value; + } + } + + const hit = await storageClient.get(domainkey); + let config: TStoreFrontConfig | undefined = undefined; + + if (!hit) { + config = await fetchSuperFastConfig(domainkey, credentials); + memoryCache[domainkey] = { + expiresAt: Math.floor(Date.now() / 1000) + ttl, + value: config, + }; + await storageClient.set(domainkey, JSON.stringify(config), ttl); + } else { + config = await JSON.parse(hit); + memoryCache[domainkey] = { + expiresAt: Math.floor(Date.now() / 1000) + ttl, + value: config, + }; + } + + if (config !== undefined) { + if (withSecrets) { + config.configuration = cypher(`${process.env.ENCRYPTED_PARAMS_SECRET}`).decryptMap( + config.configuration, + ); + } + return config; + } + throw new Error('Impossible to fetch SuperFast config'); + }, + }; +}; + +async function fetchSuperFastConfig(domainkey: string, credentials: ClientConfiguration): Promise { + const query = `query { + catalogue(path:"/tenants/${domainkey}") { + name + components{ + id + content { + __typename + ...on SingleLineContent{ + text + } + ...on RichTextContent { + html + } + ...on SelectionContent { + options { + key + value + } + } + ...on BooleanContent { + value + } + ...on ImageContent { + firstImage{ + url + } + } + ...on PropertiesTableContent { + sections { + title + properties { + key + value + } + } + } + } + } + } +}`; + + const client = createClient(credentials); + const tenant = await client.catalogueApi(query); + const components = tenant.catalogue.components.reduce((result: any, component: any) => { + function toString(component: any): string | boolean { + switch (component?.content?.__typename) { + case 'SingleLineContent': + return component.content.text; + case 'RichTextContent': + return component.content.html.join(''); + case 'SelectionContent': + return component.content.options[0].key; + case 'BooleanContent': + return component.content.value; + case 'ImageContent': + return component.content.firstImage.url; + case 'PropertiesTableContent': + return component.content.sections.reduce((result: any, section: any) => { + section.properties.forEach((property: any) => { + result[property.key] = property.value; + }); + return result; + }, {}); + default: + return false; + } + } + return { + ...result, + [component.id]: toString(component), + }; + }, {}); + return { + identifier: domainkey, + tenantIdentifier: components['tenant-identifier'], + language: 'en', + storefront: components['storefront'], + logo: components['logos'], + theme: components['theme'], + configuration: components['configuration'], + }; +} + +const cypher = ( + secret: string, +): { + encrypt: (text: string) => string; + decrypt: (text: string) => string; + decryptMap: (map: { [key: string]: string }) => { [key: string]: string }; +} => { + const key = crypto.createHash('sha256').update(String(secret)).digest('base64').substring(0, 32); + const algorithm = 'aes-256-cbc'; + function encrypt(value: string): string { + const initVector = crypto.randomBytes(16); + const cipher = crypto.createCipheriv(algorithm, key, initVector); + let encryptedData = cipher.update(value, 'utf-8', 'hex'); + encryptedData += cipher.final('hex'); + return `${initVector.toString('hex')}:${encryptedData}`; + } + + function decrypt(value: string): string { + const [initVector, encryptedData] = value.split(':'); + const decipher = crypto.createDecipheriv(algorithm, key, Buffer.from(initVector, 'hex')); + let decryptedData = decipher.update(encryptedData, 'hex', 'utf-8'); + decryptedData += decipher.final('utf8'); + return decryptedData; + } + + return { + encrypt, + decrypt, + decryptMap: (map: { [key: string]: string }) => { + let result = {}; + Object.keys(map).forEach((key: string) => { + result = { + ...result, + [key]: decrypt(map[key]), + }; + }); + return result; + }, + }; +}; diff --git a/components/js-storefrontaware-utils/src/storefront.server.ts b/components/js-storefrontaware-utils/src/storefront.server.ts new file mode 100644 index 00000000..5ee4382b --- /dev/null +++ b/components/js-storefrontaware-utils/src/storefront.server.ts @@ -0,0 +1,17 @@ +import { createClient } from '@crystallize/js-api-client'; +import { TStoreFront, TStoreFrontAdapter } from './types'; + +export const createStoreFront = async ( + adapter: TStoreFrontAdapter, + withSecret: boolean = false, +): Promise => { + const config = await adapter.config(withSecret); + return { + config, + apiClient: createClient({ + tenantIdentifier: config.tenantIdentifier, + accessTokenId: withSecret ? config.configuration?.ACCESS_TOKEN_ID || '' : '', + accessTokenSecret: withSecret ? config.configuration?.ACCESS_TOKEN_SECRET || '' : '', + }), + }; +}; diff --git a/components/js-storefrontaware-utils/src/types.ts b/components/js-storefrontaware-utils/src/types.ts new file mode 100644 index 00000000..bb43b90c --- /dev/null +++ b/components/js-storefrontaware-utils/src/types.ts @@ -0,0 +1,20 @@ +import { ClientInterface } from '@crystallize/js-api-client'; + +export type TStoreFrontConfig = { + identifier: string; + tenantIdentifier: string; + language: string; + storefront: string; + logo: string; + theme: string; + configuration: Record; +}; + +export type TStoreFrontAdapter = { + config: (withSecrets: boolean) => Promise; +}; + +export type TStoreFront = { + config: TStoreFrontConfig; + apiClient: ClientInterface; +}; diff --git a/components/js-storefrontaware-utils/tests/dummy.test.js b/components/js-storefrontaware-utils/tests/dummy.test.js new file mode 100644 index 00000000..05077434 --- /dev/null +++ b/components/js-storefrontaware-utils/tests/dummy.test.js @@ -0,0 +1,3 @@ +test('Not implemented', async () => { + expect(1).toBe(1); +}); diff --git a/components/js-storefrontaware-utils/tsconfig.json b/components/js-storefrontaware-utils/tsconfig.json new file mode 100644 index 00000000..7f4de6a1 --- /dev/null +++ b/components/js-storefrontaware-utils/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tsconfig/node16/tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": true, + "lib": ["es2021", "DOM"], + "jsx": "react-jsx", + "sourceMap": true + }, + "include": ["./src/**/*"] +} diff --git a/components/manifest.json b/components/manifest.json index ec64ad0f..d0b7d821 100644 --- a/components/manifest.json +++ b/components/manifest.json @@ -20,6 +20,10 @@ "reactjs-components": { "name": "React JS Components", "git": "git@github.com:CrystallizeAPI/reactjs-components.git" + }, + "js-storefrontaware-utils": { + "name": "(Java|Type)Script Storefront Aware Utils", + "git": "git@github.com:CrystallizeAPI/js-storefrontaware-utils.git" } } } diff --git a/package.json b/package.json index 64162137..7e7fc7e4 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "private": true, "workspaces": [ "components/js-api-client", + "components/js-storefrontaware-utils", "components/reactjs-hooks", "components/reactjs-components", "components/node-service-api-router", @@ -12,7 +13,7 @@ "prettier": "2.6.0" }, "scripts": { - "watch": "concurrently 'cd components/js-api-client && yarn watch' 'cd components/reactjs-components && yarn watch' 'cd components/reactjs-hooks && yarn watch' 'cd components/node-service-api-request-handlers && yarn watch' 'cd components/node-service-api-router && yarn watch'" + "watch": "concurrently 'cd components/js-api-client && yarn watch' 'cd components/js-storefrontaware-utils && yarn watch' 'cd components/reactjs-components && yarn watch' 'cd components/reactjs-hooks && yarn watch' 'cd components/node-service-api-request-handlers && yarn watch' 'cd components/node-service-api-router && yarn watch'" }, "volta": { "node": "16.14.2",