├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── build-test.yml ├── .gitignore ├── .istanbul.yml ├── CONTRIBUTING.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── bin └── Makefile.js ├── bitbucket-pipelines.yml ├── cartridges ├── app_api_base │ ├── .project │ ├── .tern-project │ ├── README.md │ ├── caches.json │ ├── cartridge │ │ ├── app_api_base.properties │ │ ├── config │ │ │ ├── countries.json │ │ │ ├── httpHeadersConf.json │ │ │ └── performanceMetricsConf.json │ │ ├── controllers │ │ │ ├── .eslintrc.json │ │ │ ├── Default.js │ │ │ ├── Error.js │ │ │ ├── Home.js │ │ │ ├── RedirectURL.js │ │ │ ├── SessionBridge.js │ │ │ └── SiteMap.js │ │ ├── experience │ │ │ ├── breakpoints.json │ │ │ ├── components │ │ │ │ └── commerce_assets │ │ │ │ │ ├── photoTile.js │ │ │ │ │ └── photoTile.json │ │ │ ├── hooks.js │ │ │ ├── pages │ │ │ │ ├── storePage.js │ │ │ │ └── storePage.json │ │ │ └── utilities │ │ │ │ ├── ImageTransformation.js │ │ │ │ ├── PageRenderHelper.js │ │ │ │ ├── RegionModel.js │ │ │ │ └── RegionModelRegistry.js │ │ ├── scripts │ │ │ ├── config.js │ │ │ ├── helpers │ │ │ │ ├── basketCalculationHelpers.js │ │ │ │ ├── hooks.js │ │ │ │ ├── productSearchHelper.js │ │ │ │ ├── seoHelper.js │ │ │ │ └── sessionHelper.js │ │ │ ├── hooks │ │ │ │ ├── cart │ │ │ │ │ └── calculate.js │ │ │ │ ├── category.js │ │ │ │ ├── onSession.js │ │ │ │ ├── product.js │ │ │ │ ├── search.js │ │ │ │ └── taxes.js │ │ │ ├── middleware │ │ │ │ ├── cache.js │ │ │ │ └── userLoggedIn.js │ │ │ ├── services │ │ │ │ └── SessionBridgeService.js │ │ │ └── util │ │ │ │ └── collections.js │ │ └── templates │ │ │ ├── default │ │ │ └── experience │ │ │ │ ├── components │ │ │ │ └── commerce_assets │ │ │ │ │ └── photoTile.isml │ │ │ │ └── pages │ │ │ │ └── storePage.isml │ │ │ └── resources │ │ │ ├── error.properties │ │ │ └── version.properties │ ├── hooks.json │ └── package.json ├── bm_app_api_base │ ├── .project │ ├── .tern-project │ └── cartridge │ │ ├── bm_app_api_base.properties │ │ ├── static │ │ └── default │ │ │ └── experience │ │ │ └── components │ │ │ └── commerce_assets │ │ │ └── photoTile.svg │ │ └── templates │ │ └── resources │ │ └── experience │ │ ├── componentgroups │ │ └── commerce_assets.properties │ │ ├── components │ │ └── commerce_assets │ │ │ └── photoTile.properties │ │ └── pages │ │ └── storePage.properties └── modules │ ├── .eslintrc.json │ ├── .project │ ├── .tern-project │ ├── server.js │ └── server │ ├── EventEmitter.js │ ├── README.md │ ├── assign.js │ ├── middleware.js │ ├── performanceMetrics.js │ ├── queryString.js │ ├── render.js │ ├── request.js │ ├── response.js │ ├── route.js │ ├── server.js │ └── simpleCache.js ├── docs └── screenshots │ ├── locale-mapping.jpg │ └── product-url-rules.jpg ├── ismllinter.config.js ├── metadata ├── services.xml └── system-objecttype-extensions.xml ├── package-lock.json ├── package.json └── test ├── .eslintrc.json ├── mocks ├── .eslintrc.json ├── dw.util.Collection.js ├── dw.value.Money.js ├── dw.web.URLUtils.js ├── dw │ ├── catalog │ │ ├── ProductInventoryMgr.js │ │ └── StoreMgr.js │ ├── order │ │ ├── BasketMgr.js │ │ └── ShippingMgr.js │ └── web │ │ └── Resource.js ├── modules │ └── responseMock.js └── util │ └── collections.js ├── unit ├── app_api_base │ └── scripts │ │ ├── helpers │ │ ├── basketCalculationHelpers.js │ │ ├── hooks.js │ │ ├── productSearchHelper.js │ │ ├── seoHelper.js │ │ └── sessionHelper.js │ │ ├── hooks │ │ ├── category.js │ │ ├── product.js │ │ ├── search.js │ │ └── taxes.js │ │ ├── middleware │ │ ├── cache.js │ │ └── userLoggedIn.js │ │ └── util │ │ └── collections.js └── modules │ └── server │ ├── middleware.js │ ├── performanceMetrics.js │ ├── querystring.js │ ├── render.js │ ├── request.js │ ├── response.js │ ├── route.js │ ├── server.js │ └── simpleCache.js └── util.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | orbs: 2 | node: circleci/node@1.1 3 | 4 | jobs: 5 | build: 6 | working_directory: ~/build_only 7 | executor: 8 | name: node/default 9 | tag: '12.21' 10 | steps: 11 | - checkout 12 | - run: npm install 13 | - run: npm run lint 14 | - run: npm run test 15 | version: 2.1 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | 12 | [package.json] 13 | indent_style = space 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | doc/ 3 | bin/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "airbnb-base/legacy", 4 | "rules": { 5 | "import/no-unresolved": "off", 6 | "indent": ["error", 4, { "SwitchCase": 1, "VariableDeclarator": 1 }], 7 | "func-names": "off", 8 | "require-jsdoc": "error", 9 | "valid-jsdoc": ["error", { "preferType": { "Boolean": "boolean", "Number": "number", "object": "Object", "String": "string" }, "requireReturn": false}], 10 | "vars-on-top": "off", 11 | "global-require": "off", 12 | "no-shadow": ["error", { "allow": ["err", "callback"]}], 13 | "max-len": "off" 14 | }, 15 | "globals": { 16 | "empty": true, 17 | "request": true, 18 | "response": true, 19 | "dw": true, 20 | "customer": true, 21 | "session": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | - main 7 | pull_request: 8 | branches: 9 | - develop 10 | - main 11 | jobs: 12 | build_and_test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: [12.x, 14.x, 16.x] 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - run: npm install 24 | - run: npm run lint 25 | - run: npm run test 26 | - run: npm run test:report 27 | - name: Upload coverage to Codecov 28 | uses: codecov/codecov-action@v3 29 | with: 30 | directory: "./coverage/" 31 | fail_ci_if_error: true 32 | verbose: true 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | npm-debug.log 4 | cartridges.zip 5 | .idea/ 6 | .nyc_output 7 | dw.json 8 | sitegenesisdata/ 9 | mobilefirstdata/ 10 | storefrontdata/ 11 | demo_data_sfra/ 12 | demo_data_sfra.zip 13 | .DS_Store 14 | test/appium/webdriver/config.json 15 | .vscode 16 | .history 17 | *.iml 18 | .idea 19 | test/acceptance/report 20 | test/integration/config.json -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | root: . 3 | extensions: 4 | - .js 5 | default-excludes: true 6 | excludes: [ 7 | "**/static/**", # Those are pre-processed client-side scripts 8 | "**/js/**", # Those are client-side scripts 9 | "**/controllers/**", # We can't test controllers without too much mocking 10 | "**/server/EventEmitter.js", # Third-party library 11 | "**/modules/*.js", # Those are just wrappers around modules 12 | "bin/*", # Those are task files 13 | "**/scripts/payment/processor/*", # Those are payment processor files, we don't test them 14 | ] 15 | include-all-sources: true 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Table of contents 2 | 3 | - [Conventions for branch names and commit messages ](#conventions-for-branch-names-and-commit-messages) 4 | - [Submitting your first pull request ](#submitting-your-first-pull-request) 5 | - [Submitting a pull request ](#submitting-a-pull-request) 6 | - [What to expect](#what-to-expect) 7 | - [Community contributors](#community-contributors) 8 | - [Contributor License Agreement (CLA)](#contributor-license-agreement) 9 | - [Commit signing](#commit-signing) 10 | - [Back to README](./README.md) 11 | 12 | # Contributing to HRA 13 | 14 | To contribute to the HRA base cartridge, follow the guidelines below. This helps us address your pull request in a more timely manner. 15 | 16 | ## Conventions for branch names and commit messages 17 | 18 | ### Branch names 19 | 20 | To name a branch, use the following pattern: `yourusername-description` 21 | 22 | In this pattern, `description` is dash-delimited. 23 | 24 | For example: jdoe-unify-shipping-isml 25 | 26 | ### Commit messages 27 | 28 | To create a commit message, use the following pattern: `action-term: short-description` 29 | 30 | In this pattern, `action-term` is one of the following: 31 | 32 | * Bug 33 | * Doc 34 | * Chore 35 | * Update 36 | * Breaking 37 | * New 38 | 39 | After `action-term,` add a colon, and then write a short description. You can optionally include a GUS ticket number in parentheses. 40 | 41 | For example: "Breaking: Unify the single- and multi-ship shipping isml templates (W-999999)." 42 | 43 | ## Submitting your first pull request 44 | If this is your first pull request, follow these steps: 45 | 46 | 1. Create a fork of the HRA repository 47 | 48 | 2. Download the forked repository 49 | 50 | 3. Checkout the integration branch 51 | 52 | 4. Apply your code fix 53 | 54 | 5. Create a pull request against the integration branch 55 | 56 | ## Submitting a pull request 57 | 58 | 1. Create a branch off the integration branch. 59 | * To reduce merge conflicts, rebase your branch before submitting your pull request. 60 | * If applicable, reference the issue number in the comments of your pull request. 61 | 62 | 2. In your pull request, include: 63 | * A brief description of the problem and your solution 64 | * (optional) Screen shots 65 | * (optional) Error logs 66 | * (optional) Steps to reproduce 67 | 68 | 3. Grant HRA team members access to your fork so we can run an automated test on your pull request prior to merging it into our integration branch. 69 | * From within your forked repository, find the 'Settings' link (see the site navigation on left of the page). 70 | * Under the settings menu, click 'User and group access'. 71 | * Add the new user to the input field under the heading 'Users' and give the new user write access. 72 | 73 | 4. Indicate if there is any data that needs to be included with your code submission. 74 | 75 | 5. Your code should pass the automation process. 76 | * Lint your code: 77 | `npm run lint` 78 | * Run and pass the unit test: 79 | `npm run test` 80 | * Run and pass the unit/intergration test: 81 | `npm run test:integration` 82 | 83 | ## What to expect 84 | 85 | After you submit your pull request, we'll look it over and consider it for merging. 86 | 87 | As long as your submission has met the above guidelines, we should merge it in a timely manner. 88 | 89 | ## Contributor License Agreement 90 | 91 | All external contributors must sign our Contributor License Agreement (CLA). 92 | 93 | ## Commit signing 94 | 95 | All contributors must set up [commit signing](https://help.github.com/en/github/authenticating-to-github/signing-commits). 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Thomas Theunen 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Applicable to all contributors to this repository: 2 | 3 | ### All pull requests: 4 | * [ ] Have you considered security (e.g., XSS)? 5 | * [ ] Have you considered desktop, mobile, and tablet form-factors? 6 | * [ ] Have you considered [accessibility best practices](https://www.w3.org/WAI/standards-guidelines/wcag/)? 7 | 8 | ### Code 9 | * [ ] Are the commits squashed into one commit? 10 | * [ ] Is the [branch name and commit message](CONTRIBUTING.md#conventions-for-branch-names-and-commit-messages) following team convention? 11 | * [ ] Is the code linted? 12 | * [ ] Are all open issues, questions, and concerns by reviewers answered, resolved, and reviewed? 13 | 14 | ### Quality assurance 15 | * [ ] If applicable, are unit tests written and are they passing? 16 | * [ ] If applicable, are integration tests written and are they passing? 17 | * [ ] Have checks linked in your pull request passed? 18 | 19 | ### Documentation 20 | * [ ] Are there meaningful code comments included? 21 | 22 | ## Applicable to community contributors: 23 | * [ ] Have you granted SFRA team access to your fork? [Grant access](CONTRIBUTING.md#community-contributors) 24 | 25 | ## Applicable to SFRA team members: 26 | 27 | ### Documentation 28 | * [ ] If applicable, is the change log updated? 29 | * [ ] If applicable, have any UI text changes been reviewed by Documentation? 30 | * [ ] If applicable, have any UI implementations been reviewed by the UX team? 31 | 32 | ### Security 33 | * [ ] If applicable, has Security reviewed this code? 34 | * [ ] If applicable, is a 3PP request submitted? 35 | -------------------------------------------------------------------------------- /bin/Makefile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global cat, cd, cp, echo, exec, exit, find, ls, mkdir, pwd, rm, target, test */ 4 | 5 | require('shelljs/make'); 6 | 7 | var path = require('path'), 8 | spawn = require('child_process').spawn, 9 | fs = require('fs'), 10 | shell = require('shelljs'); 11 | 12 | 13 | target.release = function (args) { 14 | if (!args) { 15 | console.log('No version type provided. Please specify release type patch/minor/major'); 16 | return; 17 | } 18 | var type = args[0].replace(/"/g, ''); 19 | if (['patch', 'minor', 'major'].indexOf(type) >= 0) { 20 | console.log('Updating package.json version with ' + args[0] + ' release.'); 21 | var version = spawn('npm version ' + args[0], { stdio: 'inherit', shell: true }); 22 | var propertiesFileName = path.resolve('./cartridges/app_storefront_base/cartridge/templates/resources/version.properties') 23 | 24 | version.on('exit', function (code) { 25 | if (code === 0) { 26 | var versionNumber = JSON.parse(fs.readFileSync('./package.json').toString()).version; 27 | //modify version.properties file 28 | var propertiesFile = fs.readFileSync(propertiesFileName).toString(); 29 | var propertiesLines = propertiesFile.split('\n'); 30 | var newLines = propertiesLines.map(function (line) { 31 | if (line.indexOf('global.version.number=') === 0) { 32 | line = 'global.version.number=' + versionNumber; 33 | } 34 | return line; 35 | }); 36 | fs.writeFileSync(propertiesFileName, newLines.join('\n')); 37 | shell.exec('git add -A'); 38 | shell.exec('git commit -m "Release ' + versionNumber + '"'); 39 | console.log('Version updated to ' + versionNumber); 40 | console.log('Please do not forget to push your changes to the integration branch'); 41 | } 42 | }); 43 | } else { 44 | console.log('Could not release new version. Please specify version type (patch/minor/major).'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /bitbucket-pipelines.yml: -------------------------------------------------------------------------------- 1 | # This is a sample build configuration for Javascript. 2 | # Check our guides at https://confluence.atlassian.com/x/VYk8Lw for more examples. 3 | # Only use spaces to indent your .yml configuration. 4 | # ----- 5 | # You can specify a custom docker image from Docker Hub as your build environment. 6 | image: node:6.9.2 7 | 8 | pipelines: 9 | default: 10 | - step: 11 | script: # Modify the commands below to build your repository. 12 | - npm install 13 | - npm run lint 14 | - npm test 15 | - node node_modules/.bin/dwupload --hostname ${HOSTNAME} --username ${USERNAME} --password "${PASSWORD}" --cartridge cartridges/app_api_base 16 | - node node_modules/.bin/dwupload --hostname ${HOSTNAME} --username ${USERNAME} --password "${PASSWORD}" --cartridge cartridges/modules 17 | - npm run test:integration -- --baseUrl https://${HOSTNAME}/on/demandware.store/Sites-RefArch-Site/en_US "test/integration/*" 18 | -------------------------------------------------------------------------------- /cartridges/app_api_base/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | app_api_base 4 | 5 | 6 | 7 | 8 | 9 | com.demandware.studio.core.beehiveElementBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.demandware.studio.core.beehiveNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /cartridges/app_api_base/.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 5, 3 | "plugins": { 4 | "guess-types": { 5 | 6 | }, 7 | "outline": { 8 | 9 | }, 10 | "demandware": { 11 | 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /cartridges/app_api_base/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to Headless Reference Architecture (HRA) 2 | 3 | The Headless Reference Architecture is fully compliant with standard JavaScript. It uses [Controllers]{@tutorial Controllers} to handle incoming requests. It provides a layer of JSON objects through the [Model-Views]{@tutorial Models}. All scripts are [Common JS modules](http://www.commonjs.org) with defined and documented exports to avoid polluting the global namespace. 4 | 5 | This documentation is meant to serve as a reference to quickly look up supported functionality and is fully based on the comments in the code. You can continue to maintain these [JSDoc comments](http://usejsdoc.org/) to generate a similar documentation for your own project. 6 | -------------------------------------------------------------------------------- /cartridges/app_api_base/caches.json: -------------------------------------------------------------------------------- 1 | { 2 | "caches": [ 3 | { 4 | "id": "ProductExtendStatic", 5 | "expireAfterSeconds": 86400 6 | }, 7 | { 8 | "id": "ProductExtendDynamic", 9 | "expireAfterSeconds": 86400 10 | }, 11 | { 12 | "id": "SearchDrivenRedirect", 13 | "expireAfterSeconds": 86400 14 | }, 15 | { 16 | "id": "MetaData", 17 | "expireAfterSeconds": 86400 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/app_api_base.properties: -------------------------------------------------------------------------------- 1 | ## cartridge.properties for cartridge app_api_base 2 | #Thu Jul 14 11:30:40 EDT 2022 3 | demandware.cartridges.app_api_base.multipleLanguageStorefront=true 4 | demandware.cartridges.app_api_base.id=app_api_base 5 | demandware.cartridges.app_api_base.version=0.1.0 6 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/config/countries.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "id": "en_US", 3 | "currencyCode": "USD" 4 | }, { 5 | "id": "en_GB", 6 | "currencyCode": "GBP" 7 | }, { 8 | "id": "ja_JP", 9 | "currencyCode": "JPY" 10 | }, { 11 | "id": "zh_CN", 12 | "currencyCode": "CNY" 13 | }, { 14 | "id": "fr_FR", 15 | "currencyCode": "EUR" 16 | }, { 17 | "id": "it_IT", 18 | "currencyCode": "EUR" 19 | }] 20 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/config/httpHeadersConf.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "Access-Control-Allow-Methods", 4 | "value": "POST, GET, PUT, DELETE, OPTIONS" 5 | }, 6 | { 7 | "id": "Content-Security-Policy", 8 | "value": "frame-ancestors 'self'" 9 | }, 10 | { 11 | "id": "X-Content-Type-Options", 12 | "value": "nosniff" 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/config/performanceMetricsConf.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": true 3 | } -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/controllers/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["sitegenesis"], 3 | "rules": { 4 | "sitegenesis/no-global-require": ["error"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/controllers/Default.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @namespace Default 5 | */ 6 | 7 | var server = require('server'); 8 | var cache = require('*/cartridge/scripts/middleware/cache'); 9 | var Resource = require('dw/web/Resource'); 10 | 11 | /** 12 | * Default-Start : This end point is the root of the site, when opening from the BM this is the end point executed 13 | * @name Base/Default-Start 14 | * @function 15 | * @memberof Default 16 | * @param {middleware} - cache.applyDefaultCache 17 | * @param {category} - non-sensitive 18 | * @param {renders} - json 19 | * @param {serverfunction} - get 20 | */ 21 | server.get('Start', cache.applyDefaultCache, function (req, res, next) { 22 | res.json({ 23 | site: Resource.msg('global.site.name', 'version', null), 24 | version: Resource.msg('global.version.number', 'version', null) 25 | }); 26 | 27 | next(); 28 | }); 29 | 30 | /** Renders the maintenance page when a site has been set to "Maintenance mode" */ 31 | server.get('Offline', cache.applyDefaultCache, function (req, res, next) { 32 | res.json({ 33 | error: Resource.msg('global.error.general', 'error', null), 34 | message: Resource.msg('global.error.offline', 'error', null) 35 | }); 36 | 37 | next(); 38 | }); 39 | 40 | module.exports = server.exports(); 41 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/controllers/Error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @namespace Error 5 | */ 6 | 7 | var server = require('server'); 8 | var Resource = require('dw/web/Resource'); 9 | 10 | /** 11 | * Error-Start : This endpoint is called when there is a server error 12 | * @name Base/Error-Start 13 | * @function 14 | * @memberof Error 15 | * @param {httpparameter} - error - message to be displayed 16 | * @param {category} - non-sensitive 17 | * @param {renders} - json 18 | * @param {serverfunction} - get/post 19 | */ 20 | server.use('Start', function (req, res, next) { 21 | var system = require('dw/system/System'); 22 | 23 | res.setStatusCode(500); 24 | 25 | var showError = system.getInstanceType() !== system.PRODUCTION_SYSTEM 26 | && system.getInstanceType() !== system.STAGING_SYSTEM; 27 | 28 | res.json({ 29 | error: showError ? req.error || {} : {}, 30 | message: Resource.msg('global.error.general', 'error', null) 31 | }); 32 | 33 | next(); 34 | }); 35 | 36 | /** 37 | * Error-ErrorCode : This endpoint can be called to display an error from a resource file 38 | * @name Base/Error-ErrorCode 39 | * @function 40 | * @memberof Error 41 | * @param {httpparameter} - err - e.g 01 (Error Code mapped in the resource file appended with 'message.error.') 42 | * @param {category} - non-sensitive 43 | * @param {renders} - json 44 | * @param {serverfunction} - get/post 45 | */ 46 | server.use('ErrorCode', function (req, res, next) { 47 | res.setStatusCode(500); 48 | var errorMessage = 'message.error.' + req.querystring.err; 49 | 50 | res.json({ 51 | error: req.error, 52 | message: Resource.msg(errorMessage, 'error', null) 53 | }); 54 | 55 | next(); 56 | }); 57 | 58 | /** 59 | * Error-Forbidden : This endpoint is called when a shopper tries to access a forbidden content. The shopper is logged out and an error is returned 60 | * @name Base/Error-Forbidden 61 | * @function 62 | * @memberof Error 63 | * @param {category} - non-sensitive 64 | * @param {serverfunction} - get 65 | */ 66 | server.get('Forbidden', function (req, res, next) { 67 | var CustomerMgr = require('dw/customer/CustomerMgr'); 68 | 69 | CustomerMgr.logoutCustomer(true); 70 | 71 | res.json({ 72 | error: Resource.msg('global.error.forbidden', 'error', null), 73 | message: Resource.msg('global.error.forbidden.message', 'error', null) 74 | }); 75 | 76 | next(); 77 | }); 78 | 79 | module.exports = server.exports(); 80 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/controllers/Home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @namespace Home 5 | */ 6 | 7 | var server = require('server'); 8 | var cache = require('*/cartridge/scripts/middleware/cache'); 9 | var Resource = require('dw/web/Resource'); 10 | 11 | /** 12 | * Any customization on this endpoint, also requires update for Default-Start endpoint 13 | */ 14 | /** 15 | * Home-Show : This endpoint is called when a shopper navigates to the home page 16 | * @name Base/Home-Show 17 | * @function 18 | * @memberof Home 19 | * @param {middleware} - cache.applyDefaultCache 20 | * @param {category} - non-sensitive 21 | * @param {renders} - json 22 | * @param {serverfunction} - get 23 | */ 24 | server.get('Show', cache.applyDefaultCache, function (req, res, next) { 25 | res.json({ 26 | site: Resource.msg('global.site.name', 'version', null), 27 | version: Resource.msg('global.version.number', 'version', null) 28 | }); 29 | 30 | next(); 31 | }); 32 | 33 | server.get('ErrorNotFound', function (req, res, next) { 34 | res.setStatusCode(404); 35 | 36 | res.json({ 37 | error: Resource.msg('global.error.general', 'error', null), 38 | message: Resource.msg('global.error.notfound', 'error', null) 39 | }); 40 | 41 | next(); 42 | }); 43 | 44 | module.exports = server.exports(); 45 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/controllers/RedirectURL.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @namespace RedirectURL 5 | */ 6 | 7 | var server = require('server'); 8 | 9 | /** 10 | * RedirectURL-Start : The RedirectURL-Start endpoint handles URL redirects 11 | * @name Base/RedirectURL-Start 12 | * @function 13 | * @memberof RedirectURL 14 | * @param {category} - non-sensitive 15 | * @param {serverfunction} - get 16 | */ 17 | server.get('Start', function (req, res, next) { 18 | var URLRedirectMgr = require('dw/web/URLRedirectMgr'); 19 | 20 | var redirect = URLRedirectMgr.redirect; 21 | var location = redirect ? redirect.location : null; 22 | var redirectStatus = redirect ? redirect.getStatus() : null; 23 | 24 | if (!location) { 25 | var Resource = require('dw/web/Resource'); 26 | 27 | res.setStatusCode(404); 28 | res.json({ 29 | error: Resource.msg('global.error.general', 'error', null), 30 | message: Resource.msg('global.error.notfound', 'error', null) 31 | }); 32 | } else { 33 | if (redirectStatus) { 34 | res.setRedirectStatus(redirectStatus); 35 | } 36 | res.redirect(location); 37 | } 38 | 39 | next(); 40 | }); 41 | 42 | /** 43 | * RedirectURL-Hostname : The RedirectURL-Hostname endpoint handles Hostname-only URL redirects 44 | * @name Base/RedirectURL-Hostname 45 | * @function 46 | * @memberof RedirectURL 47 | * @param {querystringparameter} - Location - optional parameter to provide a URL to redirect to 48 | * @param {category} - non-sensitive 49 | * @param {serverfunction} - get 50 | */ 51 | server.get('Hostname', function (req, res, next) { 52 | var URLUtils = require('dw/web/URLUtils'); 53 | 54 | var url = req.querystring.Location.stringValue; 55 | var hostRegExp = new RegExp('^https?://' + req.httpHost + '(?=/|$)'); 56 | var location; 57 | 58 | if (!url || !hostRegExp.test(url)) { 59 | location = URLUtils.httpHome().toString(); 60 | } else { 61 | location = url; 62 | } 63 | 64 | res.redirect(location); 65 | 66 | next(); 67 | }); 68 | 69 | module.exports = server.exports(); 70 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/controllers/SessionBridge.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @namespace SessionBridge 5 | */ 6 | 7 | var server = require('server'); 8 | 9 | /** 10 | * SessionBridge-Test : This endpoint is to test the Session Bridge. 11 | * @name Base/SessionBridge-Test 12 | * @function 13 | * @memberof SessionBridge 14 | * @param {category} - sensitive 15 | * @param {renders} - json 16 | * @param {serverfunction} - get 17 | */ 18 | server.get('Test', function (req, res, next) { 19 | var System = require('dw/system/System'); 20 | 21 | if ((System.getInstanceType() !== System.PRODUCTION_SYSTEM) 22 | && (System.getInstanceType() !== System.STAGING_SYSTEM)) { 23 | res.json({ 24 | customer_id: req.currentCustomer.raw.ID, 25 | customer_no: req.currentCustomer.profile ? req.currentCustomer.profile.customerNo : null 26 | }); 27 | } 28 | 29 | next(); 30 | }); 31 | 32 | module.exports = server.exports(); 33 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/controllers/SiteMap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server = require('server'); 4 | 5 | /** 6 | * Serves requests for search provider (Google, Yahoo) XML site maps. Reads a 7 | * given site map and copies it into the request output stream. If this is successful, 8 | * renders an http_200 template. If it fails, renders the http_404 template. 9 | * SiteMap Rule: 10 | * # process sitemaps 11 | * RewriteRule ^/(sitemap([^/]*))$ /on/demandware.store/%{HTTP_HOST}/-/SiteMap-Google?name=$1 [PT,L] 12 | */ 13 | server.get('Google', function (req, res, next) { 14 | var Pipelet = require('dw/system/Pipelet'); 15 | 16 | var fileName = req.querystring.name; 17 | var siteMapResult = '500'; 18 | 19 | if (fileName) { 20 | var SendGoogleSiteMapResult = new Pipelet('SendGoogleSiteMap').execute({ 21 | FileName: fileName 22 | }); 23 | if (SendGoogleSiteMapResult.result === PIPELET_ERROR) { // eslint-disable-line 24 | siteMapResult = '404'; 25 | } else { 26 | siteMapResult = '200'; 27 | } 28 | } 29 | 30 | res.setStatusCode(siteMapResult); 31 | 32 | next(); 33 | }); 34 | 35 | module.exports = server.exports(); 36 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/breakpoints.json: -------------------------------------------------------------------------------- 1 | { 2 | "mobile" : 768, 3 | "tablet" : 1024, 4 | "desktop" : 1440 5 | } -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/components/commerce_assets/photoTile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Template = require('dw/util/Template'); 4 | var HashMap = require('dw/util/HashMap'); 5 | var ImageTransformation = require('*/cartridge/experience/utilities/ImageTransformation.js'); 6 | 7 | /** 8 | * Render logic for the storefront.photoTile component. 9 | * @param {dw.experience.ComponentScriptContext} context The Component script context object. 10 | * @param {dw.util.Map} [modelIn] Additional model values created by another cartridge. This will not be passed in by Commerce Cloud Platform. 11 | * 12 | * @returns {string} The markup to be displayed 13 | */ 14 | module.exports.render = function (context, modelIn) { 15 | var model = modelIn || new HashMap(); 16 | var content = context.content; 17 | 18 | model.image = ImageTransformation.getScaledImage(content.image); 19 | 20 | // instruct 24 hours relative pagecache 21 | var expires = new Date(); 22 | expires.setDate(expires.getDate() + 1); // this handles overflow automatically 23 | response.setExpires(expires); 24 | 25 | return new Template('experience/components/commerce_assets/photoTile').render(model).text; 26 | }; 27 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/components/commerce_assets/photoTile.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Image Tile", 3 | "description": "Select a photo that you want to insert into your layout component", 4 | "group": "commerce_assets", 5 | "attribute_definition_groups": [ 6 | { 7 | "id": "photoTileImage", 8 | "name": "Image Tile", 9 | "description": "This is a simple Image Tile component where you drag and drop it into any layout component ", 10 | "attribute_definitions": [ 11 | { 12 | "id": "image", 13 | "name": "Image Tile Component Image", 14 | "description": "Select a photo to be displayed", 15 | "type": "image", 16 | "required": true 17 | } 18 | ] 19 | } 20 | ], 21 | "region_definitions": [] 22 | } 23 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * is Page Designer page in Edit Mode 5 | */ 6 | function editmode() { 7 | session.privacy.consent = true; // eslint-disable-line no-undef 8 | } 9 | 10 | exports.editmode = editmode; 11 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/pages/storePage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Template = require('dw/util/Template'); 4 | var HashMap = require('dw/util/HashMap'); 5 | var PageRenderHelper = require('*/cartridge/experience/utilities/PageRenderHelper.js'); 6 | 7 | /** 8 | * Render logic for the storepage. 9 | * 10 | * @param {dw.experience.PageScriptContext} context The page script context object. 11 | * @param {dw.util.Map} [modelIn] Additional model values created by another cartridge. This will not be passed in by Commcerce Cloud Plattform. 12 | * 13 | * @returns {string} The markup to be displayed 14 | */ 15 | module.exports.render = function (context, modelIn) { 16 | var model = modelIn || new HashMap(); 17 | 18 | var page = context.page; 19 | model.page = page; 20 | model.content = context.content; 21 | 22 | // automatically register configured regions 23 | model.regions = PageRenderHelper.getRegionModelRegistry(page); 24 | 25 | if (PageRenderHelper.isInEditMode()) { 26 | var HookManager = require('dw/system/HookMgr'); 27 | HookManager.callHook('app.experience.editmode', 'editmode'); 28 | model.resetEditPDMode = true; 29 | } 30 | 31 | model.CurrentPageMetaData = PageRenderHelper.getPageMetaData(page); 32 | 33 | // no pagecache setting here, this is dynamically determined by the components used within the page 34 | 35 | // render the page 36 | return new Template('experience/pages/storePage').render(model).text; 37 | }; 38 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/pages/storePage.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Storefront Page", 3 | "description":"A Storefront Page Type", 4 | "region_definitions":[ 5 | { 6 | "id":"headerbanner", 7 | "name":"Header Banner Region", 8 | "max_components": 1, 9 | "component_type_exclusions": [ 10 | { "type_id": "commerce_assets.categorytile" }, 11 | { "type_id": "commerce_assets.category" }, 12 | { "type_id": "commerce_assets.editorialRichText" }, 13 | { "type_id": "commerce_assets.imageAndText" }, 14 | { "type_id": "commerce_assets.mainBanner" }, 15 | { "type_id": "commerce_assets.photoTile" }, 16 | { "type_id": "commerce_assets.popularCategory" }, 17 | { "type_id": "commerce_assets.productTile" }, 18 | { "type_id": "commerce_assets.shopTheLook" }, 19 | { "type_id": "commerce_layouts.carousel" }, 20 | { "type_id": "commerce_layouts.mobileGrid1r1c" }, 21 | { "type_id": "commerce_layouts.mobileGrid2r1c" }, 22 | { "type_id": "commerce_layouts.mobileGrid2r2c" }, 23 | { "type_id": "commerce_layouts.mobileGrid2r3c" }, 24 | { "type_id": "commerce_layouts.mobileGrid3r1c" }, 25 | { "type_id": "commerce_layouts.mobileGrid3r2c" }, 26 | { "type_id": "commerce_layouts.mobileGridLookBook" }, 27 | { "type_id": "commerce_layouts.popularCategories" }, 28 | { "type_id": "einstein.einsteinCarousel" }, 29 | { "type_id": "einstein.einsteinCarouselCategory" }, 30 | { "type_id": "einstein.einsteinCarouselProduct" } 31 | ] 32 | }, 33 | { 34 | "id":"main", 35 | "name":"Main Region", 36 | "component_type_exclusions": [ 37 | { "type_id": "commerce_assets.campaignBanner" } 38 | ] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/utilities/ImageTransformation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ImageTransformation = {}; 4 | var BREAKPOINTS = require('*/cartridge/experience/breakpoints.json'); 5 | var Image = require('dw/experience/image/Image'); 6 | var MediaFile = require('dw/content/MediaFile'); 7 | 8 | var transformationCapabilities = [ 9 | 'scaleWidth', 10 | 'scaleHeight', 11 | 'scaleMode', 12 | 'imageX', 13 | 'imageY', 14 | 'imageURI', 15 | 'cropX', 16 | 'cropY', 17 | 'cropWidth', 18 | 'cropHeight', 19 | 'format', 20 | 'quality', 21 | 'strip' 22 | ]; 23 | 24 | /** 25 | * Calculates the required DIS transformation object based on the given parameters. Currently 26 | * only downscaling is performed. 27 | * 28 | * @param {Object} metaData the image meta data containing image width and height 29 | * @param {string} device the device the image should be scaled for (supported values: mobile, desktop) 30 | * @return {Object} The scaled object 31 | */ 32 | ImageTransformation.scale = function (metaData, device) { 33 | var transformObj = null; 34 | if (metaData && device) { 35 | var targetWidth = BREAKPOINTS[device]; 36 | // only downscale if image is larger than desired width 37 | if (targetWidth && targetWidth < metaData.width) { 38 | transformObj = { 39 | scaleWidth: targetWidth, 40 | format: 'jpg', 41 | scaleMode: 'fit' 42 | }; 43 | } 44 | } 45 | return transformObj; 46 | }; 47 | 48 | /** 49 | * Creates a cleaned up transformation object 50 | * @param {*} options the paarmaters object which may hold additional properties not suppoerted by DIS e.g. option.device == 'mobile' 51 | * @param {*} transform a preconstructed transformation object 52 | * @return {Object} The transformed object. 53 | */ 54 | function constructTransformationObject(options, transform) { 55 | var result = transform || {}; 56 | Object.keys(options).forEach(function (element) { 57 | if (transformationCapabilities.indexOf(element)) { 58 | result[element] = options[element]; 59 | } 60 | }); 61 | return result; 62 | } 63 | 64 | /** 65 | * Provides a url to the given media file image. DIS transformation will be applied as given. 66 | * 67 | * @param {Image|MediaFile} image the image for which the url should be obtained. In case of an Image type the options may add a device property to scale the image to 68 | * @param {Object} options the (optional) DIS transformation parameters or option with devices 69 | * 70 | * @return {string} The Absolute url 71 | */ 72 | ImageTransformation.url = function (image, options) { 73 | var transform = {}; 74 | var mediaFile = image instanceof MediaFile ? image : image.file; 75 | 76 | if (image instanceof Image && options.device) { 77 | transform = ImageTransformation.scale(image.metaData, options.device); 78 | } 79 | transform = constructTransformationObject(options, transform); 80 | 81 | if (transform && Object.keys(transform).length) { 82 | return mediaFile.getImageURL(transform); 83 | } 84 | 85 | return mediaFile.getAbsURL(); 86 | }; 87 | 88 | /** 89 | * Return an object containing the scaled image for mobile, table and desktop. Other included image details 90 | * are: alt text, focalPoint x, focalPoint y. 91 | * 92 | * @param {Image} image the image for which to be scaled. 93 | * @param {Object} The object containing the scaled image 94 | * 95 | * @return {string} The Absolute url 96 | */ 97 | ImageTransformation.getScaledImage = function (image) { 98 | return { 99 | src: { 100 | mobile: ImageTransformation.url(image.file, { device: 'mobile' }), 101 | tablet: ImageTransformation.url(image.file, { device: 'tablet' }), 102 | desktop: ImageTransformation.url(image.file, { device: 'desktop' }) 103 | }, 104 | alt: image.file.getAlt(), 105 | focalPointX: (image.focalPoint.x * 100) + '%', 106 | focalPointY: (image.focalPoint.y * 100) + '%' 107 | }; 108 | }; 109 | 110 | module.exports = ImageTransformation; 111 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/utilities/PageRenderHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RegionModelRegistry = require('*/cartridge/experience/utilities/RegionModelRegistry.js'); 4 | 5 | module.exports = { 6 | /** 7 | * Assembles the page meta data. 8 | * 9 | * @param {dw.experience.Page} page The page object 10 | * 11 | * @returns {dw.web.PageMetaData} The page meta data 12 | */ 13 | getPageMetaData: function getPageMetaData(page) { 14 | var computedMetaData = { 15 | title: page.pageTitle, 16 | description: page.pageDescription, 17 | keywords: page.pageKeywords, 18 | pageMetaTags: [] 19 | }; 20 | 21 | request.pageMetaData.pageMetaTags.forEach(function (item) { 22 | if (item.title) { 23 | computedMetaData.title = item.content; 24 | } else if (item.name && item.ID === 'description') { 25 | computedMetaData.description = item.content; 26 | } else if (item.name && item.ID === 'keywords') { 27 | computedMetaData.keywords = item.content; 28 | } else { 29 | computedMetaData.pageMetaTags.push(item); 30 | } 31 | }); 32 | 33 | return computedMetaData; 34 | }, 35 | 36 | /** 37 | * Returns the RegionModel registry for a given container (Page or Component). 38 | * 39 | * @param {dw.experience.Page|dw.experience.Component} container a component or page object 40 | * @param {string} containerType components or pages 41 | * 42 | * @returns {experience.utilities.RegionModelRegistry} The container regions 43 | */ 44 | getRegionModelRegistry: function getRegionModelRegistry(container) { 45 | var containerType; 46 | if (container && container instanceof dw.experience.Page) { 47 | containerType = 'pages'; 48 | } else if (container && container instanceof dw.experience.Component) { 49 | containerType = 'components'; 50 | } else { 51 | return null; 52 | } 53 | var metaDefinition = require('*/cartridge/experience/' + containerType + '/' + container.typeID.replace(/\./g, '/') + '.json'); 54 | 55 | return new RegionModelRegistry(container, metaDefinition); 56 | }, 57 | 58 | /** 59 | * Returns true if page is rendered via editor UI and false in the storefront 60 | * @returns {boolean} The container regions 61 | */ 62 | isInEditMode: function isInEditMode() { 63 | return request.httpPath.indexOf('__SYSTEM__Page-Show') > 0; 64 | }, 65 | 66 | /** 67 | * Returns a css safe string of a given input string 68 | * @param {string} input a css class name. 69 | * @return {string} css 70 | */ 71 | safeCSSClass: function (input) { 72 | return encodeURIComponent(input.toLowerCase()).replace(/%[0-9A-F]{2}/gi, ''); 73 | } 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/utilities/RegionModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RegionRenderSettings = require('dw/experience/RegionRenderSettings'); 4 | var ComponentRenderSettings = require('dw/experience/ComponentRenderSettings'); 5 | var PageMgr = require('dw/experience/PageMgr'); 6 | var HashMap = require('dw/util/HashMap'); 7 | 8 | /** 9 | * Set name/value attribute pair at given settings object, this can be a settings 10 | * of a component, a region and either default or a specific one. 11 | * 12 | * @param {*} renderSettings region or component rendering settings 13 | * @param {string} name the attribute name to set 14 | * @param {string} value the attribute value to set 15 | */ 16 | function setAttribute(renderSettings, name, value) { 17 | if (renderSettings !== null) { 18 | var attr = renderSettings.getAttributes() || new HashMap(); 19 | attr.put(name, value); 20 | renderSettings.setAttributes(attr); 21 | } 22 | } 23 | 24 | /** 25 | * A script representation of a region, which adds convenient access to the regions 26 | * and its components render settings and a shortcut to render given region 27 | * 28 | * @param {*} container The page or parent region 29 | * @param {string} name The name of the region 30 | * 31 | * @class 32 | */ 33 | function RegionModel(container, name) { 34 | this.region = container.getRegion(name); 35 | this.regionRenderSettings = (new RegionRenderSettings()).setTagName('div'); 36 | this.defaultComponentRenderSettings = (new ComponentRenderSettings()).setTagName('div'); 37 | this.regionRenderSettings.setDefaultComponentRenderSettings(this.defaultComponentRenderSettings); 38 | } 39 | 40 | /** 41 | * Set the tag name of a region 42 | * 43 | * @param {string} name the name of the tag (default: div) 44 | * @param {booleam} inComponents wether the tag should be used in its components too (default: div) 45 | * @return {RegionModel} The region model object 46 | */ 47 | RegionModel.prototype.setTagName = function tagName(name, inComponents) { 48 | this.regionRenderSettings.setTagName(name); 49 | if (inComponents) { 50 | this.defaultComponentRenderSettings = (new ComponentRenderSettings()).setTagName(name); 51 | this.regionRenderSettings.setDefaultComponentRenderSettings(this.defaultComponentRenderSettings); 52 | } 53 | return this; 54 | }; 55 | 56 | /** 57 | * Set the class name of a region 58 | * 59 | * @param {string} cssClass the class name(s) of the region 60 | * @return {RegionModel} The region model object 61 | */ 62 | RegionModel.prototype.setClassName = function setClassName(cssClass) { 63 | setAttribute(this.regionRenderSettings, 'class', cssClass); 64 | return this; 65 | }; 66 | 67 | /** 68 | * Set attribute of the region container 69 | * 70 | * @param {string} name the region tag attribute name 71 | * @param {string} value the region tag attribute value 72 | * @return {RegionModel} The region model object 73 | */ 74 | RegionModel.prototype.setAttribute = function attr(name, value) { 75 | setAttribute(this.regionRenderSettings, name, value); 76 | return this; 77 | }; 78 | 79 | /** 80 | * Set the tag name for a given or all components of a region 81 | * 82 | * @param {string} tagName the component tag name to set 83 | * @param {number} [position] optional position to only set it for a specific component 84 | * @return {RegionModel} The region model object 85 | */ 86 | RegionModel.prototype.setComponentTagName = function componentTagName(tagName, position) { 87 | if (typeof position === 'number') { 88 | // ignore request in case position is invalid 89 | if (!this.region.visibleComponents 90 | || position >= this.region.visibleComponents.length) { 91 | return this; 92 | } 93 | 94 | this.region.visibleComponents[position].setTagName(tagName); 95 | } else { 96 | this.defaultComponentRenderSettings.setTagName(tagName); 97 | } 98 | 99 | return this; 100 | }; 101 | 102 | /** 103 | * Set the class name for a given or all components of a region 104 | * 105 | * @param {*} cssClass the component class name to set 106 | * @param {string} componentSelector optional has position attribute to only set it for a specific component 107 | * @return {RegionModel} The region model object 108 | */ 109 | RegionModel.prototype.setComponentClassName = function setComponentClassName(cssClass, componentSelector) { 110 | this.setComponentAttribute('class', cssClass, componentSelector); 111 | return this; 112 | }; 113 | 114 | /** 115 | * Set a given attribute to a given or all components of a region 116 | * 117 | * @param {string} name the name of the attribute 118 | * @param {string} value the value of the attribute 119 | * @param {string} componentSelector optional has position attribute to only set it for a specific component 120 | * @return {RegionModel} The region model object 121 | */ 122 | RegionModel.prototype.setComponentAttribute = function setComponentAttribute(name, value, componentSelector) { 123 | // default is all components 124 | var renderSettings = this.defaultComponentRenderSettings; 125 | var position = componentSelector && componentSelector.position; 126 | var component; 127 | // when position is set, only set for the component at that position 128 | if (typeof position === 'number') { 129 | // ignore request in case position is invalid 130 | if (!this.region.visibleComponents 131 | || position >= this.region.visibleComponents.length) { 132 | return this; 133 | } 134 | component = this.region.visibleComponents[position]; 135 | renderSettings = this.regionRenderSettings.getComponentRenderSettings(component); 136 | } 137 | 138 | setAttribute(renderSettings, name, value); 139 | 140 | if (component) { 141 | this.regionRenderSettings.setComponentRenderSettings(component, renderSettings); 142 | } 143 | 144 | return this; 145 | }; 146 | 147 | /** 148 | * Renders the entire region 149 | * 150 | * @returns {Object} the rendered region 151 | */ 152 | RegionModel.prototype.render = function render() { 153 | return PageMgr.renderRegion(this.region, this.regionRenderSettings); 154 | }; 155 | 156 | module.exports = RegionModel; 157 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/experience/utilities/RegionModelRegistry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var RegionModel = require('*/cartridge/experience/utilities/RegionModel.js'); 4 | 5 | /** 6 | * Utility providing shortened access to render a subregion of the given container within a template 7 | * 8 | * @param {*} container the container obejct (a page or component) 9 | * @param {*} metaDataDefinition the object representation of the defintion JSON 10 | */ 11 | var RegionModelRegistry = function (container, metaDataDefinition) { 12 | this.container = container; 13 | this.addRegions(metaDataDefinition); 14 | }; 15 | 16 | /** 17 | * Registers all regions that are defined in the given meta definitions. 18 | * 19 | * Returns nothing as it only registers the regions for itself. 20 | * 21 | * @param {Object} metadef The components meta definitions 22 | */ 23 | RegionModelRegistry.prototype.addRegions = function (metadef) { 24 | if (metadef && metadef.region_definitions) { 25 | metadef.region_definitions.forEach(function (regionDefinition) { 26 | var name = regionDefinition.id; 27 | if (!this[name]) { 28 | this[name] = new RegionModel(this.container, name); 29 | } 30 | }, this); 31 | } 32 | }; 33 | 34 | module.exports = RegionModelRegistry; 35 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Site = require('dw/system/Site'); 4 | var currentSite = Site.getCurrent(); 5 | 6 | module.exports = { 7 | // The header name set in Customer CDN settings -> Client IP Header Name. Allows B2C to retrieve the client IP during session bridging. 8 | CLIENT_IP_HEADER_NAME: 9 | currentSite.getCustomPreferenceValue('clientIPHeaderName'), 10 | 11 | // The request URI used to fetch OCAPI Session in bridge service - SLAS 12 | OCAPI_SESSION_BRIDGE_URI: currentSite.getCustomPreferenceValue( 13 | 'ocapiSessionBridgeURI' 14 | ) 15 | }; 16 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/helpers/basketCalculationHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var HookMgr = require('dw/system/HookMgr'); 4 | 5 | /** 6 | * Calculate sales taxes 7 | * @param {dw.order.Basket} basket - current basket 8 | * @returns {Object} - object describing taxes that needs to be applied 9 | */ 10 | function calculateTaxes(basket) { 11 | var hooksHelper = require('*/cartridge/scripts/helpers/hooks'); 12 | return hooksHelper('app.basket.taxes', 'calculateTaxes', basket, require('*/cartridge/scripts/hooks/taxes').calculateTaxes); 13 | } 14 | 15 | /** 16 | * Calculate all totals as well as shipping and taxes 17 | * @param {dw.order.Basket} basket - current basket 18 | */ 19 | function calculateTotals(basket) { 20 | HookMgr.callHook('dw.order.calculate', 'calculate', basket); 21 | } 22 | 23 | module.exports = { 24 | calculateTotals: calculateTotals, 25 | calculateTaxes: calculateTaxes 26 | }; 27 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/helpers/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var HookMgr = require('dw/system/HookMgr'); 4 | 5 | module.exports = function (hookName, functionName, args) { 6 | var passedArgs = []; 7 | 8 | if (Array.isArray(args)) { 9 | passedArgs = args; 10 | } else if (arguments.length === 4) { 11 | passedArgs = [args]; 12 | } else { 13 | passedArgs = Array.prototype.slice.call(arguments, 2, arguments.length - 1); 14 | } 15 | 16 | if (HookMgr.hasHook(hookName)) { 17 | return HookMgr.callHook.apply(this, [hookName, functionName].concat(passedArgs)); 18 | } 19 | return arguments[arguments.length - 1].apply(this, passedArgs); 20 | }; 21 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/helpers/productSearchHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var CacheMgr = require('dw/system/CacheMgr'); 4 | 5 | /** 6 | * Fetch all static pricing information for the given product 7 | * 8 | * @param {dw.product.Product} product - The product 9 | * @returns {Object} - The static pricing information 10 | */ 11 | function getStaticResultData(product) { 12 | var staticCache = CacheMgr.getCache('ProductExtendStatic'); 13 | 14 | // try to cache if we can 15 | var resultSealed = staticCache.get(product.ID + ';' + request.locale, function () { 16 | var priceModel = product.priceModel; 17 | if (!priceModel) { 18 | return {}; 19 | } 20 | var originalPrice = priceModel.price; 21 | var activePriceBookId; 22 | var salePrice = priceModel.price; 23 | var parentPriceBookID; 24 | if (!empty(priceModel) && !empty(priceModel.priceInfo)) { 25 | activePriceBookId = priceModel.priceInfo.priceBook.ID; 26 | if (!empty(priceModel.priceInfo.priceBook.parentPriceBook)) { 27 | parentPriceBookID = priceModel.priceInfo.priceBook.parentPriceBook.ID; 28 | originalPrice = priceModel.getPriceBookPrice(parentPriceBookID); 29 | } 30 | } 31 | 32 | return { 33 | masterProductId: product.variationModel.master ? product.variationModel.master.ID : product.ID, 34 | id: product.ID, 35 | priceInfo: { 36 | originalPrice: { 37 | value: originalPrice.value, 38 | currency: originalPrice.currencyCode, 39 | pricebook: parentPriceBookID || activePriceBookId 40 | }, 41 | salePrice: { 42 | value: salePrice.value, 43 | currency: salePrice.currencyCode, 44 | pricebook: activePriceBookId 45 | } 46 | } 47 | }; 48 | }); 49 | 50 | // make cache entry editable, as it is sealed otherwise 51 | 52 | return JSON.parse(JSON.stringify(resultSealed)); 53 | } 54 | 55 | /** 56 | * Fetch all static pricing information for the given product 57 | * 58 | * @param {dw.product.Product} product - The product 59 | * @param {Object} result - The current result 60 | * 61 | * @returns {Object} - The result extended with promotional information 62 | */ 63 | function extendResultWithPromotionData(product, result) { 64 | var PromotionMgr = require('dw/campaign/PromotionMgr'); 65 | 66 | var modifiedResult = result; 67 | var customerPromotions = PromotionMgr.getActiveCustomerPromotions(); 68 | var promos = customerPromotions.getProductPromotions(product).iterator(); 69 | 70 | if (promos.hasNext()) { 71 | var promo = promos.next(); 72 | 73 | // add personalized information to cache entry 74 | var dynamicCache = CacheMgr.getCache('ProductExtendDynamic'); 75 | var promotionPrice = dynamicCache.get(product.ID + ';' + promo.ID + ';' + request.locale, function () { 76 | var promoPrice = promo.getPromotionalPrice(product); 77 | 78 | return { 79 | value: promoPrice.value, 80 | currency: promoPrice.currencyCode, 81 | promoDetails: { 82 | id: promo.ID, 83 | name: promo.name ? promo.name.toString() : null, 84 | callOut: promo.calloutMsg ? promo.calloutMsg.toString() : null, 85 | details: promo.details ? promo.details.toString() : null, 86 | image: promo.image ? promo.image.absURL : null 87 | } 88 | }; 89 | }); 90 | 91 | modifiedResult.priceInfo.promotionPrice = promotionPrice; 92 | } 93 | 94 | return modifiedResult; 95 | } 96 | 97 | /** 98 | * Converts a product into a extended object 99 | * 100 | * @param {string} productId - The SKU of the product 101 | * @returns {Object} - The extended attributes 102 | */ 103 | exports.createExtendedProduct = function (productId) { 104 | var ProductMgr = require('dw/catalog/ProductMgr'); 105 | 106 | var product = ProductMgr.getProduct(productId); 107 | 108 | if (!product) { 109 | return null; 110 | } 111 | 112 | var result = getStaticResultData(product); 113 | result = extendResultWithPromotionData(product, result); 114 | 115 | return result; 116 | }; 117 | 118 | exports.getSearchRedirectInformation = function (query) { 119 | if (!query) { 120 | return null; 121 | } 122 | 123 | var searchDrivenRedirectCache = CacheMgr.getCache('SearchDrivenRedirect'); 124 | 125 | var result = searchDrivenRedirectCache.get(query + ';' + request.locale, function () { 126 | var ProductSearchModel = require('dw/catalog/ProductSearchModel'); 127 | var apiProductSearch = new ProductSearchModel(); 128 | 129 | /** 130 | * @type {dw.web.URLRedirect} 131 | */ 132 | var searchRedirect = apiProductSearch.getSearchRedirect(query); 133 | 134 | if (searchRedirect) { 135 | return searchRedirect.getLocation(); 136 | } 137 | 138 | return null; 139 | }); 140 | 141 | return result; 142 | }; 143 | 144 | /** 145 | * Retrieve Custom Page Meta Tag Rules configured in the Business Manager for search 146 | * 147 | * @param {string} query - The search query 148 | * @returns {Object|null} - The configured rules 149 | */ 150 | exports.getSearchMetaData = function (query) { 151 | if (!query) { 152 | return null; 153 | } 154 | 155 | var metaDataCache = CacheMgr.getCache('MetaData'); 156 | 157 | var result = metaDataCache.get(query + ';' + request.locale, function () { 158 | var ProductSearchModel = require('dw/catalog/ProductSearchModel'); 159 | var seoHelper = require('*/cartridge/scripts/helpers/seoHelper'); 160 | 161 | var apiProductSearch = new ProductSearchModel(); 162 | 163 | return seoHelper.getPageMetaTags(apiProductSearch); 164 | }); 165 | 166 | return result; 167 | }; 168 | 169 | /** 170 | * Retrieve Custom Page Meta Tag Rules configured in the Business Manager for a category 171 | * 172 | * @param {string} category - The category 173 | * @returns {Object|null} - The configured rules 174 | */ 175 | exports.getCategoryMetaData = function (category) { 176 | if (!category) { 177 | return null; 178 | } 179 | 180 | var metaDataCache = CacheMgr.getCache('MetaData'); 181 | 182 | var result = metaDataCache.get(category.ID + ';' + request.locale, function () { 183 | var ProductSearchModel = require('dw/catalog/ProductSearchModel'); 184 | var seoHelper = require('*/cartridge/scripts/helpers/seoHelper'); 185 | 186 | var apiProductSearch = new ProductSearchModel(); 187 | apiProductSearch.setCategoryID(category.ID); 188 | 189 | return seoHelper.getPageMetaTags(apiProductSearch); 190 | }); 191 | 192 | return result; 193 | }; 194 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/helpers/seoHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Gets page meta tags to support rule based meta data 5 | * 6 | * @param {Object} object - object which contains page meta tags 7 | * 8 | * @returns {Array<{ID: string, content:string, name:boolean, property:boolean, title:boolean}>} - The configured Page Meta Tags 9 | */ 10 | function getPageMetaTags(object) { 11 | if (!object) { 12 | return null; 13 | } 14 | 15 | if ('pageMetaTags' in object) { 16 | return object.pageMetaTags.map(function (pageMetaTag) { 17 | return { 18 | ID: pageMetaTag.ID, 19 | content: pageMetaTag.content, 20 | name: pageMetaTag.name, 21 | property: pageMetaTag.property, 22 | title: pageMetaTag.title 23 | }; 24 | }); 25 | } 26 | 27 | return null; 28 | } 29 | 30 | module.exports = { 31 | getPageMetaTags: getPageMetaTags 32 | }; 33 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/helpers/sessionHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Cookie = require('dw/web/Cookie'); 4 | var sessionBridge = require('*/cartridge/scripts/services/SessionBridgeService'); 5 | 6 | /** 7 | * save cookies to HTTP response 8 | * @param {{Array}} cookieStrings - array of set-Cookie header strings 9 | * @param {{dw.system.Response}} resp - response object 10 | */ 11 | function addCookiesToResponse(cookieStrings, resp) { 12 | cookieStrings.toArray().forEach(function (cookieString) { 13 | var cookieParts = cookieString.split(';'); 14 | var nameValue = cookieParts.shift().split('='); 15 | var name = nameValue.shift(); 16 | var value = nameValue.join('='); 17 | value = decodeURIComponent(value); 18 | var newCookie = new Cookie(name, value); 19 | cookieParts.forEach(function (part) { 20 | var sides = part.split('='); 21 | var key = sides.shift().trim().toLowerCase(); 22 | value = sides.join('='); 23 | if (key === 'path') { 24 | newCookie.setPath(value); 25 | } else if (key === 'max-age') { 26 | newCookie.setMaxAge(parseInt(value, 10)); 27 | } else if (key === 'secure') { 28 | newCookie.setSecure(true); 29 | } else if (key === 'httponly') { 30 | newCookie.setHttpOnly(true); 31 | } else if (key === 'version') { 32 | newCookie.setVersion(value); 33 | } 34 | }); 35 | resp.addHttpCookie(newCookie); 36 | }); 37 | } 38 | 39 | /** 40 | * Establish session with session bridge using the access token 41 | * @param {{string}}accessToken - access_token to be used to establish session 42 | * @param {dw.system.Response} resp - response object 43 | * @param {dw.system.Request} req - request object 44 | * @returns {{Object}} - response from session bridge API call 45 | */ 46 | function setUserSession(accessToken, resp, req) { 47 | var responseObj = {}; 48 | var ip; 49 | if (req && req.httpRemoteAddress) { 50 | ip = req.httpRemoteAddress; 51 | } 52 | 53 | var result = sessionBridge.getSession(accessToken, ip); 54 | if (result && result.responseHeaders) { 55 | var cookies = result.responseHeaders.get('set-cookie'); 56 | 57 | if (cookies) { 58 | responseObj.cookies = cookies; 59 | // drop the cookies in browser 60 | addCookiesToResponse(cookies, resp); 61 | responseObj.ok = true; 62 | } else { 63 | responseObj.ok = false; 64 | } 65 | } else { 66 | responseObj.ok = false; 67 | } 68 | return responseObj; 69 | } 70 | 71 | module.exports = { 72 | setUserSession: setUserSession 73 | }; 74 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/hooks/category.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, consistent-return */ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Add customisations to the Category response. 7 | * 8 | * @param {dw.catalog.Category} dwCategory - The category 9 | * @param {Object} category - Document representing a category 10 | */ 11 | exports.modifyGETResponse = function (dwCategory, category) { 12 | if (request.isSCAPI()) { 13 | var productSearchHelper = require('*/cartridge/scripts/helpers/productSearchHelper'); 14 | 15 | category.c_metadata = productSearchHelper.getCategoryMetaData(dwCategory); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/hooks/onSession.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * The onSession hook is called for every new session in a site. For performance reasons the hook function should be kept short. 5 | * 6 | */ 7 | 8 | var Status = require('dw/system/Status'); 9 | var Encoding = require('dw/crypto/Encoding'); 10 | var URLUtils = require('dw/web/URLUtils'); 11 | var URLAction = require('dw/web/URLAction'); 12 | var URLParameter = require('dw/web/URLParameter'); 13 | var sessionHelper = require('*/cartridge/scripts/helpers/sessionHelper'); 14 | var Logger = require('dw/system/Logger').getLogger('session-bridge'); 15 | 16 | /** 17 | * Puts together the SEO URL from the httpPath and httpQueryString of a request 18 | * 19 | * The httpPath will look like /on/demandware.store/Sites-RefArch-Site/en_US/Login-Show 20 | * 21 | * @param {string} httpPath - the http path from the request url. This is the relative non SEO-optimized path 22 | * @param {string} queryString - the query string from the request url 23 | * @returns {dw.web.URL} url - the SEO optimized url path for the current page 24 | */ 25 | function getSEOUrl(httpPath, queryString) { 26 | var pathParts = httpPath.substr(1).split('/'); 27 | 28 | // If there are 3 or less parts to the httpPath there is probably no specified controller so we direct to the home page 29 | if (pathParts.length <= 3) { 30 | return URLUtils.httpsHome(); 31 | } 32 | 33 | // The action (aka the controller start node) is always the final part of the httpPath 34 | var action = new URLAction(pathParts[pathParts.length - 1]); 35 | 36 | var urlParts = []; 37 | if (queryString) { 38 | var qsParts = queryString.split('&'); 39 | urlParts = qsParts.map(function (qsParam) { 40 | var paramParts = qsParam.split('='); 41 | 42 | if (paramParts[1]) { 43 | // The query parameter is a key/value pair, e.g. `?foo=bar` 44 | 45 | var key = paramParts.shift(); 46 | // if there are `=` characters in the parameter value, rejoin them 47 | var value = paramParts.join('='); 48 | 49 | return new URLParameter(key, value); 50 | } 51 | 52 | // The query parameter is not a key/value pair, e.g. `?queryparam` 53 | 54 | return new URLParameter(undefined, qsParam, false); 55 | }); 56 | } 57 | urlParts.unshift(action); 58 | return Encoding.fromURI(URLUtils.url.apply(URLUtils, urlParts).toString()); 59 | } 60 | 61 | /** 62 | * The onSession hook function 63 | * 64 | * @returns {dw/system/Status} status - return status 65 | */ 66 | exports.onSession = function () { 67 | var isStorefrontSession = session && session.customer; 68 | var isNotRegisteredUser = !session.customer.profile; 69 | var bearerToken = request.httpHeaders.authorization ? request.httpHeaders.authorization.replace('Bearer ', '') : null; 70 | var isRedirect = request.httpParameters.sb_redirect; 71 | 72 | // For now this method only works for GET calls as a redirect is involved. 73 | var isGET = request.httpMethod === 'GET'; 74 | 75 | if (isStorefrontSession 76 | && isNotRegisteredUser 77 | && bearerToken 78 | && isGET 79 | && !isRedirect 80 | ) { 81 | // establish user session using the access token 82 | var result = sessionHelper.setUserSession( 83 | bearerToken, 84 | response, 85 | request 86 | ); 87 | 88 | if (!result.ok) { 89 | Logger.error( 90 | 'Exception: Could not establish session using session bridge, check the service response' 91 | ); 92 | } else { 93 | Logger.debug( 94 | 'Session bridge successfully completed!' 95 | ); 96 | 97 | var redirectParameter = (request.httpQueryString ? '&' : '') + 'sb_redirect=true'; 98 | 99 | response.redirect(getSEOUrl(request.httpPath, (request.httpQueryString || '') + redirectParameter)); 100 | } 101 | } else { 102 | Logger.debug( 103 | 'Could not initiate Session Bridge!' 104 | ); 105 | } 106 | 107 | return new Status(Status.OK); 108 | }; 109 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/hooks/product.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign, consistent-return */ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Add customisations to the Product response. 7 | * 8 | * @param {dw.catalog.Product} dwProduct - The product 9 | * @param {Object} product - Document representing a product 10 | */ 11 | exports.modifyGETResponse = function (dwProduct, product) { 12 | if (request.isSCAPI()) { 13 | var seoHelper = require('*/cartridge/scripts/helpers/seoHelper'); 14 | 15 | product.c_metadata = seoHelper.getPageMetaTags(dwProduct); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/hooks/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-param-reassign, consistent-return */ 4 | 5 | /** 6 | * Add customisations to the Search response. 7 | * 8 | * @param {{query: string, c_searchRedirect: string}} searchResponse - Document representing a product search result 9 | * @returns {dw.system.Status} - The status of the result (OK or Error) 10 | */ 11 | exports.modifyGETResponse = function (searchResponse) { 12 | var Status = require('dw/system/Status'); 13 | 14 | if (request.isSCAPI()) { 15 | try { 16 | var productSearchHelper = require('*/cartridge/scripts/helpers/productSearchHelper'); 17 | 18 | if (searchResponse.query) { 19 | var redirectResult = productSearchHelper.getSearchRedirectInformation(searchResponse.query); 20 | 21 | if (redirectResult) { 22 | searchResponse.search_phrase_suggestions.c_searchRedirect = redirectResult; 23 | 24 | // No need to do any other customisations, end the hook (and others after it). 25 | return new Status(Status.OK); 26 | } 27 | 28 | var metaData = productSearchHelper.getSearchMetaData(searchResponse.query); 29 | 30 | searchResponse.search_phrase_suggestions.c_metadata = metaData; 31 | } 32 | 33 | if (searchResponse.count > 0) { 34 | var hits = searchResponse.hits.toArray(); 35 | hits.forEach(function (hit) { 36 | if (hit.represented_product) { 37 | hit.c_extend = productSearchHelper.createExtendedProduct(hit.represented_product.id); 38 | } 39 | }); 40 | } 41 | } catch (e) { 42 | return new Status(Status.ERROR, 'ERR-SEARCH-01', e.message); 43 | } 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/hooks/taxes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var collections = require('*/cartridge/scripts/util/collections'); 4 | var ShippingLocation = require('dw/order/ShippingLocation'); 5 | var TaxMgr = require('dw/order/TaxMgr'); 6 | var Logger = require('dw/system/Logger'); 7 | 8 | /** 9 | * @typedef {Object} TaxField 10 | * @property {string} UUID - ID of the line item 11 | * @property {number|dw.value.Money} value - Either Tax Code or Tax Amount that should be applied to the line item. 12 | * @property {boolean} [amount=false] - Boolean indicating whether value field contains Tax Amount (true) or Tax Rate (false). 13 | */ 14 | 15 | /** 16 | * @typedef {Object} Response 17 | * @property {Array} taxes - List of taxes to line items UUIDs to be applied to the order 18 | * @property {Object} custom - List of custom properties to be attached to the basket 19 | */ 20 | 21 | /** 22 | * Calculate sales taxes 23 | * @param {dw.order.Basket} basket - current basket 24 | * @returns {Response} - An object that contains calculated taxes and custom properties 25 | */ 26 | function calculateTaxes(basket) { 27 | var taxes = []; 28 | 29 | var shipments = basket.getShipments(); 30 | collections.forEach(shipments, function (shipment) { 31 | var taxJurisdictionId = null; 32 | 33 | if (shipment.shippingAddress) { 34 | var location = new ShippingLocation(shipment.shippingAddress); 35 | taxJurisdictionId = TaxMgr.getTaxJurisdictionID(location); 36 | } 37 | 38 | if (!taxJurisdictionId) { 39 | taxJurisdictionId = TaxMgr.defaultTaxJurisdictionID; 40 | } 41 | 42 | // if we have no tax jurisdiction, we cannot calculate tax 43 | if (!taxJurisdictionId) { 44 | return; 45 | } 46 | 47 | var lineItems = shipment.getAllLineItems(); 48 | 49 | collections.forEach(lineItems, function (lineItem) { 50 | var taxClassId = lineItem.taxClassID; 51 | 52 | Logger.debug('1. Line Item {0} with Tax Class {1} and Tax Rate {2}', lineItem.lineItemText, lineItem.taxClassID, lineItem.taxRate); 53 | 54 | // do not touch line items with fix tax rate 55 | if (taxClassId === TaxMgr.customRateTaxClassID) { 56 | return; 57 | } 58 | 59 | // line item does not define a valid tax class; let's fall back to default tax class 60 | if (!taxClassId) { 61 | taxClassId = TaxMgr.defaultTaxClassID; 62 | } 63 | 64 | // if we have no tax class, we cannot calculate tax 65 | if (!taxClassId) { 66 | Logger.error('Line Item {0} has invalid Tax Class {1}', lineItem.lineItemText, lineItem.taxClassID); 67 | return; 68 | } 69 | 70 | // get the tax rate 71 | var taxRate = TaxMgr.getTaxRate(taxClassId, taxJurisdictionId); 72 | // w/o a valid tax rate, we cannot calculate tax for the line item 73 | if (!taxRate && taxRate !== 0) { 74 | return; 75 | } 76 | 77 | // calculate the tax of the line item 78 | taxes.push({ uuid: lineItem.UUID, value: taxRate, amount: false }); 79 | Logger.debug('2. Line Item {0} with Tax Class {1} and Tax Rate {2}', lineItem.lineItemText, lineItem.taxClassID, lineItem.taxRate); 80 | }); 81 | }); 82 | 83 | return { taxes: taxes, custom: {} }; 84 | } 85 | 86 | module.exports = { 87 | calculateTaxes: calculateTaxes 88 | }; 89 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/middleware/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Applies the default expiration value for the page cache. 5 | * @param {Object} req - Request object 6 | * @param {Object} res - Response object 7 | * @param {Function} next - Next call in the middleware chain 8 | * @returns {void} 9 | */ 10 | function applyDefaultCache(req, res, next) { 11 | res.cachePeriod = 24; // eslint-disable-line no-param-reassign 12 | res.cachePeriodUnit = 'hours'; // eslint-disable-line no-param-reassign 13 | next(); 14 | } 15 | 16 | /** 17 | * Applies the default price promotion page cache. 18 | * @param {Object} req - Request object 19 | * @param {Object} res - Response object 20 | * @param {Function} next - Next call in the middleware chain 21 | * @returns {void} 22 | */ 23 | function applyPromotionSensitiveCache(req, res, next) { 24 | res.cachePeriod = 24; // eslint-disable-line no-param-reassign 25 | res.cachePeriodUnit = 'hours'; // eslint-disable-line no-param-reassign 26 | res.personalized = true; // eslint-disable-line no-param-reassign 27 | next(); 28 | } 29 | 30 | /** 31 | * Applies the default price promotion page cache. 32 | * @param {Object} req - Request object 33 | * @param {Object} res - Response object 34 | * @param {Function} next - Next call in the middleware chain 35 | * @returns {void} 36 | */ 37 | function applyShortPromotionSensitiveCache(req, res, next) { 38 | res.cachePeriod = 1; // eslint-disable-line no-param-reassign 39 | res.cachePeriodUnit = 'hours'; // eslint-disable-line no-param-reassign 40 | res.personalized = true; // eslint-disable-line no-param-reassign 41 | next(); 42 | } 43 | 44 | /** 45 | * Applies the inventory sensitive page cache. 46 | * @param {Object} req - Request object 47 | * @param {Object} res - Response object 48 | * @param {Function} next - Next call in the middleware chain 49 | * @returns {void} 50 | */ 51 | function applyInventorySensitiveCache(req, res, next) { 52 | res.cachePeriod = 30; // eslint-disable-line no-param-reassign 53 | res.cachePeriodUnit = 'minutes'; // eslint-disable-line no-param-reassign 54 | next(); 55 | } 56 | 57 | module.exports = { 58 | applyDefaultCache: applyDefaultCache, 59 | applyPromotionSensitiveCache: applyPromotionSensitiveCache, 60 | applyInventorySensitiveCache: applyInventorySensitiveCache, 61 | applyShortPromotionSensitiveCache: applyShortPromotionSensitiveCache 62 | }; 63 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/middleware/userLoggedIn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Middleware validating if user logged in 5 | * @param {Object} req - Request object 6 | * @param {Object} res - Response object 7 | * @param {Function} next - Next call in the middleware chain 8 | * @returns {void} 9 | */ 10 | function validateLoggedIn(req, res, next) { 11 | if (!req.currentCustomer.profile) { 12 | var Resource = require('dw/web/Resource'); 13 | 14 | res.setStatusCode(403); 15 | 16 | res.json({ 17 | error: Resource.msg('global.error.forbidden', 'error', null), 18 | message: Resource.msg('global.error.forbidden.message', 'error', null) 19 | }); 20 | } 21 | next(); 22 | } 23 | 24 | module.exports = { 25 | validateLoggedIn: validateLoggedIn 26 | }; 27 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/services/SessionBridgeService.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry'); 4 | var Logger = require('dw/system/Logger'); 5 | var config = require('*/cartridge/scripts/config'); 6 | 7 | /** 8 | * invokes service 9 | * @param {{dw.svc.Service}}service - service instance 10 | * @returns {{Object}} result of service call 11 | */ 12 | function invokeService(service) { 13 | var result; 14 | if (service) { 15 | try { 16 | var responseObject = service.call(); 17 | if (responseObject) { 18 | result = responseObject.object; 19 | } 20 | } catch (e) { 21 | Logger.error('Exception: ' + e); 22 | } 23 | } 24 | return result; 25 | } 26 | 27 | /** 28 | * Service call to exchange JWT into a new session 29 | * @param {{string}}token - bearer token 30 | * @param {{string}}clientIP - client ip 31 | * @return {Object} response object 32 | */ 33 | function getSession(token, clientIP) { 34 | return invokeService( 35 | LocalServiceRegistry.createService('sfcc-ocapi-session-bridge', { 36 | createRequest: function (svc) { 37 | svc.setRequestMethod('POST'); 38 | svc.addHeader('Authorization', 'Bearer ' + token); 39 | svc.setURL(config.OCAPI_SESSION_BRIDGE_URI); 40 | 41 | /* 42 | If we have the client IP, send it to the session bridge API so the resulting session has the correct IP. 43 | This is required for client IP related services (ie. geolocation) to have the correct information. 44 | Otherwise, since the call to the session bridge originates from the server, the server IP would be stored in the session. 45 | */ 46 | if (clientIP) { 47 | svc.addHeader(config.CLIENT_IP_HEADER_NAME, clientIP); 48 | } 49 | 50 | return svc; 51 | }, 52 | parseResponse: function (svc, response) { 53 | return response; 54 | }, 55 | filterLogMessage: function (msg) { 56 | return msg; 57 | }, 58 | mockCall: function () { 59 | var mockResponseObj = {}; 60 | return { 61 | statusCode: 200, 62 | statusMessage: 'Success', 63 | text: JSON.stringify(mockResponseObj) 64 | }; 65 | } 66 | }) 67 | ); 68 | } 69 | 70 | module.exports = { 71 | getSession: getSession 72 | }; 73 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/scripts/util/collections.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ArrayList = require('dw/util/ArrayList'); 4 | 5 | /** 6 | * Map method for dw.util.Collection subclass instance 7 | * @param {dw.util.Collection} collection - Collection subclass instance to map over 8 | * @param {Function} callback - Callback function for each item 9 | * @param {Object} [scope] - Optional execution scope to pass to callback 10 | * @returns {Array} Array of results of map 11 | */ 12 | function map(collection, callback, scope) { 13 | var iterator = Object.hasOwnProperty.call(collection, 'iterator') 14 | ? collection.iterator() 15 | : collection; 16 | var index = 0; 17 | var item = null; 18 | var result = []; 19 | while (iterator.hasNext()) { 20 | item = iterator.next(); 21 | result.push(scope ? callback.call(scope, item, index, collection) 22 | : callback(item, index, collection)); 23 | index += 1; 24 | } 25 | return result; 26 | } 27 | 28 | /** 29 | * forEach method for dw.util.Collection subclass instances 30 | * @param {dw.util.Collection} collection - Collection subclass instance to map over 31 | * @param {Function} callback - Callback function for each item 32 | * @param {Object} [scope] - Optional execution scope to pass to callback 33 | * @returns {void} 34 | */ 35 | function forEach(collection, callback, scope) { 36 | var iterator = collection.iterator(); 37 | var index = 0; 38 | var item = null; 39 | while (iterator.hasNext()) { 40 | item = iterator.next(); 41 | if (scope) { 42 | callback.call(scope, item, index, collection); 43 | } else { 44 | callback(item, index, collection); 45 | } 46 | index += 1; 47 | } 48 | } 49 | 50 | /** 51 | * concat method for dw.util.Collection subclass instances 52 | * @param {...dw.util.Collection} arguments - first collection to concatinate 53 | * @return {dw.util.ArrayList} ArrayList containing all passed collections 54 | */ 55 | function concat() { 56 | var result = new ArrayList(); 57 | for (var i = 0, l = arguments.length; i < l; i += 1) { 58 | result.addAll(arguments[i]); 59 | } 60 | return result; 61 | } 62 | 63 | /** 64 | * reduce method for dw.util.Collection subclass instances 65 | * @param {dw.util.Collection} collection - Collection subclass instance to reduce 66 | * @param {Function} callback - Function to execute on each value in the array 67 | * @return {Object} result of the execution of callback function on all items 68 | */ 69 | function reduce(collection, callback) { 70 | if (typeof callback !== 'function') { 71 | throw new TypeError(callback + ' is not a function'); 72 | } 73 | 74 | var value; 75 | var index = 1; 76 | var iterator = collection.iterator(); 77 | 78 | if (arguments.length === 3) { 79 | value = arguments[2]; 80 | index = 0; 81 | } else if (iterator.hasNext() && (collection.getLength() !== 1)) { 82 | value = iterator.next(); 83 | } 84 | 85 | if (collection.getLength() === 0 && !value) { 86 | throw new TypeError('Reduce of empty array with no initial value'); 87 | } 88 | 89 | if ((collection.getLength() === 1 && !value) || (collection.getLength() === 0 && value)) { 90 | return collection.getLength() === 1 ? iterator.next() : value; 91 | } 92 | 93 | while (iterator.hasNext()) { 94 | var item = iterator.next(); 95 | value = callback(value, item, index, collection); 96 | index += 1; 97 | } 98 | 99 | return value; 100 | } 101 | 102 | /** 103 | * Pluck method for dw.util.Collection subclass instance 104 | * @param {dw.util.Collection|dw.util.Iterator} list - Collection subclass or Iterator instance to 105 | * pluck from 106 | * @param {string} property - Object property to pluck 107 | * @returns {Array} Array of results of plucked properties 108 | */ 109 | function pluck(list, property) { 110 | var result = []; 111 | var iterator = Object.hasOwnProperty.call(list, 'iterator') ? list.iterator() : list; 112 | while (iterator.hasNext()) { 113 | var temp = iterator.next(); 114 | if (temp[property]) { 115 | result.push(temp[property]); 116 | } 117 | } 118 | return result; 119 | } 120 | 121 | /** 122 | * Find method for dw.util.Collection subclass instance 123 | * @param {dw.util.Collection} collection - Collection subclass instance to find value in 124 | * @param {Function} match - Match function 125 | * @param {Object} [scope] - Optional execution scope to pass to the match function 126 | * @returns {Object|null} Single item from the collection 127 | */ 128 | function find(collection, match, scope) { 129 | var result = null; 130 | 131 | if (collection) { 132 | var iterator = collection.iterator(); 133 | while (iterator.hasNext()) { 134 | var item = iterator.next(); 135 | if (scope ? match.call(scope, item) : match(item)) { 136 | result = item; 137 | break; 138 | } 139 | } 140 | } 141 | 142 | return result; 143 | } 144 | 145 | /** 146 | * Gets the first item from dw.util.Collection subclass instance 147 | * @param {dw.util.Colleciton} collection - Collection subclass instance to work with 148 | * @return {Object|null} First element from the collection 149 | */ 150 | function first(collection) { 151 | var iterator = collection.iterator(); 152 | return iterator.hasNext() ? iterator.next() : null; 153 | } 154 | 155 | /** 156 | * Determines whether every list item meets callback's truthy conditional 157 | * 158 | * @param {dw.util.Collection} collection - Collection subclass instance to map over 159 | * @param {Function} callback - Callback function for each item 160 | * @return {boolean} - Whether every list item meets callback's truthy conditional 161 | */ 162 | function every(collection, callback) { 163 | if (typeof callback !== 'function') { 164 | throw new TypeError(callback + ' is not a function'); 165 | } 166 | 167 | var iterator = collection.iterator(); 168 | while (iterator.hasNext()) { 169 | var item = iterator.next(); 170 | 171 | if (!callback(item)) { 172 | return false; 173 | } 174 | } 175 | return true; 176 | } 177 | 178 | module.exports = { 179 | map: map, 180 | forEach: forEach, 181 | concat: concat, 182 | reduce: reduce, 183 | pluck: pluck, 184 | find: find, 185 | first: first, 186 | every: every 187 | }; 188 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/templates/default/experience/components/commerce_assets/photoTile.isml: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 | 8 | alt="${pdict.image.alt}" title="${pdict.image.alt}" 13 | /> 14 | 15 |
16 |
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/templates/default/experience/pages/storePage.isml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 | 7 |
8 |
9 |
10 | 11 | -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/templates/resources/error.properties: -------------------------------------------------------------------------------- 1 | global.error.offline=The Site is offline 2 | global.error.general=Sorry! 3 | global.error.forbidden=Forbidden! 4 | global.error.forbidden.message=You do not have access to this resource! 5 | global.error.notfound=Endpoint not found! -------------------------------------------------------------------------------- /cartridges/app_api_base/cartridge/templates/resources/version.properties: -------------------------------------------------------------------------------- 1 | global.version.number=0.1.0 2 | global.site.name=Salesforce Commerce Cloud 3 | -------------------------------------------------------------------------------- /cartridges/app_api_base/hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": [ 3 | { 4 | "name": "dw.system.request.onSession", 5 | "script": "./cartridge/scripts/hooks/onSession" 6 | }, 7 | { 8 | "name": "dw.ocapi.shop.product_search.modifyGETResponse", 9 | "script": "./cartridge/scripts/hooks/search" 10 | }, 11 | { 12 | "name": "dw.ocapi.shop.category.modifyGETResponse", 13 | "script": "./cartridge/scripts/hooks/category" 14 | }, 15 | { 16 | "name": "dw.ocapi.shop.product.modifyGETResponse", 17 | "script": "./cartridge/scripts/hooks/product" 18 | }, 19 | { 20 | "name": "dw.order.calculate", 21 | "script": "./cartridge/scripts/hooks/cart/calculate.js" 22 | }, 23 | { 24 | "name": "dw.order.calculateShipping", 25 | "script": "./cartridge/scripts/hooks/cart/calculate.js" 26 | }, 27 | { 28 | "name": "dw.order.calculateTax", 29 | "script": "./cartridge/scripts/hooks/cart/calculate.js" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /cartridges/app_api_base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": "./hooks.json", 3 | "caches": "./caches.json" 4 | } -------------------------------------------------------------------------------- /cartridges/bm_app_api_base/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | bm_app_api_base 4 | 5 | 6 | 7 | 8 | 9 | com.demandware.studio.core.beehiveElementBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.demandware.studio.core.beehiveNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /cartridges/bm_app_api_base/.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 5, 3 | "plugins": { 4 | "guess-types": { 5 | 6 | }, 7 | "outline": { 8 | 9 | }, 10 | "demandware": { 11 | 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /cartridges/bm_app_api_base/cartridge/bm_app_api_base.properties: -------------------------------------------------------------------------------- 1 | ## cartridge.properties for cartridge app_storefront_base 2 | #Thu Jun 09 11:30:40 EDT 2016 3 | demandware.cartridges.bm_app_api_base.multipleLanguageStorefront=true 4 | demandware.cartridges.bm_app_api_base.id=bm_app_api_base 5 | -------------------------------------------------------------------------------- /cartridges/bm_app_api_base/cartridge/static/default/experience/components/commerce_assets/photoTile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image@2x 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /cartridges/bm_app_api_base/cartridge/templates/resources/experience/componentgroups/commerce_assets.properties: -------------------------------------------------------------------------------- 1 | name=Commerce Assets 2 | -------------------------------------------------------------------------------- /cartridges/bm_app_api_base/cartridge/templates/resources/experience/components/commerce_assets/photoTile.properties: -------------------------------------------------------------------------------- 1 | name=Image Tile 2 | description=Select a photo that you want to insert into your layout component 3 | attribute_definition_group.photoTileImage.name=Image Tile 4 | attribute_definition_group.photoTileImage.description=This is a simple Image Tile component where you drag and drop it into any layout component 5 | attribute_definition.image.name=Image Tile Component Image 6 | attribute_definition.image.description=Select a photo to be displayed 7 | -------------------------------------------------------------------------------- /cartridges/bm_app_api_base/cartridge/templates/resources/experience/pages/storePage.properties: -------------------------------------------------------------------------------- 1 | name=Storefront Page 2 | description=A storefront page 3 | region.main.name=Main Region 4 | -------------------------------------------------------------------------------- /cartridges/modules/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true 4 | }, 5 | "rules": { 6 | "no-underscore-dangle": ["error", { "allow": ["__routes"] }] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /cartridges/modules/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | modules 4 | 5 | 6 | 7 | 8 | 9 | com.demandware.studio.core.beehiveElementBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.demandware.studio.core.beehiveNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /cartridges/modules/.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaVersion": 5, 3 | "plugins": { 4 | "guess-types": { 5 | 6 | }, 7 | "outline": { 8 | 9 | }, 10 | "demandware": { 11 | 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /cartridges/modules/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server = require('./server/server'); 4 | server.middleware = require('./server/middleware'); 5 | server.querystring = require('./server/queryString'); 6 | 7 | module.exports = server; 8 | -------------------------------------------------------------------------------- /cartridges/modules/server/EventEmitter.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | /* 4 | 5 | The MIT License (MIT) 6 | 7 | Copyright (c) 2014 Arnout Kazemier 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | */ 27 | 28 | 29 | 'use strict'; 30 | 31 | // 32 | // We store our EE objects in a plain object whose properties are event names. 33 | // If `Object.create(null)` is not supported we prefix the event names with a 34 | // `~` to make sure that the built-in object properties are not overridden or 35 | // used as an attack vector. 36 | // We also assume that `Object.create(null)` is available when the event name 37 | // is an ES6 Symbol. 38 | // 39 | var prefix = typeof Object.create !== 'function' ? '~' : false; 40 | 41 | /** 42 | * Representation of a single EventEmitter function. 43 | * 44 | * @param {Function} fn Event handler to be called. 45 | * @param {Mixed} context Context for function execution. 46 | * @param {Boolean} [once=false] Only emit once 47 | * @api private 48 | */ 49 | function EE(fn, context, once) { 50 | this.fn = fn; 51 | this.context = context; 52 | this.once = once || false; 53 | } 54 | 55 | /** 56 | * Minimal EventEmitter interface that is molded against the Node.js 57 | * EventEmitter interface. 58 | * 59 | * @constructor 60 | * @api public 61 | */ 62 | function EventEmitter() { /* Nothing to set */ } 63 | 64 | /** 65 | * Holds the assigned EventEmitters by name. 66 | * 67 | * @type {Object} 68 | * @private 69 | */ 70 | EventEmitter.prototype._events = undefined; 71 | 72 | /** 73 | * Return a list of assigned event listeners. 74 | * 75 | * @param {String} event The events that should be listed. 76 | * @param {Boolean} exists We only need to know if there are listeners. 77 | * @returns {Array|Boolean} 78 | * @api public 79 | */ 80 | EventEmitter.prototype.listeners = function listeners(event, exists) { 81 | var evt = prefix ? prefix + event : event 82 | , available = this._events && this._events[evt]; 83 | 84 | if (exists) { 85 | return !!available; 86 | } 87 | if (!available) { 88 | return []; 89 | } 90 | if (available.fn) { 91 | return [available.fn]; 92 | } 93 | 94 | for (var i = 0, l = available.length, ee = new Array(l); i < l; i++) { 95 | ee[i] = available[i].fn; 96 | } 97 | 98 | return ee; 99 | }; 100 | 101 | /** 102 | * Emit an event to all registered event listeners. 103 | * 104 | * @param {String} event The name of the event. 105 | * @returns {Boolean} Indication if we've emitted an event. 106 | * @api public 107 | */ 108 | EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { 109 | var evt = prefix ? prefix + event : event; 110 | 111 | if (!this._events || !this._events[evt]) { 112 | return false; 113 | } 114 | 115 | var listeners = this._events[evt] 116 | , len = arguments.length 117 | , args 118 | , i; 119 | 120 | if ('function' === typeof listeners.fn) { 121 | if (listeners.once) { 122 | this.removeListener(event, listeners.fn, undefined, true); 123 | } 124 | 125 | switch (len) { 126 | case 1: return listeners.fn.call(listeners.context), true; 127 | case 2: return listeners.fn.call(listeners.context, a1), true; 128 | case 3: return listeners.fn.call(listeners.context, a1, a2), true; 129 | case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; 130 | case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; 131 | case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; 132 | } 133 | 134 | for (i = 1, args = new Array(len - 1); i < len; i++) { 135 | args[i - 1] = arguments[i]; 136 | } 137 | 138 | listeners.fn.apply(listeners.context, args); 139 | } else { 140 | var length = listeners.length 141 | , j; 142 | 143 | for (i = 0; i < length; i++) { 144 | if (listeners[i].once) { 145 | this.removeListener(event, listeners[i].fn, undefined, true); 146 | } 147 | 148 | switch (len) { 149 | case 1: listeners[i].fn.call(listeners[i].context); break; 150 | case 2: listeners[i].fn.call(listeners[i].context, a1); break; 151 | case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; 152 | default: 153 | if (!args) { 154 | for (j = 1, args = new Array(len - 1); j < len; j++) { 155 | args[j - 1] = arguments[j]; 156 | } 157 | } 158 | 159 | listeners[i].fn.apply(listeners[i].context, args); 160 | } 161 | } 162 | } 163 | 164 | return true; 165 | }; 166 | 167 | /** 168 | * Register a new EventListener for the given event. 169 | * 170 | * @param {String} event Name of the event. 171 | * @param {Function} fn Callback function. 172 | * @param {Mixed} [context=this] The context of the function. 173 | * @api public 174 | */ 175 | EventEmitter.prototype.on = function on(event, fn, context) { 176 | var listener = new EE(fn, context || this) 177 | , evt = prefix ? prefix + event : event; 178 | 179 | if (!this._events) { 180 | this._events = prefix ? {} : Object.create(null); 181 | } 182 | if (!this._events[evt]) { 183 | this._events[evt] = listener; 184 | } else { 185 | if (!this._events[evt].fn) { 186 | this._events[evt].push(listener); 187 | } else { 188 | this._events[evt] = [ 189 | this._events[evt], listener 190 | ]; 191 | } 192 | } 193 | 194 | return this; 195 | }; 196 | 197 | /** 198 | * Add an EventListener that's only called once. 199 | * 200 | * @param {String} event Name of the event. 201 | * @param {Function} fn Callback function. 202 | * @param {Mixed} [context=this] The context of the function. 203 | * @api public 204 | */ 205 | EventEmitter.prototype.once = function once(event, fn, context) { 206 | var listener = new EE(fn, context || this, true) 207 | , evt = prefix ? prefix + event : event; 208 | 209 | if (!this._events) { 210 | this._events = prefix ? {} : Object.create(null); 211 | } 212 | if (!this._events[evt]) { 213 | this._events[evt] = listener; 214 | } else { 215 | if (!this._events[evt].fn) { 216 | this._events[evt].push(listener); 217 | } else { 218 | this._events[evt] = [ 219 | this._events[evt], listener 220 | ]; 221 | } 222 | } 223 | 224 | return this; 225 | }; 226 | 227 | /** 228 | * Remove event listeners. 229 | * 230 | * @param {String} event The event we want to remove. 231 | * @param {Function} fn The listener that we need to find. 232 | * @param {Mixed} context Only remove listeners matching this context. 233 | * @param {Boolean} once Only remove once listeners. 234 | * @api public 235 | */ 236 | EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { 237 | var evt = prefix ? prefix + event : event; 238 | 239 | if (!this._events || !this._events[evt]) { 240 | return this; 241 | } 242 | 243 | var listeners = this._events[evt] 244 | , events = []; 245 | 246 | if (fn) { 247 | if (listeners.fn) { 248 | if (listeners.fn !== fn || 249 | (once && !listeners.once) || 250 | (context && listeners.context !== context)) { 251 | events.push(listeners); 252 | } 253 | } else { 254 | for (var i = 0, length = listeners.length; i < length; i++) { 255 | if (listeners[i].fn !== fn || 256 | (once && !listeners[i].once) || 257 | (context && listeners[i].context !== context)) { 258 | events.push(listeners[i]); 259 | } 260 | } 261 | } 262 | } 263 | 264 | // 265 | // Reset the array, or remove it completely if we have no more listeners. 266 | // 267 | if (events.length) { 268 | this._events[evt] = events.length === 1 ? events[0] : events; 269 | } else { 270 | delete this._events[evt]; 271 | } 272 | 273 | return this; 274 | }; 275 | 276 | /** 277 | * Remove all listeners or only the listeners for the specified event. 278 | * 279 | * @param {String} event The event want to remove all listeners for. 280 | * @api public 281 | */ 282 | EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { 283 | if (!this._events) { 284 | return this; 285 | } 286 | 287 | if (event) { 288 | delete this._events[prefix ? prefix + event : event]; 289 | } 290 | else { 291 | this._events = prefix ? {} : Object.create(null); 292 | } 293 | 294 | return this; 295 | }; 296 | 297 | // 298 | // Alias methods names because people roll like that. 299 | // 300 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 301 | EventEmitter.prototype.addListener = EventEmitter.prototype.on; 302 | 303 | // 304 | // This function doesn't apply anymore. 305 | // 306 | EventEmitter.prototype.setMaxListeners = function setMaxListeners() { 307 | return this; 308 | }; 309 | 310 | // 311 | // Expose the prefix. 312 | // 313 | EventEmitter.prefixed = prefix; 314 | 315 | // 316 | // Expose the module. 317 | // 318 | module.exports = EventEmitter; 319 | 320 | /* eslint-enable */ 321 | -------------------------------------------------------------------------------- /cartridges/modules/server/README.md: -------------------------------------------------------------------------------- 1 | # Server Module 2 | 3 | The server module replaces guard functionality that existed in the SiteGenesis JavaScript Controllers (SGJC) reference application. The server module also provides a different approach to extensibility that is new to Storefront Reference Architecture (SFRA). 4 | 5 | The server module uses a modern JavaScript approach and borrows heavily from NodeJS's [Express](http://expressjs.com/). It also provides features specific to HRA. 6 | 7 | The server module registers routes that create a mapping between a URL and the code the server runs in response. For example: 8 | 9 | ```js 10 | var server = require('server'); 11 | 12 | server.get('Show', function(req, res, next) { 13 | res.json({ value: 'Hello World'}); 14 | next(); 15 | }); 16 | 17 | module.exports = server.exports(); 18 | ``` 19 | 20 | If you save this code to a file named `Page.js`, you register a new route for the URL matching this pattern: 21 | 22 | `http://sandbox-host-name/on/demandware.store/site-name/en_US/Page-Show.` 23 | 24 | Whenever that URL is requested, the function you passed to `server.get` is executed and renders a page whose body is `{ value: 'Hello World '}` and whose content-type is `Content-Type: application/json`. 25 | 26 | The first parameter of the `server.get` and `server.post` functions is always the name of the route (the URL endpoint). The last parameter is always the main function for the endpoint. In between these parameters, you can add as many parameters as you need. 27 | 28 | For example, you could add the `server.middleware.get` parameter after the `Show` paramenter to limit the route only to GET requests. 29 | 30 | Each parameter that you add specifies a corresponding function. The functions are executed in left-to-right order. Each function can either execute the next function (by calling `next()`) or reject the URL request (by calling `next(new Error())`). 31 | 32 | The code executed between the first and last parameter is referred to as **middleware** and the whole process is called **chaining**. 33 | 34 | You can create your own middleware functions. You can create functions to limit route access, to add information to the `pdict` variable, or for any other reason. One limitation of this approach is that you always have to call the `next()` function at the end of every step in the chain; otherwise, the next function in the chain is not executed. 35 | 36 | ## Middleware 37 | 38 | Every step of the middleware chain is a function that takes three arguments. `req`, `res` and `next`. 39 | 40 | ### `req` 41 | 42 | `req` stands for Request and contains information about the user request. For example, if you are looking for information about the user's input, accepted content-types, or login and locale, you can access this information by using the `req` object. The `req` argument automatically pre-parses query string parameters and assigns them to `req.querystring` object. 43 | 44 | ### `res` 45 | 46 | `res` stands for Response and contains functionality for outputting data back to the client. For example: 47 | 48 | * `res.cacheExpiration(24);` which sets cache expiration to 24 hours from now. `res.json({ message: 'My Message' })` outputs a JSON response back to the client and assigns `data` to `pdict`. 49 | * `res.json(data)` prints out a JSON object back to the screen. It's helpful in creating AJAX service endpoints that you want to execute from the client-side scripts. 50 | * `res.setViewData(data)` does not render anything, but sets the output object. This can be helpful if you want to add multiple objects to the `pdict` of the template, which contains all of in the information for rendering that is passed to the template. `setViewData` merges all of the data that you passed in into a single object, so you can call it at every step of the middleware chain. For example, you might want to have a separate middleware function that retrieves information about user's locale to render a language switch on the page. Actual output of the ISML template or JSON happens after every step of the middleware chain is complete. 51 | 52 | ### `next` 53 | 54 | Executing the `next` function notifies the server that you are done with this middleware step, and the server can execute next step in the chain. 55 | 56 | ## Extending Routes 57 | 58 | The power of this approach is that by chaining multiple middleware functions, you can compartmentalize your code better and extend existing or modify routes without having to rewrite them. 59 | 60 | ### Changing Wording in a Template 61 | For example, you might have a controller `Page` with the following route: 62 | 63 | ```js 64 | var server = require('server'); 65 | 66 | server.get('Show', function(req, res, next) { 67 | res.json({ message: 'Hello World' }); 68 | next(); 69 | }); 70 | 71 | module.exports = server.exports(); 72 | ``` 73 | 74 | Let's say that you are a client who is fine with the look and feel of the Page-Show template, but you want to change the wording. Instead of creating your own controller and route or modifying SFRA code, you can extend this route with the following code: 75 | 76 | ```js 77 | var page = require('app_storefront_base/cartridge/controller/Page'); 78 | var server = require('server'); 79 | 80 | server.extend(page); 81 | 82 | server.append('Show', function(req, res, next) { 83 | res.setViewData({ message: 'Hello Commerce Cloud' }); 84 | next(); 85 | }); 86 | 87 | module.exports = server.exports(); 88 | ``` 89 | 90 | Once the user loads this page, the text in the JSON response now says "Hello Commerce Cloud", since the data passed to the JSON was overwritten. 91 | 92 | 93 | ### Replacing a Route 94 | Sometimes you might want to reuse the route's name, but do not want any of the existing functionality. In those cases, you can use `replace` command to completely remove and re-add a new route. 95 | 96 | ```js 97 | var page = require('app_storefront_base/cartridge/controller/Page'); 98 | var server = require('server); 99 | 100 | server.extend(page); 101 | 102 | server.replace('Show', server.middleware.get, function(req, res, next){ 103 | res.json({ 104 | message: 'My Replacement Message' 105 | }); 106 | next(); 107 | }); 108 | 109 | module.exports = server.exports(); 110 | ``` 111 | ## Overriding Routes with module.superModule 112 | A typical storefront can have several layers of SFRA cartridges that overlay one another. Each cartridge can import from the previous cartridge and overlay it. To make this easy, Commerce Cloud provides a chaining mechanism that lets you access modules that you intend to override. 113 | 114 | The `module.superModule` global property provides access to the most recent module on the cartridge path module with the same path and name as the current module. 115 | 116 | For more information, see [SFRA Modules](https://documentation.b2c.commercecloud.salesforce.com/DOC2/topic/com.demandware.dochelp/SFRA/SFRAModules.html) 117 | 118 | ## Middleware Chain Events 119 | 120 | The server module emits events at every step of execution, and you can subscribe to events and unsubscribe from events for a given route. Here's the list of currently supported events: 121 | 122 | * `route:Start` - emitted as a first thing before middleware chain execution. 123 | * `route:Redirect` - emitted right before `res.redirect` execution. 124 | * `route:Step` - emitted before execution of every step in the middleware chain. 125 | * `route:Complete` - emitted after every step in the chain finishes execution. Currently subscribed to by the server to render JSON back to the client. 126 | 127 | All of the events provide both the `req` and `res` objects as parameters to all handlers. 128 | 129 | -------------------------------------------------------------------------------- /cartridges/modules/server/assign.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function () { 4 | var result = {}; 5 | Array.prototype.forEach.call(arguments, function (argument) { 6 | if (typeof argument === 'object') { 7 | Object.keys(argument).forEach(function (key) { 8 | result[key] = argument[key]; 9 | }); 10 | } 11 | }); 12 | return result; 13 | }; 14 | -------------------------------------------------------------------------------- /cartridges/modules/server/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Middleware filter for get requests 5 | * @param {Object} req - Request object 6 | * @param {Object} res - Response object 7 | * @param {Function} next - Next call in the middleware chain 8 | * @returns {void} 9 | */ 10 | function get(req, res, next) { 11 | if (req.httpMethod === 'GET') { 12 | next(); 13 | } else { 14 | next(new Error('Params do not match route')); 15 | } 16 | } 17 | 18 | /** 19 | * Middleware filter for put requests 20 | * @param {Object} req - Request object 21 | * @param {Object} res - Response object 22 | * @param {Function} next - Next call in the middleware chain 23 | * @returns {void} 24 | */ 25 | function put(req, res, next) { 26 | if (req.httpMethod === 'PUT') { 27 | next(); 28 | } else { 29 | next(new Error('Params do not match route')); 30 | } 31 | } 32 | 33 | /** 34 | * Middleware filter for patch requests 35 | * @param {Object} req - Request object 36 | * @param {Object} res - Response object 37 | * @param {Function} next - Next call in the middleware chain 38 | * @returns {void} 39 | */ 40 | function patch(req, res, next) { 41 | if (req.httpMethod === 'PATCH') { 42 | next(); 43 | } else { 44 | next(new Error('Params do not match route')); 45 | } 46 | } 47 | 48 | /** 49 | * Middleware filter for post requests 50 | * @param {Object} req - Request object 51 | * @param {Object} res - Response object 52 | * @param {Function} next - Next call in the middleware chain 53 | * @returns {void} 54 | */ 55 | function post(req, res, next) { 56 | if (req.httpMethod === 'POST') { 57 | next(); 58 | } else { 59 | next(new Error('Params do not match route')); 60 | } 61 | } 62 | 63 | /** 64 | * Middleware filter for delete requests 65 | * @param {Object} req - Request object 66 | * @param {Object} res - Response object 67 | * @param {Function} next - Next call in the middleware chain 68 | * @returns {void} 69 | */ 70 | function doDelete(req, res, next) { 71 | if (req.httpMethod === 'DELETE') { 72 | next(); 73 | } else { 74 | next(new Error('Params do not match route')); 75 | } 76 | } 77 | 78 | module.exports = { 79 | get: get, 80 | put: put, 81 | patch: patch, 82 | post: post, 83 | delete: doDelete 84 | }; 85 | -------------------------------------------------------------------------------- /cartridges/modules/server/performanceMetrics.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var performanceMetricsConfig = require('*/cartridge/config/performanceMetricsConf'); 4 | 5 | // Start the timer here when the script is firt parsed through the require. 6 | var scriptPerfStartTime = Date.now(); 7 | var instance = null; 8 | 9 | /** 10 | * @constructor 11 | * @classdesc Creates a singleton Performance Metrics object 12 | */ 13 | var Performance = function () { 14 | this.scriptPerfStartTime = scriptPerfStartTime; 15 | this.scriptPerformance = 0; 16 | this.renderPerformance = 0; 17 | this.route = {}; 18 | }; 19 | 20 | /** 21 | * Reset the singleton instance of the Performance Metrics object. 22 | */ 23 | Performance.reset = function () { 24 | instance = new Performance(); 25 | }; 26 | 27 | /** 28 | * Fetch the singleton instance of the Performance Metrics object 29 | * 30 | * @returns {Performance} The Performance Metrics object 31 | */ 32 | Performance.getInstance = function () { 33 | if (!instance) { 34 | instance = new Performance(); 35 | } 36 | 37 | return instance; 38 | }; 39 | 40 | /** 41 | * Function to stop the timer for total script performance. (The entire route) 42 | * @param {Object} res - Response object 43 | */ 44 | Performance.prototype.stopScriptPerformanceTimer = function (res) { 45 | if (!performanceMetricsConfig.enabled) { 46 | return; 47 | } 48 | 49 | if (!res.cachePeriod) { 50 | this.scriptPerformance = Date.now() - scriptPerfStartTime; 51 | } 52 | }; 53 | 54 | /** 55 | * Function to start the timer for script performance of a specific step in the route. 56 | * @param {int} position - Position of a step in the route 57 | */ 58 | Performance.prototype.startRoutePerformanceTimer = function (position) { 59 | if (!performanceMetricsConfig.enabled) { 60 | return; 61 | } 62 | 63 | this.route[position] = { 64 | start: Date.now(), 65 | duration: 0 66 | }; 67 | }; 68 | 69 | /** 70 | * Function to stop the timer for script performance of a specific step in the route. 71 | * @param {int} position - Position of a step in the route 72 | * @param {Object} res - Response object 73 | */ 74 | Performance.prototype.stopRoutePerformanceTimer = function (position, res) { 75 | if (!performanceMetricsConfig.enabled) { 76 | return; 77 | } 78 | 79 | if (this.route[position] && !res.cachePeriod) { 80 | this.route[position].duration = Date.now() - this.route[position].start; 81 | } 82 | }; 83 | 84 | /** 85 | * Function to start the timer for rendering (JSON/XML) performance. 86 | */ 87 | Performance.prototype.startRenderPerformanceTimer = function () { 88 | if (!performanceMetricsConfig.enabled) { 89 | return; 90 | } 91 | 92 | this.renderPerfStartTime = Date.now(); 93 | }; 94 | 95 | /** 96 | * Function to stop the timer for rendering (JSON/XML) performance. 97 | * @param {Object} res - Response object 98 | */ 99 | Performance.prototype.stopRenderPerformanceTimer = function (res) { 100 | if (!performanceMetricsConfig.enabled) { 101 | return; 102 | } 103 | 104 | if (!res.cachePeriod) { 105 | this.renderPerformance = Date.now() - this.renderPerfStartTime; 106 | } 107 | }; 108 | 109 | /** 110 | * Function to set the "Server-Timing" header on the current response. 111 | * @param {Object} res - Response object 112 | */ 113 | Performance.prototype.setServerTimingResponseHeader = function (res) { 114 | if (!performanceMetricsConfig.enabled) { 115 | return; 116 | } 117 | 118 | var route = this.route; 119 | var routeMetrics = ''; 120 | 121 | Object.keys(this.route).forEach(function (key) { 122 | routeMetrics += ', Route-Step-' + key + ';dur=' + route[key].duration; 123 | }); 124 | 125 | res.setHttpHeader('X-SF-CC-Server-Timing', 'script;dur=' + this.scriptPerformance + routeMetrics + ', render;dur=' + this.renderPerformance); 126 | }; 127 | 128 | module.exports = Performance; 129 | -------------------------------------------------------------------------------- /cartridges/modules/server/queryString.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Detect duplicate parameters and be sure to set object[key] as an array of those parameter values 5 | * 6 | * @param {Object} object The object to check for existing values. 7 | * @param {string} key The key to set on object for the new value. 8 | * @param {string} value The new value to be added to object[key]. 9 | * @return {Object} Value or array of values if object[key] has already exists. 10 | */ 11 | function parameterToArray(object, key, value) { 12 | var result = value; 13 | if (object[key]) { 14 | result = object[key]; 15 | if (!(result instanceof Array)) { 16 | result = [object[key]]; 17 | } 18 | result.push(value); 19 | } 20 | 21 | return result; 22 | } 23 | 24 | var querystring = function (raw) { 25 | var pair; 26 | var left; 27 | 28 | if (raw && raw.length > 0) { 29 | var qs = raw.substring(raw.indexOf('?') + 1).replace(/\+/g, '%20').split('&'); 30 | 31 | // eslint-disable-next-line no-plusplus 32 | for (var i = qs.length - 1; i >= 0; i--) { 33 | pair = qs[i].split('='); 34 | left = decodeURIComponent(pair[0]); 35 | 36 | this[left] = parameterToArray(this, left, decodeURIComponent(pair[1])); 37 | } 38 | } 39 | }; 40 | 41 | querystring.prototype.toString = function () { 42 | var result = []; 43 | 44 | Object.keys(this).forEach(function (key) { 45 | result.push(encodeURIComponent(key) + '=' + encodeURIComponent(this[key])); 46 | }, this); 47 | 48 | return result.sort().join('&'); 49 | }; 50 | 51 | module.exports = querystring; 52 | -------------------------------------------------------------------------------- /cartridges/modules/server/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Render JSON as an output 5 | * @param {Object} data - Object to be turned into JSON 6 | * @param {Object} response - Response object 7 | * @returns {void} 8 | */ 9 | function json(data, response) { 10 | response.setContentType('application/json'); 11 | response.base.writer.print(JSON.stringify(data, null, 2)); 12 | } 13 | 14 | /** 15 | * Determines what to render 16 | * @param {Object} res - Response object 17 | * @returns {void} 18 | */ 19 | function applyRenderings(res) { 20 | if (res.renderings.length) { 21 | res.renderings.forEach(function (element) { 22 | if (element.type === 'render') { 23 | switch (element.subType) { 24 | case 'json': 25 | json(res.viewData, res); 26 | break; 27 | default: 28 | throw new Error('Cannot JSON without name or data'); 29 | } 30 | } else if (element.type === 'print') { 31 | res.base.writer.print(element.message); 32 | } else { 33 | throw new Error('Cannot render template without name or data'); 34 | } 35 | }); 36 | } else { 37 | throw new Error('Cannot render template without name or data'); 38 | } 39 | } 40 | 41 | module.exports = { 42 | applyRenderings: applyRenderings 43 | }; 44 | -------------------------------------------------------------------------------- /cartridges/modules/server/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assign = require('./assign'); 4 | var httpHeadersConfig = require('*/cartridge/config/httpHeadersConf'); 5 | 6 | /** 7 | * @constructor 8 | * @classdesc Creates writtable response object 9 | * 10 | * @param {Object} response - Global response object 11 | */ 12 | function Response(response) { 13 | this.view = null; 14 | this.viewData = {}; 15 | this.redirectUrl = null; 16 | this.redirectStatus = null; 17 | this.messageLog = []; 18 | this.base = response; 19 | this.cachePeriod = null; 20 | this.cachePeriodUnit = null; 21 | this.personalized = false; 22 | this.renderings = []; 23 | httpHeadersConfig.forEach(function (httpHeader) { 24 | this.setHttpHeader(httpHeader.id, httpHeader.value); 25 | }, this); 26 | } 27 | 28 | /** 29 | * Stores a list of rendering steps. 30 | * @param {Array} renderings - The array of rendering steps 31 | * @param {Object} object - An object containing what type to render 32 | * @returns {void} 33 | */ 34 | function appendRenderings(renderings, object) { 35 | var hasRendering = false; 36 | 37 | if (renderings.length) { 38 | // eslint-disable-next-line no-plusplus 39 | for (var i = renderings.length - 1; i >= 0; i--) { 40 | if (renderings[i].type === 'render') { 41 | renderings[i] = object; // eslint-disable-line no-param-reassign 42 | hasRendering = true; 43 | break; 44 | } 45 | } 46 | } 47 | 48 | if (!hasRendering) { 49 | renderings.push(object); 50 | } 51 | } 52 | 53 | Response.prototype = { 54 | /** 55 | * Stores data for rendering at the later time 56 | * @param {Object} data - Data to be passed to the template 57 | * @returns {void} 58 | */ 59 | render: function render(data) { 60 | this.isJson = true; 61 | this.viewData = assign(this.viewData, data); 62 | 63 | appendRenderings(this.renderings, { type: 'render', subType: 'json' }); 64 | }, 65 | /** 66 | * Stores data to be rendered as json 67 | * @param {Object} data - Data to be rendered as json 68 | * @returns {void} 69 | */ 70 | json: function json(data) { 71 | this.isJson = true; 72 | this.viewData = assign(this.viewData, data); 73 | 74 | appendRenderings(this.renderings, { type: 'render', subType: 'json' }); 75 | }, 76 | /** 77 | * Redirects to a given url right away 78 | * @param {string} url - Url to be redirected to 79 | * @returns {void} 80 | */ 81 | redirect: function redirect(url) { 82 | this.redirectUrl = url; 83 | }, 84 | /** 85 | * Sets an optional redirect status, standard cases being 301 or 302. 86 | * @param {string} redirectStatus - HTTP redirect status code 87 | * @returns {void} 88 | */ 89 | setRedirectStatus: function setRedirectStatus(redirectStatus) { 90 | this.redirectStatus = redirectStatus; 91 | }, 92 | /** 93 | * Get data that was setup for a template 94 | * @returns {Object} Data for the template 95 | */ 96 | getViewData: function () { 97 | return this.viewData; 98 | }, 99 | /** 100 | * Updates data for the template 101 | * @param {Object} data - Data for template 102 | * @returns {void} 103 | */ 104 | setViewData: function (data) { 105 | this.viewData = assign(this.viewData, data); 106 | }, 107 | /** 108 | * Logs information for output on the error page 109 | * @param {string[]} arguments - List of items to be logged 110 | * @returns {void} 111 | */ 112 | log: function log() { 113 | var args = Array.prototype.slice.call(arguments); 114 | 115 | var output = args.map(function (item) { 116 | if (typeof item === 'object' || Array.isArray(item)) { 117 | return JSON.stringify(item); 118 | } 119 | return item; 120 | }); 121 | 122 | this.messageLog.push(output.join(' ')); 123 | }, 124 | /** 125 | * Set content type for the output 126 | * @param {string} type - Type of the output 127 | * @returns {void} 128 | */ 129 | setContentType: function setContentType(type) { 130 | this.base.setContentType(type); 131 | }, 132 | 133 | /** 134 | * Set status code of the response 135 | * @param {int} code - Valid HTTP return code 136 | * @returns {void} 137 | */ 138 | setStatusCode: function setStatusCode(code) { 139 | this.base.setStatus(code); 140 | }, 141 | 142 | /** 143 | * creates a print step to the renderings 144 | * @param {string} message - Message to be printed 145 | * @returns {void} 146 | */ 147 | print: function print(message) { 148 | this.renderings.push({ type: 'print', message: message }); 149 | }, 150 | 151 | /** 152 | * Sets current page cache expiration period value in hours 153 | * @param {int} period Number of hours from current time 154 | * @return {void} 155 | */ 156 | cacheExpiration: function cacheExpiration(period) { 157 | this.cachePeriod = period; 158 | }, 159 | 160 | /** 161 | * Adds a response header with the given name and value 162 | * @param {string} name - the name to use for the response header 163 | * @param {string} value - the value to use 164 | * @return {void} 165 | */ 166 | setHttpHeader: function setHttpHeader(name, value) { 167 | this.base.setHttpHeader(name, value); 168 | } 169 | 170 | }; 171 | 172 | module.exports = Response; 173 | -------------------------------------------------------------------------------- /cartridges/modules/server/route.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var EventEmitter = require('./EventEmitter'); 4 | var PerformanceMetrics = require('./performanceMetrics'); 5 | 6 | // Initiate the Performance Metrics 7 | var performanceMetrics = PerformanceMetrics.getInstance(); 8 | 9 | /** 10 | * @constructor 11 | * @param {string} name - Name of the route, corresponds to the second part of the URL 12 | * @param {Function[]} chain - List of functions to be executed 13 | * @param {Object} req - Request object 14 | * @param {Object} res - Response object 15 | */ 16 | function Route(name, chain, req, res) { 17 | this.name = name; 18 | this.chain = chain; 19 | this.req = req; 20 | this.res = res; 21 | EventEmitter.call(this); 22 | } 23 | 24 | Route.prototype = EventEmitter.prototype; 25 | 26 | /** 27 | * Create a single function that chains all of the calls together, one after another 28 | * @returns {Function} Function to be executed when URL is hit 29 | */ 30 | Route.prototype.getRoute = function () { 31 | var me = this; 32 | return (function (err) { 33 | var i = 0; 34 | 35 | if (err && err.ErrorText) { 36 | var system = require('dw/system/System'); 37 | var showError = system.getInstanceType() !== system.PRODUCTION_SYSTEM; 38 | me.req.error = { 39 | errorText: showError ? err.ErrorText : '', 40 | controllerName: showError ? err.ControllerName : '', 41 | startNodeName: showError ? err.CurrentStartNodeName || me.name : '' 42 | }; 43 | } 44 | 45 | // freeze request object to avoid mutations 46 | Object.freeze(me.req); 47 | 48 | /** 49 | * Go to the next step in the chain or complete the chain after the last step 50 | * @param {Object} error - Error object from the prevous step 51 | * @returns {void} 52 | */ 53 | function next(error) { 54 | if (error) { 55 | // process error here and output error template 56 | me.res.log(error); 57 | throw new Error(error.message, error.fileName, error.lineNumber); 58 | } 59 | 60 | if (me.res.redirectUrl) { 61 | // if there's a pending redirect, break the chain 62 | me.emit('route:Redirect', me.req, me.res); 63 | if (me.res.redirectStatus) { 64 | me.res.base.redirect(me.res.redirectUrl, me.res.redirectStatus); 65 | } else { 66 | me.res.base.redirect(me.res.redirectUrl); 67 | } 68 | return; 69 | } 70 | 71 | if (i < me.chain.length) { 72 | if (i > 0) { 73 | performanceMetrics.stopRoutePerformanceTimer(i - 1, me.res); 74 | } 75 | 76 | performanceMetrics.startRoutePerformanceTimer(i); 77 | 78 | me.emit('route:Step', me.req, me.res); 79 | 80 | // eslint-disable-next-line no-plusplus 81 | me.chain[i++].call(me, me.req, me.res, next); 82 | } else { 83 | performanceMetrics.stopRoutePerformanceTimer(i - 1, me.res); 84 | me.done.call(me, me.req, me.res); 85 | } 86 | } 87 | 88 | // eslint-disable-next-line no-plusplus 89 | i++; 90 | me.emit('route:Start', me.req, me.res); 91 | me.chain[0].call(me, me.req, me.res, next); 92 | }); 93 | }; 94 | 95 | /** 96 | * Append a middleware step into current chain 97 | * 98 | * @param {Function} step - New middleware step 99 | * @return {void} 100 | */ 101 | Route.prototype.append = function append(step) { 102 | this.chain.push(step); 103 | }; 104 | 105 | /** 106 | * Last step in the chain, this will render a template or output a json string 107 | * @param {Object} req - Request object 108 | * @param {Object} res - Response object 109 | * @returns {void} 110 | */ 111 | Route.prototype.done = function done(req, res) { 112 | this.emit('route:BeforeComplete', req, res); 113 | this.emit('route:Complete', req, res); 114 | }; 115 | 116 | module.exports = Route; 117 | -------------------------------------------------------------------------------- /cartridges/modules/server/simpleCache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Represents a simple key/value store 5 | * @param {Object} [store] - a bracket notation-compatible object 6 | */ 7 | function SimpleCache(store) { 8 | this.store = store || {}; 9 | } 10 | 11 | /** 12 | * Gets a value in key/value store 13 | * @param {string} key - the Key 14 | * @returns {Object} the stored value 15 | */ 16 | SimpleCache.prototype.get = function (key) { 17 | return this.store[key]; 18 | }; 19 | 20 | /** 21 | * Sets a value in key/value store 22 | * @param {string} key - the Key 23 | * @param {Object} [value] - the Value to store 24 | */ 25 | SimpleCache.prototype.set = function (key, value) { 26 | this.store[key] = value; 27 | }; 28 | 29 | /** 30 | * Clears values from KV store 31 | */ 32 | SimpleCache.prototype.clear = function () { 33 | var store = this.store; 34 | Object.keys(store).forEach(function (key) { 35 | store[key] = null; 36 | }); 37 | }; 38 | 39 | module.exports = SimpleCache; 40 | -------------------------------------------------------------------------------- /docs/screenshots/locale-mapping.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taurgis/headless-reference-architecture/7ce72628ac26a90479605b092c32ffb253338f69/docs/screenshots/locale-mapping.jpg -------------------------------------------------------------------------------- /docs/screenshots/product-url-rules.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/taurgis/headless-reference-architecture/7ce72628ac26a90479605b092c32ffb253338f69/docs/screenshots/product-url-rules.jpg -------------------------------------------------------------------------------- /ismllinter.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var config = { 4 | enableCache: true, 5 | rules: { 6 | 'no-space-only-lines': {}, 7 | 'no-tabs': {}, 8 | 'no-trailing-spaces': {} 9 | } 10 | }; 11 | 12 | module.exports = config; 13 | -------------------------------------------------------------------------------- /metadata/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 5000 6 | false 7 | 0 8 | 0 9 | true 10 | 3 11 | 10000 12 | 13 | 14 | 15 | HTTP 16 | true 17 | sfcc-ocapi-session 18 | true 19 | false 20 | false 21 | sfcc-auth 22 | 23 | 24 | -------------------------------------------------------------------------------- /metadata/system-objecttype-extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SFCC OCAPI Session Bridge URI 7 | Request URI used for OCAPI Session 8 | string 9 | true 10 | false 11 | 12 | 13 | Client IP Header Name 14 | The header name used to passthrough the client IP. This must match the Client IP Header Name set in Customer CDN settings in Business Manager. 15 | string 16 | false 17 | x-client-ip 18 | 19 | 20 | 21 | 22 | HRA Configurations 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hra", 3 | "version": "0.1.0", 4 | "description": "Headless Reference Architecture", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=4.0" 8 | }, 9 | "scripts": { 10 | "test": "nyc mocha test/unit/**/*.js", 11 | "test:report": "nyc report --reporter=json", 12 | "test:integration": "sgmf-scripts --integration 'test/integration/**/*.js'", 13 | "lint": "npm run lint:js", 14 | "lint:js": "sgmf-scripts --lint js", 15 | "init:isml": "./node_modules/.bin/isml-linter --init", 16 | "lint:isml": "./node_modules/.bin/isml-linter", 17 | "build:isml": "./node_modules/.bin/isml-linter --build", 18 | "fix:isml": "./node_modules/.bin/isml-linter --autofix", 19 | "upload": "sgmf-scripts --upload", 20 | "uploadCartridge": "sgmf-scripts --uploadCartridge app_api_base && sgmf-scripts --uploadCartridge bm_app_api_base && sgmf-scripts --uploadCartridge modules", 21 | "watch": "sgmf-scripts --watch", 22 | "release": "node bin/Makefile release --" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/taurgis/headless-reference-architecture.git" 27 | }, 28 | "author": "Thomas Theunen ", 29 | "license": "ISC", 30 | "homepage": "https://github.com/taurgis/headless-reference-architecture", 31 | "devDependencies": { 32 | "@tridnguyen/config": "2.3.1", 33 | "chai": "4.3.7", 34 | "chai-subset": "1.6.0", 35 | "eslint": "8.25.0", 36 | "eslint-config-airbnb-base": "15.0.0", 37 | "eslint-plugin-import": "2.26.0", 38 | "eslint-plugin-sitegenesis": "1.0.0", 39 | "isml-linter": "5.40.3", 40 | "mocha": "10.1.0", 41 | "mocha-junit-reporter": "2.1.0", 42 | "nyc": "15.1.0", 43 | "properties-parser": "0.3.1", 44 | "proxyquire": "2.1.3", 45 | "request-promise": "4.2.6", 46 | "sgmf-scripts": "2.4.2", 47 | "shelljs": "0.8.5", 48 | "sinon": "14.0.1" 49 | }, 50 | "browserslist": [ 51 | "last 2 versions", 52 | "ie >= 10" 53 | ], 54 | "packageName": "app_api_base", 55 | "babel": { 56 | "presets": [ 57 | "env" 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | "es6": true 5 | }, 6 | "rules": { 7 | "require-jsdoc": "off", 8 | "max-len": "off", 9 | "quote-props": "off", 10 | "no-new": "off", 11 | "valid-jsdoc": "off" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/mocks/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "sourceType": "module" 4 | }, 5 | "env": { 6 | "mocha": true, 7 | "es6": true 8 | }, 9 | "rules": { 10 | "max-len": "off", 11 | "no-use-before-define": "off", 12 | "require-jsdoc": "off" 13 | }, 14 | "globals": { 15 | "browser": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/mocks/dw.util.Collection.js: -------------------------------------------------------------------------------- 1 | module.exports = function (array) { 2 | var items = []; 3 | if (array) { 4 | items = array; 5 | } 6 | 7 | this.add = function (item) { 8 | items.push(item); 9 | }; 10 | 11 | this.iterator = function () { 12 | var i = 0; 13 | return { 14 | hasNext: function () { 15 | return i < items.length; 16 | }, 17 | next: function () { 18 | // eslint-disable-next-line no-plusplus 19 | return items[i++]; 20 | } 21 | }; 22 | }; 23 | 24 | this.getLength = function () { 25 | return items.length; 26 | }; 27 | 28 | this.length = this.getLength(); 29 | 30 | this.toArray = function () { 31 | return items; 32 | }; 33 | 34 | this.addAll = function (collection) { 35 | items = items.concat(collection.toArray()); 36 | }; 37 | 38 | this.contains = function (item) { 39 | return array.indexOf(item) >= 0; 40 | }; 41 | 42 | this.map = function () { 43 | var args = Array.from(arguments); 44 | var list = args[0]; 45 | var callback = args[1]; 46 | if (list && Object.prototype.hasOwnProperty.call(list, 'toArray')) { 47 | list = list.toArray(); 48 | } 49 | return list ? list.map(callback) : []; 50 | }; 51 | 52 | this.get = function (index) { 53 | return items[index]; 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /test/mocks/dw.value.Money.js: -------------------------------------------------------------------------------- 1 | function Money(isAvailable) { 2 | return { 3 | available: isAvailable, 4 | value: '10.99', 5 | getDecimalValue: function () { return '10.99'; }, 6 | getCurrencyCode: function () { return 'USD'; }, 7 | subtract: function () { return new Money(isAvailable); } 8 | }; 9 | } 10 | 11 | module.exports = Money; 12 | -------------------------------------------------------------------------------- /test/mocks/dw.web.URLUtils.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | http: function (id) { 3 | return id; 4 | }, 5 | staticURL: function () { 6 | return 'some url'; 7 | }, 8 | url: function () { 9 | return 'someUrl'; 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/mocks/dw/catalog/ProductInventoryMgr.js: -------------------------------------------------------------------------------- 1 | var productInventoryMgr = { 2 | getInventoryList: function (inventoryListId) { 3 | var inventoryListId0001 = { 4 | getRecord: function (productID) { 5 | var product000001 = { 6 | ATS: { value: 10 } 7 | }; 8 | 9 | var product000002 = { 10 | ATS: { value: 3 } 11 | }; 12 | 13 | var product000003 = { 14 | ATS: { value: 5 } 15 | }; 16 | 17 | switch (productID) { 18 | case '000001': 19 | return product000001; 20 | case '000002': 21 | return product000002; 22 | case '000003': 23 | return product000003; 24 | default: 25 | return {}; 26 | } 27 | } 28 | }; 29 | 30 | var inventoryListId0002 = { 31 | getRecord: function (productID) { 32 | var product000001 = { 33 | ATS: { value: 0 } 34 | }; 35 | 36 | var product000002 = { 37 | ATS: { value: 8 } 38 | }; 39 | 40 | var product000003 = { 41 | ATS: { value: 10 } 42 | }; 43 | 44 | switch (productID) { 45 | case '000001': 46 | return product000001; 47 | case '000002': 48 | return product000002; 49 | case '000003': 50 | return product000003; 51 | default: 52 | return {}; 53 | } 54 | } 55 | }; 56 | 57 | var inventoryListId0003 = { 58 | getRecord: function (productID) { 59 | var product000001 = { 60 | ATS: { value: 10 } 61 | }; 62 | 63 | var product000002 = { 64 | ATS: { value: 15 } 65 | }; 66 | 67 | var product000003 = { 68 | ATS: { value: 8 } 69 | }; 70 | 71 | switch (productID) { 72 | case '000001': 73 | return product000001; 74 | case '000002': 75 | return product000002; 76 | case '000003': 77 | return product000003; 78 | default: 79 | return {}; 80 | } 81 | } 82 | }; 83 | 84 | switch (inventoryListId) { 85 | case 'inventoryListId0001': 86 | return inventoryListId0001; 87 | case 'inventoryListId0002': 88 | return inventoryListId0002; 89 | case 'inventoryListId0003': 90 | return inventoryListId0003; 91 | default: 92 | return {}; 93 | } 94 | } 95 | }; 96 | 97 | module.exports = { 98 | getInventoryList: productInventoryMgr.getInventoryList 99 | }; 100 | -------------------------------------------------------------------------------- /test/mocks/dw/catalog/StoreMgr.js: -------------------------------------------------------------------------------- 1 | var storeMgr = { 2 | searchStoresByPostalCode: function () { 3 | return { 4 | keySet: function () { 5 | return [{ 6 | ID: 'Any ID', 7 | name: 'Downtown TV Shop', 8 | address1: '333 Washington St', 9 | address2: '', 10 | city: 'Boston', 11 | postalCode: '01803', 12 | phone: '333-333-3333', 13 | stateCode: 'MA', 14 | countryCode: { 15 | value: 'us' 16 | }, 17 | latitude: 42.5273334, 18 | longitude: -71.13758250000001, 19 | storeHours: { 20 | markup: 'Mon - Sat: 10am - 9pm' 21 | } 22 | }]; 23 | } 24 | }; 25 | }, 26 | 27 | searchStoresByCoordinates: function () { 28 | return { 29 | keySet: function () { 30 | return [{ 31 | ID: 'Any ID', 32 | name: 'Downtown TV Shop', 33 | address1: '333 Washington St', 34 | address2: '', 35 | city: 'Boston', 36 | postalCode: '01803', 37 | phone: '333-333-3333', 38 | stateCode: 'MA', 39 | countryCode: { 40 | value: 'us' 41 | }, 42 | latitude: 42.5273334, 43 | longitude: -71.13758250000001, 44 | storeHours: { 45 | markup: 'Mon - Sat: 10am - 9pm' 46 | } 47 | }]; 48 | } 49 | }; 50 | } 51 | 52 | }; 53 | 54 | module.exports = { 55 | searchStoresByPostalCode: storeMgr.searchStoresByPostalCode, 56 | searchStoresByCoordinates: storeMgr.searchStoresByCoordinates 57 | }; 58 | -------------------------------------------------------------------------------- /test/mocks/dw/order/BasketMgr.js: -------------------------------------------------------------------------------- 1 | function getCurrentBasket() { 2 | return { 3 | defaultShipment: { 4 | shippingAddress: { 5 | firstName: 'Amanda', 6 | lastName: 'Jones', 7 | address1: '65 May Lane', 8 | address2: '', 9 | city: 'Allston', 10 | postalCode: '02135', 11 | countryCode: { value: 'us' }, 12 | phone: '617-555-1234', 13 | stateCode: 'MA', 14 | 15 | setFirstName: function (firstNameInput) { this.firstName = firstNameInput; }, 16 | setLastName: function (lastNameInput) { this.lastName = lastNameInput; }, 17 | setAddress1: function (address1Input) { this.address1 = address1Input; }, 18 | setAddress2: function (address2Input) { this.address2 = address2Input; }, 19 | setCity: function (cityInput) { this.city = cityInput; }, 20 | setPostalCode: function (postalCodeInput) { this.postalCode = postalCodeInput; }, 21 | setStateCode: function (stateCodeInput) { this.stateCode = stateCodeInput; }, 22 | setCountryCode: function (countryCodeInput) { this.countryCode.value = countryCodeInput; }, 23 | setPhone: function (phoneInput) { this.phone = phoneInput; } 24 | } 25 | }, 26 | totalGrossPrice: { 27 | value: 250.00 28 | } 29 | }; 30 | } 31 | 32 | module.exports = { 33 | getCurrentBasket: getCurrentBasket 34 | }; 35 | -------------------------------------------------------------------------------- /test/mocks/dw/order/ShippingMgr.js: -------------------------------------------------------------------------------- 1 | var ArrayList = require('../../../mocks/dw.util.Collection'); 2 | 3 | var defaultShippingMethod = { 4 | description: 'Order received within 7-10 business days', 5 | displayName: 'Ground', 6 | ID: '001', 7 | custom: { 8 | estimatedArrivalTime: '7-10 Business Days' 9 | } 10 | }; 11 | 12 | function createShipmentShippingModel() { 13 | return { 14 | applicableShippingMethods: new ArrayList([ 15 | { 16 | description: 'Order received within 7-10 business days', 17 | displayName: 'Ground', 18 | ID: '001', 19 | custom: { 20 | estimatedArrivalTime: '7-10 Business Days' 21 | } 22 | }, 23 | { 24 | description: 'Order received in 2 business days', 25 | displayName: '2-Day Express', 26 | ID: '002', 27 | shippingCost: '$0.00', 28 | custom: { 29 | estimatedArrivalTime: '2 Business Days' 30 | } 31 | } 32 | ]), 33 | getApplicableShippingMethods: function () { 34 | return new ArrayList([ 35 | { 36 | description: 'Order received within 7-10 business days', 37 | displayName: 'Ground', 38 | ID: '001', 39 | custom: { 40 | estimatedArrivalTime: '7-10 Business Days' 41 | } 42 | }, 43 | { 44 | description: 'Order received in 2 business days', 45 | displayName: '2-Day Express', 46 | ID: '002', 47 | shippingCost: '$0.00', 48 | custom: { 49 | estimatedArrivalTime: '2 Business Days' 50 | } 51 | } 52 | ]); 53 | }, 54 | getShippingCost: function () { 55 | return { 56 | amount: { 57 | valueOrNull: 7.99 58 | } 59 | }; 60 | } 61 | }; 62 | } 63 | 64 | module.exports = { 65 | getDefaultShippingMethod: function () { 66 | return defaultShippingMethod; 67 | }, 68 | getShipmentShippingModel: function (shipment) { 69 | return createShipmentShippingModel(shipment); 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /test/mocks/dw/web/Resource.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var properties = require('properties-parser'); 3 | const locale = 'x_default'; 4 | 5 | function msg(key, bundleName, defaultValue) { 6 | let bundlePath; 7 | let props; 8 | const resourceDirPath = './cartridges/app_storefront_base/cartridge/templates/resources/'; 9 | if (!key) { 10 | return defaultValue; 11 | } 12 | if (bundleName) { 13 | if (locale !== 'x_default') { 14 | bundlePath = path.resolve(resourceDirPath + bundleName + '_' + locale + '.properties'); 15 | try { 16 | props = properties.read(bundlePath); 17 | if (props[key]) { 18 | return props[key]; 19 | } 20 | } catch (e) { 21 | // continue 22 | } 23 | } 24 | bundlePath = path.resolve(resourceDirPath + bundleName + '.properties'); 25 | try { 26 | props = properties.read(bundlePath); 27 | if (props[key]) { 28 | return props[key]; 29 | } 30 | } catch (e) { 31 | // continue 32 | } 33 | } 34 | return defaultValue || key; 35 | } 36 | 37 | function msgf() { 38 | // pass through to msg if there are no extra format arguments 39 | if (arguments.length < 4) { 40 | return msg.apply(null, arguments); 41 | } 42 | let args = Array.prototype.slice.call(arguments); 43 | let value = msg.apply(null, args.slice(0, 3)); 44 | return value.replace(/{(\d)}/g, function (match, p) { 45 | let position = Number(p); 46 | if (args[position + 3]) { 47 | return args[position + 3]; 48 | // if no arguments found, return the original placeholder 49 | } 50 | return match; 51 | }); 52 | } 53 | 54 | module.exports = { 55 | msg: msg, 56 | msgf: msgf, 57 | locale: locale 58 | }; 59 | -------------------------------------------------------------------------------- /test/mocks/modules/responseMock.js: -------------------------------------------------------------------------------- 1 | function Response() { 2 | this.base = {}; 3 | this.viewData = {}; 4 | } 5 | 6 | Response.prototype = { 7 | render: function render() {}, 8 | json: function json() {}, 9 | redirect: function redirect(url) { 10 | this.redirectUrl = url; 11 | }, 12 | setViewData: function () {}, 13 | setHttpHeader: function () {}, 14 | setRedirectStatus: function (status) { 15 | this.redirectStatus = status; 16 | } 17 | }; 18 | 19 | module.exports = Response; 20 | -------------------------------------------------------------------------------- /test/mocks/util/collections.js: -------------------------------------------------------------------------------- 1 | function map() { 2 | var args = Array.from(arguments); 3 | var list = args[0]; 4 | var callback = args[1]; 5 | if (list && Object.prototype.hasOwnProperty.call(list, 'toArray')) { 6 | list = list.toArray(); 7 | } 8 | return list ? list.map(callback) : []; 9 | } 10 | 11 | function find() { 12 | var args = Array.from(arguments); 13 | var list = args[0]; 14 | var callback = args[1]; 15 | if (list && Object.prototype.hasOwnProperty.call(list, 'toArray')) { 16 | list = list.toArray(); 17 | } 18 | return list ? list.find(callback) : null; 19 | } 20 | 21 | function forEach() { 22 | var args = Array.from(arguments); 23 | var list = args[0]; 24 | var callback = args[1]; 25 | if (list && Object.prototype.hasOwnProperty.call(list, 'toArray')) { 26 | list = list.toArray(); 27 | } 28 | return list ? list.forEach(callback) : null; 29 | } 30 | 31 | function every() { 32 | var args = Array.from(arguments); 33 | var list = args[0]; 34 | var callback = args[1]; 35 | if (list && Object.prototype.hasOwnProperty.call(list, 'toArray')) { 36 | list = list.toArray(); 37 | } 38 | return list ? list.every(callback) : null; 39 | } 40 | 41 | module.exports = { 42 | find: find, 43 | forEach: forEach, 44 | map: map, 45 | every: every 46 | }; 47 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/helpers/basketCalculationHelpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 5 | var sinon = require('sinon'); 6 | 7 | describe('Helpers - Totals', function () { 8 | var hookMgrSpy = sinon.spy(); 9 | var hookHelperSpy = sinon.spy(); 10 | 11 | var basketCalculationHelpers = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/helpers/basketCalculationHelpers', { 12 | 'dw/system/HookMgr': { callHook: hookMgrSpy }, 13 | '*/cartridge/scripts/helpers/hooks': hookHelperSpy, 14 | '*/cartridge/scripts/hooks/taxes': { calculateTaxes: function () {} } 15 | }); 16 | 17 | beforeEach(function () { 18 | hookMgrSpy.resetHistory(); 19 | hookHelperSpy.resetHistory(); 20 | }); 21 | 22 | it('Should call taxes hook', function () { 23 | basketCalculationHelpers.calculateTaxes(); 24 | 25 | assert.isTrue(hookHelperSpy.calledWith('app.basket.taxes', 'calculateTaxes')); 26 | }); 27 | 28 | it('Should call totals hook', function () { 29 | basketCalculationHelpers.calculateTotals(); 30 | 31 | assert.isTrue(hookMgrSpy.calledWith('dw.order.calculate', 'calculate')); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/helpers/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 5 | var sinon = require('sinon'); 6 | 7 | var hasHookSpy = sinon.spy(); 8 | var callHookSpy = sinon.spy(); 9 | 10 | describe('Call hook', function () { 11 | var hooksHelpers = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/helpers/hooks', { 12 | 'dw/system/HookMgr': { 13 | hasHook: hasHookSpy, 14 | callHook: callHookSpy 15 | } 16 | }); 17 | 18 | beforeEach(function () { 19 | hasHookSpy.resetHistory(); 20 | callHookSpy.resetHistory(); 21 | }); 22 | 23 | it('should call hook fallback with single argument', function () { 24 | hooksHelpers('app.test', 'test', 'argument', function (arg) { 25 | assert.equal(arg, 'argument'); 26 | }); 27 | }); 28 | 29 | it('should call hook fallback with array argument', function () { 30 | hooksHelpers('app.test', 'test', ['argument', 'argument2'], function (arg, arg2) { 31 | assert.equal(arg, 'argument'); 32 | assert.equal(arg2, 'argument2'); 33 | }); 34 | }); 35 | 36 | it('should call hook fallback with multiple arguments', function () { 37 | hooksHelpers('apt.test', 'test', 'argument', 'argument2', 'argument3', function () { 38 | assert.equal(arguments.length, 3); 39 | assert.equal(arguments[0], 'argument'); 40 | assert.equal(arguments[1], 'argument2'); 41 | assert.equal(arguments[2], 'argument3'); 42 | }); 43 | }); 44 | 45 | it('should call hook without parameter', function () { 46 | var definedHook = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/helpers/hooks', { 47 | 'dw/system/HookMgr': { 48 | hasHook: function () { 49 | return true; 50 | }, 51 | callHook: function () { 52 | assert.equal(arguments.length, 2); 53 | assert.equal(arguments[0], 'app.test'); 54 | assert.equal(arguments[1], 'test'); 55 | } 56 | } 57 | }); 58 | 59 | var fallbackSpy = sinon.spy(); 60 | 61 | definedHook('app.test', 'test', fallbackSpy); 62 | 63 | assert(fallbackSpy.notCalled); 64 | }); 65 | 66 | it('should call hook with parameters', function () { 67 | var definedHook = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/helpers/hooks', { 68 | 'dw/system/HookMgr': { 69 | hasHook: function () { 70 | return true; 71 | }, 72 | callHook: function () { 73 | assert.equal(arguments.length, 5); 74 | assert.equal(arguments[0], 'app.test'); 75 | assert.equal(arguments[1], 'test'); 76 | assert.equal(arguments[2], 'argument'); 77 | assert.equal(arguments[3], 'argument2'); 78 | assert.equal(arguments[4], 'argument3'); 79 | } 80 | } 81 | }); 82 | 83 | var fallbackSpy = sinon.spy(); 84 | 85 | definedHook('app.test', 'test', 'argument', 'argument2', 'argument3', fallbackSpy); 86 | 87 | assert(fallbackSpy.notCalled); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/helpers/productSearchHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 5 | 6 | const productSearchHelper = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/helpers/productSearchHelper', { 7 | 'dw/system/CacheMgr': { 8 | getCache: () => { 9 | return { 10 | get: (key, callbackFunction) => { 11 | return callbackFunction(); 12 | } 13 | }; 14 | } 15 | }, 16 | '*/cartridge/scripts/helpers/seoHelper': require('../../../../../cartridges/app_api_base/cartridge/scripts/helpers/seoHelper'), 17 | 'dw/catalog/ProductSearchModel': function () { 18 | return { 19 | pageMetaTags: [{ 20 | ID: 'id', 21 | content: 'content', 22 | name: false, 23 | property: true, 24 | title: false 25 | }], 26 | getSearchRedirect: function (query) { 27 | if (query === 'my_query_with_redirect') { 28 | return { 29 | getLocation: () => { 30 | return 'https://www.rhino-inquisitor.com'; 31 | } 32 | }; 33 | } 34 | 35 | return null; 36 | }, 37 | setCategoryID: () => { 38 | // Do Nothing 39 | } 40 | }; 41 | }, 42 | 'dw/catalog/ProductMgr': { 43 | getProduct: function (sku) { 44 | if (sku === 'existing_sku_no_pricebook') { 45 | return { 46 | ID: 'existing_sku_no_pricebook', 47 | priceModel: { 48 | price: { 49 | value: 100, 50 | currency: 'EUR' 51 | } 52 | }, 53 | variationModel: { 54 | master: null 55 | } 56 | }; 57 | } 58 | 59 | if (sku === 'existing_sku_no_priceinfo') { 60 | return { 61 | ID: 'existing_sku_no_priceinfo', 62 | priceModel: null, 63 | variationModel: { 64 | master: null 65 | } 66 | }; 67 | } 68 | 69 | if (sku === 'existing_sku_with_pricebook') { 70 | return { 71 | ID: 'existing_sku_with_pricebook', 72 | priceModel: { 73 | getPriceBookPrice: () => { 74 | return { 75 | value: 25, 76 | currency: 'EUR' 77 | }; 78 | }, 79 | price: { 80 | value: 100, 81 | currency: 'EUR' 82 | }, 83 | priceInfo: { 84 | priceBook: { 85 | ID: 'activePricebookId', 86 | parentPriceBook: { 87 | ID: 'parentPriceBookId' 88 | } 89 | } 90 | } 91 | }, 92 | variationModel: { 93 | master: null 94 | } 95 | }; 96 | } 97 | 98 | return null; 99 | } 100 | }, 101 | 'dw/campaign/PromotionMgr': { 102 | getActiveCustomerPromotions: () => { 103 | return { 104 | getProductPromotions: function (product) { 105 | if (product.ID === 'existing_sku_no_pricebook') { 106 | return { 107 | iterator: function () { 108 | return { 109 | promotions: [ 110 | { 111 | ID: 'promo_id', 112 | name: 'my_promotion', 113 | calloutMsg: 'promo message', 114 | details: 'details', 115 | image: { 116 | absURL: 'http://image.jpg' 117 | }, 118 | getPromotionalPrice: function () { 119 | return { 120 | value: 50, 121 | currencyCode: 'EUR' 122 | }; 123 | } 124 | } 125 | ], 126 | next: function () { 127 | return this.promotions.shift(); 128 | }, 129 | hasNext: function () { 130 | return this.promotions.length > 0; 131 | } 132 | }; 133 | } 134 | }; 135 | } 136 | 137 | return { 138 | iterator: () => { 139 | return { 140 | hasNext: () => false 141 | }; 142 | } 143 | }; 144 | } 145 | }; 146 | } 147 | } 148 | }); 149 | 150 | describe('createExtendedProduct', () => { 151 | before(() => { 152 | global.request = { 153 | locale: 'nl_BE' 154 | }; 155 | 156 | global.empty = function (value) { 157 | return value === null || value === undefined || value.length === 0; 158 | }; 159 | }); 160 | 161 | it('should return price and master product information when a valid product is passed with no pricebook info', () => { 162 | const result = productSearchHelper.createExtendedProduct('existing_sku_no_pricebook'); 163 | 164 | assert.deepEqual(result, { 165 | 'id': 'existing_sku_no_pricebook', 166 | 'masterProductId': 'existing_sku_no_pricebook', 167 | 'priceInfo': { 168 | 'originalPrice': { 169 | 'value': 100 170 | }, 171 | 'promotionPrice': { 172 | 'currency': 'EUR', 173 | 'promoDetails': { 174 | 'callOut': 'promo message', 175 | 'details': 'details', 176 | 'id': 'promo_id', 177 | 'image': 'http://image.jpg', 178 | 'name': 'my_promotion' 179 | }, 180 | 'value': 50 181 | }, 182 | 'salePrice': { 183 | 'value': 100 184 | } 185 | } 186 | }); 187 | }); 188 | 189 | it('should return price and master product information when a valid product is passed with pricebook info', () => { 190 | const result = productSearchHelper.createExtendedProduct('existing_sku_with_pricebook'); 191 | 192 | assert.deepEqual(result, { 193 | 'id': 'existing_sku_with_pricebook', 194 | 'masterProductId': 'existing_sku_with_pricebook', 195 | 'priceInfo': { 196 | 'originalPrice': { 197 | 'pricebook': 'parentPriceBookId', 198 | 'value': 25 199 | }, 200 | 'salePrice': { 201 | 'pricebook': 'activePricebookId', 202 | 'value': 100 203 | } 204 | } 205 | }); 206 | }); 207 | 208 | it('should return no price and master product information when a valid product is passed with no priceInfo info', () => { 209 | const result = productSearchHelper.createExtendedProduct('existing_sku_no_priceinfo'); 210 | 211 | assert.deepEqual(result, {}); 212 | }); 213 | 214 | it('should return no price and master product information when the product does not exist', () => { 215 | const result = productSearchHelper.createExtendedProduct('non_existing_sku'); 216 | assert.isNull(result); 217 | }); 218 | }); 219 | 220 | describe('getSearchRedirectInformation', () => { 221 | before(() => { 222 | global.request = { 223 | locale: 'nl_BE' 224 | }; 225 | }); 226 | 227 | it('should return the search redirect information', () => { 228 | const result = productSearchHelper.getSearchRedirectInformation('my_query_with_redirect'); 229 | 230 | assert.equal(result, 'https://www.rhino-inquisitor.com'); 231 | }); 232 | 233 | it('It should return null if there is no search redirect configured for the given query', () => { 234 | const result = productSearchHelper.getSearchRedirectInformation('my_query_without_redirect'); 235 | 236 | assert.isNull(result); 237 | }); 238 | 239 | it('It should return null if no query is passed', () => { 240 | const result = productSearchHelper.getSearchRedirectInformation(); 241 | 242 | assert.isNull(result); 243 | }); 244 | }); 245 | 246 | describe('getSearchMetaData', () => { 247 | it('should return a list of tags', () => { 248 | const result = productSearchHelper.getSearchMetaData('test'); 249 | 250 | assert.deepEqual(result, [ 251 | { 252 | 'ID': 'id', 253 | 'content': 'content', 254 | 'name': false, 255 | 'property': true, 256 | 'title': false 257 | } 258 | ]); 259 | }); 260 | 261 | it('should return null if no query is passed', () => { 262 | const result = productSearchHelper.getSearchMetaData(); 263 | 264 | assert.isNull(result); 265 | }); 266 | }); 267 | 268 | describe('getCategoryMetaData', () => { 269 | it('should return a list of tags', () => { 270 | const result = productSearchHelper.getCategoryMetaData({ 271 | ID: 'test' 272 | }); 273 | 274 | assert.deepEqual(result, [ 275 | { 276 | 'ID': 'id', 277 | 'content': 'content', 278 | 'name': false, 279 | 'property': true, 280 | 'title': false 281 | } 282 | ]); 283 | }); 284 | 285 | it('should return null if no query is passed', () => { 286 | const result = productSearchHelper.getCategoryMetaData(); 287 | 288 | assert.isNull(result); 289 | }); 290 | }); 291 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/helpers/seoHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | 5 | const seoHelper = require('../../../../../cartridges/app_api_base/cartridge/scripts/helpers/seoHelper'); 6 | 7 | describe('getPageMetaTags', () => { 8 | it('should return null when no object is passed', () => { 9 | const result = seoHelper.getPageMetaTags(); 10 | 11 | assert.isNull(result); 12 | }); 13 | 14 | it('should return null when an object is passed without the "pageMetaTags" attribute', () => { 15 | const result = seoHelper.getPageMetaTags({ }); 16 | 17 | assert.isNull(result); 18 | }); 19 | 20 | it('should return an array of formatted tags when an object is passed with the "pageMetaTags" attribute', () => { 21 | const result = seoHelper.getPageMetaTags({ 22 | pageMetaTags: [{ 23 | ID: 'id', 24 | content: 'content', 25 | name: false, 26 | property: true, 27 | title: false 28 | }] 29 | }); 30 | 31 | assert.deepEqual(result, [ 32 | { 33 | 'ID': 'id', 34 | 'content': 'content', 35 | 'name': false, 36 | 'property': true, 37 | 'title': false 38 | } 39 | ]); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/helpers/sessionHelper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 5 | 6 | let mockCookies = ['__cq_dnt=1; Path=/; Secure; SameSite=None; version=2', 'dw_dnt=1; Path=/; Secure; httponly; max-age=5; SameSite=None']; 7 | let ipResult = null; 8 | let mockResponseHeaders = { 9 | get: () => { 10 | if (mockCookies === null) { 11 | return null; 12 | } 13 | 14 | return { 15 | toArray: () => mockCookies 16 | }; 17 | } 18 | }; 19 | 20 | const mockGetSession = function (token, ip) { 21 | ipResult = ip; 22 | 23 | return { 24 | responseHeaders: mockResponseHeaders 25 | }; 26 | }; 27 | 28 | const sessionHelper = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/helpers/sessionHelper', { 29 | 'dw/web/Cookie': function (name, cookieValue) { 30 | return { 31 | name: name, 32 | value: cookieValue, 33 | setPath: function (value) { this.path = value; }, 34 | setMaxAge: function (value) { this.maxAge = value; }, 35 | setSecure: function (value) { this.secure = value; }, 36 | setHttpOnly: function (value) { this.httponly = value; }, 37 | setVersion: function (value) { this.version = value; } 38 | }; 39 | }, 40 | '*/cartridge/scripts/services/SessionBridgeService': { 41 | getSession: mockGetSession 42 | } 43 | }); 44 | 45 | describe('middleware', () => { 46 | beforeEach(() => { 47 | ipResult = null; 48 | }); 49 | 50 | it('Should process all cookies', () => { 51 | const response = { 52 | cookies: [], 53 | addHttpCookie: function (cookie) { 54 | this.cookies.push(cookie); 55 | } 56 | }; 57 | 58 | const result = sessionHelper.setUserSession('12345', response); 59 | 60 | assert.equal(response.cookies.length, 2); 61 | assert.isTrue(result.ok); 62 | 63 | const firstCookie = response.cookies[0]; 64 | const secondCookie = response.cookies[1]; 65 | 66 | assert.equal(firstCookie.name, '__cq_dnt'); 67 | assert.equal(secondCookie.name, 'dw_dnt'); 68 | assert.equal(firstCookie.value, '1'); 69 | assert.equal(secondCookie.value, '1'); 70 | assert.equal(firstCookie.path, '/'); 71 | assert.equal(secondCookie.path, '/'); 72 | assert.isTrue(firstCookie.secure); 73 | assert.isTrue(secondCookie.secure); 74 | assert.equal(firstCookie.version, 2); 75 | assert.equal(secondCookie.maxAge, 5); 76 | assert.isTrue(secondCookie.httponly); 77 | }); 78 | 79 | it('Should return a negative response if there are no cookies', () => { 80 | mockCookies = null; 81 | 82 | const response = { cookies: [] }; 83 | 84 | const result = sessionHelper.setUserSession('12345', response, null); 85 | 86 | assert.equal(response.cookies.length, 0); 87 | assert.isFalse(result.ok); 88 | }); 89 | 90 | it('Should add originating IP to the session bridge call', () => { 91 | mockCookies = null; 92 | 93 | const response = { cookies: [] }; 94 | 95 | sessionHelper.setUserSession('12345', response, { httpRemoteAddress: '123' }); 96 | 97 | assert.equal(ipResult, '123'); 98 | }); 99 | 100 | it('Should return a negative response if there are no response headers', () => { 101 | mockCookies = null; 102 | mockResponseHeaders = null; 103 | 104 | const response = { cookies: [] }; 105 | 106 | const result = sessionHelper.setUserSession('12345', response); 107 | 108 | assert.equal(response.cookies.length, 0); 109 | assert.isFalse(result.ok); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/hooks/category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 5 | 6 | let isSCAPI = true; 7 | 8 | const category = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/hooks/category', { 9 | '*/cartridge/scripts/helpers/productSearchHelper': { 10 | getCategoryMetaData: () => { 11 | return { 'test': 'test' }; 12 | } 13 | } 14 | }); 15 | 16 | describe('modifyGETResponse', () => { 17 | before(() => { 18 | global.request = { 19 | isSCAPI: () => { 20 | return isSCAPI; 21 | } 22 | }; 23 | }); 24 | 25 | beforeEach(() => { 26 | isSCAPI = true; 27 | }); 28 | 29 | it('should return category with metadata', () => { 30 | const dwCategory = {}; 31 | const categoryResponse = {}; 32 | 33 | category.modifyGETResponse(dwCategory, categoryResponse); 34 | 35 | assert.equal(categoryResponse.c_metadata.test, 'test'); 36 | }); 37 | 38 | it('should not return category with metadata if the request is not from the SCAPI', () => { 39 | const dwCategory = {}; 40 | const categoryResponse = {}; 41 | isSCAPI = false; 42 | 43 | category.modifyGETResponse(dwCategory, categoryResponse); 44 | 45 | assert.equal(categoryResponse.c_metadata, undefined); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/hooks/product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 5 | 6 | let isSCAPI = true; 7 | 8 | const product = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/hooks/product', { 9 | '*/cartridge/scripts/helpers/seoHelper': { 10 | getPageMetaTags: () => { 11 | return { 'test': 'test' }; 12 | } 13 | } 14 | }); 15 | 16 | describe('modifyGETResponse', () => { 17 | before(() => { 18 | global.request = { 19 | isSCAPI: () => { 20 | return isSCAPI; 21 | } 22 | }; 23 | }); 24 | 25 | beforeEach(() => { 26 | isSCAPI = true; 27 | }); 28 | 29 | it('should return product with metadata', () => { 30 | const dwProduct = {}; 31 | const productResponse = {}; 32 | 33 | product.modifyGETResponse(dwProduct, productResponse); 34 | 35 | assert.equal(productResponse.c_metadata.test, 'test'); 36 | }); 37 | 38 | it('should not return product with metadata if the request is not from the SCAPI', () => { 39 | const dwProduct = {}; 40 | const productResponse = {}; 41 | isSCAPI = false; 42 | 43 | product.modifyGETResponse(dwProduct, productResponse); 44 | 45 | assert.equal(productResponse.c_metadata, undefined); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/hooks/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('chai').assert; 4 | const proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 5 | 6 | let isSCAPI = true; 7 | let getSearchRedirectInformationResult = null; 8 | let getSearchMetaDataResult = null; 9 | let createExtendedProductResult = null; 10 | 11 | const product = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/hooks/search', { 12 | '*/cartridge/scripts/helpers/productSearchHelper': { 13 | getSearchRedirectInformation: () => { 14 | return getSearchRedirectInformationResult; 15 | }, 16 | getSearchMetaData: () => { 17 | return getSearchMetaDataResult; 18 | }, 19 | createExtendedProduct: () => { 20 | return createExtendedProductResult; 21 | } 22 | }, 23 | 'dw/system/Status': function (type, code) { 24 | return { 25 | OK: 1, 26 | code: code 27 | }; 28 | } 29 | }); 30 | 31 | describe('modifyGETResponse', () => { 32 | before(() => { 33 | global.request = { 34 | isSCAPI: () => { 35 | return isSCAPI; 36 | } 37 | }; 38 | }); 39 | 40 | beforeEach(() => { 41 | isSCAPI = true; 42 | getSearchRedirectInformationResult = null; 43 | getSearchMetaDataResult = null; 44 | createExtendedProductResult = null; 45 | }); 46 | 47 | it('should catch exceptions', () => { 48 | var result = product.modifyGETResponse(); 49 | 50 | assert.equal(result.code, 'ERR-SEARCH-01'); 51 | }); 52 | 53 | describe('Search Redirect', () => { 54 | it('should return search with redirect information', () => { 55 | const searchResponse = { 56 | query: 'rhino', 57 | hits: [{}, {}], 58 | count: 2, 59 | total: 2, 60 | refinements: [], 61 | sortingOptions: [], 62 | search_phrase_suggestions: {} 63 | }; 64 | 65 | getSearchRedirectInformationResult = 'https://www.rhino-inquisitor.com'; 66 | searchResponse.hits.toArray = () => searchResponse.hits; 67 | 68 | var result = product.modifyGETResponse(searchResponse); 69 | 70 | assert.equal(searchResponse.hits.length, 2); 71 | assert.equal(searchResponse.search_phrase_suggestions.c_searchRedirect, 'https://www.rhino-inquisitor.com'); 72 | assert.equal(searchResponse.count, 2); 73 | assert.equal(searchResponse.total, 2); 74 | 75 | assert.equal(result.OK, 1); 76 | }); 77 | 78 | it('should not return search with redirect information if the request is not from the SCAPI', () => { 79 | const searchResponse = { 80 | query: 'rhino', 81 | search_phrase_suggestions: {} 82 | }; 83 | getSearchRedirectInformationResult = 'https://www.rhino-inquisitor.com'; 84 | isSCAPI = false; 85 | 86 | product.modifyGETResponse(searchResponse); 87 | 88 | assert.isUndefined(searchResponse.search_phrase_suggestions.c_searchRedirect); 89 | }); 90 | 91 | it('should not return a search with redirect if no redirect has been found', () => { 92 | const searchResponse = { 93 | query: 'rhino', 94 | search_phrase_suggestions: {} 95 | }; 96 | getSearchRedirectInformationResult = null; 97 | 98 | product.modifyGETResponse(searchResponse); 99 | 100 | assert.isUndefined(searchResponse.search_phrase_suggestions.c_searchRedirect); 101 | }); 102 | }); 103 | 104 | describe('Search Meta Data', () => { 105 | it('should return search with metadata if there are results', () => { 106 | const searchResponse = { 107 | query: 'rhino', 108 | hits: [{}, {}], 109 | count: 2, 110 | total: 2, 111 | refinements: [], 112 | sortingOptions: [], 113 | search_phrase_suggestions: {} 114 | }; 115 | 116 | getSearchMetaDataResult = { 'test': 'test' }; 117 | searchResponse.hits.toArray = () => searchResponse.hits; 118 | 119 | product.modifyGETResponse(searchResponse); 120 | 121 | assert.equal(searchResponse.search_phrase_suggestions.c_metadata.test, 'test'); 122 | }); 123 | 124 | it('should return search with metadata if there are no results', () => { 125 | const searchResponse = { 126 | query: 'rhino', 127 | hits: [], 128 | count: 0, 129 | total: 0, 130 | refinements: [], 131 | sortingOptions: [], 132 | search_phrase_suggestions: {} 133 | }; 134 | 135 | getSearchMetaDataResult = { 'test': 'test' }; 136 | 137 | product.modifyGETResponse(searchResponse); 138 | 139 | assert.equal(searchResponse.search_phrase_suggestions.c_metadata.test, 'test'); 140 | }); 141 | 142 | it('should not return search with metadata if the request is not from the SCAPI', () => { 143 | const searchResponse = { 144 | query: 'rhino', 145 | hits: [{}, {}], 146 | search_phrase_suggestions: {} 147 | }; 148 | searchResponse.hits.toArray = () => searchResponse.hits; 149 | getSearchMetaDataResult = { 'test': 'test' }; 150 | isSCAPI = false; 151 | 152 | product.modifyGETResponse(searchResponse); 153 | 154 | assert.isUndefined(searchResponse.search_phrase_suggestions.c_metadata); 155 | }); 156 | }); 157 | 158 | describe('Search Extended Products', () => { 159 | it('should return search with extended products', () => { 160 | const searchResponse = { 161 | query: 'rhino', 162 | hits: [{ 163 | represented_product: { 164 | id: 'test' 165 | } 166 | }, 167 | { 168 | represented_product: { 169 | id: 'test2' 170 | } 171 | }], 172 | count: 2, 173 | total: 2, 174 | refinements: [], 175 | sortingOptions: [], 176 | search_phrase_suggestions: [] 177 | }; 178 | 179 | searchResponse.hits.toArray = () => searchResponse.hits; 180 | 181 | createExtendedProductResult = { 'test': 'test' }; 182 | 183 | product.modifyGETResponse(searchResponse); 184 | 185 | assert.equal(searchResponse.hits[0].c_extend, createExtendedProductResult); 186 | }); 187 | 188 | it('should not return search with extended products if the request is not from the SCAPI', () => { 189 | const searchResponse = { 190 | query: 'rhino', 191 | hits: [{}, {}] 192 | }; 193 | createExtendedProductResult = { 'test': 'test' }; 194 | isSCAPI = false; 195 | 196 | product.modifyGETResponse(searchResponse); 197 | 198 | assert.isUndefined(searchResponse.hits[0].c_extend); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/hooks/taxes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 5 | var ArrayList = require('../../../../mocks/dw.util.Collection'); 6 | var sinon = require('sinon'); 7 | 8 | function createBasket(shipments) { 9 | return { 10 | getShipments: function () { 11 | return new ArrayList(shipments); 12 | }, 13 | updateOrderLevelPriceAdjustmentTax: function () {}, 14 | getPriceAdjustments: function () { 15 | return { 16 | empty: false 17 | }; 18 | }, 19 | getShippingPriceAdjustments: function () { 20 | return { 21 | empty: false 22 | }; 23 | }, 24 | getAllLineItems: function () { 25 | return []; 26 | } 27 | }; 28 | } 29 | 30 | function createShipment(lineItems, address) { 31 | return { 32 | shippingAddress: address, 33 | getAllLineItems: function () { 34 | return new ArrayList(lineItems); 35 | } 36 | }; 37 | } 38 | 39 | function createLineItem(shipment, taxClassId, uuid) { 40 | return { 41 | lineItemText: '', 42 | taxClassID: taxClassId, 43 | taxRate: 1, 44 | UUID: uuid 45 | }; 46 | } 47 | 48 | var failTaxJurisdictionID = false; 49 | var failTaxClassID = false; 50 | 51 | describe('Taxes', function () { 52 | var taxesHook = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/hooks/taxes', { 53 | 'dw/order/ShippingLocation': function () { 54 | return {}; 55 | }, 56 | 'dw/order/TaxMgr': { 57 | getTaxJurisdictionID: function () { 58 | return 2; 59 | }, 60 | get defaultTaxJurisdictionID() { 61 | if (failTaxJurisdictionID) { 62 | return null; 63 | } 64 | return 1; 65 | }, 66 | customRateTaxClassID: 'custom', 67 | get defaultTaxClassID() { 68 | if (failTaxClassID) { 69 | return null; 70 | } 71 | return 1; 72 | }, 73 | getTaxRate: function (classId, jurisdictionId) { 74 | if (classId === -1) { 75 | return null; 76 | } 77 | return classId / jurisdictionId; 78 | } 79 | }, 80 | '*/cartridge/scripts/util/collections': proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/util/collections', { 81 | 'dw/util/ArrayList': ArrayList, 82 | first: function () { return true; } 83 | }), 84 | 'dw/system/Logger': { 85 | debug: function (text) { 86 | return text; 87 | }, 88 | error: function (text) { 89 | return text; 90 | } 91 | } 92 | }); 93 | 94 | var calculate = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/hooks/cart/calculate', { 95 | 'dw/util/HashMap': {}, 96 | 'dw/campaign/PromotionMgr': {}, 97 | 'dw/order/ShippingMgr': {}, 98 | 'dw/order/TaxMgr': {}, 99 | 'dw/system/Logger': { 100 | debug: function (text) { 101 | return text; 102 | }, 103 | error: function (text) { 104 | return text; 105 | } 106 | }, 107 | 'dw/system/Status': sinon.stub(), 108 | 'dw/order/ShippingLocation': {}, 109 | 'dw/system/HookMgr': {}, 110 | '*/cartridge/scripts/util/collections': { 111 | forEach: function () { 112 | 113 | }, 114 | first: function () { return true; } 115 | }, 116 | '*/cartridge/scripts/helpers/basketCalculationHelpers': { 117 | calculateTaxes: function () { 118 | return { 119 | taxes: [] 120 | }; 121 | } 122 | } 123 | }); 124 | 125 | it('should calculate taxes for simple basket with a single line item', function () { 126 | var basket = createBasket([createShipment([createLineItem(false, 0.5, 'id')], 'address')]); 127 | var taxResult = taxesHook.calculateTaxes(basket); 128 | var taxes = taxResult.taxes; 129 | 130 | assert.equal(taxes.length, 1); 131 | assert.equal(taxes[0].uuid, 'id'); 132 | assert.equal(taxes[0].value, 0.25); 133 | }); 134 | 135 | it('should calculate taxes for basket with multiple line items', function () { 136 | var basket = createBasket([createShipment([createLineItem(false, 0.5, 'id'), createLineItem(false, 2, 'id2')], 'address')]); 137 | var taxResult = taxesHook.calculateTaxes(basket); 138 | var taxes = taxResult.taxes; 139 | 140 | assert.equal(taxes.length, 2); 141 | assert.equal(taxes[0].uuid, 'id'); 142 | assert.equal(taxes[0].value, 0.25); 143 | assert.equal(taxes[1].uuid, 'id2'); 144 | assert.equal(taxes[1].value, 1); 145 | }); 146 | 147 | it('should skip taxes for line items with custom rate', function () { 148 | var basket = createBasket([createShipment([createLineItem(false, 'custom', 'id')], 'address')]); 149 | var taxResult = taxesHook.calculateTaxes(basket); 150 | var taxes = taxResult.taxes; 151 | 152 | assert.equal(taxes.length, 0); 153 | }); 154 | 155 | it('should use default taxJurisdictionID if there is no address', function () { 156 | var basket = createBasket([createShipment([createLineItem(false, 2, 'id')], null)]); 157 | var taxResult = taxesHook.calculateTaxes(basket); 158 | var taxes = taxResult.taxes; 159 | 160 | assert.equal(taxes.length, 1); 161 | assert.equal(taxes[0].value, 2); 162 | }); 163 | 164 | it('should not return taxes if taxJurisdictionId cannot be retrieved', function () { 165 | failTaxJurisdictionID = true; 166 | var basket = createBasket([createShipment([createLineItem(false, 2, 'id')], null)]); 167 | var taxResult = taxesHook.calculateTaxes(basket); 168 | var taxes = taxResult.taxes; 169 | failTaxJurisdictionID = false; 170 | 171 | assert.equal(taxes.length, 0); 172 | }); 173 | 174 | it('should use default taxClass if one is not provided', function () { 175 | var basket = createBasket([createShipment([createLineItem(false, null, 'id')], 'address')]); 176 | var taxResult = taxesHook.calculateTaxes(basket); 177 | var taxes = taxResult.taxes; 178 | 179 | assert.equal(taxes.length, 1); 180 | assert.equal(taxes[0].value, 0.5); 181 | }); 182 | 183 | it('should not return taxes if taxRate cannot be calculated', function () { 184 | var basket = createBasket([createShipment([createLineItem(false, -1, 'id')], 'address')]); 185 | var taxResult = taxesHook.calculateTaxes(basket); 186 | var taxes = taxResult.taxes; 187 | 188 | assert.equal(taxes.length, 0); 189 | }); 190 | 191 | it('should not return taxes if taxClassID cannot be retrieved', function () { 192 | failTaxClassID = true; 193 | var basket = createBasket([createShipment([createLineItem(false, null, 'id')], null)]); 194 | var taxResult = taxesHook.calculateTaxes(basket); 195 | var taxes = taxResult.taxes; 196 | failTaxClassID = false; 197 | 198 | assert.equal(taxes.length, 0); 199 | }); 200 | 201 | it('should return custom properties', function () { 202 | var basket = createBasket([createShipment([createLineItem(false, -1, 'id')], 'address')]); 203 | var taxResult = taxesHook.calculateTaxes(basket); 204 | 205 | assert.isObject(taxResult.custom); 206 | }); 207 | 208 | it('The order level price adjustment tax calculation is called when there is an order level price adjustment', function () { 209 | var testBasket = createBasket([createShipment([createLineItem(false, -1, 'id')], 'address')]); 210 | var basketSpy = sinon.spy(testBasket, 'updateOrderLevelPriceAdjustmentTax'); 211 | calculate.calculateTax(testBasket); 212 | assert(basketSpy.calledOnce); 213 | basketSpy.restore(); 214 | }); 215 | }); 216 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/middleware/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var sinon = require('sinon'); 5 | 6 | var cacheMiddleware = require('../../../../../cartridges/app_api_base/cartridge/scripts/middleware/cache'); 7 | 8 | describe('cache middleware', () => { 9 | var next = sinon.spy(); 10 | var res = { 11 | cachePeriod: 0, 12 | cachePeriodUnit: '', 13 | personalized: false 14 | }; 15 | 16 | beforeEach(() => { 17 | next = sinon.spy(); 18 | }); 19 | 20 | afterEach(() => { 21 | next.resetHistory(); 22 | }); 23 | 24 | it('Should set the page cache value to 24 hours', () => { 25 | cacheMiddleware.applyDefaultCache(null, res, next); 26 | assert.isTrue(res.cachePeriod === 24); 27 | assert.isTrue(res.cachePeriodUnit === 'hours'); 28 | assert.isFalse(res.personalized); 29 | }); 30 | it('Should set the page cache value to 30 minutes', () => { 31 | cacheMiddleware.applyInventorySensitiveCache(null, res, next); 32 | assert.isTrue(res.cachePeriod === 30); 33 | assert.isTrue(res.cachePeriodUnit === 'minutes'); 34 | assert.isFalse(res.personalized); 35 | }); 36 | it('Should set the varyby value to price_promotion', () => { 37 | cacheMiddleware.applyPromotionSensitiveCache(null, res, next); 38 | assert.isTrue(res.cachePeriod === 24); 39 | assert.isTrue(res.cachePeriodUnit === 'hours'); 40 | assert.isTrue(res.personalized); 41 | }); 42 | it('Should set the varyby value to price_promotion with a cache value of 1 hour', () => { 43 | cacheMiddleware.applyShortPromotionSensitiveCache(null, res, next); 44 | assert.isTrue(res.cachePeriod === 1); 45 | assert.isTrue(res.cachePeriodUnit === 'hours'); 46 | assert.isTrue(res.personalized); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/unit/app_api_base/scripts/middleware/userLoggedIn.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var sinon = require('sinon'); 5 | var proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 6 | 7 | var userLoggedInMiddleware = proxyquire('../../../../../cartridges/app_api_base/cartridge/scripts/middleware/userLoggedIn', { 8 | 'dw/web/URLUtils': { 9 | url: () => { 10 | return 'some url'; 11 | } 12 | }, 13 | 'dw/web/Resource': { 14 | msg: function (key) { 15 | return 'translation ' + key; 16 | } 17 | } 18 | }); 19 | 20 | describe('userLoggedInMiddleware', () => { 21 | var next = sinon.spy(); 22 | var req = { 23 | currentCustomer: { 24 | raw: 'something' 25 | }, 26 | querystring: {} 27 | }; 28 | var res = { 29 | json: sinon.spy(), 30 | setStatusCode: sinon.spy() 31 | }; 32 | 33 | afterEach(() => { 34 | next.resetHistory(); 35 | res.json.resetHistory(); 36 | res.setStatusCode.resetHistory(); 37 | req.querystring = {}; 38 | }); 39 | 40 | it('Should respond with an error if a user is not logged in', () => { 41 | userLoggedInMiddleware.validateLoggedIn(req, res, next); 42 | assert.isTrue(res.json.calledOnce); 43 | assert.isTrue(res.setStatusCode.calledOnce); 44 | assert.isTrue(res.setStatusCode.calledWith(403)); 45 | assert.isTrue(next.calledOnce); 46 | }); 47 | 48 | it('Should just call next if user is logged in', () => { 49 | req.currentCustomer.profile = 'profile'; 50 | userLoggedInMiddleware.validateLoggedIn(req, res, next); 51 | assert.isTrue(res.setStatusCode.notCalled); 52 | assert.isTrue(res.json.notCalled); 53 | assert.isTrue(next.calledOnce); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/unit/modules/server/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var sinon = require('sinon'); 5 | var middleware = require('../../../../cartridges/modules/server/middleware'); 6 | 7 | describe('middleware', function () { 8 | var next = null; 9 | var req = {}; 10 | 11 | beforeEach(function () { 12 | next = sinon.spy(); 13 | req = {}; 14 | }); 15 | 16 | afterEach(function () { 17 | next.resetHistory(); 18 | }); 19 | 20 | it('should call next for get method', function () { 21 | req.httpMethod = 'GET'; 22 | middleware.get(req, null, next); 23 | assert.isTrue(next.calledOnce); 24 | }); 25 | 26 | it('should call next with error for get method', function () { 27 | req.httpMethod = 'PUT'; 28 | middleware.get(req, null, next); 29 | assert.instanceOf(next.firstCall.args[0], Error); 30 | }); 31 | 32 | it('should call next for put method', function () { 33 | req.httpMethod = 'PUT'; 34 | middleware.put(req, null, next); 35 | assert.isTrue(next.calledOnce); 36 | }); 37 | 38 | it('should call next with error for put method', function () { 39 | req.httpMethod = 'POST'; 40 | middleware.put(req, null, next); 41 | assert.instanceOf(next.firstCall.args[0], Error); 42 | }); 43 | 44 | it('should call next for patch method', function () { 45 | req.httpMethod = 'PATCH'; 46 | middleware.patch(req, null, next); 47 | assert.isTrue(next.calledOnce); 48 | }); 49 | 50 | it('should call next with error for patch method', function () { 51 | req.httpMethod = 'GET'; 52 | middleware.patch(req, null, next); 53 | assert.instanceOf(next.firstCall.args[0], Error); 54 | }); 55 | 56 | it('should call next for post method', function () { 57 | req.httpMethod = 'POST'; 58 | middleware.post(req, null, next); 59 | assert.isTrue(next.calledOnce); 60 | }); 61 | 62 | it('should call next with error for post method', function () { 63 | req.httpMethod = 'PATCH'; 64 | middleware.post(req, null, next); 65 | assert.instanceOf(next.firstCall.args[0], Error); 66 | }); 67 | 68 | it('should call next for delete method', function () { 69 | req.httpMethod = 'DELETE'; 70 | middleware.delete(req, null, next); 71 | assert.isTrue(next.calledOnce); 72 | }); 73 | 74 | it('should call next with error for delete method', function () { 75 | req.httpMethod = 'GET'; 76 | middleware.delete(req, null, next); 77 | assert.instanceOf(next.firstCall.args[0], Error); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /test/unit/modules/server/querystring.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var sinon = require('sinon'); 5 | var proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 6 | var QueryString; 7 | var logSpy; 8 | 9 | describe('querystring', function () { 10 | beforeEach(function () { 11 | logSpy = sinon.spy(); 12 | QueryString = proxyquire('../../../../cartridges/modules/server/queryString', { 13 | 'dw/system/Logger': { 14 | warn: logSpy 15 | } 16 | }); 17 | }); 18 | 19 | describe('handling special characters', function () { 20 | it('should handle the \'+\' with a \'%20\' which leads to a \' \'', function () { 21 | var params = '?trackOrderNumber=01&trackOrderPostal=EC1A+1BB'; 22 | var result = new QueryString(params); 23 | 24 | assert.equal(result.trackOrderPostal, 'EC1A 1BB'); 25 | }); 26 | }); 27 | 28 | describe('handling url encoding of querystring', function () { 29 | it('should handle encoding properly', function () { 30 | var params = '?maat=37%2B&pid=P12345'; 31 | var result = new QueryString(params); 32 | assert.equal(result.toString(), 'maat=37%2B&pid=P12345'); 33 | }); 34 | }); 35 | 36 | describe('handling duplicate parameters in querystring', function () { 37 | it('should return an array', function () { 38 | var params = '?one=uno&cheese=1&cheese=2&cheese=3&brand=sony&brand=samsung&cheese=4'; 39 | var result = new QueryString(params); 40 | assert.deepEqual(result.one, 'uno'); 41 | assert.deepEqual(result.cheese, ['4', '3', '2', '1']); 42 | assert.deepEqual(result.brand, ['samsung', 'sony']); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/unit/modules/server/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var sinon = require('sinon'); 5 | 6 | describe('render', function () { 7 | var render = null; 8 | 9 | var response = { 10 | base: { 11 | writer: { 12 | print: sinon.spy() 13 | } 14 | }, 15 | setContentType: sinon.spy(), 16 | viewData: {}, 17 | renderings: [], 18 | view: null 19 | }; 20 | 21 | beforeEach(function () { 22 | render = require('../../../../cartridges/modules/server/render'); 23 | }); 24 | 25 | afterEach(function () { 26 | response.base.writer.print.resetHistory(); 27 | response.setContentType.resetHistory(); 28 | response.viewData = {}; 29 | response.renderings = []; 30 | }); 31 | 32 | it('should render a json output', function () { 33 | response.renderings.push({ type: 'render', subType: 'json' }); 34 | response.viewData = { name: 'value' }; 35 | render.applyRenderings(response); 36 | 37 | assert.isTrue(response.setContentType.calledWith('application/json')); 38 | assert.isTrue(response.base.writer.print.calledOnce); 39 | }); 40 | 41 | it('should print output', function () { 42 | response.renderings.push({ type: 'print', message: 'crazyMessage' }); 43 | render.applyRenderings(response); 44 | 45 | assert.isTrue(response.base.writer.print.calledOnce); 46 | assert.isTrue(response.base.writer.print.calledWith('crazyMessage')); 47 | }); 48 | 49 | it('should render error page when template failed', function () { 50 | var renderMock = require('../../../../cartridges/modules/server/render'); 51 | 52 | response.renderings.push({ type: 'render', subType: 'isml', view: 'template' }); 53 | 54 | try { 55 | renderMock.applyRenderings(response); 56 | } catch (e) { 57 | assert.isNotNull(e); 58 | } 59 | }); 60 | 61 | it('should throw error when no rendering step has been called', function () { 62 | try { 63 | render.applyRenderings(response); 64 | } catch (e) { 65 | assert.isNotNull(e); 66 | } 67 | }); 68 | 69 | it('should throw error when unidentified Type', function () { 70 | response.renderings.push({ type: 'blah', subType: 'blah', view: 'template' }); 71 | 72 | try { 73 | render.applyRenderings(response); 74 | } catch (e) { 75 | assert.isNotNull(e); 76 | } 77 | }); 78 | 79 | it('should throw error when unidentified subType', function () { 80 | response.renderings.push({ type: 'render', subType: 'blah', view: 'template' }); 81 | 82 | try { 83 | render.applyRenderings(response); 84 | } catch (e) { 85 | assert.isNotNull(e); 86 | } 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /test/unit/modules/server/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var sinon = require('sinon'); 5 | var proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 6 | var Response = proxyquire('../../../../cartridges/modules/server/response', { 7 | '*/cartridge/config/httpHeadersConf': [ 8 | { 9 | 'id': 'someName', 10 | 'value': 'someValue' 11 | } 12 | ] 13 | }); 14 | 15 | var res = { 16 | redirect: function () {}, 17 | setHttpHeader: sinon.spy(), 18 | setContentType: sinon.spy(), 19 | setStatus: sinon.spy() 20 | }; 21 | 22 | describe('response', function () { 23 | it('should create response object with passed-in base', function () { 24 | var response = new Response(res); 25 | assert.property(response, 'base'); 26 | assert.property(response.base, 'redirect'); 27 | }); 28 | 29 | it('should correctly set viewData', function () { 30 | var response = new Response(res); 31 | response.render({ name: 'value' }); 32 | assert.equal(response.viewData.name, 'value'); 33 | }); 34 | 35 | it('should extend viewData', function () { 36 | var response = new Response(res); 37 | response.setViewData({ name: 'value' }); 38 | response.setViewData({ foo: 'bar' }); 39 | response.render({ name: 'test' }); 40 | assert.equal(response.viewData.name, 'test'); 41 | assert.equal(response.viewData.foo, 'bar'); 42 | }); 43 | 44 | it('should not extend viewData with non-objects', function () { 45 | var response = new Response(res); 46 | response.setViewData({ name: 'value' }); 47 | response.setViewData(function () {}); 48 | assert.equal(response.viewData.name, 'value'); 49 | }); 50 | 51 | it('should correctly set json', function () { 52 | var response = new Response(res); 53 | response.json({ name: 'value' }); 54 | assert.isTrue(response.isJson); 55 | assert.equal(response.viewData.name, 'value'); 56 | }); 57 | 58 | it('should correctly set url', function () { 59 | var response = new Response(res); 60 | response.redirect('hello'); 61 | assert.equal(response.redirectUrl, 'hello'); 62 | }); 63 | 64 | it('should correctly set redirect status', function () { 65 | var response = new Response(res); 66 | response.setRedirectStatus('301'); 67 | assert.equal(response.redirectStatus, '301'); 68 | }); 69 | 70 | it('should set and retrieve data', function () { 71 | var response = new Response(res); 72 | response.setViewData({ name: 'value' }); 73 | assert.equal(response.getViewData().name, 'value'); 74 | }); 75 | 76 | it('should log item', function () { 77 | var response = new Response(res); 78 | response.log('one', 'two', 'three'); 79 | assert.equal(response.messageLog.length, 1); 80 | assert.equal(response.messageLog[0], 'one two three'); 81 | }); 82 | 83 | it('should convert log item to json', function () { 84 | var response = new Response(res); 85 | response.log({ name: 'value' }); 86 | assert.equal(response.messageLog.length, 1); 87 | assert.equal(response.messageLog[0], '{"name":"value"}'); 88 | }); 89 | 90 | it('should try to print out a message', function () { 91 | var response = new Response(res); 92 | response.print('hello'); 93 | 94 | assert.equal(response.renderings.length, 1); 95 | assert.equal(response.renderings[0].type, 'print'); 96 | assert.equal(response.renderings[0].message, 'hello'); 97 | }); 98 | 99 | it('should set http header', function () { 100 | var response = new Response(res); 101 | response.setHttpHeader('aName', 'aValue'); 102 | assert.isTrue(res.setHttpHeader.calledWith('aName', 'aValue')); 103 | }); 104 | 105 | it('should set content type', function () { 106 | var response = new Response(res); 107 | response.setContentType('text/html'); 108 | assert.isTrue(res.setContentType.calledWith('text/html')); 109 | }); 110 | 111 | it('should set status code', function () { 112 | var response = new Response(res); 113 | response.setStatusCode(500); 114 | assert.isTrue(res.setStatus.calledWith(500)); 115 | }); 116 | 117 | it('should set cache expiration for the page', function (done) { 118 | var response = new Response(res); 119 | response.cacheExpiration(6); 120 | assert.equal(6, response.cachePeriod); 121 | done(); 122 | }); 123 | 124 | it('should loop through and append to renderings array', function () { 125 | var response = new Response(res); 126 | response.renderings.push({ type: 'render', subType: 'isml' }); 127 | 128 | response.json({ name: 'value' }); 129 | 130 | assert.isTrue(response.isJson); 131 | assert.equal(response.viewData.name, 'value'); 132 | 133 | assert.equal(response.renderings.length, 1); 134 | assert.equal(response.renderings[0].type, 'render'); 135 | assert.equal(response.renderings[0].subType, 'json'); 136 | }); 137 | 138 | it('should loop through and append to renderings array', function () { 139 | var response = new Response(res); 140 | response.renderings.push({ type: 'print' }); 141 | 142 | response.json({ name: 'value' }); 143 | 144 | assert.equal(response.renderings.length, 2); 145 | assert.equal(response.renderings[0].type, 'print'); 146 | 147 | assert.equal(response.renderings[1].type, 'render'); 148 | assert.equal(response.renderings[1].subType, 'json'); 149 | }); 150 | }); 151 | -------------------------------------------------------------------------------- /test/unit/modules/server/route.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var proxyquire = require('proxyquire').noCallThru().noPreserveCache(); 4 | var Response = proxyquire('../../../../cartridges/modules/server/response', { 5 | '*/cartridge/config/httpHeadersConf': [{ 'id': 'testId', 'value': 'testValue' }] 6 | }); 7 | var Route = proxyquire('../../../../cartridges/modules/server/route', { 8 | './performanceMetrics': proxyquire('../../../../cartridges/modules/server/performanceMetrics', { 9 | '*/cartridge/config/performanceMetricsConf': require('../../../../cartridges/app_api_base/cartridge/config/performanceMetricsConf.json') 10 | }) 11 | }); 12 | 13 | var sinon = require('sinon'); 14 | var assert = require('chai').assert; 15 | var mockReq = {}; 16 | var mockRes = { 17 | setViewData: function () {} 18 | }; 19 | 20 | describe('route', function () { 21 | it('should create a new route with a given number of steps', function () { 22 | function tempFunc(req, res, next) { next(); } 23 | var route = new Route('test', [tempFunc, tempFunc], mockReq, mockRes); 24 | assert.equal(route.name, 'test'); 25 | assert.equal(route.chain.length, 2); 26 | }); 27 | it('should update response after last step', function (done) { 28 | function tempFunc(req, res, next) { 29 | res.test = 'Hello'; // eslint-disable-line no-param-reassign 30 | next(); 31 | } 32 | var route = new Route('test', [tempFunc], mockReq, mockRes); 33 | route.on('route:Complete', function (req, res) { 34 | assert.equal(res.test, 'Hello'); 35 | done(); 36 | }); 37 | route.getRoute()(); 38 | }); 39 | it('should execute two middleware steps', function (done) { 40 | var i = 0; 41 | 42 | function tempFunc(req, res, next) { 43 | i += 1; 44 | next(); 45 | } 46 | var route = new Route('test', [tempFunc, tempFunc], mockReq, mockRes); 47 | route.on('route:Complete', function () { 48 | assert.equal(i, 2); 49 | done(); 50 | }); 51 | route.getRoute()(); 52 | }); 53 | it('should verify that response keeps redirect variable', function (done) { 54 | function tempFunc(req, res, next) { 55 | res.redirect('test'); 56 | next(); 57 | } 58 | var response = new Response({ redirect: function () {}, setHttpHeader: function () {} }); 59 | var route = new Route('test', [tempFunc], mockReq, response); 60 | route.on('route:Redirect', function (req, res) { 61 | assert.equal(res.redirectUrl, 'test'); 62 | done(); 63 | }); 64 | route.getRoute()(); 65 | }); 66 | it('should verify that redirect with implicit (not set) redirect status works', function (done) { 67 | var baseResponseRedirectMock = sinon.spy(); 68 | function tempFunc(req, res, next) { 69 | res.redirect('test'); 70 | next(); 71 | } 72 | var response = new Response({ redirect: baseResponseRedirectMock, setHttpHeader: function () {} }); 73 | var route = new Route('test', [tempFunc], mockReq, response); 74 | route.getRoute()(); 75 | assert.isTrue(baseResponseRedirectMock.calledOnce); 76 | assert.isTrue(baseResponseRedirectMock.firstCall.calledWithExactly('test')); 77 | done(); 78 | }); 79 | it('should verify that redirect with explicit redirect status works', function (done) { 80 | var baseResponseRedirectMock = sinon.spy(); 81 | function tempFunc(req, res, next) { 82 | res.setRedirectStatus(301); 83 | res.redirect('test'); 84 | next(); 85 | } 86 | var response = new Response({ redirect: baseResponseRedirectMock, setHttpHeader: function () {} }); 87 | var route = new Route('test', [tempFunc], mockReq, response); 88 | route.getRoute()(); 89 | assert.isTrue(baseResponseRedirectMock.calledOnce); 90 | assert.isTrue(baseResponseRedirectMock.firstCall.calledWithExactly('test', 301)); 91 | done(); 92 | }); 93 | it('should throw an error', function () { 94 | function tempFunc(req, res, next) { 95 | next(new Error()); 96 | } 97 | var res = { 98 | log: function () {}, 99 | setViewData: mockRes.setViewData 100 | }; 101 | 102 | var route = new Route('test', [tempFunc], mockReq, res); 103 | assert.throws(function () { route.getRoute()(); }); 104 | }); 105 | it('should correct append a step to the route', function () { 106 | function tempFunc(req, res, next) { 107 | next(); 108 | } 109 | var route = new Route('test', [tempFunc, tempFunc], mockReq, mockRes); 110 | assert.equal(route.chain.length, 2); 111 | route.append(tempFunc); 112 | assert.equal(route.chain.length, 3); 113 | }); 114 | it('should set error object on the response', function () { 115 | var RouteStaging = proxyquire('../../../../cartridges/modules/server/route', { 116 | 'dw/system/System': { 117 | getInstanceType: function () { 118 | return false; 119 | }, 120 | 'PRODUCTION_SYSTEM': true 121 | }, 122 | './performanceMetrics': proxyquire('../../../../cartridges/modules/server/performanceMetrics', { 123 | '*/cartridge/config/performanceMetricsConf': require('../../../../cartridges/app_api_base/cartridge/config/performanceMetricsConf.json') 124 | }) 125 | }); 126 | 127 | function tempFunc(req, res, next) { 128 | next(); 129 | } 130 | var req = { 131 | path: mockReq.path, 132 | querystring: mockReq.querystring, 133 | locale: mockReq.locale 134 | }; 135 | var route = new RouteStaging('test', [tempFunc], req, mockRes); 136 | route.getRoute()({ 137 | ErrorText: 'hello', 138 | ControllerName: 'Foo', 139 | CurrentStartNodeName: 'Bar' 140 | }); 141 | assert.isNotNull(req.error); 142 | assert.equal(req.error.errorText, 'hello'); 143 | assert.equal(req.error.controllerName, 'Foo'); 144 | assert.equal(req.error.startNodeName, 'Bar'); 145 | }); 146 | it('should set error object on the response to empty string if on production', function () { 147 | var RouteProduction = proxyquire('../../../../cartridges/modules/server/route', { 148 | 'dw/system/System': { 149 | getInstanceType: function () { 150 | return true; 151 | }, 152 | 'PRODUCTION_SYSTEM': true 153 | }, 154 | './performanceMetrics': proxyquire('../../../../cartridges/modules/server/performanceMetrics', { 155 | '*/cartridge/config/performanceMetricsConf': require('../../../../cartridges/app_api_base/cartridge/config/performanceMetricsConf.json') 156 | }) 157 | }); 158 | 159 | function tempFunc(req, res, next) { 160 | next(); 161 | } 162 | var req = { 163 | path: mockReq.path, 164 | querystring: mockReq.querystring, 165 | locale: mockReq.locale 166 | }; 167 | var route = new RouteProduction('test', [tempFunc], req, mockRes); 168 | route.getRoute()({ 169 | ErrorText: 'hello', 170 | ControllerName: 'Foo', 171 | CurrentStartNodeName: 'Bar' 172 | }); 173 | assert.isNotNull(req.error); 174 | assert.equal(req.error.errorText, ''); 175 | assert.equal(req.error.controllerName, ''); 176 | assert.equal(req.error.startNodeName, ''); 177 | }); 178 | }); 179 | -------------------------------------------------------------------------------- /test/unit/modules/server/simpleCache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('chai').assert; 4 | var SimpleCache = require('../../../../cartridges/modules/server/simpleCache'); 5 | 6 | describe('simpleCache', function () { 7 | beforeEach(function () { 8 | }); 9 | 10 | afterEach(function () { 11 | }); 12 | 13 | it('should handle null argument for constructor', function () { 14 | var cache = new SimpleCache(null); 15 | 16 | assert.isTrue(cache !== null); 17 | }); 18 | 19 | it('should accept a pre-filled KV store', function () { 20 | var cache = new SimpleCache({ 'foo': 'bar' }); 21 | var value = cache.get('foo'); 22 | 23 | assert.isTrue(value === 'bar'); 24 | }); 25 | 26 | it('should get a value previously set', function () { 27 | var cache = new SimpleCache({}); 28 | cache.set('foo', 'bar'); 29 | var value = cache.get('foo'); 30 | 31 | assert.isTrue(value === 'bar'); 32 | }); 33 | 34 | it('should correctly clear() values previously set', function () { 35 | var cache = new SimpleCache({}); 36 | cache.set('foo', 'bar'); 37 | cache.clear(); 38 | var value = cache.get('foo'); 39 | 40 | assert.isTrue(value === null); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var ArrayList = require('./mocks/dw.util.Collection'); 4 | 5 | module.exports = function toProductMock(mock) { 6 | if (!mock || typeof mock === 'function' || mock instanceof ArrayList) { 7 | return mock; 8 | } 9 | 10 | var result = {}; 11 | if (typeof mock === 'object') { 12 | Object.keys(mock).forEach(function (item) { 13 | if (typeof mock[item] === 'object') { 14 | if (mock[item] && mock[item].type === 'function') { 15 | var innerMock = typeof mock[item].return !== 'undefined' 16 | ? toProductMock(mock[item].return) 17 | : toProductMock(mock[item]); 18 | result[item] = function () { return innerMock; }; 19 | } else { 20 | result[item] = toProductMock(mock[item]); 21 | } 22 | } else if (item !== 'function' || item !== 'return') { 23 | result[item] = mock[item]; 24 | } 25 | }); 26 | } else { 27 | result = mock; 28 | } 29 | 30 | return result; 31 | }; 32 | --------------------------------------------------------------------------------