├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.yaml ├── .gitattributes ├── .gitignore ├── .jest-runner-eslintrc ├── .jest-runner-testrc.js ├── .jestrc.lint.json ├── .jestrc.test.json ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .renovaterc.json ├── README.md ├── codecov.yml ├── lerna.json ├── package.json ├── packages ├── example-inventory │ ├── 001-hello-world │ │ ├── README.md │ │ └── index.js │ ├── 002-connecting-ct │ │ ├── README.md │ │ ├── config.json │ │ └── index.js │ ├── README.md │ └── package.json └── extension-base │ ├── README.md │ ├── ctp-extension-azure-adapters.js │ ├── ctp-extension-azure-adapters.spec.js │ ├── ctp-extension-gcf-adapters.js │ ├── ctp-extension-gcf-adapters.spec.js │ ├── ctp-extension-lambda-adapters.js │ ├── function.json │ ├── index.js │ └── package.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | node6Environment: &node6Environment 2 | docker: 3 | - image: circleci/node:6 4 | working_directory: ~/nodejs 5 | node8Environment: &node8Environment 6 | docker: 7 | - image: circleci/node:8 8 | working_directory: ~/nodejs 9 | 10 | aliases: 11 | - &restore_yarn_cache 12 | keys: 13 | - v1-yarn-cache-{{ checksum "yarn.lock" }} 14 | - v1-yarn-cache 15 | 16 | - &save_yarn_cache 17 | key: v1-yarn-cache-{{ checksum "yarn.lock" }} 18 | paths: 19 | - node_modules 20 | 21 | - &yarn_install 22 | name: Installing 23 | # Ignoring scripts (e.g. post-install) to gain more control 24 | # in the jobs to only e.g. build when explicitely needed. 25 | command: yarn install --pure-lockfile --ignore-scripts 26 | 27 | - &yarn_bootstrap 28 | name: Bootstraping 29 | # Limiting the default concurrency (4) of lerna to 2 30 | # as the build otherwise dies due to resouce restrictions. 31 | command: yarn bootstrap --concurrency=2 32 | 33 | - &yarn_build 34 | name: Building 35 | command: yarn build 36 | 37 | - &unit_test 38 | name: Unit testing 39 | # Limiting the workers of Jest to 10 40 | # as the build otherwise dies due to resouce restrictions. 41 | command: yarn test:ci --maxWorkers=10 42 | 43 | - &unit_test_with_coverage 44 | name: Unit testing (with coverage report) 45 | command: yarn test:coverage:ci 46 | 47 | version: 2 48 | jobs: 49 | lint: 50 | <<: *node8Environment 51 | steps: 52 | - checkout 53 | - restore-cache: *restore_yarn_cache 54 | - run: *yarn_install 55 | - run: *yarn_bootstrap 56 | - run: *yarn_build 57 | - save_cache: *save_yarn_cache 58 | - run: 59 | name: Linting 60 | command: yarn lint 61 | test_unit_node_6: 62 | <<: *node6Environment 63 | steps: 64 | - checkout 65 | - restore-cache: *restore_yarn_cache 66 | - run: *yarn_install 67 | - run: *yarn_bootstrap 68 | - run: *yarn_build 69 | - save_cache: *save_yarn_cache 70 | - run: *unit_test 71 | test_unit_node_8: 72 | <<: *node8Environment 73 | steps: 74 | - checkout 75 | - restore-cache: *restore_yarn_cache 76 | - run: *yarn_install 77 | - run: *yarn_bootstrap 78 | - run: *yarn_build 79 | - save_cache: *save_yarn_cache 80 | - run: *unit_test_with_coverage 81 | 82 | workflows: 83 | version: 2 84 | build_and_test: 85 | jobs: 86 | - lint 87 | - test_unit_node_6: 88 | requires: 89 | - lint 90 | - test_unit_node_8: 91 | requires: 92 | - lint 93 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://github.com/editorconfig/editorconfig 2 | 3 | # Top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | # Set default charset to utf-8 8 | charset = utf-8 9 | # Set default indentation to spaces 10 | indent_style = space 11 | # Linux-style newlines with a newline ending every file 12 | end_of_line = lf 13 | insert_final_newline = true 14 | # Remove whitespace characters preceding newline characters 15 | trim_trailing_whitespace = true 16 | 17 | # Two space indentation for JavaScript files 18 | [*.{js,json}] 19 | indent_size = 2 20 | 21 | # Disable trimming trailing whitespaces so that double space newlines work 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/* 2 | **/coverage/* 3 | dist/* 4 | **/dist/* 5 | lib/* 6 | **/lib/* 7 | target/* 8 | **/target/* 9 | scripts/* 10 | **/scripts/* 11 | node_modules/* 12 | **/node_modules/* 13 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | parser: babel-eslint 3 | parserOptions: 4 | ecmaVersion: 8 5 | sourceType: module 6 | extends: 7 | - airbnb-base 8 | - plugin:jest/recommended 9 | - prettier 10 | plugins: 11 | - jest 12 | - prettier 13 | env: 14 | es6: true 15 | browser: true 16 | jest: true 17 | node: true 18 | rules: 19 | import/no-extraneous-dependencies: 20 | - error 21 | prettier/prettier: 22 | - error 23 | - trailingComma: es5 24 | singleQuote: true 25 | jest/no-disabled-tests: warn 26 | jest/no-focused-tests: error 27 | jest/no-identical-title: error 28 | jest/valid-expect: error 29 | no-underscore-dangle: 0 30 | prefer-destructuring: 0 31 | settings: 32 | import/resolver: 33 | babel-module: {} 34 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Env files 9 | *.env 10 | # Dependencies 11 | node_modules 12 | .npm 13 | 14 | # Integration tests dependencies cache 15 | *.tgz 16 | package-lock.json 17 | 18 | dist 19 | lib 20 | coverage 21 | 22 | # OS clutter 23 | .DS_Store 24 | 25 | # IDE tools 26 | .idea 27 | -------------------------------------------------------------------------------- /.jest-runner-eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "cliOptions": { 3 | "format": "node_modules/eslint-formatter-pretty" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.jest-runner-testrc.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/commercetools/commercetools-serverless-examples/2949bf792ddfb27faeb2106d132df3eac36ca930/.jest-runner-testrc.js -------------------------------------------------------------------------------- /.jestrc.lint.json: -------------------------------------------------------------------------------- 1 | { 2 | "runner": "jest-runner-eslint", 3 | "displayName": "lint", 4 | "testMatch": ["/packages/**/*.js"] 5 | } 6 | -------------------------------------------------------------------------------- /.jestrc.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "displayName": "test", 3 | "setupFiles": [], 4 | "setupTestFrameworkScriptFile": "./.jest-runner-testrc.js", 5 | "snapshotSerializers": [], 6 | "testRegex": "\\.spec\\.js$", 7 | "testEnvironment": "jsdom", 8 | "testPathIgnorePatterns": [ 9 | "/node_modules/", 10 | "/packages/.*/node_modules", 11 | "/packages/.*/dist" 12 | ], 13 | "coveragePathIgnorePatterns": ["/node_modules/"] 14 | } 15 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.renovaterc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":pinOnlyDevDependencies", 5 | "group:monorepos", 6 | "schedule:weekly" 7 | ], 8 | "statusCheckVerify": true 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | commercetools logo 4 | 5 | Serverless Microservice Examples 6 |

7 | 8 |

9 | 10 | CircleCI Status 11 | 12 | 13 | Codecov Coverage Status 14 | 15 | Made with Coffee 16 |

17 | 18 | This repository will act as a basic guide to getting started writing serverless microservices that interact with commercetools. 19 | 20 | We'll provide examples for iron.io functions, google cloud functions and AWS Lambda. 21 | 22 | ## Packages 23 | 24 | | Package | Version | Dependencies | 25 | | -------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | 26 | | [`example-inventory`](/packages/example-inventory) | [![example-inventory Version][example-inventory-icon]][example-inventory-version] | [![example-inventory Dependencies Status][example-inventory-dependencies-icon]][example-inventory-dependencies] | 27 | | [`extension-base`](/packages/extension-base) | [![extension-base Version][extension-base-icon]][extension-base-version] | [![extension-base Dependencies Status][extension-base-dependencies-icon]][extension-base-dependencies] | 28 | 29 | [example-inventory-version]: https://www.npmjs.com/package/@commercetools/example-inventory 30 | [example-inventory-icon]: https://img.shields.io/npm/v/@commercetools/example-inventory.svg?style=flat-square 31 | [example-inventory-dependencies]: https://david-dm.org/commercetools/commercetools-serverless-examples?path=packages/example-inventory 32 | [example-inventory-dependencies-icon]: https://img.shields.io/david/commercetools/commercetools-serverless-examples.svg?path=packages/example-inventory&style=flat-square 33 | [extension-base-version]: https://www.npmjs.com/package/@commercetools/extension-base 34 | [extension-base-icon]: https://img.shields.io/npm/v/@commercetools/extension-base.svg?style=flat-square 35 | [extension-base-dependencies]: https://david-dm.org/commercetools/commercetools-serverless-examples?path=packages/extension-base 36 | [extension-base-dependencies-icon]: https://img.shields.io/david/commercetools/commercetools-serverless-examples.svg?path=packages/extension-base&style=flat-square 37 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | layout: "reach, diff, flags, files" 3 | behavior: default 4 | require_changes: false 5 | require_base: no 6 | require_head: yes 7 | branches: null 8 | coverage: 9 | status: 10 | project: 11 | default: 12 | target: 80% 13 | patch: 14 | default: 15 | target: 80% 16 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.7.1", 3 | "version": "independent", 4 | "changelog": { 5 | "rootPath": ".", 6 | "repo": "commercetools/commercetools-serverless-examples", 7 | "labels": { 8 | "Type: New Feature": ":rocket: New Feature", 9 | "Type: Breaking Change": ":boom: Breaking Change", 10 | "Type: Bug": ":bug: Bug Fix", 11 | "Type: Enhancement": ":nail_care: Enhancement", 12 | "Type: Documentation": ":memo: Documentation", 13 | "Type: Maintenance": ":house: Maintenance" 14 | }, 15 | "cacheDir": ".changelog", 16 | "ignoreCommitters": ["renovate", "renovate-bot"] 17 | }, 18 | "command": { 19 | "publish": { 20 | "message": "chore(release): releasing package" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "MIT", 4 | "engines": { 5 | "node": ">=6", 6 | "npm": ">=5" 7 | }, 8 | "scripts": { 9 | "postinstall": "check-node-version --package --print && npm run bootstrap && cross-env NODE_ENV=development npm run build", 10 | "bootstrap": "lerna bootstrap", 11 | "build": "lerna run --parallel build", 12 | "develop": "jest --projects .jestrc.*.json --watch", 13 | "precommit": "lint-staged", 14 | "lint": "jest --config .jestrc.lint.json", 15 | "test": "cross-env NODE_ENV=test jest --config .jestrc.test.json", 16 | "test:watch": "cross-env NODE_ENV=test npm test -- --watch", 17 | "test:ci": "cross-env NODE_ENV=test npm test -- --no-watchman --maxWorkers=10", 18 | "test:ci:coverage": "cross-env NODE_ENV=test npm test -- --no-watchman --coverage && codecov", 19 | "format": "npm run format:js && npm run format:md", 20 | "format:js": "prettier --write 'packages/**/*.js'", 21 | "format:md": "prettier --parser markdown --write '*.md'", 22 | "fix:eslint": "eslint --fix --format=node_modules/eslint-formatter-pretty", 23 | "test:coverage:ci": "npm run test:ci -- --coverage && codecov", 24 | "release": "cross-env NODE_ENV=cli npm run build && lerna publish --registry=https://registry.npmjs.org/" 25 | }, 26 | "devDependencies": { 27 | "babel-cli": "6.26.0", 28 | "babel-core": "6.26.0", 29 | "babel-eslint": "8.2.2", 30 | "babel-jest": "22.4.3", 31 | "babel-plugin-array-includes": "2.0.3", 32 | "babel-plugin-external-helpers": "6.22.0", 33 | "babel-plugin-module-resolver": "3.1.1", 34 | "babel-plugin-transform-export-extensions": "6.22.0", 35 | "babel-plugin-transform-flow-strip-types": "6.22.0", 36 | "babel-plugin-transform-object-entries": "1.0.0", 37 | "babel-plugin-transform-object-rest-spread": "6.26.0", 38 | "babel-preset-env": "1.6.1", 39 | "babel-preset-stage-0": "6.24.1", 40 | "check-node-version": "3.2.0", 41 | "codecov": "3.0.0", 42 | "commitizen": "2.9.6", 43 | "common-tags": "1.7.2", 44 | "cross-env": "5.1.4", 45 | "cz-lerna-changelog": "1.2.1", 46 | "eslint": "4.19.1", 47 | "eslint-config-airbnb": "16.1.0", 48 | "eslint-config-airbnb-base": "12.1.0", 49 | "eslint-config-prettier": "2.9.0", 50 | "eslint-formatter-pretty": "1.3.0", 51 | "eslint-import-resolver-babel-module": "4.0.0", 52 | "eslint-plugin-import": "2.9.0", 53 | "eslint-plugin-jest": "21.15.0", 54 | "eslint-plugin-jsx-a11y": "6.0.3", 55 | "eslint-plugin-prettier": "2.6.0", 56 | "eslint-plugin-react": "7.7.0", 57 | "husky": "0.14.3", 58 | "jest": "22.4.3", 59 | "jest-runner-eslint": "0.4.0", 60 | "lerna": "2.9.0", 61 | "lint-staged": "7.0.0", 62 | "prettier": "1.11.1", 63 | "rimraf": "2.6.2", 64 | "rollup": "0.57.1", 65 | "rollup-plugin-babel": "3.0.3", 66 | "rollup-plugin-commonjs": "9.1.0", 67 | "rollup-plugin-filesize": "1.5.0", 68 | "rollup-plugin-flow": "1.1.1", 69 | "rollup-plugin-includepaths": "0.2.2", 70 | "rollup-plugin-json": "2.3.0", 71 | "rollup-plugin-node-resolve": "3.3.0", 72 | "rollup-plugin-replace": "2.0.0", 73 | "rollup-plugin-uglify": "3.0.0", 74 | "rollup-watch": "4.3.1" 75 | }, 76 | "workspaces": ["packages/*"], 77 | "lint-staged": { 78 | "packages/**/*.js": ["npm run fix:eslint", "npm run format:js", "git add"], 79 | "*.md": ["npm run format:md", "git add"] 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/example-inventory/001-hello-world/README.md: -------------------------------------------------------------------------------- 1 | In this example, we're just attempting to get a basic serverless function set up and echo back a random number when a sku is provided 2 | 3 | Simply copy and paste the provided index.js and package.json to the Google Cloud Functions editor at https://console.cloud.google.com/functions/add 4 | 5 | You may want to set the Trigger to HTTP trigger, and take note of the URL to call your microservice. It will look like https://{region}-{project}.cloudfunctions.net/{function-name} 6 | 7 | Once you've created your function, and its been deployed, go ahead and visit the trigger url. 8 | 9 | You'll end up at a 404 with the following body: No sku defined! 10 | 11 | Now if you supply a sku (via a request body), you'll see a random number returned 12 | If you don't know how to do this, try the following in a terminal: 13 | 14 | curl --data "sku=fun-product-123" {triggerurl} 15 | 16 | Great! You've just created your first serverless microservice! Let's hook up commercetools in the next example. 17 | -------------------------------------------------------------------------------- /packages/example-inventory/001-hello-world/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Responds to any HTTP request that can provide a "sku" field in the body. 3 | * 4 | * @param {!Object} req Cloud Function request context. 5 | * @param {!Object} res Cloud Function response context. 6 | */ 7 | exports.helloWorld = function helloWorld(req, res) { 8 | // Example input: {"sku": "cool-product-123"} 9 | if (req.body.sku === undefined) { 10 | // This is an error case, as "sku" is required. 11 | res.status(400).send('No sku defined!'); 12 | } else { 13 | // Everything is okay. 14 | // eslint-disable-next-line no-console 15 | console.log(req.body.sku); 16 | res.status(200).send(`Inventory: ${parseInt(Math.random() * 1000, 10)}`); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /packages/example-inventory/002-connecting-ct/README.md: -------------------------------------------------------------------------------- 1 | In this example we're going to build ontop of the previous example and connect to commercetools. 2 | 3 | You can either edit your existing function, or create a new one. 4 | 5 | First, you'll need to edit config.json and provide your commercetools client information. 6 | 7 | Now that you've modified that file, you can upload the code to google cloud functions. 8 | This time, instead of editing inline, you will need to create a zip file of config.json, index.js and package.json and send it to google cloud functions. 9 | 10 | Try running your function with a valid and invalid sku! 11 | -------------------------------------------------------------------------------- /packages/example-inventory/002-connecting-ct/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "CT_CLIENT_ID": "", 3 | "CT_CLIENT_SECRET": "", 4 | "CT_PROJECT_KEY": "", 5 | "CT_SCOPES": "", 6 | "CT_AUTH_HOST": "", 7 | "CT_API_HOST": "" 8 | } 9 | -------------------------------------------------------------------------------- /packages/example-inventory/002-connecting-ct/index.js: -------------------------------------------------------------------------------- 1 | // require CT sdk libraries 2 | const createClient = require('@commercetools/sdk-client').createClient; 3 | const createHttpMiddleware = require('@commercetools/sdk-middleware-http') 4 | .createHttpMiddleware; 5 | const createQueueMiddleware = require('@commercetools/sdk-middleware-queue') 6 | .createQueueMiddleware; 7 | const createAuthMiddlewareForClientCredentialsFlow = require('@commercetools/sdk-middleware-auth') 8 | .createAuthMiddlewareForClientCredentialsFlow; 9 | const createRequestBuilder = require('@commercetools/api-request-builder') 10 | .createRequestBuilder; 11 | const createLoggerMiddleware = require('@commercetools/sdk-middleware-logger') 12 | .createLoggerMiddleware; 13 | 14 | // import config from config.json 15 | const config = require('./config.json'); 16 | 17 | /** 18 | * Provides a client to communicate with commercetools. 19 | */ 20 | const createCTClient = () => { 21 | const client = createClient({ 22 | middlewares: [ 23 | createQueueMiddleware({ concurrency: 10 }), 24 | createAuthMiddlewareForClientCredentialsFlow({ 25 | host: config.CT_AUTH_HOST, 26 | projectKey: config.CT_PROJECT_KEY, 27 | credentials: { 28 | clientId: config.CT_CLIENT_ID, 29 | clientSecret: config.CT_CLIENT_SECRET, 30 | }, 31 | scopes: 32 | config.CT_SCOPES.indexOf(' ') > 0 33 | ? config.CT_SCOPES.split(' ') 34 | : [config.CT_SCOPES], 35 | }), 36 | createHttpMiddleware({ 37 | host: config.CT_API_HOST, 38 | }), 39 | createLoggerMiddleware(), 40 | ], 41 | }); 42 | return client; 43 | }; 44 | 45 | /** 46 | * Creates a request builder 47 | */ 48 | const createCTRequestBuilder = () => 49 | createRequestBuilder({ projectKey: config.CT_PROJECT_KEY }); 50 | 51 | /** 52 | * Responds to any HTTP request that can provide a "sku" field in the body. 53 | * 54 | * @param {!Object} req Cloud Function request context. 55 | * @param {!Object} res Cloud Function response context. 56 | */ 57 | exports.helloWorld = function helloWorld(req, res) { 58 | // Example input: {"sku": "cool-product-123"} 59 | if (req.body.sku === undefined) { 60 | // This is an error case, as "sku" is required. 61 | res.status(400).send('No sku defined!'); 62 | } else { 63 | // Everything is okay. 64 | // Let's attempt to look up the inventory for that sku 65 | const requestBuilder = createCTRequestBuilder(); 66 | const inventoryUri = requestBuilder.inventory 67 | .where(`sku="${req.body.sku}"`) 68 | .build(); 69 | const inventoryRequest = { 70 | method: 'GET', 71 | uri: inventoryUri, 72 | }; 73 | const client = createCTClient(); 74 | client 75 | .execute(inventoryRequest) 76 | .then(result => { 77 | const results = result.body.results; 78 | if (!results.length) { 79 | return res 80 | .status(404) 81 | .json({ error: 'Inventory for SKU not found!' }); 82 | } 83 | 84 | return res.status(200).json(results); 85 | }) 86 | .catch(e => { 87 | // eslint-disable-next-line no-console 88 | console.error(e); 89 | res.status(400).json(e); 90 | }); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /packages/example-inventory/README.md: -------------------------------------------------------------------------------- 1 | Inventory Example 2 | =========== 3 | 4 | These examples will show the basic stages of developing a microservice to act on inventory. 5 | Follow along in order, or skip straight to the end to see the finished microservice code. 6 | -------------------------------------------------------------------------------- /packages/example-inventory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inventory-microservice", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "@commercetools/api-request-builder": "3.7.0", 6 | "@commercetools/sdk-client": "1.5.4", 7 | "@commercetools/sdk-middleware-auth": "3.5.1", 8 | "@commercetools/sdk-middleware-http": "2.3.1", 9 | "@commercetools/sdk-middleware-logger": "1.0.4", 10 | "@commercetools/sdk-middleware-queue": "1.1.4" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/extension-base/README.md: -------------------------------------------------------------------------------- 1 | # Wrapper around FaaS as common base for API Extensions 2 | 3 | Problem: We want to write examples once, but our customers want to run them on AWS Lambda, Azure Functions, or Google Cloud Functions. 4 | Solution: Put a small wrapper between the specific APIs offered by the FaaS providers and our examples. 5 | 6 | To implement an extension, you create a function that takes three parameters: 7 | 8 | * `input` - This JSON: https://docs.commercetools.com/http-api-projects-api-extensions.html#input 9 | * `ctpResponse` - An object that defines helpers for all possible responses defined here: https://docs.commercetools.com/http-api-projects-api-extensions.html#response It contains the helper functions `pass`, `update`, `updates`, `error`, `errors` that you need to use to make your Extension respond back to ctp. 10 | * `log` - Because it would have been too easy if all FaaS would just use `console.log` ;) 11 | 12 | # Installation per cloud provider 13 | 14 | First, check that `index.js` requires the addapter for your cloud provider (it may be commented out). 15 | 16 | * For AWS Lambda, `upload index.js` and `ctp-extension-lambda-adapters.js` 17 | * For Azure, upload `index.js`, `ctp-extension-azure-adapters.js` and `function.json` 18 | * For GCF, upload `index.js`, `ctp-extension-gcf-adapters.js` and `package.json` 19 | -------------------------------------------------------------------------------- /packages/extension-base/ctp-extension-azure-adapters.js: -------------------------------------------------------------------------------- 1 | // Azure 2 | 3 | const createReject = ctx => error => { 4 | ctx.res = { 5 | status: 400, 6 | body: { 7 | errors: Array.isArray(error) ? error : [error], 8 | }, 9 | }; 10 | ctx.done(); 11 | }; 12 | 13 | const createAccept = ctx => (update = []) => { 14 | ctx.res = { 15 | status: 200, 16 | body: { 17 | actions: Array.isArray(update) ? update : [update], 18 | }, 19 | }; 20 | ctx.done(); 21 | }; 22 | 23 | const ctpResponse = ctx => ({ 24 | reject: createReject(ctx), 25 | accept: createAccept(ctx), 26 | }); 27 | 28 | module.exports = { 29 | createExtensionAdapter: fn => (ctx, req) => { 30 | fn(req.body, ctpResponse(ctx), ctx.log); 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /packages/extension-base/ctp-extension-azure-adapters.spec.js: -------------------------------------------------------------------------------- 1 | const { createExtensionAdapter } = require('./ctp-extension-azure-adapters'); 2 | 3 | const createResponse = () => ({ json: jest.fn() }); 4 | const createContext = response => ({ 5 | log: jest.fn(), 6 | done: jest.fn(), 7 | status: jest.fn(() => response), 8 | }); 9 | const createRequest = () => ({ 10 | body: 'foo-body', 11 | }); 12 | 13 | describe('when creating', () => { 14 | let fn; 15 | let context; 16 | let response; 17 | let request; 18 | 19 | beforeEach(() => { 20 | fn = jest.fn(); 21 | request = createRequest(); 22 | response = createResponse(); 23 | context = createContext(response); 24 | 25 | createExtensionAdapter(fn)(context, request); 26 | }); 27 | 28 | it('should invoke the `fn`', () => { 29 | expect(fn).toHaveBeenCalled(); 30 | }); 31 | 32 | it('should invoke the `fn` with arguments', () => { 33 | expect(fn).toHaveBeenCalledWith( 34 | request.body, 35 | expect.objectContaining({ 36 | accept: expect.any(Function), 37 | reject: expect.any(Function), 38 | }), 39 | context.log 40 | ); 41 | }); 42 | 43 | describe('when accepting', () => { 44 | const actions = ['foo', 'bar']; 45 | 46 | beforeEach(() => { 47 | fn.mock.calls[0][1].accept(actions); 48 | }); 49 | 50 | it('should add a `res` to the context', () => { 51 | expect(context.res).toBeDefined(); 52 | }); 53 | 54 | it('should add a `status` `200` to the `res`', () => { 55 | expect(context.res.status).toBe(200); 56 | }); 57 | 58 | it('should add a `body` with `actions` to the `res`', () => { 59 | expect(context.res.body).toEqual({ 60 | actions, 61 | }); 62 | }); 63 | 64 | it('should invoke `done` on the context', () => { 65 | expect(context.done).toHaveBeenCalled(); 66 | }); 67 | }); 68 | 69 | describe('when rejecting', () => { 70 | const errors = ['foo-error']; 71 | 72 | beforeEach(() => { 73 | fn.mock.calls[0][1].reject(errors); 74 | }); 75 | 76 | it('should add a `res` to the context', () => { 77 | expect(context.res).toBeDefined(); 78 | }); 79 | 80 | it('should add a `status` `400` to the `res`', () => { 81 | expect(context.res.status).toBe(400); 82 | }); 83 | 84 | it('should add a `body` with `errors` to the `res`', () => { 85 | expect(context.res.body).toEqual({ 86 | errors, 87 | }); 88 | }); 89 | 90 | it('should invoke `done` on the context', () => { 91 | expect(context.done).toHaveBeenCalled(); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /packages/extension-base/ctp-extension-gcf-adapters.js: -------------------------------------------------------------------------------- 1 | // Google Cloud Functions 2 | 3 | const createReject = ctx => error => { 4 | ctx.status(400).json({ 5 | errors: Array.isArray(error) ? error : [error], 6 | }); 7 | }; 8 | 9 | const createAccept = ctx => (update = []) => { 10 | ctx.status(200).json({ 11 | actions: Array.isArray(update) ? update : [update], 12 | }); 13 | }; 14 | 15 | const ctpResponse = ctx => ({ 16 | reject: createReject(ctx), 17 | accept: createAccept(ctx), 18 | }); 19 | 20 | module.exports = { 21 | createExtensionAdapter: fn => (req, res) => { 22 | fn(req.body, ctpResponse(res), console.log); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/extension-base/ctp-extension-gcf-adapters.spec.js: -------------------------------------------------------------------------------- 1 | const { createExtensionAdapter } = require('./ctp-extension-gcf-adapters'); 2 | 3 | const createResponse = () => ({ json: jest.fn() }); 4 | const createContext = response => ({ 5 | log: jest.fn(), 6 | status: jest.fn(() => response), 7 | }); 8 | const createRequest = () => ({ 9 | body: 'foo-body', 10 | }); 11 | 12 | describe('when creating', () => { 13 | let fn; 14 | let context; 15 | let response; 16 | let request; 17 | 18 | beforeEach(() => { 19 | fn = jest.fn(); 20 | request = createRequest(); 21 | response = createResponse(); 22 | context = createContext(response); 23 | 24 | createExtensionAdapter(fn)(context, request); 25 | }); 26 | 27 | it('should invoke the `fn`', () => { 28 | expect(fn).toHaveBeenCalled(); 29 | }); 30 | 31 | it('should invoke the `fn` with arguments', () => { 32 | expect(fn).toHaveBeenCalledWith( 33 | request.body, 34 | expect.objectContaining({ 35 | accept: expect.any(Function), 36 | reject: expect.any(Function), 37 | }), 38 | context.log 39 | ); 40 | }); 41 | 42 | describe('when accepting', () => { 43 | const actions = ['foo', 'bar']; 44 | 45 | beforeEach(() => { 46 | fn.mock.calls[0][1].accept(actions); 47 | }); 48 | 49 | it('should set status to `200`', () => { 50 | expect(context.status).toHaveBeenCalledWith(200); 51 | }); 52 | 53 | it('should pass actions to response', () => { 54 | expect(response.json).toHaveBeenCalledWith({ actions }); 55 | }); 56 | }); 57 | 58 | describe('when rejecting', () => { 59 | const errors = ['foo-error']; 60 | 61 | beforeEach(() => { 62 | fn.mock.calls[0][1].reject(errors); 63 | }); 64 | 65 | it('should set status to `200`', () => { 66 | expect(context.status).toHaveBeenCalledWith(400); 67 | }); 68 | 69 | it('should pass errors to response', () => { 70 | expect(response.json).toHaveBeenCalledWith({ errors }); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/extension-base/ctp-extension-lambda-adapters.js: -------------------------------------------------------------------------------- 1 | // AWS Lambda 2 | 3 | const createReject = callback => error => { 4 | callback(null, { 5 | responseType: 'FailedValidation', 6 | errors: Array.isArray(error) ? error : [error], 7 | }); 8 | }; 9 | 10 | const createAccept = callback => (update = []) => { 11 | callback(null, { 12 | responseType: 'UpdateRequest', 13 | actions: Array.isArray(update) ? update : [update], 14 | }); 15 | }; 16 | 17 | const ctpResponse = callback => ({ 18 | reject: createReject(callback), 19 | accept: createAccept(callback), 20 | }); 21 | 22 | module.exports = { 23 | createExtensionAdapter: fn => (event, context, callback) => { 24 | fn(event, ctpResponse(callback), console.log); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/extension-base/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req" 8 | }, 9 | { 10 | "type": "http", 11 | "direction": "out", 12 | "name": "res" 13 | } 14 | ], 15 | "disabled": false 16 | } 17 | -------------------------------------------------------------------------------- /packages/extension-base/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | createExtensionAdapter, 3 | } = require('./ctp-extension-lambda-adapters.js'); 4 | // const { createExtensionAdapter } = require('./ctp-extension-azure-adapters.js'); 5 | // const { createExtensionAdapter } = require('./ctp-extension-gcf-adapters.js'); 6 | 7 | const addInsurance = (request, ctpResponse, log) => { 8 | // Use an ID from your project! 9 | const taxCategoryId = 'af6532f2-2f74-4e0d-867f-cc9f6d0b7c5a'; 10 | 11 | const cart = request.resource.obj; 12 | // If the cart contains any line item that is worth more than $500, 13 | // mandatory insurance needs to be added. 14 | const cartRequiresInsurance = cart.lineItems.some( 15 | lineItem => lineItem.totalPrice.centAmount > 50000 16 | ); 17 | const insuranceItem = cart.customLineItems.find( 18 | customLineItem => customLineItem.slug === 'mandatory-insurance' 19 | ); 20 | const cartHasInsurance = insuranceItem !== undefined; 21 | 22 | if (cartRequiresInsurance && !cartHasInsurance) { 23 | log('adding insurance'); 24 | ctpResponse.accept([ 25 | { 26 | action: 'addCustomLineItem', 27 | name: { en: 'Mandatory Insurance for Items above $500' }, 28 | money: { 29 | currencyCode: cart.totalPrice.currencyCode, 30 | centAmount: 1000, 31 | }, 32 | slug: 'mandatory-insurance', 33 | taxCategory: { 34 | typeId: 'tax-category', 35 | id: taxCategoryId, 36 | }, 37 | }, 38 | ]); 39 | } else if (!cartRequiresInsurance && cartHasInsurance) { 40 | log('removing insurance'); 41 | ctpResponse.accept({ 42 | action: 'removeCustomLineItem', 43 | customLineItemId: insuranceItem.id, 44 | }); 45 | } else { 46 | log('nothing to do'); 47 | ctpResponse.accept(); 48 | } 49 | }; 50 | 51 | // module.exports = createExtensionAdapter(addInsurance); 52 | exports.handler = createExtensionAdapter(addInsurance); 53 | -------------------------------------------------------------------------------- /packages/extension-base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-extension", 3 | "version": "1.0.0" 4 | } 5 | --------------------------------------------------------------------------------