├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── deploy.yml │ └── nodejs.yml ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Procfile ├── README.md ├── babel.config.js ├── config ├── .gitignore ├── certs │ └── .gitkeep ├── custom-environment-variables.json ├── default.json ├── elastic.schema.attribute.extension.json ├── elastic.schema.attribute.json ├── elastic.schema.category.extension.json ├── elastic.schema.category.json ├── elastic.schema.cms_block.json ├── elastic.schema.cms_page.json ├── elastic.schema.index.extension.json ├── elastic.schema.index.json ├── elastic.schema.product.extension.json ├── elastic.schema.product.json ├── elastic.schema.taxrule.extension.json ├── elastic.schema.taxrule.json └── test.json ├── dev └── docker │ ├── Dockerfile │ └── vue-storefront-api.sh ├── doc ├── 1. Data schema and migrations.md ├── 2. graphQl support.md ├── 3. FAQ and Recipes.md ├── Vue-storefront-architecture-backend.xml ├── attribute-data-format.json ├── category-data-format.json ├── media │ └── Vue-storefront-architecture-backend.png ├── product-data-format.json ├── queries-examples.json └── taxrule-data-format.json ├── docker-compose.elastic7.yml ├── docker-compose.nodejs.yml ├── docker-compose.varnish.yml ├── docker-compose.yml ├── docker ├── elasticsearch │ ├── Dockerfile │ ├── config │ │ └── elasticsearch.yml │ └── data │ │ └── .gitkeep ├── elasticsearch7 │ ├── Dockerfile │ └── config │ │ └── elasticsearch.yml ├── kibana │ ├── Dockerfile │ └── config │ │ └── kibana.yml ├── varnish │ ├── Dockerfile │ ├── README.md │ ├── config.vcl │ └── docker-compose │ │ ├── docker-compose.nodejs.yml │ │ ├── docker-compose.varnish.yml │ │ └── docker-compose.yml └── vue-storefront-api │ ├── Dockerfile │ ├── default.env │ └── vue-storefront-api.sh ├── ecosystem.json ├── kubernetes ├── elasticsearch-deployment.yaml ├── elasticsearch-service.yaml ├── kibana-deployment.yaml ├── kibana-service.yaml ├── redis-deployment.yaml ├── redis-service.yaml ├── vue-storefront-api-configmap.yaml ├── vue-storefront-api-deployment.yaml └── vue-storefront-api-service.yaml ├── migrations ├── .common.js └── 1530101328854-local_es_config_fix.js ├── nodemon.json ├── package.json ├── scripts ├── cache.js ├── db.js ├── elastic.js ├── kue.js └── mage2vs.js ├── src ├── api │ ├── attribute │ │ └── service.ts │ ├── cart.ts │ ├── catalog.ts │ ├── extensions │ │ ├── cms-data │ │ │ ├── README.md │ │ │ └── index.js │ │ ├── elastic-stock │ │ │ └── index.js │ │ ├── example-custom-filter │ │ │ ├── filter │ │ │ │ └── catalog │ │ │ │ │ └── SampleFilter.ts │ │ │ └── index.ts │ │ ├── example-magento-api │ │ │ └── index.js │ │ ├── example-processor │ │ │ ├── index.js │ │ │ └── processors │ │ │ │ └── my-product-processor.js │ │ ├── mail-service │ │ │ └── index.js │ │ └── mailchimp-subscribe │ │ │ └── index.js │ ├── img.ts │ ├── index.ts │ ├── invalidate.ts │ ├── order.ts │ ├── product.ts │ ├── review.ts │ ├── stock.ts │ ├── sync.ts │ ├── url │ │ ├── index.ts │ │ └── map.ts │ └── user.ts ├── db.js ├── graphql │ ├── elasticsearch │ │ ├── attribute │ │ │ ├── resolver.js │ │ │ └── schema.graphqls │ │ ├── catalog │ │ │ ├── processor.js │ │ │ ├── resolver.js │ │ │ └── schema.graphqls │ │ ├── category │ │ │ ├── resolver.js │ │ │ └── schema.graphqls │ │ ├── client.js │ │ ├── cms │ │ │ ├── resolver.js │ │ │ └── schema.graphqls │ │ ├── json_type │ │ │ └── resolver.js │ │ ├── mapping.js │ │ ├── queryBuilder.ts │ │ ├── review │ │ │ ├── resolver.js │ │ │ └── schema.graphqls │ │ └── taxrule │ │ │ ├── resolver.js │ │ │ └── schema.graphqls │ ├── resolvers.js │ └── schema.js ├── helpers │ ├── loadAdditionalCertificates.ts │ ├── loadCustomFilters.ts │ └── priceTiers.js ├── image │ ├── action │ │ ├── abstract │ │ │ └── index.ts │ │ ├── factory.ts │ │ └── local │ │ │ └── index.ts │ └── cache │ │ ├── abstract │ │ └── index.ts │ │ ├── factory.ts │ │ ├── file │ │ └── index.ts │ │ └── google-cloud-storage │ │ └── index.ts ├── index.ts ├── lib │ ├── boost.js │ ├── cache-instance.js │ ├── countrymapper.js │ ├── elastic.js │ ├── image.js │ ├── redis.js │ ├── test │ │ └── unit │ │ │ └── boost.spec.ts │ └── util.js ├── middleware │ └── index.ts ├── models │ ├── .gitignore │ ├── catalog-category.md │ ├── catalog-product.md │ ├── order.md │ ├── order.schema.extension.json │ ├── order.schema.js │ ├── review.schema.json │ ├── userProfile.schema.extension.json │ ├── userProfile.schema.json │ ├── userProfileUpdate.schema.extension.json │ ├── userProfileUpdate.schema.json │ ├── userRegister.schema.extension.json │ └── userRegister.schema.json ├── platform │ ├── abstract │ │ ├── address.js │ │ ├── cart.js │ │ ├── contact.js │ │ ├── newsletter.js │ │ ├── order.js │ │ ├── product.js │ │ ├── review.js │ │ ├── stock.js │ │ ├── stock_alert.js │ │ ├── tax.js │ │ ├── user.js │ │ └── wishlist.js │ ├── factory.ts │ ├── magento1 │ │ ├── address.js │ │ ├── cart.js │ │ ├── contact.js │ │ ├── newsletter.js │ │ ├── order.js │ │ ├── stock.js │ │ ├── stock_alert.js │ │ ├── tax.js │ │ ├── user.js │ │ ├── util.js │ │ └── wishlist.js │ └── magento2 │ │ ├── cart.js │ │ ├── o2m.js │ │ ├── order.js │ │ ├── product.js │ │ ├── review.js │ │ ├── stock.js │ │ ├── tax.js │ │ ├── user.js │ │ └── util.js ├── processor │ ├── default.ts │ ├── factory.js │ └── product.js └── worker │ ├── log.js │ └── order_to_magento2.js ├── test └── unit │ └── jest.conf.js ├── tsconfig.json ├── var ├── catalog.json ├── catalog_attribute.json ├── catalog_category.json ├── catalog_cms_block.json ├── catalog_cms_page.json ├── catalog_de_attribute.json ├── catalog_de_category.json ├── catalog_de_cms_block.json ├── catalog_de_cms_page.json ├── catalog_de_product.json ├── catalog_de_review.json ├── catalog_de_taxrule.json ├── catalog_it_attribute.json ├── catalog_it_category.json ├── catalog_it_cms_block.json ├── catalog_it_cms_page.json ├── catalog_it_product.json ├── catalog_it_review.json ├── catalog_it_taxrule.json ├── catalog_product.json ├── catalog_review.json ├── catalog_taxrule.json ├── testOrderAnon.json ├── testOrderAuth.json └── testUser.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=vue-storefront-api 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { jest: true, node: true, es6: true}, 4 | parserOptions: { 5 | parser: '@typescript-eslint/parser', 6 | ecmaVersion: 2018, 7 | sourceType: 'module' 8 | }, 9 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 10 | extends: [ 11 | 'standard', 12 | 'plugin:@typescript-eslint/recommended' 13 | ], 14 | plugins: ['vue-storefront', '@typescript-eslint'], 15 | // add your custom rules here 16 | rules: { 17 | '@typescript-eslint/no-var-requires': 1, 18 | '@typescript-eslint/indent': ['error', 2], 19 | '@typescript-eslint/camelcase': 0, 20 | semi: 'off', 21 | '@typescript-eslint/semi': 0, 22 | '@typescript-eslint/member-delimiter-style': ['error', { 'multiline': { 'delimiter': 'comma', 'requireLast': false }, 'singleline': { 'delimiter': 'comma' } }], 23 | '@typescript-eslint/no-empty-interface': 1, 24 | '@typescript-eslint/no-use-before-define': 1, 25 | '@typescript-eslint/no-explicit-any': 0, 26 | '@typescript-eslint/class-name-casing': 1, 27 | '@typescript-eslint/no-unused-vars': 0, 28 | '@typescript-eslint/explicit-function-return-type': 0, 29 | '@typescript-eslint/no-var-requires': 0, 30 | 'handle-callback-err': 1, 31 | 'prefer-promise-reject-errors': 0, 32 | 'import/no-duplicates': ['warning'], 33 | // allow paren-less arrow functions 34 | 'arrow-parens': 0, 35 | 'prefer-arrow-callback': 1, 36 | // allow async-await 37 | 'generator-star-spacing': 0, 38 | // allow debugger during development 39 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 40 | 'no-restricted-imports': [2, { paths: ['lodash-es'] }], 41 | 'vue-storefront/no-corecomponent-import': 'error', 42 | 'vue-storefront/no-corecomponent': 'error', 43 | 'vue-storefront/no-corepage-import': 'error', 44 | 'vue-storefront/no-corepage': 'error', 45 | 'no-console': 0, 46 | 'no-unused-vars': 1 47 | }, 48 | overrides: [ 49 | { 50 | // @todo check if this is closed https://github.com/typescript-eslint/typescript-eslint/issues/342 51 | // This is an issue with interfaces so we need to wait until it fixed. 52 | files: ['core/**/*.ts'], 53 | rules: { 54 | 'no-undef': 1 55 | } 56 | } 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: yarn lint 21 | run: | 22 | yarn 23 | yarn lint 24 | env: 25 | CI: true 26 | - name: yarn build 27 | run: | 28 | yarn build 29 | env: 30 | CI: true 31 | 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | .vscode/ 6 | .idea/ 7 | api_start.sh 8 | api_test.sh 9 | config/local.json 10 | package-lock.json 11 | src/config.json 12 | config/certs/*.pem 13 | var/magento2-sample-data/ 14 | .migrate 15 | *.iml 16 | /docker/elasticsearch/data/ 17 | !/docker/elasticsearch/data/.gitkeep 18 | /docker/elasticsearch7/data/ 19 | !/docker/elasticsearch7/data/.gitkeep 20 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | REGISTRY_URL: "registry.storefrontcloud.io" 3 | REGISTRY_IMAGE: "registry.storefrontcloud.io/demo-storefrontcloud-io/vue-storefront-api" 4 | 5 | stages: 6 | - build 7 | - deploy 8 | 9 | build: 10 | stage: build 11 | image: 12 | name: gcr.io/kaniko-project/executor:debug 13 | entrypoint: [""] 14 | script: 15 | - cat config/local.json 16 | - echo "{\"auths\":{\"$REGISTRY_URL\":{\"username\":\"$REGISTRY_USER\",\"password\":\"$REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json 17 | - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dev/docker/Dockerfile --destination $REGISTRY_IMAGE:${CI_COMMIT_SHA:0:8} 18 | only: 19 | - develop 20 | - master 21 | 22 | deploy-to-prod: 23 | stage: deploy 24 | image: alpine 25 | script: 26 | - apk add --no-cache curl 27 | - > 28 | curl -H "Content-Type: application/json" -X POST -d "{\"code\":\"test\", \"apiContainerVersion\":\"${CI_COMMIT_SHA:0:8}\"}" http://10.29.1.1:4000/instances 29 | environment: 30 | name: staging 31 | url: https://test.storefrontcloud.io/ 32 | only: 33 | - master 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: node_js 3 | 4 | cache: 5 | yarn: true 6 | directories: 7 | - node_modules 8 | 9 | install: 10 | - yarn 11 | 12 | jobs: 13 | include: 14 | - &build 15 | stage: Build 16 | script: 17 | - yarn lint 18 | - yarn build 19 | node_js: '10' 20 | 21 | - &unit 22 | stage: Test 23 | script: yarn test:unit 24 | name: "NodeJS 10 unit tests" 25 | node_js: "10" 26 | - <<: *build 27 | node_js: '12' 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Divante Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', { 5 | targets: { 6 | node: "10" 7 | } 8 | } 9 | ] 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | *.extension.json -------------------------------------------------------------------------------- /config/certs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront/vue-storefront-api/69135117ab8d342adeee811d5c2a914c89efe888/config/certs/.gitkeep -------------------------------------------------------------------------------- /config/custom-environment-variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "server": { 3 | "host": "BIND_HOST", 4 | "port": "BIND_PORT" 5 | }, 6 | "elasticsearch": { 7 | "host": "ELASTICSEARCH_HOST", 8 | "port": "ELASTICSEARCH_PORT" 9 | }, 10 | "redis": { 11 | "host": "REDIS_HOST", 12 | "port": "REDIS_PORT", 13 | "db": "REDIS_DB", 14 | "auth": "REDIS_AUTH" 15 | } 16 | } -------------------------------------------------------------------------------- /config/elastic.schema.attribute.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /config/elastic.schema.attribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "long" 5 | }, 6 | "attribute_id": { 7 | "type": "long" 8 | }, 9 | "options": { 10 | "properties": { 11 | "value": { 12 | "type": "text" 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /config/elastic.schema.category.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /config/elastic.schema.category.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "url_key": {"type": "keyword"}, 4 | "url_path": {"type": "keyword"}, 5 | "slug": {"type": "keyword"}, 6 | "is_active": {"type": "boolean"}, 7 | "product_count": {"type": "integer"}, 8 | "parent_id": {"type": "integer"}, 9 | "position": {"type": "integer"}, 10 | "created_at": { 11 | "type": "date", 12 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 13 | }, 14 | "updated_at": { 15 | "type": "date", 16 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/elastic.schema.cms_block.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "long" 5 | }, 6 | "identifier": { 7 | "type": "keyword" 8 | }, 9 | "creation_time": { 10 | "type": "date", 11 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 12 | }, 13 | "update_time": { 14 | "type": "date", 15 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /config/elastic.schema.cms_page.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "long" 5 | }, 6 | "identifier": { 7 | "type": "keyword" 8 | }, 9 | "creation_time": { 10 | "type": "date", 11 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 12 | }, 13 | "update_time": { 14 | "type": "date", 15 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /config/elastic.schema.index.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /config/elastic.schema.index.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "analysis": { 4 | "tokenizer": { 5 | "comma": { 6 | "type": "pattern", 7 | "pattern" : "," 8 | } 9 | }, 10 | "analyzer": { 11 | "comma": { 12 | "type": "custom", 13 | "tokenizer": "comma" 14 | } 15 | } 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /config/elastic.schema.product.extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "position": {"type": "integer"}, 4 | "tax_class_id": {"type": "integer"}, 5 | "required_options": {"type": "integer"}, 6 | "has_options": {"type": "integer"} , 7 | "Size_options": {"type": "keyword"}, 8 | "Color_options": {"type": "keyword"} 9 | } 10 | } -------------------------------------------------------------------------------- /config/elastic.schema.product.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "sku": {"type": "keyword"}, 4 | "url_key": {"type": "keyword"}, 5 | "url_path": {"type": "keyword"}, 6 | "slug": {"type": "keyword"}, 7 | "size": {"type": "integer"}, 8 | "size_options": {"type": "integer"}, 9 | "price": {"type": "float"}, 10 | "has_options": {"type": "keyword"}, 11 | "special_price": {"type": "float"}, 12 | "color": {"type": "integer"}, 13 | "color_options": {"type": "integer"}, 14 | "pattern": {"type": "text"}, 15 | "id": {"type": "long"}, 16 | "status": {"type": "integer"}, 17 | "weight": {"type": "integer"}, 18 | "visibility": {"type": "integer"}, 19 | "created_at": { 20 | "type": "date", 21 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 22 | }, 23 | "updated_at": { 24 | "type": "date", 25 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 26 | }, 27 | "special_from_date": { 28 | "type": "date", 29 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 30 | }, 31 | "special_to_date": { 32 | "type": "date", 33 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 34 | }, 35 | "news_from_date": { 36 | "type": "date", 37 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 38 | }, 39 | "news_to_date": { 40 | "type": "date", 41 | "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis" 42 | }, 43 | "description": {"type": "text"}, 44 | "name": {"type": "text"}, 45 | "configurable_children": { 46 | "properties": { 47 | "url_key": {"type": "keyword"}, 48 | "sku": {"type": "keyword"}, 49 | "has_options": {"type": "keyword"}, 50 | "price": {"type": "float"}, 51 | "special_price": {"type": "float"} 52 | } 53 | }, 54 | "configurable_options": { 55 | "properties": { 56 | "attribute_id": {"type": "long"}, 57 | "default_label": {"type": "text"}, 58 | "label": {"type": "text"}, 59 | "frontend_label": {"type": "text"}, 60 | "store_label": {"type": "text"}, 61 | "values": { 62 | "properties": { 63 | "default_label": {"type": "text"}, 64 | "label": {"type": "text"}, 65 | "frontend_label": {"type": "text"}, 66 | "store_label": {"type": "text"}, 67 | "value_index": {"type": "keyword"} 68 | } 69 | } 70 | } 71 | }, 72 | "category_ids": {"type": "long"}, 73 | "eco_collection": {"type": "integer"}, 74 | "eco_collection_options": {"type": "integer"}, 75 | "erin_recommends": {"type": "integer"}, 76 | "tax_class_id": {"type": "integer"}, 77 | "gender": {"type": "integer"}, 78 | "material": {"type": "integer"}, 79 | "category_gear": {"type": "integer"}, 80 | "attributes_metadata": { 81 | "properties": { 82 | "id": {"type": "integer"}, 83 | "attribute_id": {"type": "integer"}, 84 | "default_frontend_label": {"type": "text"}, 85 | "is_visible_on_front": {"type": "text"}, 86 | "is_visible" : {"type": "boolean"}, 87 | "frontend_input": {"type": "text"}, 88 | "is_user_defined": {"type": "boolean"}, 89 | "is_comparable": {"type": "text"}, 90 | "attribute_code": {"type": "text"}, 91 | "options": { 92 | "properties": { 93 | "value": {"type": "text"}, 94 | "label": {"type": "text"} 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /config/elastic.schema.taxrule.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /config/elastic.schema.taxrule.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "long" 5 | }, 6 | "rates": { 7 | "properties": { 8 | "rate": { 9 | "type": "float" 10 | } 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /dev/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10 2 | 3 | ENV BIND_HOST=0.0.0.0 ELASTICSEARCH_HOST=elasticsearch ELASTICSEARCH_PORT=9200 REDIS_HOST=redis REDIS_PORT=6379 REDIS_DB=0 PM2_ARGS=--no-daemon VS_ENV=prod 4 | 5 | WORKDIR /var/www 6 | 7 | COPY . . 8 | 9 | RUN apt update && apt install -y git \ 10 | && yarn install 11 | 12 | COPY dev/docker/vue-storefront-api.sh /usr/local/bin/ 13 | 14 | ENTRYPOINT ["vue-storefront-api.sh"] 15 | -------------------------------------------------------------------------------- /dev/docker/vue-storefront-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ "$VS_ENV" = 'dev' ]; then 5 | yarn dev 6 | else 7 | yarn start 8 | fi 9 | -------------------------------------------------------------------------------- /doc/1. Data schema and migrations.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Vue storefront uses ElasticSearch as a primary data store. We're using Redis as cache layer and Kue for queue processing. 4 | Although all of these data stores are basicaly schema-free some mappings and meta data should be used for setting ES indices and so forth. 5 | 6 | Vue storefront uses data migration mechanism based on https://github.com/tj/node-migrate. 7 | 8 | ## Migration tool 9 | 10 | We're using node-migrate wich is pre-configured with npm. So we're using the following alias: 11 | 12 | ``` 13 | npm run migrate 14 | ``` 15 | 16 | which runs the migrations against `migrations` folder. 17 | 18 | ## How to add new migration? 19 | 20 | You can add new migration adding file to `migrations` directory - which is not recommended OR using cmdline tool: 21 | 22 | ``` 23 | npm run migrate create name-of-my-migration 24 | ``` 25 | 26 | the tool automaticaly generates the file under `migrations` folder. 27 | 28 | 29 | ## Examples 30 | 31 | The example migrations shows how to manipulate on products and mappings. Let's take a look at the mapping modification: 32 | 33 | ```js 34 | 35 | 36 | // Migration scripts use: https://github.com/tj/node-migrate 37 | 'use strict' 38 | 39 | let config = require('config') 40 | let common = require('./.common') 41 | 42 | 43 | module.exports.up = function (next) { 44 | 45 | 46 | // example of adding a field to the schema 47 | // other examples: https://stackoverflow.com/questions/22325708/elasticsearch-create-index-with-mappings-using-javascript, 48 | common.db.indices.putMapping({ 49 | index: config.elasticsearch.indices[0], 50 | type: "product", 51 | body: { 52 | properties: { 53 | slug: { type: "string" }, // add slug field 54 | suggest: { 55 | type: "completion", 56 | analyzer: "simple", 57 | search_analyzer: "simple" 58 | } 59 | } 60 | } 61 | }).then((res) => { 62 | 63 | console.dir(res, {depth: null, colors: true}) 64 | next() 65 | }) 66 | 67 | } 68 | 69 | module.exports.down = function (next) { 70 | next() 71 | } 72 | ``` 73 | 74 | ... and that's all :) 75 | -------------------------------------------------------------------------------- /doc/2. graphQl support.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Vue storefront now supports using graphQl as an alternative API endpoint to get data for products, categories, products, and taxerules. 4 | For now Graphql uses resolver what fetch data from Elasticsearch. But potentialy can have different resolvers working with different Search Engines / 3rd Party APIs 5 | 6 | ## GraphQl Schema 7 | 8 | Graphql request to this API have to match query schema defined 9 | For product it is 10 | 11 | ```graphql 12 | type Query { 13 | products ( 14 | search: String @doc(description: "Performs a full-text search using the specified key words."), 15 | filter: ProductFilterInput @doc(description: "Identifies which product attributes to search for and return."), 16 | pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), 17 | currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), 18 | sort: ProductSortInput @doc(description: "Specifies which attribute to sort on, and whether to return the results in ascending or descending order.") 19 | ): Products 20 | } 21 | ``` 22 | 23 | which has result of type Products 24 | 25 | ```graphql 26 | 27 | type Products @doc(description: "The Products object is the top-level object returned in a product search") { 28 | items: JSON @doc(description: "An array of products that match the specified search criteria") 29 | page_info: SearchResultPageInfo @doc(description: "An object that includes the page_info and currentPage values specified in the query") 30 | total_count: Int @doc(description: "The number of products returned") 31 | aggregations: JSON @doc(description: "Layered navigation filters array as aggregations") 32 | sort_fields: SortFields @doc(description: "An object that includes the default sort field and all available sort fields") 33 | } 34 | ``` 35 | 36 | and uses defined resolver 37 | ```js 38 | const resolver = { 39 | Query: { 40 | products: (_, { search, filter, sort, currentPage, pageSize }, context, rootValue) => 41 | list(filter, sort, currentPage, pageSize, search, context, rootValue) 42 | } 43 | } 44 | ``` 45 | 46 | For other entity types you can check schemas and resolvers in the /src/graphql/elasticsearch correspond subfolder 47 | 48 | ## GraphQl server host and test tool 49 | 50 | graphql server host is: 51 | 52 | :/graphql 53 | 54 | So by default it is http://localhost:8080/graphql 55 | 56 | Also for testing graphql requests Graphiql web tool can be used. It can be accesible by url: 57 | 58 | :/graphiql 59 | 60 | This tool allow to test graphql request online and show graphql server response immediatelly nad could be helpful fro development process. 61 | 62 | 63 | ## Example request 64 | 65 | Below is an example request for product 66 | 67 | ```graphql 68 | { 69 | products(search: "bag", filter: { 70 | status: { 71 | in: [0, 1], scope: "default" 72 | }, 73 | stock: { 74 | is_in_stock: {eq: true, scope: "default"} 75 | }, 76 | visibility: { 77 | in: [3, 4], scope: "default"} 78 | }, 79 | sort: { 80 | updated_at: DESC 81 | } 82 | ) { 83 | items 84 | total_count 85 | aggregations 86 | sort_fields { 87 | options { 88 | value 89 | } 90 | } 91 | page_info { 92 | page_size 93 | current_page 94 | } 95 | } 96 | } 97 | 98 | 99 | ``` 100 | 101 | To see all available product filter options please check ProductFilterInput type in the graphQl product schema 102 | 103 | 104 | -------------------------------------------------------------------------------- /doc/3. FAQ and Recipes.md: -------------------------------------------------------------------------------- 1 | # FAQ and Recipes 2 | 3 | Below, you can find solutions for the most common problems and advice for typical config changes required by Vue Storefront API. If you solved any new issues by yourself, please let us know on [Slack](http://vuestorefront.slack.com) or [Forum](https://forum.vuestorefront.io/) and we will add them to the list so others don't need to reinvent the wheel. 4 | 5 | ## Problem starting Vue Storefront API on Windows Docker 6 | 7 | In case you can't get vue storefront api (app container) to work on Windows and in logs you can see: 8 | 9 | ``` 10 | app_1 | standard_init_linux.go:207: exec user process caused "no such file or directory" 11 | ``` 12 | 13 | then your end of line character is probably set to native Windows crlf. To change that you need to run: 14 | 15 | ``` 16 | git config core.autocrlf false 17 | git config core.eol lf 18 | git reset --hard 19 | docker-compose -f docker-compose.yml -f docker-compose.nodejs.yml up --build 20 | ``` -------------------------------------------------------------------------------- /doc/attribute-data-format.json: -------------------------------------------------------------------------------- 1 | { 2 | "_index": "vue_storefront_catalog1510349212167", 3 | "_type": "attribute", 4 | "_id": "79", 5 | "_score": 1, 6 | "_source": { 7 | "is_wysiwyg_enabled": false, 8 | "is_html_allowed_on_front": false, 9 | "used_for_sort_by": false, 10 | "is_filterable": false, 11 | "is_filterable_in_search": false, 12 | "is_used_in_grid": true, 13 | "is_visible_in_grid": false, 14 | "is_filterable_in_grid": false, 15 | "position": 0, 16 | "apply_to": [ 17 | "simple", 18 | "virtual", 19 | "bundle", 20 | "downloadable", 21 | "configurable" 22 | ], 23 | "is_searchable": "0", 24 | "is_visible_in_advanced_search": "0", 25 | "is_comparable": "0", 26 | "is_used_for_promo_rules": "0", 27 | "is_visible_on_front": "0", 28 | "used_in_product_listing": "1", 29 | "is_visible": true, 30 | "scope": "website", 31 | "attribute_id": 79, 32 | "attribute_code": "special_from_date", 33 | "frontend_input": "date", 34 | "entity_type_id": "4", 35 | "is_required": false, 36 | "options": [], 37 | "is_user_defined": false, 38 | "default_frontend_label": "Special Price From Date", 39 | "frontend_labels": null, 40 | "backend_type": "datetime", 41 | "backend_model": "Magento\\Catalog\\Model\\Attribute\\Backend\\Startdate", 42 | "is_unique": "0", 43 | "validation_rules": [], 44 | "id": 79, 45 | "tsk": 1510353353440 46 | } 47 | } -------------------------------------------------------------------------------- /doc/category-data-format.json: -------------------------------------------------------------------------------- 1 | { 2 | "_index": "vue_storefront_catalog1510349212167", 3 | "_type": "category", 4 | "_id": "22", 5 | "_score": 1, 6 | "_source": { 7 | "id": 22, 8 | "parent_id": 20, 9 | "name": "Bottoms", 10 | "is_active": true, 11 | "position": 2, 12 | "level": 3, 13 | "product_count": 0, 14 | "children_data": [ 15 | { 16 | "is_active": true, 17 | "level": 4, 18 | "parent_id": 22, 19 | "product_count": 91, 20 | "name": "Pants", 21 | "id": 27, 22 | "position": 1, 23 | "children_data": [] 24 | }, 25 | { 26 | "is_active": true, 27 | "level": 4, 28 | "parent_id": 22, 29 | "product_count": 137, 30 | "name": "Shorts", 31 | "id": 28, 32 | "position": 2, 33 | "children_data": [] 34 | } 35 | ], 36 | "tsk": 1509551138285 37 | } 38 | } -------------------------------------------------------------------------------- /doc/media/Vue-storefront-architecture-backend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront/vue-storefront-api/69135117ab8d342adeee811d5c2a914c89efe888/doc/media/Vue-storefront-architecture-backend.png -------------------------------------------------------------------------------- /doc/taxrule-data-format.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2, 3 | "code": "Poland", 4 | "priority": 0, 5 | "position": 0, 6 | "customer_tax_class_ids": [ 7 | 3 8 | ], 9 | "product_tax_class_ids": [ 10 | 2 11 | ], 12 | "tax_rate_ids": [ 13 | 4 14 | ], 15 | "calculate_subtotal": false, 16 | "rates": [ 17 | { 18 | "id": 4, 19 | "tax_country_id": "PL", 20 | "tax_region_id": 0, 21 | "tax_postcode": "*", 22 | "rate": 23, 23 | "code": "VAT23%", 24 | "titles": [] 25 | } 26 | ], 27 | "tsk": 1510603185144 28 | } -------------------------------------------------------------------------------- /docker-compose.elastic7.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | es7: 4 | container_name: es7 5 | build: docker/elasticsearch7/ 6 | ulimits: 7 | memlock: 8 | soft: -1 9 | hard: -1 10 | volumes: 11 | - ./docker/elasticsearch7/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro 12 | ports: 13 | - '9200:9200' 14 | - '9300:9300' 15 | environment: 16 | - discovery.type=single-node 17 | - cluster.name=docker-cluster 18 | - bootstrap.memory_lock=true 19 | - "ES_JAVA_OPTS=-Xmx512m -Xms512m" 20 | 21 | redis: 22 | image: 'redis:4-alpine' 23 | ports: 24 | - '6379:6379' 25 | -------------------------------------------------------------------------------- /docker-compose.nodejs.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | app: 4 | # image: divante/vue-storefront-api:latest 5 | build: 6 | context: . 7 | dockerfile: docker/vue-storefront-api/Dockerfile 8 | depends_on: 9 | - es1 10 | - redis 11 | env_file: docker/vue-storefront-api/default.env 12 | environment: 13 | VS_ENV: dev 14 | volumes: 15 | - './config:/var/www/config' 16 | - './ecosystem.json:/var/www/ecosystem.json' 17 | - './migrations:/var/www/migrations' 18 | - './package.json:/var/www/package.json' 19 | - './babel.config.js:/var/www/babel.config.js' 20 | - './tsconfig.json:/var/www/tsconfig.json' 21 | - './nodemon.json:/var/www/nodemon.json' 22 | - './scripts:/var/www/scripts' 23 | - './src:/var/www/src' 24 | - './var:/var/www/var' 25 | tmpfs: 26 | - /var/www/dist 27 | ports: 28 | - '8080:8080' 29 | -------------------------------------------------------------------------------- /docker-compose.varnish.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | varnish: 4 | build: 5 | context: . 6 | dockerfile: varnish/Dockerfile 7 | volumes: 8 | - ./docker/varnish/config.vcl:/usr/local/etc/varnish/default.vcl 9 | ports: 10 | - '1234:80' 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | es1: 4 | container_name: elasticsearch 5 | build: docker/elasticsearch/ 6 | ulimits: 7 | memlock: 8 | soft: -1 9 | hard: -1 10 | volumes: 11 | - ./docker/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro 12 | ports: 13 | - '9200:9200' 14 | - '9300:9300' 15 | environment: 16 | - discovery.type=single-node 17 | - cluster.name=docker-cluster 18 | - bootstrap.memory_lock=true 19 | - "ES_JAVA_OPTS=-Xmx512m -Xms512m" 20 | 21 | kibana: 22 | build: docker/kibana/ 23 | volumes: 24 | - ./docker/kibana/config/:/usr/share/kibana/config:ro 25 | ports: 26 | - '5601:5601' 27 | depends_on: 28 | - es1 29 | 30 | redis: 31 | image: 'redis:4-alpine' 32 | ports: 33 | - '6379:6379' 34 | 35 | volumes: 36 | esdat1: 37 | -------------------------------------------------------------------------------- /docker/elasticsearch/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/elasticsearch/elasticsearch:5.6.11 2 | 3 | RUN bin/elasticsearch-plugin remove x-pack --purge 4 | 5 | # Add your elasticsearch plugins setup here 6 | # Example: RUN elasticsearch-plugin install analysis-icu 7 | -------------------------------------------------------------------------------- /docker/elasticsearch/config/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ## Default Elasticsearch configuration from elasticsearch-docker. 3 | ## from https://github.com/elastic/elasticsearch-docker/blob/master/build/elasticsearch/elasticsearch.yml 4 | # 5 | cluster.name: "docker-cluster" 6 | network.host: 0.0.0.0 7 | 8 | # minimum_master_nodes need to be explicitly set when bound on a public IP 9 | # set to 1 to allow single node clusters 10 | # Details: https://github.com/elastic/elasticsearch/pull/17288 11 | discovery.zen.minimum_master_nodes: 1 12 | 13 | ## Use single node discovery in order to disable production mode and avoid bootstrap checks 14 | ## see https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html 15 | # 16 | discovery.type: single-node 17 | -------------------------------------------------------------------------------- /docker/elasticsearch/data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront/vue-storefront-api/69135117ab8d342adeee811d5c2a914c89efe888/docker/elasticsearch/data/.gitkeep -------------------------------------------------------------------------------- /docker/elasticsearch7/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/elasticsearch/elasticsearch:7.3.2 2 | 3 | -------------------------------------------------------------------------------- /docker/elasticsearch7/config/elasticsearch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ## Default Elasticsearch configuration from elasticsearch-docker. 3 | ## from https://github.com/elastic/elasticsearch-docker/blob/master/build/elasticsearch/elasticsearch.yml 4 | # 5 | cluster.name: "docker-cluster" 6 | network.host: 0.0.0.0 7 | 8 | # minimum_master_nodes need to be explicitly set when bound on a public IP 9 | # set to 1 to allow single node clusters 10 | # Details: https://github.com/elastic/elasticsearch/pull/17288 11 | discovery.zen.minimum_master_nodes: 1 12 | 13 | ## Use single node discovery in order to disable production mode and avoid bootstrap checks 14 | ## see https://www.elastic.co/guide/en/elasticsearch/reference/current/bootstrap-checks.html 15 | # 16 | discovery.type: single-node 17 | -------------------------------------------------------------------------------- /docker/kibana/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/kibana/kibana:5.6.11 2 | 3 | RUN bin/kibana-plugin remove x-pack && \ 4 | kibana 2>&1 | grep -m 1 "Optimization of .* complete" 5 | 6 | # Add your kibana plugins setup here 7 | # Example: RUN kibana-plugin install 8 | -------------------------------------------------------------------------------- /docker/kibana/config/kibana.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | server.name: kibana 4 | server.host: "0.0.0.0" 5 | elasticsearch.url: http://elasticsearch:9200 6 | -------------------------------------------------------------------------------- /docker/varnish/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM cooptilleuls/varnish:6.0-stretch 2 | 3 | # install varnish-modules 4 | RUN apt-get update -y && \ 5 | apt-get install -y build-essential automake libtool curl git python-docutils && \ 6 | curl -s https://packagecloud.io/install/repositories/varnishcache/varnish60/script.deb.sh | bash; 7 | 8 | RUN apt-get install -y pkg-config libvarnishapi1 libvarnishapi-dev autotools-dev; 9 | 10 | RUN git clone https://github.com/varnish/varnish-modules.git /tmp/vm; 11 | RUN cd /tmp/vm; \ 12 | git checkout 6.0; \ 13 | ./bootstrap && \ 14 | ./configure; 15 | 16 | RUN cd /tmp/vm && \ 17 | make && \ 18 | make check && \ 19 | make install; -------------------------------------------------------------------------------- /docker/varnish/config.vcl: -------------------------------------------------------------------------------- 1 | 2 | 3 | vcl 4.0; 4 | 5 | import std; 6 | import bodyaccess; 7 | 8 | acl purge { 9 | "app"; // IP which can BAN cache - it should be VSF-API's IP 10 | } 11 | 12 | 13 | backend default { 14 | .host = "app"; 15 | .port = "8080"; 16 | } 17 | 18 | sub vcl_recv { 19 | unset req.http.X-Body-Len; 20 | # Only allow BAN requests from IP addresses in the 'purge' ACL. 21 | if (req.method == "BAN") { 22 | # Same ACL check as above: 23 | if (!client.ip ~ purge) { 24 | return (synth(403, "Not allowed.")); 25 | } 26 | 27 | # Logic for the ban, using the X-Cache-Tags header. 28 | if (req.http.X-VS-Cache-Tag) { 29 | ban("obj.http.X-VS-Cache-Tag ~ " + req.http.X-VS-Cache-Tag); 30 | } 31 | if (req.http.X-VS-Cache-Ext) { 32 | ban("req.url ~ " + req.http.X-VS-Cache-Ext); 33 | } 34 | if (!req.http.X-VS-Cache-Tag && !req.http.X-VS-Cache-Ext) { 35 | return (synth(403, "X-VS-Cache-Tag or X-VS-Cache-Ext header missing.")); 36 | } 37 | 38 | # Throw a synthetic page so the request won't go to the backend. 39 | return (synth(200, "Ban added.")); 40 | } 41 | 42 | if (req.url ~ "^\/api\/catalog\/") { 43 | if (req.method == "POST") { 44 | # It will allow me to cache by req body in the vcl_hash 45 | std.cache_req_body(500KB); 46 | set req.http.X-Body-Len = bodyaccess.len_req_body(); 47 | } 48 | 49 | if ((req.method == "POST" || req.method == "GET")) { 50 | return (hash); 51 | } 52 | } 53 | 54 | if (req.url ~ "^\/api\/ext\/") { 55 | if (req.method == "GET") { 56 | # Custom packs GET - M2 - /jimmylion/pack/${req.params.packId} 57 | if (req.url ~ "^\/api\/ext\/custom-packs\/") { 58 | return (hash); 59 | } 60 | 61 | # Countries for storecode GET - M2 - /directory/countries 62 | if (req.url ~ "^\/api\/ext\/directory\/") { 63 | return (hash); 64 | } 65 | 66 | # Menus GET - M2 - /menus & /nodes 67 | if (req.url ~ "^\/api\/ext\/menus\/") { 68 | return (hash); 69 | } 70 | } 71 | } 72 | 73 | if (req.url ~ "^\/api\/stock\/") { 74 | if (req.method == "GET") { 75 | # M2 Stock 76 | return (hash); 77 | } 78 | } 79 | 80 | return (pipe); 81 | 82 | } 83 | 84 | sub vcl_hash { 85 | # To cache POST and PUT requests 86 | if (req.http.X-Body-Len) { 87 | bodyaccess.hash_req_body(); 88 | } else { 89 | hash_data(""); 90 | } 91 | } 92 | 93 | sub vcl_backend_fetch { 94 | if (bereq.http.X-Body-Len) { 95 | set bereq.method = "POST"; 96 | } 97 | } 98 | 99 | sub vcl_backend_response { 100 | # Set ban-lurker friendly custom headers. 101 | if (beresp.http.X-VS-Cache && beresp.http.X-VS-Cache ~ "Miss") { 102 | set beresp.ttl = 0s; 103 | } 104 | if (bereq.url ~ "^\/api\/stock\/") { 105 | set beresp.ttl = 900s; // 15 minutes 106 | } 107 | set beresp.http.X-Url = bereq.url; 108 | set beresp.http.X-Host = bereq.http.host; 109 | } 110 | 111 | sub vcl_deliver { 112 | if (obj.hits > 0) { 113 | set resp.http.X-Cache = "HIT_1"; 114 | set resp.http.X-Cache-Hits = obj.hits; 115 | } else { 116 | set resp.http.X-Cache = "MISS_1"; 117 | } 118 | set resp.http.X-Cache-Expires = resp.http.Expires; 119 | unset resp.http.X-Varnish; 120 | unset resp.http.Via; 121 | unset resp.http.Age; 122 | unset resp.http.X-Purge-URL; 123 | unset resp.http.X-Purge-Host; 124 | # Remove ban-lurker friendly custom headers when delivering to client. 125 | unset resp.http.X-Url; 126 | unset resp.http.X-Host; 127 | # Comment these for easier Drupal cache tag debugging in development. 128 | unset resp.http.X-Cache-Tags; 129 | unset resp.http.X-Cache-Contexts; 130 | } -------------------------------------------------------------------------------- /docker/varnish/docker-compose/docker-compose.nodejs.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | app: 4 | # image: divante/vue-storefront-api:latest 5 | build: 6 | context: . 7 | dockerfile: docker/vue-storefront-api/Dockerfile 8 | depends_on: 9 | - es1 10 | - redis 11 | env_file: docker/vue-storefront-api/default.env 12 | environment: 13 | VS_ENV: dev 14 | volumes: 15 | - './config:/var/www/config' 16 | - './ecosystem.json:/var/www/ecosystem.json' 17 | - './migrations:/var/www/migrations' 18 | - './package.json:/var/www/package.json' 19 | - './babel.config.js:/var/www/babel.config.js' 20 | - './tsconfig.json:/var/www/tsconfig.json' 21 | - './nodemon.json:/var/www/nodemon.json' 22 | - './scripts:/var/www/scripts' 23 | - './src:/var/www/src' 24 | - './var:/var/www/var' 25 | tmpfs: 26 | - /var/www/dist 27 | ports: 28 | - '8080:8080' 29 | networks: 30 | - some-net 31 | 32 | networks: 33 | some-net: 34 | driver: bridge -------------------------------------------------------------------------------- /docker/varnish/docker-compose/docker-compose.varnish.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | varnish: 4 | build: 5 | context: . 6 | dockerfile: varnish/Dockerfile 7 | volumes: 8 | - ./docker/varnish/config.vcl:/usr/local/etc/varnish/default.vcl 9 | ports: 10 | - '1234:80' 11 | networks: 12 | - vuestorefrontapi_some-net 13 | 14 | networks: 15 | vuestorefrontapi_some-net: 16 | external: true -------------------------------------------------------------------------------- /docker/varnish/docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | es1: 4 | container_name: elasticsearch 5 | build: docker/elasticsearch/ 6 | ulimits: 7 | memlock: 8 | soft: -1 9 | hard: -1 10 | volumes: 11 | - ./docker/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml:ro 12 | ports: 13 | - '9200:9200' 14 | - '9300:9300' 15 | environment: 16 | - discovery.type=single-node 17 | - cluster.name=docker-cluster 18 | - bootstrap.memory_lock=true 19 | - "ES_JAVA_OPTS=-Xmx512m -Xms512m" 20 | networks: 21 | - vuestorefrontapi_some-net 22 | 23 | kibana: 24 | build: docker/kibana/ 25 | volumes: 26 | - ./docker/kibana/config/:/usr/share/kibana/config:ro 27 | ports: 28 | - '5601:5601' 29 | depends_on: 30 | - es1 31 | networks: 32 | - vuestorefrontapi_some-net 33 | 34 | redis: 35 | image: 'redis:4-alpine' 36 | ports: 37 | - '6379:6379' 38 | networks: 39 | - vuestorefrontapi_some-net 40 | 41 | volumes: 42 | esdat1: 43 | 44 | networks: 45 | vuestorefrontapi_some-net: 46 | external: true -------------------------------------------------------------------------------- /docker/vue-storefront-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | 3 | ENV VS_ENV prod 4 | 5 | WORKDIR /var/www 6 | 7 | RUN apk add --no-cache curl git 8 | 9 | COPY package.json ./ 10 | COPY yarn.lock ./ 11 | 12 | RUN apk add --no-cache --virtual .build-deps ca-certificates wget python make g++ && \ 13 | yarn install --no-cache && \ 14 | apk del .build-deps 15 | 16 | COPY docker/vue-storefront-api/vue-storefront-api.sh /usr/local/bin/ 17 | 18 | CMD ["vue-storefront-api.sh"] 19 | -------------------------------------------------------------------------------- /docker/vue-storefront-api/default.env: -------------------------------------------------------------------------------- 1 | BIND_HOST=0.0.0.0 2 | ELASTICSEARCH_HOST=elasticsearch 3 | ELASTICSEARCH_PORT=9200 4 | REDIS_HOST=redis 5 | VS_ENV=prod 6 | PM2_ARGS=--no-daemon 7 | -------------------------------------------------------------------------------- /docker/vue-storefront-api/vue-storefront-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | yarn install || exit $? 5 | 6 | if [ "$VS_ENV" = 'dev' ]; then 7 | yarn dev 8 | else 9 | yarn start 10 | fi 11 | -------------------------------------------------------------------------------- /ecosystem.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "api", 5 | "script": "./dist/src/index.js", 6 | "log_date_format": "YYYY-MM-DD HH:mm:ss", 7 | "ignore_watch": ["node_modules"] 8 | }, 9 | { 10 | "name": "o2m", 11 | "script": "./dist/src/worker/order_to_magento2.js", 12 | "args": "start", 13 | "log_date_format": "YYYY-MM-DD HH:mm:ss", 14 | "ignore_watch": ["node_modules"] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /kubernetes/elasticsearch-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: elasticsearch 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: elasticsearch 9 | template: 10 | metadata: 11 | labels: 12 | app: elasticsearch 13 | spec: 14 | containers: 15 | - name: elasticsearch 16 | image: divante/vue-storefront-elasticsearch:5.6.9 17 | env: 18 | - name: ES_JAVA_OPTS 19 | value: -Xms512m -Xmx512m 20 | - name: bootstrap.memory_lock 21 | value: "true" 22 | - name: discovery.type 23 | value: single-node 24 | securityContext: 25 | privileged: false 26 | capabilities: 27 | add: 28 | - IPC_LOCK 29 | - SYS_RESOURCE 30 | ports: 31 | - containerPort: 9200 32 | volumeMounts: 33 | - mountPath: /usr/share/elasticsearch/data 34 | name: data 35 | volumes: 36 | - name: data 37 | emptyDir: {} 38 | -------------------------------------------------------------------------------- /kubernetes/elasticsearch-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: elasticsearch 5 | labels: 6 | app: elasticsearch 7 | spec: 8 | ports: 9 | - port: 9200 10 | targetPort: 9200 11 | selector: 12 | app: elasticsearch 13 | -------------------------------------------------------------------------------- /kubernetes/kibana-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: kibana 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: kibana 9 | template: 10 | metadata: 11 | labels: 12 | app: kibana 13 | spec: 14 | containers: 15 | - name: kibana 16 | image: divante/vue-storefront-kibana:5.6.9 17 | env: 18 | - name: ELASTICSEARCH_URL 19 | value: '"http://elasticsearch:9200"' 20 | ports: 21 | - containerPort: 5601 22 | -------------------------------------------------------------------------------- /kubernetes/kibana-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: kibana 5 | labels: 6 | app: kibana 7 | spec: 8 | selector: 9 | app: kibana 10 | ports: 11 | - port: 5601 12 | targetPort: 5601 13 | -------------------------------------------------------------------------------- /kubernetes/redis-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: redis 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: redis 9 | template: 10 | metadata: 11 | labels: 12 | app: redis 13 | spec: 14 | containers: 15 | - name: redis 16 | image: redis:4-alpine 17 | ports: 18 | - containerPort: 6379 19 | -------------------------------------------------------------------------------- /kubernetes/redis-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: redis 5 | labels: 6 | app: redis 7 | spec: 8 | selector: 9 | app: redis 10 | ports: 11 | - port: 6379 12 | targetPort: 6379 13 | -------------------------------------------------------------------------------- /kubernetes/vue-storefront-api-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: vue-storefront-api-config 5 | data: 6 | BIND_HOST: 0.0.0.0 7 | ELASTICSEARCH_HOST: elasticsearch 8 | ELASTICSEARCH_PORT: "9200" 9 | REDIS_HOST: redis 10 | REDIS_PORT: "6379" 11 | REDIS_DB: "0" 12 | VS_ENV: dev 13 | -------------------------------------------------------------------------------- /kubernetes/vue-storefront-api-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: vue-storefront-api 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: vue-storefront-api 9 | template: 10 | metadata: 11 | labels: 12 | app: vue-storefront-api 13 | spec: 14 | containers: 15 | - name: vue-storefront-api 16 | image: divante/vue-storefront-api:latest 17 | envFrom: 18 | - configMapRef: 19 | name: vue-storefront-api-config 20 | ports: 21 | - containerPort: 8080 22 | volumeMounts: 23 | - mountPath: /var/www/config 24 | name: code 25 | subPath: config 26 | - mountPath: /var/www/migrations 27 | name: code 28 | subPath: migrations 29 | readOnly: true 30 | - mountPath: /var/www/package.json 31 | name: code 32 | subPath: package.json 33 | readOnly: true 34 | - mountPath: /var/www/scripts 35 | name: code 36 | subPath: scripts 37 | readOnly: true 38 | - mountPath: /var/www/src 39 | name: code 40 | subPath: src 41 | readOnly: true 42 | - mountPath: /var/www/var 43 | name: code 44 | subPath: var 45 | readOnly: true 46 | - mountPath: /var/www/dist 47 | name: dist 48 | volumes: 49 | - name: code 50 | hostPath: 51 | path: "/root/vue-storefront-api" 52 | - name: dist 53 | emptyDir: 54 | medium: Memory 55 | -------------------------------------------------------------------------------- /kubernetes/vue-storefront-api-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: vue-storefront-api 5 | labels: 6 | app: vue-storefront-api 7 | spec: 8 | selector: 9 | app: vue-storefront-api 10 | ports: 11 | - port: 8080 12 | targetPort: 8080 13 | -------------------------------------------------------------------------------- /migrations/.common.js: -------------------------------------------------------------------------------- 1 | 2 | const config = require('config') 3 | const kue = require('kue') 4 | const queue = kue.createQueue(Object.assign(config.kue, { redis: config.redis })) 5 | const es = require('../src/lib/elastic') 6 | const client = es.getClient(config) 7 | 8 | exports.db = client 9 | exports.queue = queue 10 | -------------------------------------------------------------------------------- /migrations/1530101328854-local_es_config_fix.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _set = require('lodash/set') 4 | 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | const configDir = path.resolve('./config') 9 | 10 | var files = fs.readdirSync(configDir).filter((file) => { 11 | if (file === 'default.json') return false 12 | 13 | if (file.startsWith('elastic.schema.')) return false 14 | 15 | return path.extname(file) === '.json' 16 | }) 17 | 18 | module.exports.up = next => { 19 | files.forEach((file) => { 20 | var filePath = path.join(configDir, file) 21 | 22 | try { 23 | console.log(`Searching for deprecated parameters in file '${filePath}'...`) 24 | let config = JSON.parse(fs.readFileSync(filePath)) 25 | 26 | if ('esHost' in config) { 27 | console.log("Parameter 'esHost' found - rewriting...", filePath) 28 | let esHostPort = config.esHost.split(':') 29 | _set(config, 'elasticsearch.host', esHostPort[0]) 30 | _set(config, 'elasticsearch.port', esHostPort[1]) 31 | delete config.esHost 32 | } 33 | 34 | if ('esIndexes' in config) { 35 | console.log("Parameter 'esIndexes' found - rewriting...") 36 | _set(config, 'elasticsearch.indices', config.esIndexes) 37 | delete config.esIndexes 38 | } 39 | 40 | fs.writeFileSync(filePath, JSON.stringify(config, null, 2)) 41 | console.log(`File '${filePath}' updated.`) 42 | } catch (e) { 43 | throw e 44 | } 45 | }) 46 | 47 | next() 48 | } 49 | 50 | module.exports.down = next => { 51 | next() 52 | } 53 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "debug": false, 4 | "exec": "node -r ts-node/register src/", 5 | "watch": ["./src"], 6 | "ext": "ts, js", 7 | "inspect": true 8 | } 9 | -------------------------------------------------------------------------------- /scripts/cache.js: -------------------------------------------------------------------------------- 1 | const program = require('commander') 2 | const config = require('config') 3 | const cache = require('../src/lib/cache-instance') 4 | 5 | program 6 | .command('clear') 7 | .option('-t|--tag ', 'tag name, available tags: ' + config.server.availableCacheTags.join(', '), '*') 8 | .action((cmd) => { // TODO: add parallel processing 9 | if (!cmd.tag) { 10 | console.error('error: tag must be specified') 11 | process.exit(1) 12 | } else { 13 | console.log(`Clear cache request for [${cmd.tag}]`) 14 | let tags = [] 15 | if (cmd.tag === '*') { 16 | tags = config.server.availableCacheTags 17 | } else { 18 | tags = cmd.tag.split(',') 19 | } 20 | const subPromises = [] 21 | tags.forEach(tag => { 22 | if (config.server.availableCacheTags.indexOf(tag) >= 0 || config.server.availableCacheTags.find(t => { 23 | return tag.indexOf(t) === 0 24 | })) { 25 | subPromises.push(cache.invalidate(tag).then(() => { 26 | console.log(`Tags invalidated successfully for [${tag}]`) 27 | })) 28 | } else { 29 | console.error(`Invalid tag name ${tag}`) 30 | } 31 | }) 32 | Promise.all(subPromises).then(r => { 33 | console.log(`All tags invalidated successfully [${cmd.tag}]`) 34 | process.exit(0) 35 | }).catch(error => { 36 | console.error(error) 37 | }) 38 | } 39 | }) 40 | 41 | program.parse(process.argv) 42 | -------------------------------------------------------------------------------- /scripts/elastic.js: -------------------------------------------------------------------------------- 1 | const program = require('commander') 2 | const config = require('config').elasticsearch 3 | const spawnSync = require('child_process').spawnSync 4 | 5 | function stdOutErr (stdout, stderr) { 6 | if (stdout.length > 0) { console.log(stdout.toString('utf8')) } 7 | if (stderr.length > 0) { console.error(stderr.toString('utf8')) } 8 | } 9 | 10 | /** 11 | * DUMP COMMAND 12 | */ 13 | const es5DumpCommand = (cmd) => { 14 | console.warn(`es5 is deprecated and will be removed in 1.13`) 15 | const input = `http://${config.host}:${config.port}/${cmd.inputIndex}` 16 | 17 | const child = spawnSync('node', [ 18 | 'node_modules/elasticdump/bin/elasticdump', 19 | `--input=${input}`, 20 | `--output=${cmd.outputFile}` 21 | ]) 22 | stdOutErr(child.stdout, child.stderr) 23 | } 24 | 25 | const es7DumpCommand = (cmd) => { 26 | if (!cmd.outputFile.indexOf('.json')) { 27 | console.error('Please provide the file name ending with .json ext.') 28 | } 29 | for (var indexTypeIterator in config.indexTypes) { 30 | var collectionName = config.indexTypes[indexTypeIterator] 31 | var inputIndex = `${cmd.inputIndex}_${collectionName}` 32 | var outputFile = cmd.outputFile.replace('.json', `_${collectionName}.json`) 33 | const input = `http://${config.host}:${config.port}/${inputIndex}` 34 | 35 | const child = spawnSync('node', [ 36 | 'node_modules/elasticdump/bin/elasticdump', 37 | `--input=${input}`, 38 | `--output=${outputFile}` 39 | ]) 40 | stdOutErr(child.stdout, child.stderr) 41 | } 42 | } 43 | 44 | program 45 | .command('dump') 46 | .option('--input-index ', 'index to dump', 'vue_storefront_catalog') 47 | .option('--output-file ', 'path to the output file', 'var/catalog.json') 48 | .action((cmd) => { 49 | if (parseInt(config.apiVersion) < 6) { 50 | return es5DumpCommand(cmd) 51 | } else { 52 | return es7DumpCommand(cmd) 53 | } 54 | }) 55 | 56 | /** 57 | * RESTORE COMMAND 58 | */ 59 | 60 | const es5RestoreCommand = (cmd) => { 61 | console.warn(`es5 is deprecated and will be removed in 1.13`) 62 | const output = `http://${config.host}:${config.port}/${cmd.outputIndex}` 63 | 64 | const child = spawnSync('node', [ 65 | 'node_modules/elasticdump/bin/elasticdump', 66 | `--input=${cmd.inputFile}`, 67 | `--output=${output}` 68 | ]) 69 | stdOutErr(child.stdout, child.stderr) 70 | } 71 | 72 | const es7RestoreCommand = (cmd) => { 73 | if (!cmd.inputFile.indexOf('.json')) { 74 | console.error('Please provide the file name ending with .json ext.') 75 | } 76 | for (var indexTypeIterator in config.indexTypes) { 77 | var collectionName = config.indexTypes[indexTypeIterator] 78 | var outputIndex = `${cmd.outputIndex}_${collectionName}` 79 | var inputFile = cmd.inputFile.replace('.json', `_${collectionName}.json`) 80 | 81 | const output = `http://${config.host}:${config.port}/${outputIndex}` 82 | 83 | const child = spawnSync('node', [ 84 | 'node_modules/elasticdump/bin/elasticdump', 85 | `--input=${inputFile}`, 86 | `--output=${output}` 87 | ]) 88 | stdOutErr(child.stdout, child.stderr) 89 | } 90 | } 91 | 92 | program 93 | .command('restore') 94 | .option('--output-index ', 'index to restore', 'vue_storefront_catalog') 95 | .option('--input-file ', 'path to the input file', 'var/catalog.json') 96 | .action((cmd) => { 97 | if (parseInt(config.apiVersion) < 6) { 98 | return es5RestoreCommand(cmd) 99 | } else { 100 | return es7RestoreCommand(cmd) 101 | } 102 | }) 103 | 104 | program 105 | .on('command:*', () => { 106 | console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' ')); 107 | process.exit(1); 108 | }); 109 | 110 | program 111 | .parse(process.argv) 112 | 113 | process.on('unhandledRejection', (reason, p) => { 114 | console.log('Unhandled Rejection at: Promise ', p, ' reason: ', reason) 115 | }) 116 | 117 | process.on('uncaughtException', (exception) => { 118 | console.log(exception) 119 | }) 120 | -------------------------------------------------------------------------------- /scripts/kue.js: -------------------------------------------------------------------------------- 1 | const program = require('commander') 2 | const config = require('config').redis 3 | const kue = require('kue') 4 | 5 | program 6 | .command('dashboard') 7 | .option('-p|--port ', 'port on which to run kue dashboard', 3000) 8 | .option('-q|--prefix ', 'prefix', 'q') 9 | .action((cmd) => { 10 | kue.createQueue({ 11 | redis: config, 12 | prefix: cmd.prefix 13 | }) 14 | 15 | kue.app.listen(cmd.port) 16 | }); 17 | 18 | program 19 | .on('command:*', () => { 20 | console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' ')); 21 | process.exit(1); 22 | }); 23 | 24 | program 25 | .parse(process.argv) 26 | 27 | process.on('unhandledRejection', (reason, p) => { 28 | console.error(`Unhandled Rejection at: Promise ${p}, reason: ${reason}`) 29 | // application specific logging, throwing an error, or other logic here 30 | }) 31 | 32 | process.on('uncaughtException', (exception) => { 33 | console.error(exception) // to see your exception details in the console 34 | // if you are on production, maybe you can send the exception details to your 35 | // email as well ? 36 | }) 37 | -------------------------------------------------------------------------------- /src/api/extensions/cms-data/README.md: -------------------------------------------------------------------------------- 1 | # Magento 2 cms data extension 2 | 3 | This API extension get data from cms page and cms static block your Magento 2 instance. 4 | It use `snowdog/module-cms-api` composer module so you have to install it in your Magento instance. 5 | 6 | in your `local.json` file you should register the extension: 7 | `"registeredExtensions": ["mailchimp-subscribe", "example-magento-api", "cms-data"],` 8 | 9 | The API endpoitns are: 10 | ``` 11 | /api/ext/cms-data/cmsPage/:id 12 | /api/ext/cms-data/cmsBlock/:id 13 | ``` 14 | 15 | where `:id` is an id of page or block 16 | -------------------------------------------------------------------------------- /src/api/extensions/cms-data/index.js: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '../../../lib/util'; 2 | import { Router } from 'express'; 3 | const Magento2Client = require('magento2-rest-client').Magento2Client 4 | 5 | module.exports = ({ config, db }) => { 6 | let cmsApi = Router(); 7 | 8 | cmsApi.get('/cmsPage/:id', (req, res) => { 9 | const client = Magento2Client(config.magento2.api); 10 | client.addMethods('cmsPage', (restClient) => { 11 | let module = {}; 12 | module.getPage = function () { 13 | return restClient.get('/snowdog/cmsPage/' + req.params.id); 14 | } 15 | return module; 16 | }) 17 | client.cmsPage.getPage().then((result) => { 18 | apiStatus(res, result, 200); // just dump it to the browser, result = JSON object 19 | }).catch(err => { 20 | apiStatus(res, err, 500); 21 | }) 22 | }) 23 | 24 | cmsApi.get('/cmsBlock/:id', (req, res) => { 25 | const client = Magento2Client(config.magento2.api); 26 | client.addMethods('cmsBlock', (restClient) => { 27 | let module = {}; 28 | module.getBlock = function () { 29 | return restClient.get('/snowdog/cmsBlock/' + req.params.id); 30 | } 31 | return module; 32 | }) 33 | client.cmsBlock.getBlock().then((result) => { 34 | apiStatus(res, result, 200); // just dump it to the browser, result = JSON object 35 | }).catch(err => { 36 | apiStatus(res, err, 500); 37 | }) 38 | }) 39 | 40 | cmsApi.get('/cmsPageIdentifier/:identifier/storeId/:storeId', (req, res) => { 41 | const client = Magento2Client(config.magento2.api); 42 | client.addMethods('cmsPageIdentifier', (restClient) => { 43 | let module = {}; 44 | module.getPageIdentifier = function () { 45 | return restClient.get(`/snowdog/cmsPageIdentifier/${req.params.identifier}/storeId/${req.params.storeId}`); 46 | } 47 | return module; 48 | }) 49 | client.cmsPageIdentifier.getPageIdentifier().then((result) => { 50 | apiStatus(res, result, 200); // just dump it to the browser, result = JSON object 51 | }).catch(err => { 52 | apiStatus(res, err, 500); 53 | }) 54 | }) 55 | 56 | cmsApi.get('/cmsBlockIdentifier/:identifier/storeId/:storeId', (req, res) => { 57 | const client = Magento2Client(config.magento2.api); 58 | client.addMethods('cmsBlockIdentifier', (restClient) => { 59 | let module = {}; 60 | module.getBlockIdentifier = function () { 61 | return restClient.get(`/snowdog/cmsBlockIdentifier/${req.params.identifier}/storeId/${req.params.storeId}`); 62 | } 63 | return module; 64 | }) 65 | client.cmsBlockIdentifier.getBlockIdentifier().then((result) => { 66 | apiStatus(res, result, 200); // just dump it to the browser, result = JSON object 67 | }).catch(err => { 68 | apiStatus(res, err, 500); 69 | }) 70 | }) 71 | 72 | return cmsApi 73 | } 74 | -------------------------------------------------------------------------------- /src/api/extensions/elastic-stock/index.js: -------------------------------------------------------------------------------- 1 | import { apiStatus, getCurrentStoreView, getCurrentStoreCode } from '../../../lib/util'; 2 | import { getClient as getElasticClient, adjustQuery, getHits } from '../../../lib/elastic' 3 | import { Router } from 'express'; 4 | const bodybuilder = require('bodybuilder') 5 | 6 | module.exports = ({ 7 | config, 8 | db 9 | }) => { 10 | let api = Router(); 11 | 12 | const getStockList = (storeCode, skus) => { 13 | let storeView = getCurrentStoreView(storeCode) 14 | const esQuery = adjustQuery({ 15 | index: storeView.elasticsearch.index, // current index name 16 | type: 'product', 17 | _source_includes: ['stock'], 18 | body: bodybuilder().filter('term', 'status', 1).andFilter('terms', 'sku', skus).build() 19 | }, 'product', config) 20 | return getElasticClient(config).search(esQuery).then((products) => { // we're always trying to populate cache - when online 21 | return getHits(products).map(el => { return el._source.stock }) 22 | }).catch(err => { 23 | console.error(err) 24 | }) 25 | } 26 | 27 | /** 28 | * GET get stock item 29 | */ 30 | api.get('/check/:sku', (req, res) => { 31 | if (!req.params.sku) { 32 | return apiStatus(res, 'sku parameter is required', 500); 33 | } 34 | 35 | getStockList(getCurrentStoreCode(req), [req.params.sku]).then((result) => { 36 | if (result && result.length > 0) { 37 | apiStatus(res, result[0], 200); 38 | } else { 39 | apiStatus(res, 'No stock information for given sku', 500); 40 | } 41 | }).catch(err => { 42 | apiStatus(res, err, 500); 43 | }) 44 | }) 45 | 46 | /** 47 | * GET get stock item - 2nd version with the query url parameter 48 | */ 49 | api.get('/check', (req, res) => { 50 | if (!req.query.sku) { 51 | return apiStatus(res, 'sku parameter is required', 500); 52 | } 53 | getStockList(getCurrentStoreCode(req), [req.query.sku]).then((result) => { 54 | if (result && result.length > 0) { 55 | apiStatus(res, result[0], 200); 56 | } else { 57 | apiStatus(res, 'No stock information for given sku', 500); 58 | } 59 | }).catch(err => { 60 | apiStatus(res, err, 500); 61 | }) 62 | }) 63 | 64 | /** 65 | * GET get stock item list by skus (comma separated) 66 | */ 67 | api.get('/list', (req, res) => { 68 | if (!req.query.skus) { 69 | return apiStatus(res, 'skus parameter is required', 500); 70 | } 71 | const skuArray = req.query.skus.split(',') 72 | getStockList(getCurrentStoreCode(req), skuArray).then((result) => { 73 | if (result && result.length > 0) { 74 | apiStatus(res, result, 200); 75 | } else { 76 | apiStatus(res, 'No stock information for given sku', 500); 77 | } 78 | }).catch(err => { 79 | apiStatus(res, err, 500); 80 | }) 81 | }) 82 | 83 | return api 84 | } 85 | -------------------------------------------------------------------------------- /src/api/extensions/example-custom-filter/filter/catalog/SampleFilter.ts: -------------------------------------------------------------------------------- 1 | import { FilterInterface } from 'storefront-query-builder' 2 | 3 | const filter: FilterInterface = { 4 | priority: 1, 5 | check: ({ operator, value, attribute, queryChain }) => attribute === 'custom-filter-name', 6 | filter ({ value, attribute, operator, queryChain }) { 7 | // Do you custom filter logic like: queryChain.filter('terms', attribute, value) 8 | return queryChain 9 | }, 10 | mutator: (value) => typeof value !== 'object' ? { 'in': [value] } : value 11 | } 12 | 13 | export default filter 14 | -------------------------------------------------------------------------------- /src/api/extensions/example-custom-filter/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | module.exports = () => { 4 | let exampleFilter = Router() 5 | return exampleFilter 6 | } 7 | -------------------------------------------------------------------------------- /src/api/extensions/example-magento-api/index.js: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '../../../lib/util'; 2 | import { Router } from 'express'; 3 | const Magento2Client = require('magento2-rest-client').Magento2Client 4 | 5 | module.exports = ({ config, db }) => { 6 | let mcApi = Router(); 7 | 8 | /** 9 | * This is just an example on how to extend magento2 api client and get the cms blocks 10 | * https://devdocs.magento.com/swagger/#!/cmsBlockRepositoryV1/cmsBlockRepositoryV1GetListGet 11 | * 12 | * NOTICE: vue-storefront-api should be platform agnostic. This is just for the customization example 13 | */ 14 | mcApi.get('/cmsBlock', (req, res) => { 15 | const client = Magento2Client(config.magento2.api); 16 | client.addMethods('cmsBlock', (restClient) => { 17 | var module = {}; 18 | 19 | module.search = function () { 20 | return restClient.get('/cmsPage/search'); 21 | } 22 | return module; 23 | }) 24 | console.log(client) 25 | client.cmsBlock.search().then((result) => { 26 | apiStatus(res, result, 200); // just dump it to the browser, result = JSON object 27 | }).catch(err => { 28 | apiStatus(res, err, 500); 29 | }) 30 | }) 31 | 32 | return mcApi 33 | } 34 | -------------------------------------------------------------------------------- /src/api/extensions/example-processor/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | module.exports = () => { 4 | let exampleApi = Router() 5 | 6 | return exampleApi 7 | } 8 | -------------------------------------------------------------------------------- /src/api/extensions/example-processor/processors/my-product-processor.js: -------------------------------------------------------------------------------- 1 | class MyProductProcessor { 2 | constructor (config, request) { 3 | this._request = request 4 | this._config = config 5 | } 6 | 7 | process (productList) { 8 | // Product search results can be modified here. 9 | // For example, the following would add a paragraph to the short description of every product 10 | // 11 | // for (const prod of productList) { 12 | // prod._source.short_description = prod._source.short_description + '

Free Shipping Today Only!

' 13 | // } 14 | // 15 | // For a real-life example processor, see src/platform/magento2/tax.js 16 | // For more details and another example, see https://docs.vuestorefront.io/guide/extensions/extensions-to-modify-results.html 17 | return productList 18 | } 19 | } 20 | 21 | module.exports = MyProductProcessor 22 | -------------------------------------------------------------------------------- /src/api/extensions/mail-service/index.js: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '../../../lib/util' 2 | import { Router } from 'express' 3 | import EmailCheck from 'email-check' 4 | import jwt from 'jwt-simple' 5 | import NodeMailer from 'nodemailer' 6 | 7 | module.exports = ({ config }) => { 8 | const msApi = Router() 9 | let token 10 | 11 | /** 12 | * GET send token to authorize email 13 | */ 14 | msApi.get('/get-token', (req, res) => { 15 | token = jwt.encode(Date.now(), config.extensions.mailService.secretString) 16 | apiStatus(res, token, 200) 17 | }) 18 | 19 | /** 20 | * POST send an email 21 | */ 22 | msApi.post('/send-email', (req, res) => { 23 | const userData = req.body 24 | if (!userData.token || userData.token !== token) { 25 | apiStatus(res, 'Email is not authorized!', 500) 26 | } 27 | const { host, port, secure, user, pass } = config.extensions.mailService.transport 28 | if (!host || !port || !user || !pass) { 29 | apiStatus(res, 'No transport is defined for mail service!', 500) 30 | } 31 | if (!userData.sourceAddress) { 32 | apiStatus(res, 'Source email address is not provided!', 500) 33 | return 34 | } 35 | if (!userData.targetAddress) { 36 | apiStatus(res, 'Target email address is not provided!', 500) 37 | return 38 | } 39 | // Check if email address we're sending to is from the white list from config 40 | const whiteList = config.extensions.mailService.targetAddressWhitelist 41 | const email = userData.confirmation ? userData.sourceAddress : userData.targetAddress 42 | if (!whiteList.includes(email)) { 43 | apiStatus(res, `Target email address (${email}) is not from the whitelist!`, 500) 44 | return 45 | } 46 | 47 | // check if provided email addresses actually exist 48 | EmailCheck(userData.sourceAddress) 49 | .then(response => { 50 | if (response) return EmailCheck(userData.targetAddress) 51 | else { 52 | apiStatus(res, 'Source email address is invalid!', 500) 53 | } 54 | }) 55 | .then(response => { 56 | if (response) { 57 | let transporter = NodeMailer.createTransport({ 58 | host, 59 | port, 60 | secure, 61 | auth: { 62 | user, 63 | pass 64 | } 65 | }) 66 | const mailOptions = { 67 | from: userData.sourceAddress, 68 | to: userData.targetAddress, 69 | subject: userData.subject, 70 | text: userData.emailText 71 | } 72 | transporter.sendMail(mailOptions, (error) => { 73 | if (error) { 74 | apiStatus(res, error, 500) 75 | return 76 | } 77 | apiStatus(res, 'OK', 200) 78 | transporter.close() 79 | }) 80 | } else { 81 | apiStatus(res, 'Target email address is invalid!', 500) 82 | } 83 | }) 84 | .catch(() => { 85 | apiStatus(res, 'Invalid email address is provided!', 500) 86 | }) 87 | }) 88 | 89 | return msApi 90 | } 91 | -------------------------------------------------------------------------------- /src/api/extensions/mailchimp-subscribe/index.js: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '../../../lib/util' 2 | import { Router } from 'express' 3 | const request = require('request') 4 | const md5 = require('md5') 5 | 6 | module.exports = ({ config, db }) => { 7 | let mcApi = Router(); 8 | 9 | /** 10 | * Retrieve user subscription status 11 | */ 12 | mcApi.get('/subscribe', (req, res) => { 13 | const email = req.query.email 14 | if (!email) { 15 | apiStatus(res, 'Invalid e-mail provided!', 500) 16 | return 17 | } 18 | return request({ 19 | url: config.extensions.mailchimp.apiUrl + '/lists/' + config.extensions.mailchimp.listId + '/members/' + md5(email.toLowerCase()), 20 | method: 'GET', 21 | json: true, 22 | headers: { 'Authorization': 'apikey ' + config.extensions.mailchimp.apiKey } 23 | }, (error, response, body) => { 24 | if (error || response.statusCode !== 200) { 25 | console.error(error, body) 26 | apiStatus(res, 'An error occured while accessing Mailchimp', 500) 27 | } else { 28 | apiStatus(res, body.status, 200) 29 | } 30 | }) 31 | }) 32 | 33 | /** 34 | * POST subscribe a user 35 | */ 36 | mcApi.post('/subscribe', (req, res) => { 37 | let userData = req.body 38 | if (!userData.email) { 39 | apiStatus(res, 'Invalid e-mail provided!', 500) 40 | return 41 | } 42 | request({ 43 | url: config.extensions.mailchimp.apiUrl + '/lists/' + config.extensions.mailchimp.listId, 44 | method: 'POST', 45 | headers: { 'Authorization': 'apikey ' + config.extensions.mailchimp.apiKey }, 46 | json: true, 47 | body: { members: [ { email_address: userData.email, status: config.extensions.mailchimp.userStatus } ], 'update_existing': true } 48 | }, (error, response, body) => { 49 | if (error || response.statusCode !== 200) { 50 | console.error(error, body) 51 | apiStatus(res, 'An error occured while accessing Mailchimp', 500) 52 | } else { 53 | apiStatus(res, body.status, 200) 54 | } 55 | }) 56 | }) 57 | 58 | /** 59 | * DELETE unsubscribe a user 60 | */ 61 | mcApi.delete('/subscribe', (req, res) => { 62 | let userData = req.body 63 | if (!userData.email) { 64 | apiStatus(res, 'Invalid e-mail provided!', 500) 65 | return 66 | } 67 | 68 | let request = require('request'); 69 | request({ 70 | url: config.extensions.mailchimp.apiUrl + '/lists/' + config.extensions.mailchimp.listId, 71 | method: 'POST', 72 | headers: { 'Authorization': 'apikey ' + config.extensions.mailchimp.apiKey }, 73 | json: true, 74 | body: { members: [ { email_address: userData.email, status: 'unsubscribed' } ], 'update_existing': true } 75 | }, (error, response, body) => { 76 | if (error || response.statusCode !== 200) { 77 | console.error(error, body) 78 | apiStatus(res, 'An error occured while accessing Mailchimp', 500) 79 | } else { 80 | apiStatus(res, body.status, 200) 81 | } 82 | }) 83 | }) 84 | 85 | return mcApi 86 | } 87 | -------------------------------------------------------------------------------- /src/api/img.ts: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import CacheFactory from '../image/cache/factory'; 3 | import ActionFactory from '../image/action/factory'; 4 | 5 | const asyncMiddleware = fn => (req, res, next) => { 6 | Promise.resolve(fn(req, res, next)).catch(next); 7 | }; 8 | 9 | export default ({ config, db }) => 10 | asyncMiddleware(async (req, res, next) => { 11 | if (!(req.method === 'GET')) { 12 | res.set('Allow', 'GET'); 13 | return res.status(405).send('Method Not Allowed'); 14 | } 15 | const cacheFactory = new CacheFactory(config, req) 16 | 17 | req.socket.setMaxListeners(config.imageable.maxListeners || 50); 18 | 19 | let imageBuffer 20 | 21 | const actionFactory = new ActionFactory(req, res, next, config) 22 | const imageAction = actionFactory.getAdapter(config.imageable.action.type) 23 | imageAction.getOption() 24 | imageAction.validateOptions() 25 | imageAction.isImageSourceHostAllowed() 26 | imageAction.validateMIMEType() 27 | 28 | const cache = cacheFactory.getAdapter(config.imageable.caching.type) 29 | 30 | if (config.imageable.caching.active && await cache.check()) { 31 | await cache.getImageFromCache() 32 | imageBuffer = cache.image 33 | } else { 34 | await imageAction.prossesImage() 35 | 36 | if (config.imageable.caching.active) { 37 | cache.image = imageAction.imageBuffer 38 | await cache.save() 39 | } 40 | 41 | imageBuffer = imageAction.imageBuffer 42 | } 43 | 44 | if (res.headersSent) { 45 | return 46 | } 47 | 48 | return res 49 | .type(imageAction.mimeType) 50 | .set({ 'Cache-Control': `max-age=${imageAction.maxAgeForResponse}` }) 51 | .send(imageBuffer); 52 | }); 53 | -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | import { version } from '../../package.json'; 2 | import { Router } from 'express'; 3 | import order from './order'; 4 | import catalog from './catalog'; 5 | import user from './user'; 6 | import stock from './stock'; 7 | import review from './review'; 8 | import cart from './cart'; 9 | import product from './product'; 10 | import sync from './sync'; 11 | import url from './url'; 12 | 13 | export default ({ config, db }) => { 14 | let api = Router(); 15 | 16 | // mount the catalog resource 17 | api.use('/catalog', catalog({ config, db })) 18 | 19 | // mount the order resource 20 | api.use('/order', order({ config, db })); 21 | 22 | // mount the user resource 23 | api.use('/user', user({ config, db })); 24 | 25 | // mount the stock resource 26 | api.use('/stock', stock({ config, db })); 27 | 28 | // mount the review resource 29 | api.use('/review', review({ config, db })); 30 | 31 | // mount the cart resource 32 | api.use('/cart', cart({ config, db })); 33 | 34 | // mount the product resource 35 | api.use('/product', product({ config, db })) 36 | 37 | // mount the sync resource 38 | api.use('/sync', sync({ config, db })) 39 | 40 | // mount the url resource 41 | api.use('/url', url({ config })) 42 | 43 | // perhaps expose some API metadata at the root 44 | api.get('/', (req, res) => { 45 | res.json({ version }); 46 | }); 47 | 48 | /** Register the custom extensions */ 49 | for (let ext of config.registeredExtensions) { 50 | let entryPoint 51 | 52 | try { 53 | entryPoint = require('./extensions/' + ext) 54 | } catch (err) { 55 | try { 56 | entryPoint = require(ext) 57 | } catch (err) { 58 | console.error(err) 59 | } 60 | } 61 | 62 | if (entryPoint) { 63 | api.use('/ext/' + ext, entryPoint({ config, db })) 64 | console.log('Extension ' + ext + ' registered under /ext/' + ext + ' base URL') 65 | } 66 | } 67 | 68 | return api; 69 | } 70 | -------------------------------------------------------------------------------- /src/api/invalidate.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import { apiStatus } from '../lib/util' 3 | import cache from '../lib/cache-instance' 4 | import request from 'request' 5 | 6 | function invalidateCache (req, res) { 7 | if (config.get('server.useOutputCache')) { 8 | if (!req.query.key || req.query.key !== config.get('server.invalidateCacheKey')) { 9 | console.error('Invalid cache invalidation key') 10 | apiStatus(res, 'Invalid cache invalidation key', 500) 11 | return 12 | } 13 | 14 | if (req.query.tag) { // clear cache pages for specific query tag 15 | console.log(`Clear cache request for [${req.query.tag}]`) 16 | let tags = [] 17 | if (req.query.tag === '*') { 18 | tags = config.get('server.availableCacheTags') 19 | } else { 20 | tags = req.query.tag.split(',') 21 | } 22 | const subPromises = [] 23 | tags.forEach(tag => { 24 | if ((config.get('server.availableCacheTags') as [string]).indexOf(tag) >= 0 || (config.get('server.availableCacheTags') as [string]).find(t => { 25 | return tag.indexOf(t) === 0 26 | })) { 27 | subPromises.push(cache.invalidate(tag).then(() => { 28 | console.log(`Tags invalidated successfully for [${tag}]`) 29 | if (config.get('varnish.enabled')) { 30 | request( 31 | { 32 | uri: `http://${config.get('varnish.host')}:${config.get('varnish.port')}/`, 33 | method: 'BAN', 34 | headers: { 35 | // I should change Tags -> tag 36 | 'X-VS-Cache-Tag': tag 37 | } 38 | }, 39 | (err, res, body) => { 40 | if (body && body.includes('200 Ban added')) { 41 | console.log( 42 | `Tags invalidated successfully for [${tag}] in the Varnish` 43 | ); 44 | } else { 45 | console.log(body) 46 | console.error(`Couldn't ban tag: ${tag} in the Varnish`); 47 | } 48 | } 49 | ); 50 | } 51 | })) 52 | } else { 53 | console.error(`Invalid tag name ${tag}`) 54 | } 55 | }) 56 | Promise.all(subPromises).then(r => { 57 | apiStatus(res, `Tags invalidated successfully [${req.query.tag}]`, 200) 58 | }).catch(error => { 59 | apiStatus(res, error, 500) 60 | console.error(error) 61 | }) 62 | if (config.get('server.invalidateCacheForwarding')) { // forward invalidate request to the next server in the chain 63 | if (!req.query.forwardedFrom && config.get('server.invalidateCacheForwardUrl')) { // don't forward forwarded requests 64 | request(config.get('server.invalidateCacheForwardUrl') + req.query.tag + '&forwardedFrom=vs', {}, (err, res, body) => { 65 | if (err) { console.error(err); } 66 | try { 67 | if (body && JSON.parse(body).code !== 200) console.log(body); 68 | } catch (e) { 69 | console.error('Invalid Cache Invalidation response format', e) 70 | } 71 | }); 72 | } 73 | } 74 | } else if (config.get('varnish.enabled') && req.query.ext) { 75 | const exts = req.query.ext.split(',') 76 | for (let ext of exts) { 77 | request( 78 | { 79 | uri: `http://${config.get('varnish.host')}:${config.get('varnish.port')}/`, 80 | method: 'BAN', 81 | headers: { 82 | 'X-VS-Cache-Ext': ext 83 | } 84 | }, 85 | (err, res, body) => { 86 | if (body && body.includes('200 Ban added')) { 87 | console.log( 88 | `Cache invalidated successfully for [${ext}] in the Varnish` 89 | ); 90 | } else { 91 | console.error(`Couldn't ban extension: ${ext} in the Varnish`); 92 | } 93 | } 94 | ); 95 | } 96 | apiStatus( 97 | res, 98 | 'Cache invalidation succeed', 99 | 200 100 | ); 101 | } else { 102 | apiStatus(res, 'Invalid parameters for Clear cache request', 500) 103 | console.error('Invalid parameters for Clear cache request') 104 | } 105 | } else { 106 | apiStatus(res, 'Cache invalidation is not required, output cache is disabled', 200) 107 | } 108 | } 109 | 110 | export default invalidateCache 111 | -------------------------------------------------------------------------------- /src/api/order.ts: -------------------------------------------------------------------------------- 1 | import resource from 'resource-router-middleware'; 2 | import { apiStatus, apiError } from '../lib/util'; 3 | import { merge } from 'lodash'; 4 | import PlatformFactory from '../platform/factory'; 5 | 6 | const Ajv = require('ajv'); // json validator 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const kue = require('kue'); 10 | const jwa = require('jwa'); 11 | const hmac = jwa('HS256'); 12 | 13 | const _getProxy = (req, config) => { 14 | const platform = config.platform 15 | const factory = new PlatformFactory(config, req) 16 | return factory.getAdapter(platform, 'order') 17 | }; 18 | 19 | export default ({ config, db }) => resource({ 20 | 21 | /** Property name to store preloaded entity on `request`. */ 22 | id: 'order', 23 | 24 | /** 25 | * POST create an order with JSON payload compliant with models/order.md 26 | */ 27 | create (req, res) { 28 | const ajv = new Ajv(); 29 | require('ajv-keywords')(ajv, 'regexp'); 30 | 31 | const orderSchema = require('../models/order.schema.js') 32 | let orderSchemaExtension = {} 33 | if (fs.existsSync(path.resolve(__dirname, '../models/order.schema.extension.json'))) { 34 | orderSchemaExtension = require('../models/order.schema.extension.json') 35 | } 36 | const validate = ajv.compile(merge(orderSchema, orderSchemaExtension)); 37 | 38 | if (!validate(req.body)) { // schema validation of upcoming order 39 | console.dir(validate.errors); 40 | apiStatus(res, validate.errors, 400); 41 | return; 42 | } 43 | const incomingOrder = { title: 'Incoming order received on ' + new Date() + ' / ' + req.ip, ip: req.ip, agent: req.headers['user-agent'], receivedAt: new Date(), order: req.body }/* parsed using bodyParser.json middleware */ 44 | console.log(JSON.stringify(incomingOrder)) 45 | 46 | for (let product of req.body.products) { 47 | let key: { id?: string, sku?: string, priceInclTax?: number, price?: number } = config.tax.calculateServerSide ? { priceInclTax: product.priceInclTax } : { price: product.price } 48 | if (config.tax.alwaysSyncPlatformPricesOver) { 49 | key.id = product.id 50 | } else { 51 | key.sku = product.sku 52 | } 53 | // console.log(key) 54 | 55 | if (!config.tax.usePlatformTotals) { 56 | if (!hmac.verify(key, product.sgn, config.objHashSecret)) { 57 | console.error('Invalid hash for ' + product.sku + ': ' + product.sgn) 58 | apiStatus(res, 'Invalid signature validation of ' + product.sku, 200); 59 | return; 60 | } 61 | } 62 | } 63 | 64 | if (config.orders.useServerQueue) { 65 | try { 66 | let queue = kue.createQueue(Object.assign(config.kue, { redis: config.redis })); 67 | const job = queue.create('order', incomingOrder).save((err) => { 68 | if (err) { 69 | console.error(err) 70 | apiError(res, err); 71 | } else { 72 | apiStatus(res, job.id, 200); 73 | } 74 | }) 75 | } catch (e) { 76 | apiStatus(res, e, 500); 77 | } 78 | } else { 79 | const orderProxy = _getProxy(req, config) 80 | orderProxy.create(req.body).then((result) => { 81 | apiStatus(res, result, 200); 82 | }).catch(err => { 83 | apiError(res, err); 84 | }) 85 | } 86 | } 87 | }); 88 | -------------------------------------------------------------------------------- /src/api/product.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus, sgnSrc, apiError } from '../lib/util'; 2 | import { Router } from 'express'; 3 | import PlatformFactory from '../platform/factory'; 4 | 5 | const jwa = require('jwa'); 6 | const hmac = jwa('HS256'); 7 | 8 | export default ({ config, db }) => { 9 | let productApi = Router(); 10 | 11 | const _getProxy = (req) => { 12 | const platform = config.platform 13 | const factory = new PlatformFactory(config, req) 14 | return factory.getAdapter(platform, 'product') 15 | }; 16 | 17 | /** 18 | * GET get products info 19 | */ 20 | productApi.get('/list', (req, res) => { 21 | const productProxy = _getProxy(req) 22 | 23 | if (!req.query.skus) { return apiStatus(res, 'skus parameter is required', 500); } 24 | 25 | productProxy.list((req.query.skus as string).split(',')).then((result) => { 26 | apiStatus(res, result, 200); 27 | }).catch(err => { 28 | apiError(res, err); 29 | }) 30 | }) 31 | 32 | /** 33 | * GET get products info 34 | */ 35 | productApi.get('/render-list', (req, res) => { 36 | const productProxy = _getProxy(req) 37 | 38 | if (!req.query.skus) { return apiStatus(res, 'skus parameter is required', 500); } 39 | 40 | productProxy.renderList((req.query.skus as string).split(','), req.query.currencyCode, (req.query.storeId && parseInt((req.query.storeId as string)) > 0) ? req.query.storeId : 1).then((result) => { 41 | result.items = result.items.map((item) => { 42 | let sgnObj = item 43 | if (config.tax.calculateServerSide === true) { 44 | sgnObj = { priceInclTax: item.price_info.final_price } 45 | } else { 46 | sgnObj = { price: item.price_info.extension_attributes.tax_adjustments.final_price } 47 | } 48 | 49 | item.sgn = hmac.sign(sgnSrc(sgnObj, item), config.objHashSecret); // for products we sign off only price and id becase only such data is getting back with orders 50 | return item 51 | }) 52 | apiStatus(res, result, 200); 53 | }).catch(err => { 54 | apiError(res, err); 55 | }) 56 | }) 57 | 58 | return productApi 59 | } 60 | -------------------------------------------------------------------------------- /src/api/review.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus, apiError } from '../lib/util'; import { Router } from 'express'; 2 | import PlatformFactory from '../platform/factory' 3 | 4 | const Ajv = require('ajv'); // json validator 5 | 6 | export default ({config, db}) => { 7 | const reviewApi = Router(); 8 | 9 | const _getProxy = (req) => { 10 | const platform = config.platform 11 | const factory = new PlatformFactory(config, req) 12 | return factory.getAdapter(platform, 'review') 13 | }; 14 | 15 | reviewApi.post('/create', (req, res) => { 16 | const ajv = new Ajv(); 17 | const reviewProxy = _getProxy(req) 18 | const reviewSchema = require('../models/review.schema') 19 | const validate = ajv.compile(reviewSchema) 20 | 21 | req.body.review.review_status = config.review.defaultReviewStatus 22 | 23 | if (!validate(req.body)) { 24 | console.dir(validate.errors); 25 | apiStatus(res, validate.errors, 500); 26 | return; 27 | } 28 | 29 | reviewProxy.create(req.body.review).then((result) => { 30 | apiStatus(res, result, 200); 31 | }).catch(err => { 32 | apiError(res, err); 33 | }) 34 | }) 35 | 36 | return reviewApi 37 | } 38 | -------------------------------------------------------------------------------- /src/api/stock.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus, apiError } from '../lib/util'; import { Router } from 'express'; 2 | import PlatformFactory from '../platform/factory' 3 | 4 | export default ({ config, db }) => { 5 | let api = Router(); 6 | 7 | const _getProxy = (req) => { 8 | const platform = config.platform 9 | const factory = new PlatformFactory(config, req) 10 | return factory.getAdapter(platform, 'stock') 11 | }; 12 | 13 | const _getStockId = (storeCode) => { 14 | let storeView = config.storeViews[storeCode] 15 | return storeView ? storeView.msi.stockId : config.msi.defaultStockId 16 | }; 17 | 18 | /** 19 | * GET get stock item 20 | */ 21 | api.get('/check/:sku', (req, res) => { 22 | const stockProxy = _getProxy(req) 23 | 24 | if (!req.params.sku) { 25 | return apiStatus(res, 'sku parameter is required', 500); 26 | } 27 | 28 | stockProxy.check({ 29 | sku: req.params.sku, 30 | stockId: config.msi.enabled ? (req.params.stockId ? req.params.stockId : _getStockId(req.params.storeCode)) : null 31 | }).then((result) => { 32 | apiStatus(res, result, 200); 33 | }).catch(err => { 34 | apiStatus(res, err, 500); 35 | }) 36 | }) 37 | 38 | /** 39 | * GET get stock item - 2nd version with the query url parameter 40 | */ 41 | api.get('/check', (req, res) => { 42 | const stockProxy = _getProxy(req) 43 | 44 | if (!req.query.sku) { 45 | return apiStatus(res, 'sku parameter is required', 500); 46 | } 47 | 48 | stockProxy.check({ 49 | sku: req.query.sku, 50 | stockId: config.msi.enabled ? (req.query.stockId ? req.query.stockId : _getStockId(req.query.storeCode)) : null 51 | }).then((result) => { 52 | apiStatus(res, result, 200); 53 | }).catch(err => { 54 | apiStatus(res, err, 500); 55 | }) 56 | }) 57 | 58 | /** 59 | * GET get stock item list by skus (comma separated) 60 | */ 61 | api.get('/list', (req, res) => { 62 | const stockProxy = _getProxy(req) 63 | 64 | if (!req.query.skus) { 65 | return apiStatus(res, 'skus parameter is required', 500); 66 | } 67 | 68 | const skuArray = (req.query.skus as string).split(',') 69 | const promisesList = [] 70 | for (const sku of skuArray) { 71 | promisesList.push(stockProxy.check({sku: sku, stockId: config.msi.enabled ? _getStockId(req.query.storeCode) : null})) 72 | } 73 | Promise.all(promisesList).then((results) => { 74 | apiStatus(res, results, 200); 75 | }).catch(err => { 76 | apiError(res, err); 77 | }) 78 | }) 79 | 80 | return api 81 | } 82 | -------------------------------------------------------------------------------- /src/api/sync.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '../lib/util'; import { Router } from 'express'; 2 | import * as redis from '../lib/redis' 3 | 4 | export default ({ config, db }) => { 5 | let syncApi = Router(); 6 | 7 | /** 8 | * GET get stock item 9 | */ 10 | syncApi.get('/order/:order_id', (req, res) => { 11 | const redisClient = db.getRedisClient(config) 12 | 13 | redisClient.get('order$$id$$' + req.param('order_id'), (err, reply) => { 14 | const orderMetaData = JSON.parse(reply) 15 | if (orderMetaData) { 16 | orderMetaData.order = null // for security reasons we're just clearing out the real order data as it's set by `order_2_magento2.js` 17 | } 18 | apiStatus(res, err || orderMetaData, err ? 500 : 200); 19 | }) 20 | }) 21 | 22 | return syncApi 23 | } 24 | -------------------------------------------------------------------------------- /src/api/url/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import createMapRoute from './map'; 3 | 4 | const url = ({ config }) => { 5 | const router = Router() 6 | 7 | router.use('/map', createMapRoute({ config })) 8 | 9 | return router 10 | } 11 | 12 | export default url 13 | -------------------------------------------------------------------------------- /src/api/url/map.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { apiStatus, getCurrentStoreView, getCurrentStoreCode } from '../../lib/util' 3 | import { getClient as getElasticClient } from '../../lib/elastic' 4 | import ProcessorFactory from '../../processor/factory' 5 | import get from 'lodash/get' 6 | 7 | const adjustQueryForOldES = ({ config }) => { 8 | const searchedEntities = get(config, 'urlModule.map.searchedEntities', []) 9 | .map((entity) => ({ type: { value: entity } })) 10 | if (parseInt(config.elasticsearch.apiVersion) < 6) { 11 | return { 12 | filter: { 13 | bool: { 14 | should: searchedEntities 15 | } 16 | } 17 | } 18 | } else { 19 | return {} 20 | } 21 | } 22 | 23 | /** 24 | * Builds ES query based on config 25 | */ 26 | const buildQuery = ({ value, config }) => { 27 | const searchedFields = get(config, 'urlModule.map.searchedFields', []) 28 | .map((field) => ({ match_phrase: { [field]: { query: value } } })) 29 | 30 | return { 31 | query: { 32 | bool: { 33 | filter: { 34 | bool: { 35 | should: searchedFields, 36 | ...adjustQueryForOldES({ config }) 37 | } 38 | } 39 | } 40 | }, 41 | size: 1 // we need only one record 42 | } 43 | } 44 | 45 | const buildIndex = ({ indexName, config }) => { 46 | return parseInt(config.elasticsearch.apiVersion) < 6 47 | ? indexName 48 | : get(config, 'urlModule.map.searchedEntities', []) 49 | .map(entity => `${indexName}_${entity}`) 50 | } 51 | 52 | const adjustResultType = ({ result, config, indexName }) => { 53 | if (parseInt(config.elasticsearch.apiVersion) < 6) return result 54 | 55 | // extract type from index for es 7 56 | const type = result._index.replace(new RegExp(`^(${indexName}_)|(_[^_]*)$`, 'g'), '') 57 | result._type = type 58 | 59 | return result 60 | } 61 | 62 | /** 63 | * checks result equality because ES can return record even if searched value is not EXACLY what we want (check `match_phrase` in ES docs) 64 | */ 65 | const checkFieldValueEquality = ({ config, result, value }) => { 66 | const isEqualValue = get(config, 'urlModule.map.searchedFields', []) 67 | .find((field) => result._source[field] === value) 68 | 69 | return Boolean(isEqualValue) 70 | } 71 | 72 | const map = ({ config }) => { 73 | const router = Router() 74 | router.post('/', async (req, res) => { 75 | const { url, excludeFields, includeFields } = req.body 76 | if (!url) { 77 | return apiStatus(res, 'Missing url', 500) 78 | } 79 | 80 | const indexName = getCurrentStoreView(getCurrentStoreCode(req)).elasticsearch.index 81 | const esQuery = { 82 | index: buildIndex({ indexName, config }), // current index name 83 | _source_includes: includeFields ? includeFields.concat(get(config, 'urlModule.map.includeFields', [])) : [], 84 | _source_excludes: excludeFields, 85 | body: buildQuery({ value: url, config }) 86 | } 87 | 88 | try { 89 | const esResponse = await getElasticClient(config).search(esQuery) 90 | let result = get(esResponse, 'body.hits.hits[0]', null) 91 | 92 | if (result && checkFieldValueEquality({ config, result, value: req.body.url })) { 93 | result = adjustResultType({ result, config, indexName }) 94 | if (result._type === 'product') { 95 | const factory = new ProcessorFactory(config) 96 | let resultProcessor = factory.getAdapter('product', indexName, req, res) 97 | if (!resultProcessor) { 98 | resultProcessor = factory.getAdapter('default', indexName, req, res) 99 | } 100 | 101 | resultProcessor 102 | .process(esResponse.body.hits.hits, null) 103 | .then(pResult => { 104 | pResult = pResult.map(h => Object.assign(h, { _score: h._score })) 105 | return res.json(pResult[0]) 106 | }).catch((err) => { 107 | console.error(err) 108 | return res.json() 109 | }) 110 | } else { 111 | return res.json(result) 112 | } 113 | } else { 114 | return res.json(null) 115 | } 116 | } catch (err) { 117 | console.error(err) 118 | return apiStatus(res, new Error('ES search error'), 500) 119 | } 120 | }) 121 | 122 | return router 123 | } 124 | 125 | export default map 126 | -------------------------------------------------------------------------------- /src/db.js: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import * as redis from './lib/redis' 3 | import * as elastic from './lib/elastic' 4 | 5 | export default callback => { 6 | // connect to a database if needed, then pass it to `callback`: 7 | const dbContext = { 8 | getRedisClient: () => redis.getClient(config), 9 | getElasticClient: () => elastic.getClient(config) 10 | } 11 | callback(dbContext); 12 | } 13 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/attribute/resolver.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery } from './../../../lib/elastic' 6 | 7 | async function listAttributes (attributes, context, rootValue, _sourceIncludes) { 8 | let query = buildQuery({ filter: attributes, pageSize: 150, type: 'attribute' }); 9 | 10 | if (_sourceIncludes === undefined) { 11 | _sourceIncludes = config.entities.attribute.includeFields 12 | } 13 | 14 | const esQuery = { 15 | index: getIndexName(context.req.url), 16 | body: query, 17 | _sourceIncludes 18 | }; 19 | 20 | const response = await client.search(adjustQuery(esQuery, 'attribute', config)); 21 | 22 | return response.body; 23 | } 24 | 25 | const resolver = { 26 | Query: { 27 | customAttributeMetadata: (_, { attributes, _sourceInclude }, context, rootValue) => listAttributes(attributes, context, rootValue, _sourceInclude) 28 | } 29 | }; 30 | 31 | export default resolver; 32 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/attribute/schema.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | customAttributeMetadata( 3 | attributes: AttributeInput!, 4 | _sourceInclude: [String] @doc(description: "Specifies which attribute we include in result.") 5 | ): ESResponse 6 | } 7 | 8 | type CustomAttributeMetadata @doc(description: "CustomAttributeMetadata defines an array of attribute_codes and entity_types") { 9 | items: [Attribute] @doc(description: "An array of attributes") 10 | } 11 | 12 | type Attribute @doc(description: "Attribute contains the attribute_type of the specified attribute_code and entity_type") { 13 | attribute_code: String @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") 14 | entity_type: String @doc(description: "The type of entity that defines the attribute") 15 | attribute_type: String @doc(description: "The data type of the attribute") 16 | } 17 | 18 | input AttributeInput @doc(description: "AttributeInput specifies the attribute_code and entity_type to search") { 19 | attribute_code: FilterTypeInput @doc(description: "The unique identifier for an attribute code. This value should be in lowercase letters without spaces.") 20 | entity_type: String @doc(description: "The type of entity that defines the attribute") 21 | attribute_id: FilterTypeInput @doc(description: "The id of attribute. This value should be in lowercase letters without spaces.") 22 | is_user_defined: FilterTypeInput @doc(description: "THe attribute is user defined parameter.") 23 | } 24 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/catalog/processor.js: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import ProcessorFactory from '../../../processor/factory' 3 | 4 | export default function esResultsProcessor (response, esRequest, entityType, indexName) { 5 | return new Promise((resolve, reject) => { 6 | const factory = new ProcessorFactory(config) 7 | let resultProcessor = factory.getAdapter(entityType, indexName, esRequest, response) 8 | 9 | if (!resultProcessor) { 10 | resultProcessor = factory.getAdapter('default', indexName, esRequest, response) // get the default processor 11 | } 12 | 13 | resultProcessor.process(response.body.hits.hits) 14 | .then((result) => { 15 | resolve(result) 16 | }) 17 | .catch((err) => { 18 | console.error(err) 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/catalog/resolver.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import esResultsProcessor from './processor' 5 | import { getIndexName } from '../mapping' 6 | import { adjustQuery } from './../../../lib/elastic' 7 | import AttributeService from './../../../api/attribute/service' 8 | 9 | const resolver = { 10 | Query: { 11 | products: (_, { search, filter, sort, currentPage, pageSize, _sourceInclude, _sourceExclude }, context, rootValue) => 12 | list(filter, sort, currentPage, pageSize, search, context, rootValue, _sourceInclude, _sourceExclude) 13 | } 14 | }; 15 | 16 | async function list (filter, sort, currentPage, pageSize, search, context, rootValue, _sourceInclude, _sourceExclude) { 17 | let _req = { 18 | query: { 19 | _source_exclude: _sourceExclude, 20 | _source_include: _sourceInclude 21 | } 22 | } 23 | 24 | let query = buildQuery({ 25 | filter: filter, 26 | sort: sort, 27 | currentPage: currentPage, 28 | pageSize: pageSize, 29 | search: search, 30 | type: 'product' 31 | }); 32 | 33 | let esIndex = getIndexName(context.req.url) 34 | 35 | let esResponse = await client.search(adjustQuery({ 36 | index: esIndex, 37 | body: query, 38 | _sourceInclude, 39 | _sourceExclude 40 | }, 'product', config)); 41 | 42 | if (esResponse && esResponse.body.hits && esResponse.body.hits.hits) { 43 | // process response result (caluclate taxes etc...) 44 | esResponse.body.hits.hits = await esResultsProcessor(esResponse, _req, config.elasticsearch.indexTypes[0], esIndex); 45 | } 46 | 47 | let response = {} 48 | 49 | // Process hits 50 | response.items = [] 51 | esResponse.body.hits.hits.forEach(hit => { 52 | let item = hit._source 53 | item._score = hit._score 54 | response.items.push(item) 55 | }); 56 | 57 | response.total_count = esResponse.body.hits.total 58 | 59 | // Process sort 60 | let sortOptions = [] 61 | for (var sortAttribute in sort) { 62 | sortOptions.push( 63 | { 64 | label: sortAttribute, 65 | value: sortAttribute 66 | } 67 | ) 68 | } 69 | 70 | response.aggregations = esResponse.aggregations 71 | 72 | if (response.aggregations && config.entities.attribute.loadByAttributeMetadata) { 73 | const attributeListParam = AttributeService.transformAggsToAttributeListParam(response.aggregations) 74 | const attributeList = await AttributeService.list(attributeListParam, config, esIndex) 75 | response.attribute_metadata = attributeList.map(AttributeService.transformToMetadata) 76 | } 77 | 78 | response.sort_fields = {} 79 | if (sortOptions.length > 0) { 80 | response.sort_fields.options = sortOptions 81 | } 82 | 83 | response.page_info = { 84 | page_size: pageSize, 85 | current_page: currentPage 86 | } 87 | 88 | return response; 89 | } 90 | 91 | export default resolver; 92 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/category/resolver.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder' 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery } from './../../../lib/elastic' 6 | 7 | async function list (search, filter, currentPage, pageSize = 200, sort, context, rootValue, _sourceIncludes) { 8 | let query = buildQuery({ search, filter, currentPage, pageSize, sort, type: 'category' }); 9 | 10 | if (_sourceIncludes === undefined) { 11 | _sourceIncludes = config.entities.category.includeFields 12 | } 13 | 14 | const response = await client.search(adjustQuery({ 15 | index: getIndexName(context.req.url), 16 | body: query, 17 | _sourceIncludes 18 | }, 'category', config)); 19 | 20 | return response.body; 21 | } 22 | 23 | const resolver = { 24 | Query: { 25 | categories: (_, { search, filter, currentPage, pageSize, sort, _sourceInclude }, context, rootValue) => 26 | list(search, filter, currentPage, pageSize, sort, context, rootValue, _sourceInclude) 27 | } 28 | }; 29 | 30 | export default resolver; 31 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/client.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import es from '../../lib/elastic' 3 | 4 | export default es.getClient(config) 5 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/cms/resolver.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery } from './../../../lib/elastic' 6 | 7 | async function list (filter, currentPage, pageSize = 200, _sourceInclude, type, context) { 8 | let query = buildQuery({ filter, currentPage, pageSize, _sourceInclude, type }); 9 | 10 | const response = await client.search(adjustQuery({ 11 | index: getIndexName(context.req.url), 12 | body: query, 13 | _sourceInclude 14 | }, 'cms', config)); 15 | 16 | return buildItems(response.body) 17 | } 18 | 19 | function buildItems (response) { 20 | response.items = [] 21 | response.hits.hits.forEach(hit => { 22 | let item = hit._source 23 | item._score = hit._score 24 | response.items.push(item) 25 | }); 26 | 27 | return response; 28 | } 29 | 30 | const resolver = { 31 | Query: { 32 | cmsPages: (_, { filter, currentPage, pageSize, _sourceInclude, type = 'cms_page' }, context) => 33 | list(filter, currentPage, pageSize, _sourceInclude, type, context), 34 | cmsBlocks: (_, { filter, currentPage, pageSize, _sourceInclude, type = 'cms_block' }, context) => 35 | list(filter, currentPage, pageSize, _sourceInclude, type, context), 36 | cmsHierarchies: (_, { filter, currentPage, pageSize, _sourceInclude, type = 'cms_hierarchy' }, context) => 37 | list(filter, currentPage, pageSize, _sourceInclude, type, context) 38 | } 39 | }; 40 | 41 | export default resolver; 42 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/cms/schema.graphqls: -------------------------------------------------------------------------------- 1 | # Copyright © Magento, Inc. All rights reserved. 2 | # See COPYING.txt for license details. 3 | 4 | type Query { 5 | cmsPages ( 6 | filter: CmsInput @doc(description: "An array of options for search criteria") 7 | ): CmsPage @doc(description: "The CMS page query returns information about a CMS page") 8 | cmsBlocks ( 9 | filter: CmsInput @doc(description: "An array of options for search criteria") 10 | ): CmsBlocks @doc(description: "The CMS block query returns information about CMS blocks") 11 | cmsHierarchies ( 12 | filter: CmsInput @doc(description: "An array of options for search criteria") 13 | ): CmsHierarchies @doc(description: "The CMS block query returns information about CMS blocks") 14 | } 15 | 16 | input CmsInput @doc(description: "ProductFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { 17 | id: FilterTypeInput @doc(description: "Id of the CMS entity") 18 | identifier: FilterTypeInput @doc(description: "Identifiers of the CMS entity") 19 | store_id: FilterTypeInput @doc(description: "Store Id of the CMS entity") 20 | url_path: FilterTypeInput @doc(description: "The url path assigned to the cms_page") 21 | } 22 | 23 | type CmsPage @doc(description: "CMS pages information") { 24 | items: [CmsPages] @doc(description: "An array of CMS pages") 25 | } 26 | 27 | type CmsPages @doc(description: "CMS page defines all CMS page information") { 28 | page_id: Int @doc(description: "Id of CMS page") 29 | title: String @doc(description: "CMS page title") 30 | identifier: String @doc(description: "URL key of CMS page") 31 | content: String @doc(description: "CMS page content") 32 | content_heading: String @doc(description: "CMS page content heading") 33 | meta_description: String @doc(description: "CMS page meta description") 34 | meta_keywords: String @doc(description: "CMS page meta keywords") 35 | store_id: Int @doc(description: "Store Id of CMS page") 36 | url_path: String @doc(description: "CMS page meta keywords") 37 | } 38 | 39 | type CmsBlocks @doc(description: "CMS blocks information") { 40 | items: [CmsBlock] @doc(description: "An array of CMS blocks") 41 | } 42 | 43 | type CmsBlock @doc(description: "CMS block defines all CMS block information") { 44 | identifier: String @doc(description: "CMS block identifier") 45 | id: Int @doc(description: "CMS block identifier") 46 | title: String @doc(description: "CMS block title") 47 | content: String @doc(description: "CMS block content") 48 | creation_time: String @doc(description: "Timestamp indicating when the CMS block was created") 49 | store_id: Int @doc(description: "Store Id of CMS block") 50 | } 51 | 52 | type CmsHierarchies @doc(description: "CMS hierarchies information") { 53 | items: [CmsHierarchy] @doc(description: "An array of CMS hierarchies") 54 | } 55 | 56 | type CmsHierarchy { 57 | node_id: Int @doc(description: "Node Id of the CMS hierarchy") 58 | parent_node_id: Int @doc(description: "Parent node Id of the CMS hierarchy node") 59 | page_id: Int @doc(description: "Id of the CMS page") 60 | identifier: String @doc(description: "Identifier of the CMS hierarchy node") 61 | label: String @doc(description: "Label of CMS hierarchy node") 62 | level: Int @doc(description: "Level of CMS hierarchy node") 63 | request_url: String @doc(description: "Request URL of CMS hierarchy node") 64 | xpath: String @doc(description: "XPATH of CMS hierarchy node") 65 | store_id: Int @doc(description: "Store Id of CMS hierarchy node") 66 | } 67 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/json_type/resolver.js: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql'; 2 | import { Kind } from 'graphql/language'; 3 | 4 | function identity (value) { 5 | return value; 6 | } 7 | 8 | function parseLiteral (ast, variables) { 9 | switch (ast.kind) { 10 | case Kind.STRING: 11 | case Kind.BOOLEAN: 12 | return ast.value; 13 | case Kind.INT: 14 | case Kind.FLOAT: 15 | return parseFloat(ast.value); 16 | case Kind.OBJECT: { 17 | const value = Object.create(null); 18 | ast.fields.forEach(field => { 19 | value[field.name.value] = parseLiteral(field.value, variables); 20 | }); 21 | 22 | return value; 23 | } 24 | case Kind.LIST: 25 | return ast.values.map(n => parseLiteral(n, variables)); 26 | case Kind.NULL: 27 | return null; 28 | case Kind.VARIABLE: { 29 | const name = ast.name.value; 30 | return variables ? variables[name] : undefined; 31 | } 32 | default: 33 | return undefined; 34 | } 35 | } 36 | 37 | const resolver = { 38 | JSON: new GraphQLScalarType({ 39 | name: 'JSON', 40 | description: 41 | 'The `JSON` scalar type represents JSON values as specified by ' + 42 | '[ECMA-404](http://www.ecma-international.org/' + 43 | 'publications/files/ECMA-ST/ECMA-404.pdf).', 44 | serialize: identity, 45 | parseValue: identity, 46 | parseLiteral 47 | }) 48 | }; 49 | 50 | export default resolver; 51 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/mapping.js: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | 3 | export function getIndexName (url) { 4 | const parseURL = url.replace(/^\/+|\/+$/g, ''); 5 | let urlParts = parseURL.split('/'); 6 | let esIndex = config.elasticsearch.indices[0] 7 | 8 | if (urlParts.length >= 1 && urlParts[0] !== '' && urlParts[0] !== '?') { 9 | esIndex = config.storeViews[urlParts[0]].elasticsearch.index 10 | } 11 | 12 | return esIndex 13 | } 14 | 15 | export default function getMapping (attribute, entityType = 'product') { 16 | let mapping = [ 17 | ] 18 | 19 | if (typeof config.entities[entityType].filterFieldMapping !== 'undefined') { 20 | mapping = config.entities[entityType].filterFieldMapping 21 | } 22 | 23 | if (typeof mapping[attribute] !== 'undefined') { 24 | return mapping[attribute] 25 | } 26 | 27 | return attribute 28 | } 29 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/queryBuilder.ts: -------------------------------------------------------------------------------- 1 | import bodybuilder from 'bodybuilder'; 2 | import { elasticsearch, ElasticsearchQueryConfig } from 'storefront-query-builder' 3 | import config from 'config' 4 | 5 | export function buildQuery ({ 6 | filter = [], 7 | sort = '', 8 | currentPage = 1, 9 | pageSize = 10, 10 | search = '', 11 | type = 'product' 12 | }) { 13 | let queryChain = bodybuilder(); 14 | elasticsearch.buildQueryBodyFromFilterObject({ config: (config as ElasticsearchQueryConfig), queryChain, filter, search }) 15 | queryChain = elasticsearch.applySort({ sort, queryChain }); 16 | queryChain = queryChain.from((currentPage - 1) * pageSize).size(pageSize); 17 | 18 | let builtQuery = queryChain.build() 19 | if (search !== '') { 20 | builtQuery['min_score'] = config.get('elasticsearch.min_score') 21 | } 22 | return builtQuery; 23 | } 24 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/review/resolver.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery } from './../../../lib/elastic' 6 | 7 | async function list (search, filter, currentPage, pageSize = 200, sort, context, rootValue, _sourceInclude) { 8 | let query = buildQuery({ search, filter, currentPage, pageSize, sort, type: 'review' }); 9 | 10 | const response = await client.search(adjustQuery({ 11 | index: getIndexName(context.req.url), 12 | body: query, 13 | _sourceInclude 14 | }, 'review', config)); 15 | 16 | return response.body; 17 | } 18 | 19 | const resolver = { 20 | Query: { 21 | reviews: (_, { search, filter, currentPage, pageSize, sort, _sourceInclude }, context, rootValue) => 22 | list(search, filter, currentPage, pageSize, sort, context, rootValue, _sourceInclude) 23 | } 24 | }; 25 | 26 | export default resolver; 27 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/review/schema.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | reviews ( 3 | search: String @doc(description: "Performs a full-text search using the specified key words."), 4 | filter: ReviewFilterInput @doc(description: "An array of categories that match the specified search criteria"), 5 | pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. This attribute is optional."), 6 | currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1."), 7 | _sourceInclude: [String] @doc(description: "Specifies which attribute we include in result.") 8 | ): ESResponse 9 | } 10 | 11 | input ReviewFilterInput @doc(description: "ReviewFilterInput defines the filters to be used in the search. A filter contains at least one attribute, a comparison operator, and the value that is being searched for.") { 12 | review_status: FilterTypeInput @doc(description: "Review satus") 13 | product_id: FilterTypeInput @doc(description: "An ID that uniquely identifies the parent product") 14 | } 15 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/taxrule/resolver.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery } from './../../../lib/elastic' 6 | 7 | async function taxrule (filter, context, rootValue) { 8 | let query = buildQuery({ filter, pageSize: 150, type: 'taxrule' }); 9 | 10 | const response = await client.search(adjustQuery({ 11 | index: getIndexName(context.req.url), 12 | body: query 13 | }, 'taxrule', config)); 14 | 15 | return response.body; 16 | } 17 | 18 | const resolver = { 19 | Query: { 20 | taxrule: (_, { filter }, context, rootValue) => taxrule(filter, context, rootValue) 21 | } 22 | }; 23 | 24 | export default resolver; 25 | -------------------------------------------------------------------------------- /src/graphql/elasticsearch/taxrule/schema.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | taxrule(filter: TaxRuleInput): ESResponse 3 | } 4 | 5 | input TaxRuleInput @doc(description: "TaxRuleInput specifies the tax rules information to search") { 6 | id: FilterTypeInput @doc(description: "An ID that uniquely identifies the tax rule") 7 | code: FilterTypeInput @doc(description: "The unique identifier for an tax rule. This value should be in lowercase letters without spaces.") 8 | priority: FilterTypeInput @doc(description: "Priority of the tax rule") 9 | position: FilterTypeInput @doc(description: "Position of the tax rule") 10 | customer_tax_class_ids: FilterTypeInput @doc(description: "Cunstomer tax class ids of the tax rule") 11 | product_tax_class_ids: FilterTypeInput @doc(description: "Products tax class ids of the tax rule") 12 | tax_rate_ids: FilterTypeInput @doc(description: "Tax rates ids of the tax rule") 13 | calculate_subtotal: FilterTypeInput @doc(description: "Calculating subtotals of the tax rule") 14 | rates: FilterTypeInput @doc(description: "Rates of the tax rule") 15 | } 16 | -------------------------------------------------------------------------------- /src/graphql/resolvers.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import config from 'config'; 3 | import { fileLoader, mergeResolvers } from 'merge-graphql-schemas'; 4 | 5 | const coreResolvers = fileLoader( 6 | path.join(__dirname, `./${config.server.searchEngine}/**/resolver.js`) 7 | ); 8 | const extensionsResolvers = fileLoader( 9 | path.join(__dirname, `../api/extensions/**/resolver.js`) 10 | ); 11 | const resolversArray = coreResolvers.concat(extensionsResolvers) 12 | 13 | export default mergeResolvers(resolversArray, { all: true }); 14 | -------------------------------------------------------------------------------- /src/graphql/schema.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import config from 'config'; 3 | import { fileLoader, mergeTypes } from 'merge-graphql-schemas'; 4 | 5 | const coreSchemas = fileLoader( 6 | path.join(__dirname, `./${config.server.searchEngine}/**/*.graphqls`) 7 | ); 8 | const extensionsSchemas = fileLoader( 9 | path.join(__dirname, `../api/extensions/**/*.graphqls`) 10 | ); 11 | const typesArray = coreSchemas.concat(extensionsSchemas) 12 | 13 | export default mergeTypes(typesArray, { all: true }); 14 | -------------------------------------------------------------------------------- /src/helpers/loadAdditionalCertificates.ts: -------------------------------------------------------------------------------- 1 | import syswidecas from 'syswide-cas' 2 | import * as fs from 'fs'; 3 | 4 | const CERTS_DIRECTORY = 'config/certs' 5 | 6 | /** 7 | * load certificates from certs directory and consider them trusted 8 | */ 9 | export const loadAdditionalCertificates = () => { 10 | if (fs.existsSync(CERTS_DIRECTORY)) { 11 | syswidecas.addCAs(CERTS_DIRECTORY); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/helpers/loadCustomFilters.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | export default async function loadModuleCustomFilters (config: Record, type: string = 'catalog'): Promise { 4 | let filters: any = {} 5 | let filterPromises: Promise[] = [] 6 | 7 | for (const mod of config.registeredExtensions) { 8 | if (config.extensions.hasOwnProperty(mod) && config.extensions[mod].hasOwnProperty(type + 'Filter') && Array.isArray(config.extensions[mod][type + 'Filter'])) { 9 | const moduleFilter = config.extensions[mod][type + 'Filter'] 10 | const dirPath = [__dirname, '../api/extensions/' + mod + '/filter/', type] 11 | for (const filterName of moduleFilter) { 12 | const filePath = path.resolve(...dirPath, filterName) 13 | filterPromises.push( 14 | import(filePath) 15 | .then(module => { 16 | filters[filterName] = module.default 17 | }) 18 | .catch(e => { 19 | console.log(e) 20 | }) 21 | ) 22 | } 23 | } 24 | } 25 | 26 | return Promise.all(filterPromises).then((e) => filters) 27 | } 28 | -------------------------------------------------------------------------------- /src/helpers/priceTiers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default not logged user grouped ID 3 | * @type {number} 4 | */ 5 | const NotLoggedUserGroupId = 0; 6 | 7 | /** 8 | * Update product final price 9 | * 10 | * @param productData 11 | * @param groupId 12 | * @returns {*} 13 | */ 14 | function updatePrices (productData, groupId) { 15 | if (productData.tier_prices && productData.tier_prices.length) { 16 | for (let i = productData.tier_prices.length - 1; i >= 0; i--) { 17 | const tier = productData.tier_prices[i]; 18 | // Check group 19 | 20 | if (tier.customer_group_id === groupId) { 21 | if (tier.qty === 1) { 22 | productData.specialPriceInclTax = 0; 23 | 24 | if (productData.price > tier.value) { 25 | productData.price = tier.value 26 | } 27 | } 28 | } else { 29 | productData.tier_prices.splice(i, 1) 30 | } 31 | } 32 | } 33 | 34 | return productData; 35 | } 36 | 37 | /** 38 | * Set price by tier and reduce tiers 39 | * 40 | * @param productData 41 | * @param groupId 42 | * @returns {*} 43 | */ 44 | export default (productData, groupId) => { 45 | groupId = groupId || NotLoggedUserGroupId; 46 | 47 | if (productData.type_id === 'configurable') { 48 | const children = productData.configurable_children; 49 | 50 | if (children) { 51 | for (let i = children.length - 1; i >= 0; i--) { 52 | const child = children[i]; 53 | updatePrices(child, groupId); 54 | 55 | if (child.price < productData.price) { 56 | productData.price = child.price; 57 | } 58 | } 59 | } 60 | } else { 61 | updatePrices(productData, groupId); 62 | } 63 | 64 | return productData; 65 | } 66 | -------------------------------------------------------------------------------- /src/image/action/abstract/index.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express' 2 | import URL from 'url' 3 | 4 | export default abstract class ImageAction { 5 | public readonly SUPPORTED_ACTIONS = ['fit', 'resize', 'identify'] 6 | public readonly SUPPORTED_MIMETYPES 7 | 8 | public req: Request 9 | public res: Response 10 | public next: NextFunction 11 | public options 12 | public mimeType: string 13 | 14 | public constructor (req: Request, res: Response, next: NextFunction, options) { 15 | this.req = req 16 | this.res = res 17 | this.next = next 18 | this.options = options 19 | } 20 | 21 | abstract getOption(): void 22 | 23 | abstract validateOptions(): void 24 | 25 | abstract getImageURL(): string 26 | 27 | abstract get whitelistDomain(): string[] 28 | 29 | abstract validateMIMEType(): void 30 | 31 | abstract prossesImage(): void 32 | 33 | public isImageSourceHostAllowed () { 34 | if (!this._isUrlWhitelisted(this.getImageURL(), 'allowedHosts', true, this.whitelistDomain)) { 35 | return this.res.status(400).send({ 36 | code: 400, 37 | result: `Host is not allowed` 38 | }) 39 | } 40 | } 41 | 42 | public _isUrlWhitelisted (url, whitelistType, defaultValue, whitelist) { 43 | if (arguments.length !== 4) throw new Error('params are not optional!') 44 | 45 | if (whitelist && whitelist.hasOwnProperty(whitelistType)) { 46 | const requestedHost = URL.parse(url).host 47 | 48 | const matches = whitelist[whitelistType].map(allowedHost => { 49 | allowedHost = allowedHost instanceof RegExp ? allowedHost : new RegExp(allowedHost) 50 | return !!requestedHost.match(allowedHost) 51 | }) 52 | return matches.indexOf(true) > -1 53 | } else { 54 | return defaultValue 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/image/action/factory.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { NextFunction, Request, Response } from 'express' 4 | import { IConfig } from 'config'; 5 | 6 | export default class ActionFactory { 7 | public request: Request 8 | public next: NextFunction 9 | public response: Response 10 | public config: IConfig 11 | 12 | public constructor (req: Request, res, next, app_config) { 13 | this.request = req 14 | this.response = res 15 | this.next = next 16 | this.config = app_config; 17 | } 18 | 19 | public getAdapter (type: string): any { 20 | let AdapterClass = require(`./${type}`).default 21 | if (!AdapterClass) { 22 | throw new Error(`Invalid adapter ${type}`); 23 | } else { 24 | let adapter_instance = new AdapterClass(this.request, this.response, this.next, this.config); 25 | if ((typeof adapter_instance.isValidFor === 'function') && !adapter_instance.isValidFor(type)) { throw new Error(`Not valid adapter class or adapter is not valid for ${type}`); } 26 | return adapter_instance; 27 | } 28 | } 29 | } 30 | 31 | export { 32 | ActionFactory 33 | }; 34 | -------------------------------------------------------------------------------- /src/image/action/local/index.ts: -------------------------------------------------------------------------------- 1 | import ImageAction from '../abstract' 2 | import mime from 'mime-types' 3 | import { downloadImage, fit, identify, resize } from '../../../lib/image' 4 | 5 | export default class LocalImageAction extends ImageAction { 6 | public imageOptions 7 | public SUPPORTED_MIMETYPES = ['image/gif', 'image/png', 'image/jpeg', 'image/webp', 'image/svg+xml'] 8 | public imageBuffer: Buffer 9 | 10 | public get whitelistDomain (): string[] { 11 | return this.options.imageable.whitelist 12 | } 13 | 14 | public get maxAgeForResponse () { 15 | return 365.25 * 86400 16 | } 17 | 18 | public getImageURL (): string { 19 | return this.imageOptions.imgUrl 20 | } 21 | 22 | public getOption () { 23 | let imgUrl: string 24 | let width: number 25 | let height: number 26 | let action: string 27 | if (this.req.query.url) { // url provided as the query param 28 | imgUrl = decodeURIComponent(this.req.query.url as string) 29 | width = parseInt(this.req.query.width as string) 30 | height = parseInt(this.req.query.height as string) 31 | action = this.req.query.action as string 32 | } else { 33 | let urlParts = this.req.url.split('/') 34 | width = parseInt(urlParts[1]) 35 | height = parseInt(urlParts[2]) 36 | action = urlParts[3] 37 | imgUrl = `${this.options[this.options.platform].imgUrl}/${urlParts.slice(4).join('/')}` // full original image url 38 | if (urlParts.length < 5) { 39 | this.res.status(400).send({ 40 | code: 400, 41 | result: 'Please provide following parameters: /img/////' 42 | }) 43 | this.next() 44 | } 45 | } 46 | 47 | this.imageOptions = { 48 | imgUrl, 49 | width, 50 | height, 51 | action 52 | } 53 | } 54 | 55 | public validateOptions () { 56 | const { width, height, action } = this.imageOptions 57 | if (isNaN(width) || isNaN(height) || !this.SUPPORTED_ACTIONS.includes(action)) { 58 | return this.res.status(400).send({ 59 | code: 400, 60 | result: 'Please provide following parameters: /img///// OR ?url=&width=&height=&action=' 61 | }) 62 | } 63 | 64 | if (width > this.options.imageable.imageSizeLimit || width < 0 || height > this.options.imageable.imageSizeLimit || height < 0) { 65 | return this.res.status(400).send({ 66 | code: 400, 67 | result: `Width and height must have a value between 0 and ${this.options.imageable.imageSizeLimit}` 68 | }) 69 | } 70 | } 71 | 72 | public validateMIMEType () { 73 | const mimeType = mime.lookup(this.imageOptions.imgUrl) 74 | 75 | if (mimeType === false || !this.SUPPORTED_MIMETYPES.includes(mimeType)) { 76 | return this.res.status(400).send({ 77 | code: 400, 78 | result: 'Unsupported file type' 79 | }) 80 | } 81 | 82 | this.mimeType = mimeType 83 | } 84 | 85 | public async prossesImage () { 86 | const { imgUrl } = this.imageOptions 87 | 88 | try { 89 | this.imageBuffer = await downloadImage(imgUrl) 90 | } catch (err) { 91 | return this.res.status(400).send({ 92 | code: 400, 93 | result: `Unable to download the requested image ${imgUrl}` 94 | }) 95 | } 96 | const { action, width, height } = this.imageOptions 97 | switch (action) { 98 | case 'resize': 99 | this.imageBuffer = await resize(this.imageBuffer, width, height) 100 | break 101 | case 'fit': 102 | this.imageBuffer = await fit(this.imageBuffer, width, height) 103 | break 104 | case 'identify': 105 | this.imageBuffer = await identify(this.imageBuffer) 106 | break 107 | default: 108 | throw new Error('Unknown action') 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/image/cache/abstract/index.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | 3 | export default abstract class ImageCache { 4 | public image: Buffer 5 | public config 6 | public req: Request 7 | public key: string 8 | 9 | public constructor (config, req) { 10 | this.config = config 11 | this.req = req 12 | this.key = this.createKey() 13 | } 14 | 15 | abstract getImageFromCache() 16 | 17 | abstract save() 18 | 19 | abstract check() 20 | 21 | abstract createKey(): string 22 | 23 | abstract isValidFor(type: string): boolean 24 | } 25 | 26 | interface Cache { 27 | image: Buffer, 28 | config: any, 29 | req: Request, 30 | key: string, 31 | new(config, req: Request), 32 | getImageFromCache(): void, 33 | save(): void, 34 | check(): void, 35 | createKey(): string, 36 | isValidFor(type: string): boolean 37 | } 38 | 39 | export { 40 | // eslint-disable-next-line no-undef 41 | Cache, 42 | ImageCache 43 | } 44 | -------------------------------------------------------------------------------- /src/image/cache/factory.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Request } from 'express' 4 | import { IConfig } from 'config' 5 | import ImageCache, { Cache } from './abstract' 6 | 7 | export default class CacheFactory { 8 | private request: Request 9 | private config: IConfig 10 | 11 | public constructor (app_config: IConfig, req: Request) { 12 | this.config = app_config 13 | this.request = req 14 | } 15 | 16 | public getAdapter (type: string, ...constructorParams): any { 17 | let AdapterClass: Cache = require(`./${type}`).default 18 | if (!AdapterClass) { 19 | throw new Error(`Invalid adapter ${type}`) 20 | } else { 21 | const adapterInstance: ImageCache = new AdapterClass(this.config, this.request) 22 | if ((typeof adapterInstance.isValidFor === 'function') && !adapterInstance.isValidFor(type)) { throw new Error(`Not valid adapter class or adapter is not valid for ${type}`) } 23 | return adapterInstance 24 | } 25 | } 26 | } 27 | 28 | export { 29 | CacheFactory 30 | } 31 | -------------------------------------------------------------------------------- /src/image/cache/file/index.ts: -------------------------------------------------------------------------------- 1 | import ImageCache from '../abstract' 2 | import fs from 'fs-extra' 3 | import { createHash } from 'crypto' 4 | 5 | export default class FileImageCache extends ImageCache { 6 | public async getImageFromCache () { 7 | this.image = await fs.readFile( 8 | `${this.config.imageable.caching.file.path}/${this.path}` 9 | ) 10 | } 11 | 12 | public async save () { 13 | await fs.outputFile( 14 | `${this.config.imageable.caching.file.path}/${this.path}`, 15 | this.image 16 | ) 17 | } 18 | 19 | public async check () { 20 | const response = await fs.pathExists(`${this.config.imageable.caching.file.path}/${this.path}`) 21 | return response 22 | } 23 | 24 | private get path (): string { 25 | return `${this.key.substring(0, 2)}/${this.key.substring(2, 4)}/${this.key}` 26 | } 27 | 28 | public createKey (): string { 29 | console.log(createHash('md5').update(this.req.url).digest('hex')) 30 | return createHash('md5').update(this.req.url).digest('hex') 31 | } 32 | 33 | public isValidFor (type) { 34 | return type === 'file' 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/image/cache/google-cloud-storage/index.ts: -------------------------------------------------------------------------------- 1 | import ImageCache from '../abstract' 2 | import { createHash } from 'crypto' 3 | import { Bucket, Storage } from '@google-cloud/storage' 4 | export default class GoogleCloudStorageImageCache extends ImageCache { 5 | private static storage: Storage 6 | private static bucket: Bucket 7 | 8 | public constructor (config, req) { 9 | super(config, req) 10 | if (GoogleCloudStorageImageCache.storage === undefined) { 11 | GoogleCloudStorageImageCache.storage = new Storage( 12 | this.moduleConfig.libraryOptions 13 | ) 14 | } 15 | if (GoogleCloudStorageImageCache.bucket === undefined) { 16 | GoogleCloudStorageImageCache.bucket = GoogleCloudStorageImageCache.storage.bucket(this.bucketName) 17 | } 18 | } 19 | 20 | public get bucketName (): string { 21 | return this.moduleConfig.bucket 22 | } 23 | 24 | public get moduleConfig (): any { 25 | return this.config.imageable.caching[`google-cloud-storage`] 26 | } 27 | 28 | public async getImageFromCache () { 29 | const donwload = await GoogleCloudStorageImageCache.bucket.file('testing/cache/image/' + this.key).download() 30 | this.image = donwload[0] 31 | } 32 | 33 | public async save () { 34 | await GoogleCloudStorageImageCache.bucket.file('testing/cache/image/' + this.key).save(this.image, { 35 | gzip: true 36 | }) 37 | } 38 | 39 | public async check () { 40 | const response = await GoogleCloudStorageImageCache.bucket.file('testing/cache/image/' + this.key).exists() 41 | return response[0] 42 | } 43 | 44 | public createKey (): string { 45 | return createHash('md5').update(this.req.url).digest('hex') 46 | } 47 | 48 | public isValidFor (type) { 49 | return type === 'google-cloud-storage' 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import morgan from 'morgan'; 4 | import bodyParser from 'body-parser'; 5 | import initializeDb from './db'; 6 | import middleware from './middleware'; 7 | import { loadAdditionalCertificates } from './helpers/loadAdditionalCertificates' 8 | import api from './api'; 9 | import config from 'config'; 10 | import img from './api/img'; 11 | import invalidateCache from './api/invalidate' 12 | import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'; 13 | import { makeExecutableSchema } from 'graphql-tools'; 14 | import resolvers from './graphql/resolvers'; 15 | import typeDefs from './graphql/schema'; 16 | import * as path from 'path' 17 | 18 | const app = express(); 19 | 20 | // logger 21 | app.use(morgan('dev')); 22 | 23 | app.use('/media', express.static(path.join(__dirname, config.get(`${config.get('platform')}.assetPath`)))) 24 | 25 | // 3rd party middleware 26 | app.use(cors({ 27 | exposedHeaders: config.get('corsHeaders') 28 | })); 29 | 30 | app.use(bodyParser.json({ 31 | limit: config.get('bodyLimit') 32 | })); 33 | 34 | loadAdditionalCertificates() 35 | 36 | // connect to db 37 | initializeDb(db => { 38 | // internal middleware 39 | app.use(middleware({ config, db })); 40 | 41 | // api router 42 | app.use('/api', api({ config, db })); 43 | app.use('/img', img({ config, db })); 44 | app.use('/img/:width/:height/:action/:image', (req, res, next) => { 45 | console.log(req.params) 46 | }); 47 | app.post('/invalidate', invalidateCache) 48 | app.get('/invalidate', invalidateCache) 49 | 50 | const port = process.env.PORT || config.get('server.port') 51 | const host = process.env.HOST || config.get('server.host') 52 | app.listen(parseInt(port), host, () => { 53 | console.log(`Vue Storefront API started at http://${host}:${port}`); 54 | }); 55 | }); 56 | 57 | // graphQl Server part 58 | const schema = makeExecutableSchema({ 59 | typeDefs, 60 | resolvers 61 | }); 62 | 63 | app.use(bodyParser.urlencoded({ extended: true })); 64 | app.use(bodyParser.json()); 65 | 66 | app.use('/graphql', graphqlExpress(req => ({ 67 | schema, 68 | context: { req: req }, 69 | rootValue: global 70 | }))); 71 | 72 | app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); 73 | 74 | app.use((err, req, res, next) => { 75 | const { statusCode = 500, message = '', stack = '' } = err; 76 | const stackTrace = stack 77 | .split(/\r?\n/) 78 | .map(string => string.trim()) 79 | .filter(string => string !== '') 80 | 81 | res.status(statusCode).json({ 82 | code: statusCode, 83 | result: message, 84 | ...(config.get('server.showErrorStack') ? { stack: stackTrace } : {}) 85 | }); 86 | }); 87 | 88 | export default app; 89 | -------------------------------------------------------------------------------- /src/lib/boost.js: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | 3 | export default function getBoosts (attribute = '') { 4 | let boosts = [ 5 | ] 6 | 7 | if (config.boost) { 8 | boosts = config.boost 9 | } 10 | 11 | if (boosts.hasOwnProperty(attribute)) { 12 | return boosts[attribute] 13 | } 14 | 15 | return 1 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/cache-instance.js: -------------------------------------------------------------------------------- 1 | const TagCache = require('redis-tag-cache').default 2 | const config = require('config') 3 | 4 | if (config.server.useOutputCache) { 5 | const redisConfig = Object.assign(config.redis) 6 | module.exports = new TagCache({ 7 | redis: redisConfig, 8 | defaultTimeout: config.server.outputCacheDefaultTtl 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/countrymapper.js: -------------------------------------------------------------------------------- 1 | function mapCountryRegion (countryList, countryId, regionCode) { 2 | let regionId = 0 3 | for (let country of countryList) { 4 | if (country.id === countryId) { 5 | if (country.available_regions && country.available_regions.length > 0) { 6 | for (let region of country.available_regions) { 7 | if (region.code === regionCode) { 8 | return { regionId: region.id, regionCode: region.code } 9 | } 10 | } 11 | } 12 | } 13 | } 14 | return { regionId: regionId, regionCode: '' } 15 | } 16 | 17 | module.exports = { 18 | mapCountryRegion 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/image.js: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | import rp from 'request-promise-native'; 3 | import config from 'config'; 4 | 5 | sharp.cache(config.imageable.cache); 6 | sharp.concurrency(config.imageable.concurrency); 7 | sharp.counters(config.imageable.counters); 8 | sharp.simd(config.imageable.simd); 9 | 10 | export async function downloadImage (url) { 11 | const response = await rp.get(url, { encoding: null }); 12 | return response 13 | } 14 | 15 | export async function identify (buffer) { 16 | try { 17 | const transformer = sharp(buffer); 18 | 19 | return transformer.metadata(); 20 | } catch (err) { 21 | console.log(err); 22 | } 23 | } 24 | 25 | export async function resize (buffer, width, height) { 26 | try { 27 | const transformer = sharp(buffer); 28 | 29 | if (width || height) { 30 | const options = { 31 | withoutEnlargement: true, 32 | fit: sharp.fit.inside 33 | } 34 | transformer.resize(width, height, options) 35 | } 36 | 37 | return transformer.toBuffer(); 38 | } catch (err) { 39 | console.log(err); 40 | } 41 | } 42 | 43 | export async function fit (buffer, width, height) { 44 | try { 45 | const transformer = sharp(buffer); 46 | 47 | if (width || height) { 48 | transformer.resize(width, height, { fit: sharp.fit.cover }); 49 | } 50 | 51 | return transformer.toBuffer(); 52 | } catch (err) { 53 | console.log(err); 54 | } 55 | } 56 | 57 | export async function crop (buffer, width, height, x, y) { 58 | try { 59 | const transformer = sharp(buffer); 60 | 61 | if (width || height || x || y) { 62 | transformer.extract({ left: x, top: y, width, height }); 63 | } 64 | 65 | return transformer.toBuffer(); 66 | } catch (err) { 67 | console.log(err); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/redis.js: -------------------------------------------------------------------------------- 1 | import Redis from 'redis' 2 | 3 | /** 4 | * Return Redis Client 5 | * @param {config} config 6 | */ 7 | export function getClient (config) { 8 | let redisClient = Redis.createClient(config.redis); // redis client 9 | redisClient.on('error', (err) => { // workaround for https://github.com/NodeRedis/node_redis/issues/713 10 | redisClient = Redis.createClient(config.redis); // redis client 11 | }); 12 | if (config.redis.auth) { 13 | redisClient.auth(config.redis.auth); 14 | } 15 | return redisClient 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/test/unit/boost.spec.ts: -------------------------------------------------------------------------------- 1 | import getBoosts from '../../boost' 2 | 3 | describe('getBoosts method', () => { 4 | describe('with empty boost config', () => { 5 | beforeEach(() => { 6 | jest.mock('config', () => ({})) 7 | }) 8 | 9 | it('Should return 1', () => { 10 | const result = getBoosts('color'); 11 | expect(result).toEqual(1); 12 | }); 13 | }) 14 | 15 | describe('with boost config', () => { 16 | beforeEach(() => { 17 | jest.mock('config', () => ({ 18 | boost: { 19 | name: 3 20 | } 21 | })) 22 | }) 23 | 24 | it('color not in config and should be 1', () => { 25 | const result = getBoosts('color'); 26 | expect(result).toEqual(1); 27 | }); 28 | 29 | it('name is in config and should be 3', () => { 30 | const result = getBoosts('name'); 31 | expect(result).toEqual(3); 32 | }); 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /src/lib/util.js: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import crypto from 'crypto'; 3 | const algorithm = 'aes-256-ctr'; 4 | 5 | /** 6 | * Get current store code from parameter passed from the vue storefront frotnend app 7 | * @param {Express.Request} req 8 | */ 9 | export function getCurrentStoreCode (req) { 10 | if (req.headers['x-vs-store-code']) { 11 | return req.headers['x-vs-store-code'] 12 | } 13 | if (req.query.storeCode) { 14 | return req.query.storeCode 15 | } 16 | return null 17 | } 18 | 19 | /** 20 | * Get the config.storeViews[storeCode] 21 | * @param {string} storeCode 22 | */ 23 | export function getCurrentStoreView (storeCode = null) { 24 | let storeView = { // current, default store 25 | tax: config.tax, 26 | i18n: config.i18n, 27 | elasticsearch: config.elasticsearch, 28 | storeCode: null, 29 | storeId: config.defaultStoreCode && config.defaultStoreCode !== '' ? config.storeViews[config.defaultStoreCode].storeId : 1 30 | } 31 | if (storeCode && config.storeViews[storeCode]) { 32 | storeView = config.storeViews[storeCode] 33 | } 34 | return storeView // main config is used as default storeview 35 | } 36 | 37 | /** Creates a callback that proxies node callback style arguments to an Express Response object. 38 | * @param {express.Response} res Express HTTP Response 39 | * @param {number} [status=200] Status code to send on success 40 | * 41 | * @example 42 | * list(req, res) { 43 | * collection.find({}, toRes(res)); 44 | * } 45 | */ 46 | export function toRes (res, status = 200) { 47 | return (err, thing) => { 48 | if (err) return res.status(500).send(err); 49 | 50 | if (thing && typeof thing.toObject === 'function') { 51 | thing = thing.toObject(); 52 | } 53 | res.status(status).json(thing); 54 | }; 55 | } 56 | 57 | export function sgnSrc (sgnObj, item) { 58 | if (config.tax.alwaysSyncPlatformPricesOver) { 59 | sgnObj.id = item.id 60 | } else { 61 | sgnObj.sku = item.sku 62 | } 63 | // console.log(sgnObj) 64 | return sgnObj 65 | } 66 | 67 | /** Creates a api status call and sends it thru to Express Response object. 68 | * @param {express.Response} res Express HTTP Response 69 | * @param {number} [code=200] Status code to send on success 70 | * @param {json} [result='OK'] Text message or result information object 71 | */ 72 | export function apiStatus (res, result = 'OK', code = 200, meta = null) { 73 | let apiResult = { code: code, result: result }; 74 | if (meta !== null) { 75 | apiResult.meta = meta; 76 | } 77 | res.status(code).json(apiResult); 78 | return result; 79 | } 80 | 81 | /** 82 | * Creates an error for API status of Express Response object. 83 | * 84 | * @param {express.Response} res Express HTTP Response 85 | * @param {object|string} error Error object or error message 86 | * @return {json} [result='OK'] Text message or result information object 87 | */ 88 | export function apiError (res, error) { 89 | let errorCode = error.code || error.status; 90 | let errorMessage = error.errorMessage || error; 91 | if (error instanceof Error) { 92 | // Class 'Error' is not serializable with JSON.stringify, extract data explicitly. 93 | errorCode = error.code || errorCode; 94 | errorMessage = error.message; 95 | } 96 | return apiStatus(res, errorMessage, Number(errorCode) || 500); 97 | } 98 | 99 | export function encryptToken (textToken, secret) { 100 | const cipher = crypto.createCipher(algorithm, secret) 101 | let crypted = cipher.update(textToken, 'utf8', 'hex') 102 | crypted += cipher.final('hex'); 103 | return crypted; 104 | } 105 | 106 | export function decryptToken (textToken, secret) { 107 | const decipher = crypto.createDecipher(algorithm, secret) 108 | let dec = decipher.update(textToken, 'hex', 'utf8') 109 | dec += decipher.final('utf8'); 110 | return dec; 111 | } 112 | 113 | export function getToken (req) { 114 | return config.users.tokenInHeader 115 | ? (req.headers.authorization || '').replace('Bearer ', '') 116 | : req.query.token 117 | } 118 | -------------------------------------------------------------------------------- /src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { json } from 'body-parser'; 3 | import { NextHandleFunction } from 'connect'; 4 | import { IConfig } from 'config'; 5 | 6 | export default ({ config, db }: { config: IConfig, db: CallableFunction }): [ NextHandleFunction, Router ] => { 7 | let routes: Router = Router(); 8 | let bp: NextHandleFunction = json(); 9 | return [ bp, routes ]; 10 | } 11 | -------------------------------------------------------------------------------- /src/models/.gitignore: -------------------------------------------------------------------------------- 1 | *.extension.json -------------------------------------------------------------------------------- /src/models/catalog-category.md: -------------------------------------------------------------------------------- 1 | Category model 2 | ============== 3 | 4 | Categories are stored in ElasticSearch repository in the following format: 5 | 6 | ```json 7 | { 8 | "_index":"magento2_default_catalog_category_20170817_075531", 9 | "_type":"category", 10 | "_id":"8", 11 | "_version":1, 12 | "_score":1, 13 | "_source":{ 14 | "entity_id":"8", 15 | "attribute_set_id":"3", 16 | "parent_id":"7", 17 | "created_at":"2017-04-06 15:42:48", 18 | "updated_at":"2017-07-12 08:24:10", 19 | "path":"1/2/8", 20 | "position":"143", 21 | "level":"3", 22 | "children_count":"0", 23 | "erp_id":"1074115879", 24 | "is_active":"1", 25 | "name":[ 26 | "pojzady" 27 | ], 28 | "url_path":[ 29 | "Zabawki/pojzady-3848" 30 | ] 31 | } 32 | } 33 | ``` -------------------------------------------------------------------------------- /src/models/catalog-product.md: -------------------------------------------------------------------------------- 1 | Product model 2 | ============= 3 | 4 | Products are stored in ElasticSearch repository in the following format: 5 | 6 | ```json 7 | { 8 | "_index":"magento2_default_catalog_product_20170817_112451", 9 | "_type":"product", 10 | "_id":"8", 11 | "_version":1, 12 | "_score":1, 13 | "_source":{ 14 | "entity_id":"8", 15 | "attribute_set_id":"10", 16 | "type_id":"simple", 17 | "sku":"6347471", 18 | "has_options":"0", 19 | "required_options":"0", 20 | "created_at":"2017-04-06 15:55:49", 21 | "updated_at":"2017-04-06 15:55:49", 22 | "visibility":"4", 23 | "price":[ 24 | { 25 | "price":"6.0000", 26 | "original_price":"6.9000", 27 | "is_discount":true, 28 | "customer_group_id":"0" 29 | }, 30 | { 31 | "price":"6.0000", 32 | "original_price":"6.9000", 33 | "is_discount":true, 34 | "customer_group_id":"1" 35 | }, 36 | { 37 | "price":"6.0000", 38 | "original_price":"6.9000", 39 | "is_discount":true, 40 | "customer_group_id":"4" 41 | }, 42 | { 43 | "price":"6.0000", 44 | "original_price":"6.9000", 45 | "is_discount":true, 46 | "customer_group_id":"5" 47 | } 48 | ], 49 | "category":[ 50 | { 51 | "category_id":2 52 | }, 53 | { 54 | "category_id":4, 55 | "is_parent":true, 56 | "name":"Książki" 57 | }, 58 | { 59 | "category_id":34, 60 | "is_parent":true, 61 | "name":"dla dzieci" 62 | }, 63 | { 64 | "category_id":249, 65 | "is_parent":true, 66 | "name":" 4-8 lat" 67 | } 68 | ], 69 | "name":[ 70 | "W kosmosie" 71 | ], 72 | "image":[ 73 | "/9/9/99906347471.jpg" 74 | ], 75 | "ean":[ 76 | "9788374373319" 77 | ], 78 | "pkwiu":[ 79 | "58.11.13" 80 | ], 81 | "availability":[ 82 | "1" 83 | ], 84 | "isbn":[ 85 | "978-83-7437-331-9" 86 | ], 87 | "authors_es":[ 88 | "_24819_,_24819_,_24819_" 89 | ], 90 | "publishers_es":[ 91 | "_62024_,_62024_,_62024_" 92 | ], 93 | "producers_es":[ 94 | "_62024_,_62024_,_62024_" 95 | ], 96 | "status":[ 97 | 1 98 | ], 99 | "option_text_status":[ 100 | "Enabled" 101 | ], 102 | "tax_class_id":[ 103 | 2 104 | ], 105 | "option_text_tax_class_id":[ 106 | "Taxable Goods" 107 | ], 108 | "description":[ 109 | "Opowieści dla ciekawskich świata, dla małych odkrywców i podróżników. Dziecko dowie się: kogo ratuje pogotowie, gdzie na akcję wyjeżdża straż pożarna i policja, jak podróżować koleją, po co ludzie lecą w kosmos i dlaczego warto zostać marynarzem. Na kartach książeczki maluch odnajdzie wiele niespodzianek i ciekawostek, a wesołe i mądre rymowanki zachęcą go do zabawy." 110 | ], 111 | "short_description":[ 112 | "Opowieści dla ciekawskich świata, dla małych odkrywców i podróżników. Dziecko dowie się: kogo ratuje pogotowie, gdzie na akcję wyjeżdża straż pożarna i policja, jak podróżować koleją, po co ludzie lecą w kosmos i dlaczego warto zostać marynarzem...." 113 | ], 114 | "special_price":[ 115 | 6 116 | ], 117 | "stock":{ 118 | "is_in_stock":false, 119 | "qty":0 120 | } 121 | } 122 | } 123 | ``` -------------------------------------------------------------------------------- /src/models/order.md: -------------------------------------------------------------------------------- 1 | Order model 2 | ============= 3 | 4 | Orders are queued - the format is very similar to the original Magento2 json format for the following API method: 5 | 6 | ``` 7 | curl -g -X POST "/rest/V1/guest-carts/56241bf6bc084cd7589426c8754fc9c5/shipping-information" \ 8 | ``` 9 | 10 | The difference is that for vue-storefront order format MUST be 100% self-sufficient. So we merged the cart items reference from: 11 | 12 | ``` 13 | curl -g -X GET "/rest/V1/guest-carts/e3e1fd447e0e315dd761942cf949ce5d/items" 14 | ``` 15 | 16 | So the format is as following: 17 | 18 | ```json 19 | { 20 | "items": [ 21 | { 22 | "item_id": 100, 23 | "sku": "abc-1", 24 | "qty": 1, 25 | "name": "Product one", 26 | "price": 19, 27 | "product_type": "simple", 28 | "quote_id": "e3e1fd447e0e315dd761942cf949ce5d" 29 | }, 30 | { 31 | "item_id": 101, 32 | "sku": "abc-2", 33 | "qty": 1, 34 | "name": "Product two", 35 | "price": 54, 36 | "product_type": "simple", 37 | "quote_id": "e3e1fd447e0e315dd761942cf949ce5d" 38 | } 39 | ], 40 | "addressInformation": { 41 | "shippingAddress": { 42 | "region": "MH", 43 | "region_id": 0, 44 | "country_id": "PL", 45 | "street": [ 46 | "Street name line no 1", 47 | "Street name line no 2" 48 | ], 49 | "company": "Company name", 50 | "telephone": "0048 123 123 123", 51 | "postcode": "00123", 52 | "city": "Cityname", 53 | "firstname": "John ", 54 | "lastname": "Doe", 55 | "email": "john@doe.com", 56 | "region_code": "MH", 57 | "sameAsBilling": 1 58 | }, 59 | "billingAddress": { 60 | "region": "MH", 61 | "region_id": 0, 62 | "country_id": "PL", 63 | "street": [ 64 | "Street name line no 1", 65 | "Street name line no 2" 66 | ], 67 | "company": "abc", 68 | "telephone": "1111111", 69 | "postcode": "00123", 70 | "city": "Mumbai", 71 | "firstname": "Sameer", 72 | "lastname": "Sawant", 73 | "email": "john@doe.com", 74 | "prefix": "address_", 75 | "region_code": "MH" 76 | }, 77 | "shipping_method_code": "flatrate", 78 | "shipping_carrier_code": "flatrate", 79 | "payment_method_code": "flatrate" 80 | } 81 | } 82 | ``` -------------------------------------------------------------------------------- /src/models/order.schema.extension.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/models/review.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "required": [ 4 | "review" 5 | ], 6 | "properties": { 7 | "review": { 8 | "required": [ 9 | "product_id", 10 | "title", 11 | "detail", 12 | "nickname", 13 | "review_entity", 14 | "review_status" 15 | ], 16 | "properties": { 17 | "product_id": { 18 | "type": "integer" 19 | }, 20 | "title": { 21 | "type": "string" 22 | }, 23 | "detail": { 24 | "type": "string" 25 | }, 26 | "nickname": { 27 | "type": "string" 28 | }, 29 | "review_entity": { 30 | "type": "string" 31 | }, 32 | "review_status": { 33 | "type": "integer" 34 | }, 35 | "customer_id": { 36 | "type": ["integer", "null"] 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/models/userProfile.schema.extension.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/models/userProfile.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": false, 3 | "required": ["customer"], 4 | "properties": { 5 | "customer": { 6 | "additionalProperties": true, 7 | "required": [ 8 | "email", 9 | "firstname", 10 | "lastname" 11 | ], 12 | "properties": { 13 | "email": { 14 | "type": "string", 15 | "pattern": "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?" 16 | }, 17 | "firstname": { 18 | "type": "string", 19 | "pattern": "[a-zA-Z]+" 20 | }, 21 | "lastname": { 22 | "type": "string", 23 | "pattern": "[a-zA-Z]+" 24 | }, 25 | "website_id": { 26 | "type": "integer" 27 | }, 28 | "addresses": { 29 | "maxItems": 2, 30 | "items": { 31 | "required": [ 32 | "firstname", 33 | "lastname", 34 | "street", 35 | "city", 36 | "country_id", 37 | "postcode" 38 | ], 39 | "properties": { 40 | "firstname": { 41 | "type": "string", 42 | "pattern": "[a-zA-Z]+" 43 | }, 44 | "lastname": { 45 | "type": "string", 46 | "pattern": "[a-zA-Z]+" 47 | }, 48 | "street": { 49 | "minItems": 2, 50 | "items": { 51 | "type": "string", 52 | "minLength": 1 53 | } 54 | }, 55 | "city": { 56 | "type": "string" 57 | }, 58 | "region": { 59 | "required": ["region"], 60 | "properties": { 61 | "region": { 62 | "type": ["string", "null"] 63 | } 64 | } 65 | }, 66 | "country_id": { 67 | "type": "string", 68 | "minLength": 2, 69 | "pattern": "[A-Z]+" 70 | }, 71 | "postcode": { 72 | "type": "string", 73 | "minLength": 3 74 | }, 75 | "company": { 76 | "type": "string", 77 | "minLength": 1 78 | }, 79 | "vat_id": { 80 | "type": "string", 81 | "minLength": 3 82 | }, 83 | "telephone": { 84 | "type": "string" 85 | }, 86 | "default_billing": { 87 | "type": "boolean" 88 | }, 89 | "default_shipping": { 90 | "type": "boolean" 91 | } 92 | } 93 | } 94 | }, 95 | "custom_attributes": { 96 | "maxItems": 5, 97 | "items": { 98 | "required": [ 99 | "attribute_code", 100 | "value" 101 | ], 102 | "properties": { 103 | "attribute_code": { 104 | "type": "string", 105 | "minLength": 1 106 | }, 107 | "value": { 108 | "type": ["string", "null"] 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/models/userProfileUpdate.schema.extension.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/models/userProfileUpdate.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": false, 3 | "required": ["customer"], 4 | "properties": { 5 | "customer": { 6 | "additionalProperties": false, 7 | "required": [ 8 | "email", 9 | "firstname", 10 | "lastname" 11 | ], 12 | "properties": { 13 | "email": { 14 | "type": "string", 15 | "pattern": "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?" 16 | }, 17 | "firstname": { 18 | "type": "string", 19 | "pattern": "[a-zA-Z]+" 20 | }, 21 | "lastname": { 22 | "type": "string", 23 | "pattern": "[a-zA-Z]+" 24 | }, 25 | "addresses": { 26 | "maxItems": 2, 27 | "items": { 28 | "required": [ 29 | "firstname", 30 | "lastname", 31 | "street", 32 | "city", 33 | "country_id", 34 | "postcode" 35 | ], 36 | "properties": { 37 | "firstname": { 38 | "type": "string", 39 | "pattern": "[a-zA-Z]+" 40 | }, 41 | "lastname": { 42 | "type": "string", 43 | "pattern": "[a-zA-Z]+" 44 | }, 45 | "street": { 46 | "minItems": 2, 47 | "items": { 48 | "type": "string", 49 | "minLength": 1 50 | } 51 | }, 52 | "city": { 53 | "type": "string" 54 | }, 55 | "region": { 56 | "required": ["region"], 57 | "properties": { 58 | "region": { 59 | "type": ["string", "null"] 60 | } 61 | } 62 | }, 63 | "country_id": { 64 | "type": "string", 65 | "minLength": 2, 66 | "pattern": "[A-Z]+" 67 | }, 68 | "postcode": { 69 | "type": "string", 70 | "minLength": 3 71 | }, 72 | "company": { 73 | "type": "string", 74 | "minLength": 1 75 | }, 76 | "vat_id": { 77 | "type": "string", 78 | "minLength": 3 79 | }, 80 | "telephone": { 81 | "type": "string" 82 | }, 83 | "default_billing": { 84 | "type": "boolean" 85 | }, 86 | "default_shipping": { 87 | "type": "boolean" 88 | } 89 | } 90 | } 91 | }, 92 | "custom_attributes": { 93 | "maxItems": 5, 94 | "items": { 95 | "required": [ 96 | "attribute_code", 97 | "value" 98 | ], 99 | "properties": { 100 | "attribute_code": { 101 | "type": "string", 102 | "minLength": 1 103 | }, 104 | "value": { 105 | "type": ["string", "null"] 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/models/userRegister.schema.extension.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /src/models/userRegister.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "additionalProperties": true, 3 | "required": [ 4 | "customer", 5 | "password" 6 | ], 7 | "properties": { 8 | "customer": { 9 | "required": [ 10 | "email", 11 | "firstname", 12 | "lastname" 13 | ], 14 | "properties": { 15 | "email": { 16 | "type": "string", 17 | "pattern": "[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?" 18 | }, 19 | "firstname": { 20 | "type": "string", 21 | "pattern": "[a-zA-Z]+" 22 | }, 23 | "lastname": { 24 | "type": "string", 25 | "pattern": "[a-zA-Z]+" 26 | }, 27 | "addresses": { 28 | "maxItems": 2, 29 | "items": { 30 | "required": [ 31 | "firstname", 32 | "lastname", 33 | "street", 34 | "city", 35 | "country_id", 36 | "postcode" 37 | ], 38 | "properties": { 39 | "firstname": { 40 | "type": "string", 41 | "pattern": "[a-zA-Z]+" 42 | }, 43 | "lastname": { 44 | "type": "string", 45 | "pattern": "[a-zA-Z]+" 46 | }, 47 | "street": { 48 | "minItems": 2, 49 | "items": { 50 | "type": "string", 51 | "minLength": 1 52 | } 53 | }, 54 | "city": { 55 | "type": "string" 56 | }, 57 | "region": { 58 | "required": ["region"], 59 | "properties": { 60 | "region": { 61 | "type": ["string", "null"] 62 | } 63 | } 64 | }, 65 | "country_id": { 66 | "type": "string", 67 | "minLength": 2, 68 | "pattern": "[A-Z]+" 69 | }, 70 | "postcode": { 71 | "type": "string", 72 | "minLength": 3 73 | }, 74 | "telephone": { 75 | "type": "string" 76 | }, 77 | "default_shipping": { 78 | "type": "boolean" 79 | } 80 | } 81 | } 82 | } 83 | } 84 | }, 85 | "password": { 86 | "type": "string" 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/platform/abstract/address.js: -------------------------------------------------------------------------------- 1 | class AbstractAddressProxy { 2 | constructor (config, req) { 3 | this._config = config 4 | this._request = req 5 | } 6 | 7 | list (customerToken) { 8 | throw new Error('AbstractAddressProxy::list must be implemented for specific platform') 9 | } 10 | update (customerToken, addressData) { 11 | throw new Error('AbstractAddressProxy::update must be implemented for specific platform') 12 | } 13 | get (customerToken, addressId) { 14 | throw new Error('AbstractAddressProxy::get must be implemented for specific platform') 15 | } 16 | delete (customerToken, addressData) { 17 | throw new Error('AbstractAddressProxy::delete must be implemented for specific platform') 18 | } 19 | } 20 | 21 | export default AbstractAddressProxy 22 | -------------------------------------------------------------------------------- /src/platform/abstract/cart.js: -------------------------------------------------------------------------------- 1 | class AbstractCartProxy { 2 | /** 3 | * 4 | * @param {*} customerToken 5 | * 6 | * @returns { 7 | * "code": 200, 8 | * "result": "a7b8e47aef108a8d0731c368a603a9af" <-- cart id 9 | * } 10 | */ 11 | create (customerToken) { 12 | } 13 | 14 | update (customerToken, cartId, cartItem) { 15 | } 16 | 17 | applyCoupon (customerToken, cartId, coupon) { 18 | } 19 | 20 | deleteCoupon (customerToken, cartId) { 21 | } 22 | 23 | getCoupon (customerToken, cartId) { 24 | } 25 | 26 | delete (customerToken, cartId, cartItem) { 27 | } 28 | 29 | pull (customerToken, cartId, params) { 30 | } 31 | 32 | totals (customerToken, cartId, params) { 33 | } 34 | } 35 | 36 | module.exports = AbstractCartProxy 37 | -------------------------------------------------------------------------------- /src/platform/abstract/contact.js: -------------------------------------------------------------------------------- 1 | class AbstractContactProxy { 2 | submit (formData) { 3 | throw new Error('AbstractContactProxy::check must be implemented for specific platform') 4 | } 5 | } 6 | 7 | module.exports = AbstractContactProxy 8 | -------------------------------------------------------------------------------- /src/platform/abstract/newsletter.js: -------------------------------------------------------------------------------- 1 | class AbstractNewsletterProxy { 2 | subscribe (emailAddress) { 3 | } 4 | 5 | unsubscribe (customerToken) { 6 | } 7 | } 8 | 9 | module.exports = AbstractNewsletterProxy 10 | -------------------------------------------------------------------------------- /src/platform/abstract/order.js: -------------------------------------------------------------------------------- 1 | class AbstractOrderProxy { 2 | create (orderData) { 3 | } 4 | } 5 | 6 | module.exports = AbstractOrderProxy 7 | -------------------------------------------------------------------------------- /src/platform/abstract/product.js: -------------------------------------------------------------------------------- 1 | class AbstractProductProxy { 2 | constructor (config, req) { 3 | this._config = config 4 | this._request = req 5 | } 6 | 7 | list (skus) { 8 | throw new Error('ProductProxy::list must be implemented for specific platform') 9 | } 10 | } 11 | 12 | export default AbstractProductProxy 13 | -------------------------------------------------------------------------------- /src/platform/abstract/review.js: -------------------------------------------------------------------------------- 1 | class AbstractReviewProxy { 2 | constructor (config, req) { 3 | this._config = config 4 | this._request = req 5 | } 6 | 7 | create (reviewData) { 8 | throw new Error('ReviewProxy::check must be implemented for specific platform') 9 | } 10 | } 11 | 12 | export default AbstractReviewProxy 13 | -------------------------------------------------------------------------------- /src/platform/abstract/stock.js: -------------------------------------------------------------------------------- 1 | class AbstractStockProxy { 2 | constructor (config, req) { 3 | this._config = config 4 | this._request = req 5 | } 6 | 7 | /** 8 | * 9 | * EXAMPLE INPUT: 10 | * ?sku= 11 | * 12 | * EXAMPLE OUTPUT: 13 | * 14 | * { 15 | * "item_id": 14, 16 | * "product_id": 14, 17 | * "stock_id": 1, 18 | * "qty": 100, 19 | * "is_in_stock": true, 20 | * "is_qty_decimal": false, 21 | * "show_default_notification_message": false, 22 | * "use_config_min_qty": true, 23 | * "min_qty": 0, 24 | * "use_config_min_sale_qty": 1, 25 | * "min_sale_qty": 1, 26 | * "use_config_max_sale_qty": true, 27 | * "max_sale_qty": 10000, 28 | * "use_config_backorders": true, 29 | * "backorders": 0, 30 | * "use_config_notify_stock_qty": true, 31 | * "notify_stock_qty": 1, 32 | * "use_config_qty_increments": true, 33 | * "qty_increments": 0, 34 | * "use_config_enable_qty_inc": true, 35 | * "enable_qty_increments": false, 36 | * "use_config_manage_stock": true, 37 | * "manage_stock": true, 38 | * "low_stock_date": null, 39 | * "is_decimal_divided": false, 40 | * "stock_status_changed_auto": 0 41 | * } 42 | * 43 | */ 44 | check (sku) { 45 | throw new Error('UserProxy::check must be implemented for specific platform') 46 | } 47 | } 48 | 49 | export default AbstractStockProxy 50 | -------------------------------------------------------------------------------- /src/platform/abstract/stock_alert.js: -------------------------------------------------------------------------------- 1 | class AbstractStockAlertProxy { 2 | constructor (config, req) { 3 | this._config = config 4 | this._request = req 5 | } 6 | subscribe (customerToken, productId, emailAddress) { 7 | throw new Error('AbstractContactProxy::subscribe must be implemented for specific platform') 8 | } 9 | } 10 | 11 | export default AbstractStockAlertProxy 12 | -------------------------------------------------------------------------------- /src/platform/abstract/tax.js: -------------------------------------------------------------------------------- 1 | class AbstractTaxProxy { 2 | constructor (config, req) { 3 | this._config = config 4 | this._request = req 5 | } 6 | 7 | taxFor (product) { 8 | throw new Error('TaxProxy::taxFor must be implemented for specific platform') 9 | } 10 | 11 | /** 12 | * @param Array productList 13 | * @returns Promise 14 | */ 15 | process (productList, groupId = null) { 16 | throw new Error('TaxProxy::process must be implemented for specific platform') 17 | } 18 | } 19 | 20 | export default AbstractTaxProxy 21 | -------------------------------------------------------------------------------- /src/platform/abstract/user.js: -------------------------------------------------------------------------------- 1 | class AbstractUserProxy { 2 | constructor (config, req) { 3 | this._config = config 4 | this._request = req 5 | } 6 | 7 | /** 8 | * 9 | * EXAMPLE INPUT: 10 | * { 11 | * "customer": { 12 | * "email": "jfoe@vuestorefront.io", 13 | * "firstname": "Jon", 14 | * "lastname": "Foe" 15 | * }, 16 | * "password": "!@#foearwato" 17 | * } 18 | * 19 | * EXAMPLE OUTPUT: 20 | * 21 | * { 22 | * "code": 200, 23 | * "result": { 24 | * "id": 3, 25 | * "group_id": 1, 26 | * "created_at": "2017-11-28 19:22:51", 27 | * "updated_at": "2017-11-28 19:22:51", 28 | * "created_in": "Default Store View", 29 | * "email": "pkarwatka@divante.pl", 30 | * "firstname": "Piotr", 31 | * "lastname": "Karwatka", 32 | * "store_id": 1, 33 | * "website_id": 1, 34 | * "addresses": [], 35 | * "disable_auto_group_change": 0 36 | * } 37 | * } 38 | * @param {*} userData 39 | */ 40 | register (userData) { 41 | throw new Error('UserProxy::register must be implemented for specific platform') 42 | } 43 | 44 | /** 45 | * EXAMPLE INPUT: 46 | * 47 | * { 48 | * "username": "pkarwatka@divante.pl", 49 | * "password": "********" 50 | * } 51 | * 52 | * EXAMPLE OUTPUT: 53 | * { 54 | * "code": 200, 55 | * "result": "3tx80s4f0rhkoonqe4ifcoloktlw9glo" 56 | * } 57 | */ 58 | login (userData) { 59 | throw new Error('UserProxy::login must be implemented for specific platform') 60 | } 61 | 62 | /** 63 | * EXAMPLE INPUT: 64 | * - just provide an consumer token from login method 65 | * 66 | * EXAMPLE OUTPUT: 67 | * 68 | * { 69 | * "code": 200, 70 | * "result": { 71 | * "id": 3, 72 | * "group_id": 1, 73 | * "created_at": "2017-11-28 19:22:51", 74 | * "updated_at": "2017-11-28 20:01:17", 75 | * "created_in": "Default Store View", 76 | * "email": "pkarwatka@divante.pl", 77 | * "firstname": "Piotr", 78 | * "lastname": "Karwatka", 79 | * "store_id": 1, 80 | * "website_id": 1, 81 | * "addresses": [], 82 | * "disable_auto_group_change": 0 83 | * } 84 | * } 85 | * 86 | * } requestToken 87 | */ 88 | me (requestToken) { 89 | throw new Error('UserProxy::me must be implemented for specific platform') 90 | } 91 | orderHistory (requestToken) { 92 | throw new Error('UserProxy::orderHistory must be implemented for specific platform') 93 | } 94 | resetPassword (emailData) { 95 | throw new Error('UserProxy::resetPassword must be implemented for specific platform') 96 | } 97 | } 98 | 99 | export default AbstractUserProxy 100 | -------------------------------------------------------------------------------- /src/platform/abstract/wishlist.js: -------------------------------------------------------------------------------- 1 | class AbstractWishlistProxy { 2 | pull (customerToken) { 3 | throw new Error('AbstractWishlistProxy::pull must be implemented for specific platform') 4 | } 5 | update (customerToken, wishListItem) { 6 | throw new Error('AbstractWishlistProxy::update must be implemented for specific platform') 7 | } 8 | delete (customerToken, wishListItem) { 9 | throw new Error('AbstractWishlistProxy::delete must be implemented for specific platform') 10 | } 11 | } 12 | 13 | module.exports = AbstractWishlistProxy 14 | -------------------------------------------------------------------------------- /src/platform/factory.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Request } from 'express'; 4 | import { IConfig } from 'config'; 5 | 6 | class PlatformFactory { 7 | private request: Request 8 | private config: IConfig 9 | 10 | public constructor (app_config: IConfig, req: Request|null = null) { 11 | this.config = app_config; 12 | this.request = req 13 | } 14 | 15 | public getAdapter (platform: string, type: string, ...constructorParams): any { 16 | let AdapterClass = require(`./${platform}/${type}`); 17 | if (!AdapterClass) { 18 | throw new Error(`Invalid adapter ${platform} / ${type}`); 19 | } else { 20 | let adapter_instance = new AdapterClass(this.config, this.request, ...constructorParams); 21 | if ((typeof adapter_instance.isValidFor === 'function') && !adapter_instance.isValidFor(type)) { throw new Error(`Not valid adapter class or adapter is not valid for ${type}`); } 22 | return adapter_instance; 23 | } 24 | } 25 | } 26 | 27 | export default PlatformFactory; 28 | -------------------------------------------------------------------------------- /src/platform/magento1/address.js: -------------------------------------------------------------------------------- 1 | import AbstractAddressProxy from '../abstract/address' 2 | import {multiStoreConfig} from './util'; 3 | 4 | class AddressProxy extends AbstractAddressProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | list (customerToken) { 11 | return this.api.address.list(customerToken) 12 | } 13 | update (customerToken, addressData) { 14 | return this.api.address.update(customerToken, addressData); 15 | } 16 | get (customerToken, addressId) { 17 | return this.api.address.get(customerToken, addressId) 18 | } 19 | delete (customerToken, addressData) { 20 | return this.api.address.delete(customerToken, addressData) 21 | } 22 | } 23 | 24 | module.exports = AddressProxy 25 | -------------------------------------------------------------------------------- /src/platform/magento1/cart.js: -------------------------------------------------------------------------------- 1 | import AbstractCartProxy from '../abstract/cart'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class CartProxy extends AbstractCartProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | create (customerToken) { 11 | return this.api.cart.create(customerToken); 12 | } 13 | update (customerToken, cartId, cartItem) { 14 | return this.api.cart.update(customerToken, cartId, cartItem); 15 | } 16 | delete (customerToken, cartId, cartItem) { 17 | return this.api.cart.delete(customerToken, cartId, cartItem); 18 | } 19 | pull (customerToken, cartId, params) { 20 | return this.api.cart.pull(customerToken, cartId, params); 21 | } 22 | totals (customerToken, cartId, params) { 23 | return this.api.cart.totals(customerToken, cartId, params); 24 | } 25 | getShippingMethods (customerToken, cartId, address) { 26 | return this.api.cart.shippingMethods(customerToken, cartId, address); 27 | } 28 | getPaymentMethods (customerToken, cartId) { 29 | return this.api.cart.paymentMethods(customerToken, cartId); 30 | } 31 | setShippingInformation (customerToken, cartId, address) { 32 | return this.api.cart.shippingInformation(customerToken, cartId, address); 33 | } 34 | collectTotals (customerToken, cartId, shippingMethod) { 35 | return this.api.cart.collectTotals(customerToken, cartId, shippingMethod); 36 | } 37 | applyCoupon (customerToken, cartId, coupon) { 38 | return this.api.cart.applyCoupon(customerToken, cartId, coupon); 39 | } 40 | deleteCoupon (customerToken, cartId) { 41 | return this.api.cart.deleteCoupon(customerToken, cartId); 42 | } 43 | getCoupon (customerToken, cartId) { 44 | return this.api.cart.getCoupon(customerToken, cartId); 45 | } 46 | } 47 | 48 | module.exports = CartProxy; 49 | -------------------------------------------------------------------------------- /src/platform/magento1/contact.js: -------------------------------------------------------------------------------- 1 | import AbstractContactProxy from '../abstract/contact'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class ContactProxy extends AbstractContactProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | submit (form) { 11 | return this.api.contact.submit(form); 12 | } 13 | } 14 | 15 | module.exports = ContactProxy; 16 | -------------------------------------------------------------------------------- /src/platform/magento1/newsletter.js: -------------------------------------------------------------------------------- 1 | import AbstractNewsletterProxy from '../abstract/newsletter'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class NewsletterProxy extends AbstractNewsletterProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | subscribe (emailAddress) { 11 | return this.api.newsletter.subscribe(emailAddress); 12 | } 13 | unsubscribe (customerToken) { 14 | return this.api.newsletter.unsubscribe(customerToken); 15 | } 16 | } 17 | 18 | module.exports = NewsletterProxy; 19 | -------------------------------------------------------------------------------- /src/platform/magento1/order.js: -------------------------------------------------------------------------------- 1 | import AbstractOrderProxy from '../abstract/order' 2 | import { multiStoreConfig } from './util' 3 | 4 | class OrderProxy extends AbstractOrderProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | 11 | create (orderData) { 12 | return this.api.order.create(orderData); 13 | } 14 | } 15 | 16 | module.exports = OrderProxy 17 | -------------------------------------------------------------------------------- /src/platform/magento1/stock.js: -------------------------------------------------------------------------------- 1 | import AbstractStockProxy from '../abstract/stock'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class StockProxy extends AbstractStockProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | check (data) { 11 | return this.api.stock.check(data.sku); 12 | } 13 | } 14 | 15 | module.exports = StockProxy; 16 | -------------------------------------------------------------------------------- /src/platform/magento1/stock_alert.js: -------------------------------------------------------------------------------- 1 | import AbstractStockAlertProxy from '../abstract/stock_alert'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class StockAlertProxy extends AbstractStockAlertProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | subscribe (customerToken, productId, emailAddress) { 11 | return this.api.stockAlert.subscribe(customerToken, productId, emailAddress); 12 | } 13 | } 14 | 15 | module.exports = StockAlertProxy; 16 | -------------------------------------------------------------------------------- /src/platform/magento1/tax.js: -------------------------------------------------------------------------------- 1 | import AbstractTaxProxy from '../abstract/tax' 2 | import { calculateProductTax, checkIfTaxWithUserGroupIsActive, getUserGroupIdToUse } from 'vsf-utilities' 3 | import TierHelper from '../../helpers/priceTiers' 4 | import bodybuilder from 'bodybuilder' 5 | import es from '../../lib/elastic' 6 | 7 | class TaxProxy extends AbstractTaxProxy { 8 | constructor (config, entityType, indexName, taxCountry, taxRegion = '', sourcePriceInclTax = null, finalPriceInclTax = null) { 9 | super(config) 10 | this._entityType = entityType 11 | this._indexName = indexName 12 | this._sourcePriceInclTax = sourcePriceInclTax 13 | this._finalPriceInclTax = finalPriceInclTax 14 | this._userGroupId = this._config.tax.userGroupId 15 | this._storeConfigTax = this._config.tax 16 | 17 | if (this._config.storeViews && this._config.storeViews.multistore) { 18 | for (let storeCode in this._config.storeViews) { 19 | const store = this._config.storeViews[storeCode] 20 | 21 | if (typeof store === 'object') { 22 | if (store.elasticsearch && store.elasticsearch.index) { // workaround to map stores 23 | if (store.elasticsearch.index === indexName) { 24 | taxRegion = store.tax.defaultRegion 25 | taxCountry = store.tax.defaultCountry 26 | sourcePriceInclTax = store.tax.sourcePriceIncludesTax || null 27 | finalPriceInclTax = store.tax.finalPriceIncludesTax || null 28 | this._storeConfigTax = store.tax 29 | break; 30 | } 31 | } 32 | } 33 | } 34 | } else { 35 | if (!taxRegion) { 36 | taxRegion = this._config.tax.defaultRegion 37 | } 38 | if (!taxCountry) { 39 | taxCountry = this._config.tax.defaultCountry 40 | } 41 | } 42 | if (sourcePriceInclTax == null) { 43 | sourcePriceInclTax = this._config.tax.sourcePriceIncludesTax 44 | } 45 | if (finalPriceInclTax == null) { 46 | finalPriceInclTax = this._config.tax.finalPriceIncludesTax 47 | } 48 | this._deprecatedPriceFieldsSupport = this._config.tax.deprecatedPriceFieldsSupport 49 | this._taxCountry = taxCountry 50 | this._taxRegion = taxRegion 51 | this._sourcePriceInclTax = sourcePriceInclTax 52 | this._finalPriceInclTax = finalPriceInclTax 53 | this.taxFor = this.taxFor.bind(this) 54 | } 55 | 56 | taxFor (product, groupId) { 57 | return calculateProductTax({ 58 | product, 59 | taxClasses: this._taxClasses, 60 | taxCountry: this._taxCountry, 61 | taxRegion: this._taxRegion, 62 | sourcePriceInclTax: this._sourcePriceInclTax, 63 | deprecatedPriceFieldsSupport: this._deprecatedPriceFieldsSupport, 64 | finalPriceInclTax: this._finalPriceInclTax, 65 | userGroupId: groupId, 66 | isTaxWithUserGroupIsActive: checkIfTaxWithUserGroupIsActive(this._storeConfigTax) && typeof groupId === 'number' 67 | }) 68 | } 69 | 70 | applyTierPrices (productList, groupId) { 71 | if (this._config.usePriceTiers) { 72 | for (let item of productList) { 73 | TierHelper(item._source, groupId) 74 | } 75 | } 76 | } 77 | 78 | process (productList, groupId = null) { 79 | const inst = this 80 | return new Promise((resolve, reject) => { 81 | inst.applyTierPrices(productList, groupId) 82 | 83 | if (this._config.tax.calculateServerSide) { 84 | const client = es.getClient(this._config) 85 | const esQuery = es.adjustQuery({ 86 | index: this._indexName, 87 | body: bodybuilder() 88 | }, 'taxrule', this._config) 89 | 90 | client.search(esQuery).then((result) => { // we're always trying to populate cache - when online 91 | inst._taxClasses = es.getHits(result).map(el => { return el._source }) 92 | for (let item of productList) { 93 | const isActive = checkIfTaxWithUserGroupIsActive(inst._storeConfigTax) 94 | if (isActive) { 95 | groupId = getUserGroupIdToUse(inst._userGroupId, inst._storeConfigTax) 96 | } else { 97 | groupId = null 98 | } 99 | 100 | inst.taxFor(item._source, groupId) 101 | } 102 | 103 | resolve(productList) 104 | }).catch(err => { 105 | reject(err) 106 | }) 107 | } else { 108 | resolve(productList) 109 | } 110 | }) 111 | } 112 | } 113 | 114 | module.exports = TaxProxy 115 | -------------------------------------------------------------------------------- /src/platform/magento1/user.js: -------------------------------------------------------------------------------- 1 | import AbstractUserProxy from '../abstract/user' 2 | import { multiStoreConfig } from './util' 3 | 4 | class UserProxy extends AbstractUserProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | register (userData) { 11 | return this.api.user.create(userData) 12 | } 13 | login (userData) { 14 | return this.api.user.login(userData) 15 | } 16 | me (customerToken) { 17 | return this.api.user.me(customerToken) 18 | } 19 | orderHistory (customerToken, page, pageSize) { 20 | return this.api.user.orderHistory(customerToken, page, pageSize) 21 | } 22 | creditValue (customerToken) { 23 | return this.api.user.creditValue(customerToken) 24 | } 25 | refillCredit (customerToken, creditCode) { 26 | return this.api.user.refillCredit(customerToken, creditCode) 27 | } 28 | resetPassword (emailData) { 29 | return this.api.user.resetPassword(emailData) 30 | } 31 | update (userData) { 32 | return this.api.user.update(userData) 33 | } 34 | changePassword (passwordData) { 35 | return this.api.user.changePassword(passwordData) 36 | } 37 | resetPasswordUsingResetToken (resetData) { 38 | return this.api.user.resetPasswordUsingResetToken(resetData) 39 | } 40 | } 41 | 42 | module.exports = UserProxy 43 | -------------------------------------------------------------------------------- /src/platform/magento1/util.js: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import { getCurrentStoreCode } from '../../lib/util' 3 | /** 4 | * Adjust the config provided to the current store selected via request params 5 | * @param Object config configuration 6 | * @param Express request req 7 | */ 8 | export function multiStoreConfig (apiConfig, req) { 9 | let confCopy = Object.assign({}, apiConfig) 10 | let storeCode = getCurrentStoreCode(req) 11 | if (storeCode && config.availableStores.indexOf(storeCode) >= 0) { 12 | if (config.magento1['api_' + storeCode]) { 13 | confCopy = Object.assign({}, config.magento1['api_' + storeCode]) // we're to use the specific api configuration - maybe even separate magento instance 14 | } else { 15 | if (new RegExp('/(' + config.availableStores.join('|') + ')/', 'gm').exec(confCopy.url) === null) { 16 | confCopy.url = (confCopy.url).replace(/(vsbridge)/gm, `${storeCode}/$1`); 17 | } 18 | } 19 | } else { 20 | if (storeCode) { 21 | console.error('Unavailable store code', storeCode) 22 | } 23 | } 24 | 25 | return confCopy 26 | } 27 | -------------------------------------------------------------------------------- /src/platform/magento1/wishlist.js: -------------------------------------------------------------------------------- 1 | import AbstractWishlistProxy from '../abstract/wishlist'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class WishlistProxy extends AbstractWishlistProxy { 5 | constructor (config, req) { 6 | const Magento1Client = require('magento1-vsbridge-client').Magento1Client; 7 | super(config, req) 8 | this.api = Magento1Client(multiStoreConfig(config.magento1.api, req)); 9 | } 10 | pull (customerToken) { 11 | return this.api.wishlist.pull(customerToken); 12 | } 13 | update (customerToken, wishListItem) { 14 | return this.api.wishlist.update(customerToken, wishListItem); 15 | } 16 | delete (customerToken, wishListItem) { 17 | return this.api.wishlist.delete(customerToken, wishListItem); 18 | } 19 | } 20 | 21 | module.exports = WishlistProxy; 22 | -------------------------------------------------------------------------------- /src/platform/magento2/cart.js: -------------------------------------------------------------------------------- 1 | import AbstractCartProxy from '../abstract/cart' 2 | import { multiStoreConfig } from './util' 3 | 4 | class CartProxy extends AbstractCartProxy { 5 | constructor (config, req) { 6 | const Magento2Client = require('magento2-rest-client').Magento2Client; 7 | super(config, req) 8 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 9 | } 10 | 11 | create (customerToken) { 12 | return this.api.cart.create(customerToken) 13 | } 14 | 15 | update (customerToken, cartId, cartItem) { 16 | return this.api.cart.update(customerToken, cartId, cartItem) 17 | } 18 | 19 | delete (customerToken, cartId, cartItem) { 20 | return this.api.cart.delete(customerToken, cartId, cartItem) 21 | } 22 | 23 | pull (customerToken, cartId, params) { 24 | return this.api.cart.pull(customerToken, cartId, params) 25 | } 26 | 27 | totals (customerToken, cartId, params) { 28 | return this.api.cart.totals(customerToken, cartId, params) 29 | } 30 | 31 | getShippingMethods (customerToken, cartId, address) { 32 | return this.api.cart.shippingMethods(customerToken, cartId, address) 33 | } 34 | 35 | getPaymentMethods (customerToken, cartId) { 36 | return this.api.cart.paymentMethods(customerToken, cartId) 37 | } 38 | 39 | setShippingInformation (customerToken, cartId, address) { 40 | return this.api.cart.shippingInformation(customerToken, cartId, address) 41 | } 42 | 43 | collectTotals (customerToken, cartId, shippingMethod) { 44 | return this.api.cart.collectTotals(customerToken, cartId, shippingMethod) 45 | } 46 | 47 | applyCoupon (customerToken, cartId, coupon) { 48 | return this.api.cart.applyCoupon(customerToken, cartId, coupon) 49 | } 50 | 51 | deleteCoupon (customerToken, cartId) { 52 | return this.api.cart.deleteCoupon(customerToken, cartId) 53 | } 54 | 55 | getCoupon (customerToken, cartId) { 56 | return this.api.cart.getCoupon(customerToken, cartId) 57 | } 58 | } 59 | 60 | module.exports = CartProxy 61 | -------------------------------------------------------------------------------- /src/platform/magento2/order.js: -------------------------------------------------------------------------------- 1 | import AbstractOrderProxy from '../abstract/order' 2 | import { multiStoreConfig } from './util' 3 | import { processSingleOrder } from './o2m' 4 | 5 | class OrderProxy extends AbstractOrderProxy { 6 | constructor (config, req) { 7 | const Magento2Client = require('magento2-rest-client').Magento2Client; 8 | super(config, req) 9 | this.config = config 10 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 11 | } 12 | 13 | create (orderData) { 14 | const inst = this 15 | return new Promise((resolve, reject) => { 16 | try { 17 | processSingleOrder(orderData, inst.config, null, (error, result) => { 18 | console.log(error) 19 | if (error) reject(error) 20 | resolve(result) 21 | }) 22 | } catch (e) { 23 | reject(e) 24 | } 25 | }) 26 | } 27 | } 28 | 29 | module.exports = OrderProxy 30 | -------------------------------------------------------------------------------- /src/platform/magento2/product.js: -------------------------------------------------------------------------------- 1 | import AbstractProductProxy from '../abstract/product' 2 | import { multiStoreConfig } from './util' 3 | 4 | class ProductProxy extends AbstractProductProxy { 5 | constructor (config, req) { 6 | const Magento2Client = require('magento2-rest-client').Magento2Client; 7 | super(config, req) 8 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 9 | } 10 | 11 | renderList (skus, currencyCode, storeId = 1) { 12 | const query = '&searchCriteria[filter_groups][0][filters][0][field]=sku&' + 13 | 'searchCriteria[filter_groups][0][filters][0][value]=' + encodeURIComponent(skus.join(',')) + '&' + 14 | 'searchCriteria[filter_groups][0][filters][0][condition_type]=in'; 15 | return this.api.products.renderList(query, currencyCode, storeId) 16 | } 17 | 18 | list (skus) { 19 | const query = '&searchCriteria[filter_groups][0][filters][0][field]=sku&' + 20 | 'searchCriteria[filter_groups][0][filters][0][value]=' + encodeURIComponent(skus.join(',')) + '&' + 21 | 'searchCriteria[filter_groups][0][filters][0][condition_type]=in'; 22 | return this.api.products.list(query) 23 | } 24 | } 25 | 26 | module.exports = ProductProxy 27 | -------------------------------------------------------------------------------- /src/platform/magento2/review.js: -------------------------------------------------------------------------------- 1 | import AbstractReviewProxy from '../abstract/review' 2 | import { multiStoreConfig } from './util' 3 | const Magento2Client = require('magento2-rest-client').Magento2Client; 4 | 5 | class ReviewProxy extends AbstractReviewProxy { 6 | constructor (config, req) { 7 | super(config, req) 8 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 9 | } 10 | 11 | create (reviewData) { 12 | reviewData.entity_pk_value = reviewData.product_id 13 | delete reviewData.product_id 14 | 15 | return this.api.reviews.create(reviewData) 16 | } 17 | } 18 | 19 | module.exports = ReviewProxy 20 | -------------------------------------------------------------------------------- /src/platform/magento2/stock.js: -------------------------------------------------------------------------------- 1 | import AbstractUserProxy from '../abstract/user' 2 | import { multiStoreConfig } from './util' 3 | 4 | class StockProxy extends AbstractUserProxy { 5 | constructor (config, req) { 6 | const Magento2Client = require('magento2-rest-client').Magento2Client; 7 | super(config, req) 8 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 9 | } 10 | 11 | check ({sku, stockId = 0}) { 12 | return this.api.stockItems.list(sku).then((result) => { 13 | if (this._config.msi.enabled) { 14 | return this.api.stockItems.getSalableQty(sku, stockId).then((salableQty) => { 15 | result.qty = salableQty; 16 | return result; 17 | }).then((result) => { 18 | return this.api.stockItems.isSalable(sku, stockId).then((isSalable) => { 19 | result.is_in_stock = isSalable; 20 | return result 21 | }) 22 | }) 23 | } else { 24 | return result; 25 | } 26 | }) 27 | } 28 | } 29 | 30 | module.exports = StockProxy 31 | -------------------------------------------------------------------------------- /src/platform/magento2/tax.js: -------------------------------------------------------------------------------- 1 | import AbstractTaxProxy from '../abstract/tax' 2 | import { calculateProductTax, checkIfTaxWithUserGroupIsActive, getUserGroupIdToUse } from 'vsf-utilities'; 3 | import TierHelper from '../../helpers/priceTiers' 4 | import es from '../../lib/elastic' 5 | import bodybuilder from 'bodybuilder' 6 | 7 | class TaxProxy extends AbstractTaxProxy { 8 | constructor (config, entityType, indexName, taxCountry, taxRegion = '', sourcePriceInclTax = null, finalPriceInclTax = null) { 9 | super(config) 10 | this._entityType = entityType 11 | this._indexName = indexName 12 | this._sourcePriceInclTax = sourcePriceInclTax 13 | this._finalPriceInclTax = finalPriceInclTax 14 | this._userGroupId = this._config.tax.userGroupId 15 | this._storeConfigTax = this._config.tax 16 | 17 | if (this._config.storeViews && this._config.storeViews.multistore) { 18 | for (let storeCode in this._config.storeViews) { 19 | const store = this._config.storeViews[storeCode] 20 | if (typeof store === 'object') { 21 | if (store.elasticsearch && store.elasticsearch.index) { // workaround to map stores 22 | if (store.elasticsearch.index === indexName) { 23 | taxRegion = store.tax.defaultRegion 24 | taxCountry = store.tax.defaultCountry 25 | sourcePriceInclTax = store.tax.sourcePriceIncludesTax 26 | finalPriceInclTax = store.tax.finalPriceIncludesTax 27 | this._storeConfigTax = store.tax 28 | break; 29 | } 30 | } 31 | } 32 | } 33 | } else { 34 | if (!taxRegion) { 35 | taxRegion = this._config.tax.defaultRegion 36 | } 37 | if (!taxCountry) { 38 | taxCountry = this._config.tax.defaultCountry 39 | } 40 | } 41 | if (sourcePriceInclTax == null) { 42 | sourcePriceInclTax = this._config.tax.sourcePriceIncludesTax 43 | } 44 | if (finalPriceInclTax == null) { 45 | finalPriceInclTax = this._config.tax.finalPriceIncludesTax 46 | } 47 | this._deprecatedPriceFieldsSupport = this._config.tax.deprecatedPriceFieldsSupport 48 | this._taxCountry = taxCountry 49 | this._taxRegion = taxRegion 50 | this._sourcePriceInclTax = sourcePriceInclTax 51 | this._finalPriceInclTax = finalPriceInclTax 52 | this.taxFor = this.taxFor.bind(this) 53 | } 54 | 55 | taxFor (product, groupId) { 56 | return calculateProductTax({ 57 | product, 58 | taxClasses: this._taxClasses, 59 | taxCountry: this._taxCountry, 60 | taxRegion: this._taxRegion, 61 | sourcePriceInclTax: this._sourcePriceInclTax, 62 | deprecatedPriceFieldsSupport: this._deprecatedPriceFieldsSupport, 63 | finalPriceInclTax: this._finalPriceInclTax, 64 | userGroupId: groupId, 65 | isTaxWithUserGroupIsActive: checkIfTaxWithUserGroupIsActive(this._storeConfigTax) && typeof groupId === 'number' 66 | }) 67 | } 68 | 69 | applyTierPrices (productList, groupId) { 70 | if (this._config.usePriceTiers) { 71 | for (let item of productList) { 72 | TierHelper(item._source, groupId) 73 | } 74 | } 75 | } 76 | 77 | process (productList, groupId = null) { 78 | const inst = this 79 | return new Promise((resolve, reject) => { 80 | inst.applyTierPrices(productList, groupId) 81 | 82 | if (this._config.tax.calculateServerSide) { 83 | const client = es.getClient(this._config) 84 | const esQuery = es.adjustQuery({ 85 | index: this._indexName, 86 | body: bodybuilder() 87 | }, 'taxrule', this._config) 88 | client.search(esQuery).then((body) => { // we're always trying to populate cache - when online 89 | inst._taxClasses = es.getHits(body).map(el => { return el._source }) 90 | for (let item of productList) { 91 | const isActive = checkIfTaxWithUserGroupIsActive(inst._storeConfigTax) 92 | if (isActive) { 93 | groupId = getUserGroupIdToUse(inst._userGroupId, inst._storeConfigTax) 94 | } else { 95 | groupId = null 96 | } 97 | 98 | inst.taxFor(item._source, groupId) 99 | } 100 | 101 | resolve(productList) 102 | }).catch(err => { 103 | reject(err) 104 | }) 105 | } else { 106 | resolve(productList) 107 | } 108 | }) 109 | } 110 | } 111 | 112 | module.exports = TaxProxy 113 | -------------------------------------------------------------------------------- /src/platform/magento2/user.js: -------------------------------------------------------------------------------- 1 | import AbstractUserProxy from '../abstract/user' 2 | import { multiStoreConfig } from './util' 3 | 4 | class UserProxy extends AbstractUserProxy { 5 | constructor (config, req) { 6 | const Magento2Client = require('magento2-rest-client').Magento2Client; 7 | super(config, req) 8 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 9 | } 10 | 11 | register (userData) { 12 | return this.api.customers.create(userData) 13 | } 14 | 15 | login (userData) { 16 | return this.api.customers.token(userData) 17 | } 18 | 19 | me (requestToken) { 20 | return this.api.customers.me(requestToken) 21 | } 22 | orderHistory (requestToken, pageSize = 20, currentPage = 1) { 23 | return this.api.customers.orderHistory(requestToken, pageSize, currentPage) 24 | } 25 | resetPassword (emailData) { 26 | return this.api.customers.resetPassword(emailData) 27 | } 28 | 29 | update (userData) { 30 | return this.api.customers.update(userData) 31 | } 32 | 33 | changePassword (passwordData) { 34 | return this.api.customers.changePassword(passwordData) 35 | } 36 | 37 | resetPasswordUsingResetToken (resetData) { 38 | return this.api.customers.resetPasswordUsingResetToken(resetData) 39 | } 40 | } 41 | 42 | module.exports = UserProxy 43 | -------------------------------------------------------------------------------- /src/platform/magento2/util.js: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import { getCurrentStoreCode } from '../../lib/util' 3 | /** 4 | * Adjust the config provided to the current store selected via request params 5 | * @param Object config configuration 6 | * @param Express request req 7 | */ 8 | export function multiStoreConfig (apiConfig, req) { 9 | let confCopy = Object.assign({}, apiConfig) 10 | let storeCode = getCurrentStoreCode(req) 11 | 12 | if (storeCode && config.availableStores.indexOf(storeCode) >= 0) { 13 | if (config.magento2['api_' + storeCode]) { 14 | confCopy = Object.assign({}, config.magento2['api_' + storeCode]) // we're to use the specific api configuration - maybe even separate magento instance 15 | } 16 | confCopy.url = confCopy.url + '/' + storeCode 17 | } else { 18 | if (storeCode) { 19 | console.error('Unavailable store code', storeCode) 20 | } 21 | } 22 | 23 | return confCopy 24 | } 25 | -------------------------------------------------------------------------------- /src/processor/default.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { IConfig } from 'config'; 3 | const jwa = require('jwa'); 4 | const hmac = jwa('HS256'); 5 | 6 | class HmacProcessor { 7 | private _config: IConfig 8 | private _entityType: any 9 | private _indexName: any 10 | private _req: Request 11 | private _res: Response 12 | 13 | public constructor (config: IConfig, entityType: any, indexName: any, req: Request, res: Response) { 14 | this._config = config 15 | this._entityType = entityType 16 | this._indexName = indexName 17 | this._req = req 18 | this._res = res 19 | } 20 | 21 | public process (items) { 22 | const processorChain = [] 23 | return new Promise((resolve, reject) => { 24 | const rs = items.map((item) => { 25 | if (this._req.query._source_exclude && (this._req.query._source_exclude as string[]).indexOf('sgn') < 0) { 26 | item._source.sgn = hmac.sign(item._source, this._config.get('objHashSecret')); // for products we sign off only price and id becase only such data is getting back with orders 27 | } 28 | return item 29 | }) 30 | 31 | // return first resultSet 32 | resolve(rs) 33 | }) 34 | } 35 | } 36 | 37 | module.exports = HmacProcessor 38 | -------------------------------------------------------------------------------- /src/processor/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Check if the module exists 5 | * @param module name name 6 | */ 7 | function module_exists (name) { 8 | try { return require.resolve(name) } catch (e) { return false } 9 | } 10 | 11 | class ProcessorFactory { 12 | constructor (app_config) { 13 | this.config = app_config; 14 | } 15 | 16 | getAdapter (entityType, indexName, req, res) { 17 | const moduleName = './' + entityType 18 | 19 | if (!module_exists(moduleName)) { 20 | console.log('No additional data adapter for ' + entityType) 21 | return null 22 | } 23 | 24 | let AdapterClass = require(moduleName); 25 | if (!AdapterClass) { 26 | console.log('No additional data adapter for ' + entityType) 27 | return null 28 | } else { 29 | let adapter_instance = new AdapterClass(this.config, entityType, indexName, req, res); 30 | 31 | if ((typeof adapter_instance.isValidFor === 'function') && !adapter_instance.isValidFor(entityType)) { throw new Error('Not valid adapter class or adapter is not valid for ' + entityType); } 32 | 33 | return adapter_instance; 34 | } 35 | } 36 | } 37 | 38 | module.exports = ProcessorFactory; 39 | -------------------------------------------------------------------------------- /src/worker/log.js: -------------------------------------------------------------------------------- 1 | var winston = require('winston'); 2 | 3 | winston.emitErrs = true; 4 | 5 | if (!global.logger) { 6 | global.logger = new winston.Logger({ 7 | transports: [ 8 | new winston.transports.Console({ 9 | level: 'info', 10 | handleExceptions: false, 11 | json: false, 12 | prettyPrint: true, 13 | colorize: true, 14 | timestamp: true 15 | }) 16 | ], 17 | exitOnError: false 18 | }); 19 | } 20 | 21 | module.exports = global.logger; 22 | -------------------------------------------------------------------------------- /src/worker/order_to_magento2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CLI tool 3 | * Queue worker in charge of syncing the Sales order to Magento2 via REST API * 4 | */ 5 | 6 | const program = require('commander'); 7 | const kue = require('kue'); 8 | const logger = require('./log'); 9 | 10 | const config = require('config') 11 | let queue = kue.createQueue(Object.assign(config.kue, { redis: config.redis })); 12 | 13 | let numCPUs = require('os').cpus().length; 14 | const processSingleOrder = require('../platform/magento2/o2m').processSingleOrder 15 | 16 | // RUN 17 | program 18 | .command('start') 19 | .option('--partitions ', 'number of partitions', numCPUs) 20 | .action((cmd) => { // default command is to run the service worker 21 | let partition_count = parseInt(cmd.partitions); 22 | logger.info(`Starting KUE worker for "order" message [${partition_count}]...`); 23 | queue.process('order', partition_count, (job, done) => { 24 | logger.info('Processing order: ' + job.data.title); 25 | return processSingleOrder(job.data.order, config, job, done); 26 | }); 27 | }); 28 | 29 | program 30 | .command('testAuth') 31 | .action(() => { 32 | processSingleOrder(require('../../var/testOrderAuth.json'), config, null, (err, result) => {}); 33 | }); 34 | 35 | program 36 | .command('testAnon') 37 | .action(() => { 38 | processSingleOrder(require('../../var/testOrderAnon.json'), config, null, (err, result) => {}); 39 | }); 40 | 41 | program 42 | .on('command:*', () => { 43 | console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' ')); 44 | process.exit(1); 45 | }); 46 | 47 | program 48 | .parse(process.argv) 49 | -------------------------------------------------------------------------------- /test/unit/jest.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: '../../', 3 | moduleFileExtensions: [ 4 | 'js', 5 | 'ts', 6 | 'json' 7 | ], 8 | testMatch: [ 9 | '/src/**/test/unit/**/*.spec.(js|ts)', 10 | ], 11 | transform: { 12 | '^.+\\.js$': '/node_modules/ts-jest', 13 | '^.+\\.ts$': '/node_modules/ts-jest', 14 | }, 15 | coverageDirectory: '/test/unit/coverage', 16 | collectCoverageFrom: [ 17 | 'src/**/*.{js,ts}', 18 | '!src/**/types/*.{js,ts}', 19 | ], 20 | moduleNameMapper: { 21 | '^src(.*)$': '/src$1' 22 | }, 23 | transformIgnorePatterns: [ 24 | '/node_modules/(?!lodash)' 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "strict": false, 5 | "allowJs": true, 6 | "importHelpers": false, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "outDir": "dist", 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "lib": ["es7"], 17 | "preserveSymlinks": true 18 | }, 19 | "include": [ 20 | "src/**/*" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /var/catalog_de_taxrule.json: -------------------------------------------------------------------------------- 1 | {"_index":"vue_storefront_catalog_taxrule_1587031505","_type":"_doc","_id":"2","_score":1,"_source":{"id":2,"code":"General Taxes","priority":0,"position":0,"customer_tax_class_ids":[3],"product_tax_class_ids":[2],"tax_rate_ids":[6,5,7,4],"calculate_subtotal":false,"rates":[{"id":6,"tax_country_id":"DE","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-DE","titles":[]},{"id":4,"tax_country_id":"US","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23","titles":[]},{"id":5,"tax_country_id":"IT","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-IT","titles":[]},{"id":7,"tax_country_id":"PL","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-PL","titles":[]}],"tsk":1587030446469}} 2 | -------------------------------------------------------------------------------- /var/catalog_it_taxrule.json: -------------------------------------------------------------------------------- 1 | {"_index":"vue_storefront_catalog_taxrule_1587031505","_type":"_doc","_id":"2","_score":1,"_source":{"id":2,"code":"General Taxes","priority":0,"position":0,"customer_tax_class_ids":[3],"product_tax_class_ids":[2],"tax_rate_ids":[6,5,7,4],"calculate_subtotal":false,"rates":[{"id":6,"tax_country_id":"DE","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-DE","titles":[]},{"id":4,"tax_country_id":"US","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23","titles":[]},{"id":5,"tax_country_id":"IT","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-IT","titles":[]},{"id":7,"tax_country_id":"PL","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-PL","titles":[]}],"tsk":1587030446469}} 2 | -------------------------------------------------------------------------------- /var/catalog_taxrule.json: -------------------------------------------------------------------------------- 1 | {"_index":"vue_storefront_catalog_taxrule_1587031505","_type":"_doc","_id":"2","_score":1,"_source":{"id":2,"code":"General Taxes","priority":0,"position":0,"customer_tax_class_ids":[3],"product_tax_class_ids":[2],"tax_rate_ids":[6,5,7,4],"calculate_subtotal":false,"rates":[{"id":6,"tax_country_id":"DE","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-DE","titles":[]},{"id":4,"tax_country_id":"US","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23","titles":[]},{"id":5,"tax_country_id":"IT","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-IT","titles":[]},{"id":7,"tax_country_id":"PL","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-PL","titles":[]}],"tsk":1587030446469}} 2 | -------------------------------------------------------------------------------- /var/testUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "customer": { 3 | "email": "jfoe@vuestorefront.io", 4 | "firstname": "Jon", 5 | "lastname": "Foe" 6 | }, 7 | "password": "!@#foearwato" 8 | } --------------------------------------------------------------------------------