├── .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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------