├── config ├── certs │ └── .gitkeep ├── test.json ├── .gitignore └── custom-environment-variables.json ├── Procfile ├── .env ├── .eslintignore ├── packages ├── .gitignore ├── default-vsf │ ├── models │ │ ├── .gitignore │ │ └── review.schema.json │ ├── tsconfig.json │ ├── api │ │ ├── url.ts │ │ ├── extensions │ │ │ ├── cms-data │ │ │ │ ├── README.md │ │ │ │ └── index.ts │ │ │ ├── example-magento-api │ │ │ │ └── index.ts │ │ │ └── mail-service │ │ │ │ └── index.ts │ │ ├── sync.ts │ │ └── review.ts │ ├── middleware │ │ └── index.ts │ ├── worker │ │ ├── log.ts │ │ └── order_to_magento2.ts │ ├── package.json │ └── index.ts ├── default-catalog │ ├── elasticsearch │ │ ├── elastic.schema.index.extension.json │ │ ├── elastic.schema.taxrule.extension.json │ │ ├── elastic.schema.attribute.extension.json │ │ ├── elastic.schema.category.extension.json │ │ ├── elastic.schema.taxrule.json │ │ ├── elastic.schema.product.extension.json │ │ ├── elastic.schema.attribute.json │ │ ├── elastic.schema.index.json │ │ ├── elastic.schema.cms_page.json │ │ ├── elastic.schema.cms_block.json │ │ └── elastic.schema.category.json │ ├── graphql │ │ ├── elasticsearch │ │ │ ├── client.ts │ │ │ ├── root.resolver.ts │ │ │ ├── product │ │ │ │ ├── processor.ts │ │ │ │ └── service.ts │ │ │ ├── mapping.ts │ │ │ ├── queryBuilder.ts │ │ │ ├── root.graphqls │ │ │ ├── attribute │ │ │ │ ├── aggregations.ts │ │ │ │ └── resolver.ts │ │ │ ├── taxrule │ │ │ │ ├── resolver.ts │ │ │ │ └── schema.graphqls │ │ │ ├── category │ │ │ │ ├── resolver.ts │ │ │ │ └── service.ts │ │ │ ├── json_type │ │ │ │ └── resolver.ts │ │ │ ├── cms │ │ │ │ └── resolver.ts │ │ │ └── review │ │ │ │ ├── resolver.ts │ │ │ │ └── schema.graphqls │ │ ├── schema.ts │ │ └── resolvers.ts │ ├── tsconfig.json │ ├── api │ │ ├── extensions │ │ │ ├── example-processor │ │ │ │ ├── index.ts │ │ │ │ └── processors │ │ │ │ │ └── my-product-processor.ts │ │ │ ├── example-custom-filter │ │ │ │ ├── index.ts │ │ │ │ └── filter │ │ │ │ │ └── catalog │ │ │ │ │ └── SampleFilter.ts │ │ │ └── elastic-stock │ │ │ │ └── index.ts │ │ └── invalidate.ts │ ├── helper │ │ └── loadCustomFilters.ts │ ├── processor │ │ ├── default.ts │ │ └── factory.ts │ ├── package.json │ └── index.ts ├── core │ ├── tsconfig.json │ └── package.json ├── lib │ ├── tsconfig.json │ ├── boost.ts │ ├── helpers │ │ ├── loadAdditionalCertificates.ts │ │ ├── graphql.ts │ │ └── priceTiers.ts │ ├── cache-instance.ts │ ├── error.ts │ ├── db.ts │ ├── index.ts │ ├── countrymapper.ts │ ├── redis.ts │ ├── test │ │ └── unit │ │ │ └── boost.spec.ts │ ├── module │ │ └── types.ts │ ├── image.ts │ └── package.json ├── hooks │ ├── tsconfig.json │ ├── package.json │ └── index.ts ├── platform │ ├── tsconfig.json │ ├── helpers.ts │ ├── factory.ts │ └── package.json ├── default-img │ ├── tsconfig.json │ ├── image │ │ ├── cache │ │ │ ├── abstract │ │ │ │ └── index.ts │ │ │ ├── file │ │ │ │ └── index.ts │ │ │ ├── factory.ts │ │ │ └── google-cloud-storage │ │ │ │ └── index.ts │ │ └── action │ │ │ ├── factory.ts │ │ │ └── abstract │ │ │ └── index.ts │ ├── index.ts │ ├── package.json │ └── api │ │ └── img.ts ├── platform-abstract │ ├── tsconfig.json │ ├── review.ts │ ├── contact.ts │ ├── stock_alert.ts │ ├── newsletter.ts │ ├── wishlist.ts │ ├── tax.ts │ ├── address.ts │ ├── package.json │ ├── stock.ts │ └── order.ts ├── platform-magento1 │ ├── tsconfig.json │ ├── stock.ts │ ├── contact.ts │ ├── order.ts │ ├── index.ts │ ├── stock_alert.ts │ ├── newsletter.ts │ ├── wishlist.ts │ ├── address.ts │ ├── util.ts │ ├── user.ts │ ├── package.json │ └── cart.ts ├── platform-magento2 │ ├── tsconfig.json │ ├── index.ts │ ├── review.ts │ ├── order.ts │ ├── stock.ts │ ├── util.ts │ ├── product.ts │ ├── user.ts │ ├── package.json │ └── cart.ts └── tsconfig.base.json ├── .lintstagedrc.js ├── src ├── modules │ ├── template-module │ │ ├── elasticsearch │ │ │ ├── elastic.schema.custom.extension.json │ │ │ └── elastic.schema.custom.json │ │ ├── graphql │ │ │ ├── schema.js │ │ │ ├── resolvers.js │ │ │ └── hello │ │ │ │ ├── schema.graphqls │ │ │ │ └── resolver.ts │ │ ├── api │ │ │ └── version.ts │ │ └── index.ts │ ├── index.ts │ └── sample-api │ │ ├── index.ts │ │ └── api │ │ └── order.js ├── index.ts └── test │ └── integration │ └── main.spec.ts ├── docker ├── elasticsearch7 │ ├── Dockerfile │ └── config │ │ └── elasticsearch.yml └── storefront-api │ ├── default.env │ ├── storefront-api.sh │ └── Dockerfile ├── docs ├── now.json ├── Storefront API architecture.png ├── README.md ├── guide │ ├── default-modules │ │ ├── extensions.md │ │ ├── introduction.md │ │ ├── platforms.md │ │ └── graphql.md │ └── integration │ │ ├── format-category.md │ │ └── prices-how-to.md └── .vuepress │ └── config.js ├── lerna.json ├── graphql-schema-linter.config.js ├── var ├── testUser.json ├── catalog_de_review.json ├── catalog_de_taxrule.json ├── catalog_it_taxrule.json └── catalog_taxrule.json ├── .editorconfig ├── nodemon.json ├── kubernetes ├── redis-service.yaml ├── kibana-service.yaml ├── elasticsearch-service.yaml ├── storefront-graphql-api-service.yaml ├── storefront-graphql-api-configmap.yaml ├── redis-deployment.yaml ├── kibana-deployment.yaml ├── elasticsearch-deployment.yaml └── storefront-graphql-api-deployment.yaml ├── .huskyrc.js ├── migrations ├── .common.js └── 1530101328854-local_es_config_fix.js ├── integration-sdk └── sample-data │ ├── package.json │ ├── fetch_demo_categories.sh │ ├── import.js │ ├── fetch_demo_attributes.sh │ └── fetch_demo_products.sh ├── .gitignore ├── .travis.yml ├── ecosystem.json ├── tsconfig.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── nodejs.yml ├── tsconfig.build.json ├── test ├── unit │ └── jest.conf.js └── integration │ └── jest.conf.js ├── LICENSE ├── scripts ├── kue.ts └── cache.ts ├── docker-compose.yml ├── .eslintrc.js └── CONTRIBUTING.md /config/certs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | COMPOSE_PROJECT_NAME=sfa 2 | -------------------------------------------------------------------------------- /config/.gitignore: -------------------------------------------------------------------------------- 1 | *.extension.json -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules 3 | -------------------------------------------------------------------------------- /packages/.gitignore: -------------------------------------------------------------------------------- 1 | */dist 2 | **/*.js 3 | **/*.d.ts 4 | -------------------------------------------------------------------------------- /packages/default-vsf/models/.gitignore: -------------------------------------------------------------------------------- 1 | *.extension.json -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.{js,ts}": "eslint", 3 | } 4 | -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/elastic.schema.index.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/elastic.schema.taxrule.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/elastic.schema.attribute.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/elastic.schema.category.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/modules/template-module/elasticsearch/elastic.schema.custom.extension.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /docker/elasticsearch7/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.elastic.co/elasticsearch/elasticsearch:7.3.2 2 | 3 | -------------------------------------------------------------------------------- /docs/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sfa-docs", 3 | "routes": [{ "src": "/public", "dest": "/" }] 4 | } 5 | -------------------------------------------------------------------------------- /docs/Storefront API architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vuestorefront/storefront-api/HEAD/docs/Storefront API architecture.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "useWorkspaces": true, 4 | "packages": [ 5 | "packages/*" 6 | ], 7 | "version": "1.0.0-rc.3" 8 | } 9 | -------------------------------------------------------------------------------- /graphql-schema-linter.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: ['enum-values-sorted-alphabetically', 'defined-types-are-used', 'deprecations-have-a-reason'], 3 | }; -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/lib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/client.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import es from '@storefront-api/lib/elastic' 3 | 4 | export default es.getClient(config) 5 | -------------------------------------------------------------------------------- /packages/hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/platform/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/default-img/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/default-vsf/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docker/storefront-api/default.env: -------------------------------------------------------------------------------- 1 | BIND_HOST=0.0.0.0 2 | ELASTICSEARCH_HOST=sfa_elasticsearch 3 | ELASTICSEARCH_PORT=9200 4 | REDIS_HOST=sfa_redis 5 | VS_ENV=dev 6 | PM2_ARGS=--no-daemon 7 | -------------------------------------------------------------------------------- /packages/default-catalog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/platform-abstract/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/platform-magento1/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/platform-magento2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": "./" 5 | }, 6 | "include": ["*.ts", "**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /docker/storefront-api/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 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: https://divante.com/github/storefront-api/sfa-logo.png 4 | actionText: Get Started → 5 | actionLink: /guide/general/introduction.html 6 | --- 7 | -------------------------------------------------------------------------------- /var/testUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "customer": { 3 | "email": "jfoe@vuestorefront.io", 4 | "firstname": "Jon", 5 | "lastname": "Foe" 6 | }, 7 | "password": "!@#foearwato" 8 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/default-catalog/api/extensions/example-processor/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | module.exports = () => { 4 | let exampleApi = Router() 5 | 6 | return exampleApi 7 | } 8 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "debug": false, 4 | "exec": "yarn lint-gql && ts-node src", 5 | "watch": ["./src", "./packages"], 6 | "ext": "ts, js, graphqls", 7 | "inspect": true 8 | } 9 | -------------------------------------------------------------------------------- /packages/default-catalog/api/extensions/example-custom-filter/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | 3 | module.exports = () => { 4 | let exampleFilter = Router() 5 | 6 | return exampleFilter 7 | } 8 | -------------------------------------------------------------------------------- /packages/platform/helpers.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | export function getCurrentPlatformConfig (): any { 3 | const currentPlatform: string = config.get('platform') 4 | return config.get(currentPlatform) 5 | } 6 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.huskyrc.js: -------------------------------------------------------------------------------- 1 | const tasks = arr => arr.join(' && ') 2 | 3 | module.exports = { 4 | 'hooks': { 5 | 'pre-commit': tasks([ 6 | 'lint-staged' 7 | ]), 8 | 'pre-push': tasks([ 9 | 'yarn test:unit' 10 | ]) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import { Server } from '@storefront-api/core' 3 | import modules from './modules'; 4 | 5 | const server = new Server({ 6 | modules 7 | }) 8 | 9 | server.start(); 10 | 11 | export default server; 12 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/default-vsf/api/url.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import createMapRoute from './map'; 3 | 4 | export default ({ config, db }) => { 5 | const router = Router() 6 | 7 | router.use('/map', createMapRoute({ config })) 8 | 9 | return router 10 | } 11 | -------------------------------------------------------------------------------- /kubernetes/storefront-graphql-api-service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: storefront-api 5 | labels: 6 | app: storefront-api 7 | spec: 8 | selector: 9 | app: storefront-api 10 | ports: 11 | - port: 8080 12 | targetPort: 8080 13 | -------------------------------------------------------------------------------- /src/modules/template-module/graphql/schema.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import config from 'config'; 3 | import { loadModuleSchemaArray } from '@storefront-api/lib/helpers/graphql' 4 | 5 | export default loadModuleSchemaArray(__dirname, `./${config.server.searchEngine}`, path.join(__dirname, `../api/extensions`)) 6 | -------------------------------------------------------------------------------- /src/modules/template-module/graphql/resolvers.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import config from 'config'; 3 | import { loadModuleResolversArray } from '@storefront-api/lib/helpers/graphql' 4 | 5 | export default loadModuleResolversArray(__dirname, `./${config.server.searchEngine}`, path.join(__dirname, `../api/extensions`)) 6 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/schema.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import config from 'config'; 3 | import { loadModuleSchemaArray } from '@storefront-api/lib/helpers/graphql' 4 | 5 | export default loadModuleSchemaArray(path.join(__dirname, `./${config.get('server.searchEngine')}`), path.join(__dirname, `../api/extensions`)) 6 | -------------------------------------------------------------------------------- /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('@storefront-api/lib/elastic') 6 | const client = es.getClient(config) 7 | 8 | exports.db = client 9 | exports.queue = queue 10 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/resolvers.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import config from 'config'; 3 | import { loadModuleResolversArray } from '@storefront-api/lib/helpers/graphql' 4 | 5 | export default loadModuleResolversArray(path.join(__dirname, `${config.get('server.searchEngine')}`), path.join(__dirname, `../api/extensions`)) 6 | -------------------------------------------------------------------------------- /kubernetes/storefront-graphql-api-configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: storefront-api-config 5 | data: 6 | BIND_HOST: 0.0.0.0 7 | ELASTICSEARCH_HOST: sfa_elasticsearch 8 | ELASTICSEARCH_PORT: "9200" 9 | REDIS_HOST: sfa_redis 10 | REDIS_PORT: "6379" 11 | REDIS_DB: "0" 12 | VS_ENV: dev 13 | -------------------------------------------------------------------------------- /packages/lib/boost.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | 3 | export default function getBoosts (attribute: string = '') { 4 | let boosts = [] 5 | 6 | if (config.has('boost')) { 7 | boosts = config.get('boost') 8 | } 9 | 10 | if (boosts.hasOwnProperty(attribute)) { 11 | return boosts[attribute] 12 | } 13 | 14 | return 1 15 | } 16 | -------------------------------------------------------------------------------- /var/catalog_de_review.json: -------------------------------------------------------------------------------- 1 | {"_index":"vue_storefront_catalog_de_review","_type":"_doc","_id":"639","_score":1,"_source":{"id":639,"title":"test","detail":"test","nickname":"Ali","customer_id":9126,"review_entity":"product","review_type":1,"review_status":2,"created_at":"2019-09-18 09:57:28","store_id":3,"stores":[0,3],"product_id":898,"tsk":1568837420043}} 2 | -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/elastic.schema.taxrule.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "integer" 5 | }, 6 | "rates": { 7 | "properties": { 8 | "rate": { 9 | "type": "float" 10 | } 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /integration-sdk/sample-data/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-data", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "import.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "elasticsearch": "^16.3.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/template-module/graphql/hello/schema.graphqls: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | # This is an example call to just display your name :) 3 | sayHello ( 4 | name: String 5 | ): String 6 | 7 | # Execute simple ElasticSearch Query 8 | testElastic( 9 | # Product SKU to get from Elastic 10 | sku: String 11 | ): Product 12 | } 13 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/root.resolver.ts: -------------------------------------------------------------------------------- 1 | 2 | const resolver = { 3 | Query: { 4 | version: (_, { search }, context, rootValue) => { return process.env.npm_package_version } // entry point for product extensions 5 | }, 6 | ESResponseInterface: { 7 | __resolveType () { 8 | return null; 9 | } 10 | } 11 | }; 12 | 13 | export default resolver; 14 | -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/lib/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 | -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/elastic.schema.attribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "integer" 5 | }, 6 | "attribute_id": { 7 | "type": "integer" 8 | }, 9 | "options": { 10 | "properties": { 11 | "value": { 12 | "type": "text" 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/template-module/elasticsearch/elastic.schema.custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "integer" 5 | }, 6 | "attribute_id": { 7 | "type": "integer" 8 | }, 9 | "options": { 10 | "properties": { 11 | "value": { 12 | "type": "text" 13 | } 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | docs/public 5 | npm-debug.log 6 | .vscode/ 7 | .idea/ 8 | api_start.sh 9 | api_test.sh 10 | package-lock.json 11 | src/config.json 12 | config/local.json 13 | config/certs/*.pem 14 | var/magento2-sample-data/ 15 | var/* 16 | .migrate 17 | *.iml 18 | /docker/elasticsearch/data/ 19 | !/docker/elasticsearch/data/.gitkeep 20 | /docker/elasticsearch7/data/ 21 | !/docker/elasticsearch7/data/.gitkeep 22 | yarn-error.log 23 | -------------------------------------------------------------------------------- /packages/lib/cache-instance.ts: -------------------------------------------------------------------------------- 1 | import TagCache from 'redis-tag-cache' 2 | import config from 'config' 3 | let cache: boolean|TagCache = false 4 | 5 | if (config.has('server.useOutputCache') && config.get('server.useOutputCache')) { 6 | const redisConfig = Object.assign(config.get('redis')) 7 | cache = new TagCache({ 8 | redis: redisConfig, 9 | defaultTimeout: config.get('server.outputCacheDefaultTtl') 10 | }) 11 | } 12 | 13 | export default cache 14 | -------------------------------------------------------------------------------- /.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 | 27 | -------------------------------------------------------------------------------- /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/modules/default-vsf/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 | -------------------------------------------------------------------------------- /packages/default-vsf/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 | import { DbContext } from '@storefront-api/lib/module/types' 6 | 7 | export default ({ config, db }: { config: IConfig, db: DbContext }): [ NextHandleFunction, Router ] => { 8 | let routes: Router = Router(); 9 | let bp: NextHandleFunction = json(); 10 | return [ bp, routes ]; 11 | } 12 | -------------------------------------------------------------------------------- /packages/platform-magento2/index.ts: -------------------------------------------------------------------------------- 1 | import order from './order' 2 | import user from './user' 3 | import stock from './stock' 4 | import review from './review' 5 | import cart from './cart' 6 | import product from './product' 7 | import tax from './tax' 8 | 9 | export { 10 | order, 11 | user, 12 | stock, 13 | review, 14 | cart, 15 | product, 16 | tax 17 | } 18 | export default { 19 | order, 20 | user, 21 | stock, 22 | review, 23 | cart, 24 | product, 25 | tax 26 | } 27 | -------------------------------------------------------------------------------- /packages/lib/error.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | 3 | export function catchInvalidRequests (err, req, res, next) { 4 | const { statusCode, message = '', stack = '' } = err; 5 | const stackTrace = stack 6 | .split(/\r?\n/) 7 | .map(string => string.trim()) 8 | .filter(string => string !== '') 9 | 10 | res.status(statusCode).json({ 11 | code: statusCode, 12 | result: message, 13 | ...(config.get('server.showErrorStack') ? { stack: stackTrace } : {}) 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/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 | } -------------------------------------------------------------------------------- /packages/lib/db.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import * as redis from './redis' 3 | import * as elastic from './elastic' 4 | import { DbContext } from './module/types'; 5 | 6 | export const dbContext: DbContext = { 7 | getRedisClient: () => redis.getClient(config), 8 | getElasticClient: () => elastic.getClient(config) 9 | } 10 | 11 | export default function initializeDb (callback: (db: DbContext) => void) { 12 | // connect to a database if needed, then pass it to `callback`: 13 | callback(dbContext); 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './boost' 2 | export { default as cache } from './cache-instance' 3 | export * from './countrymapper' 4 | export * from './db' 5 | export * from './elastic' 6 | export * from './image' 7 | export * from './logger' 8 | export { getClient as getRedisClient } from './redis' 9 | export * from './taxcalc' 10 | export * from './util' 11 | export * from './helpers/graphql' 12 | export * from './helpers/loadAdditionalCertificates' 13 | export * from './helpers/priceTiers' 14 | export * from './module/index' 15 | export * from './module/types' 16 | -------------------------------------------------------------------------------- /packages/platform-magento1/stock.ts: -------------------------------------------------------------------------------- 1 | import AbstractStockProxy from '@storefront-api/platform-abstract/stock'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class StockProxy extends AbstractStockProxy { 5 | public 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 | public check (data) { 11 | return this.api.stock.check(data.sku); 12 | } 13 | } 14 | 15 | export default StockProxy; 16 | -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/elastic.schema.cms_page.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "integer" 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 | } -------------------------------------------------------------------------------- /packages/default-vsf/worker/log.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston' 2 | 3 | winston['emitErrs'] = true; 4 | 5 | if (!global['logger']) { 6 | // @ts-ignore 7 | global['logger'] = new winston.Logger({ 8 | transports: [ 9 | new winston.transports.Console({ 10 | level: 'info', 11 | handleExceptions: false, 12 | // @ts-ignore 13 | json: false, 14 | prettyPrint: true, 15 | colorize: true, 16 | timestamp: true 17 | }) 18 | ], 19 | exitOnError: false 20 | }); 21 | } 22 | 23 | module.exports = global['logger']; 24 | -------------------------------------------------------------------------------- /packages/platform-abstract/review.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractReviewProxy { 5 | protected _request: Request 6 | protected _config: IConfig 7 | public api: Record> 8 | 9 | protected constructor (config, req) { 10 | this._config = config 11 | this._request = req 12 | } 13 | public create (reviewData): Promise { 14 | throw new Error('ReviewProxy::check must be implemented for specific platform') 15 | } 16 | } 17 | 18 | export default AbstractReviewProxy 19 | -------------------------------------------------------------------------------- /packages/platform-magento1/contact.ts: -------------------------------------------------------------------------------- 1 | import AbstractContactProxy from '@storefront-api/platform-abstract/contact'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class ContactProxy extends AbstractContactProxy { 5 | public 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 | public submit (form) { 11 | return this.api.contact.submit(form); 12 | } 13 | } 14 | 15 | export default ContactProxy; 16 | -------------------------------------------------------------------------------- /packages/platform-magento1/order.ts: -------------------------------------------------------------------------------- 1 | import AbstractOrderProxy from '@storefront-api/platform-abstract/order' 2 | import { multiStoreConfig } from './util' 3 | 4 | class OrderProxy extends AbstractOrderProxy { 5 | public 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 | public create (orderData) { 12 | return this.api.order.create(orderData); 13 | } 14 | } 15 | 16 | export default OrderProxy 17 | -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/elastic.schema.cms_block.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": { 3 | "id": { 4 | "type": "integer" 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 | } -------------------------------------------------------------------------------- /packages/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "declaration": true, 5 | "strict": false, 6 | "allowJs": false, 7 | "importHelpers": false, 8 | "module": "commonjs", 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | "noLib": false, 14 | "skipLibCheck": true, 15 | "sourceMap": false, 16 | "lib": ["es7"] 17 | }, 18 | "include": [], 19 | "exclude": ["../node_modules", "./**/*.spec.ts", "./**/*.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/platform-abstract/contact.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractContactProxy { 5 | protected _request: Request 6 | protected _config: IConfig 7 | public api: Record> 8 | 9 | public constructor (config, req) { 10 | this._config = config 11 | this._request = req 12 | } 13 | 14 | public submit (formData): Promise { 15 | throw new Error('AbstractContactProxy::check must be implemented for specific platform') 16 | } 17 | } 18 | 19 | export default AbstractContactProxy 20 | -------------------------------------------------------------------------------- /packages/platform-magento1/index.ts: -------------------------------------------------------------------------------- 1 | import cart from './cart' 2 | import order from './order' 3 | import stock from './stock' 4 | import user from './user' 5 | import tax from './tax' 6 | import newsletter from './newsletter' 7 | import stock_alert from './stock_alert' 8 | import address from './address' 9 | 10 | export { 11 | order, 12 | user, 13 | stock, 14 | cart, 15 | tax, 16 | newsletter, 17 | stock_alert, 18 | address 19 | } 20 | 21 | export default { 22 | order, 23 | user, 24 | stock, 25 | cart, 26 | tax, 27 | newsletter, 28 | stock_alert, 29 | address 30 | } 31 | -------------------------------------------------------------------------------- /packages/default-catalog/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 | -------------------------------------------------------------------------------- /packages/default-vsf/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 | -------------------------------------------------------------------------------- /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 | "preserveSymlinks": true, 11 | "emitDecoratorMetadata": true, 12 | "esModuleInterop": true, 13 | "resolveJsonModule": true, 14 | "outDir": "dist", 15 | "sourceMap": true, 16 | "baseUrl": ".", 17 | "lib": ["es7"] 18 | }, 19 | "include": [ 20 | "src/**/*", 21 | "scripts/**/*", 22 | "migrations/**/*" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /packages/lib/countrymapper.ts: -------------------------------------------------------------------------------- 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 | export { 18 | mapCountryRegion 19 | } 20 | -------------------------------------------------------------------------------- /packages/platform-abstract/stock_alert.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractStockAlertProxy { 5 | protected _request: Request 6 | protected _config: IConfig 7 | public api: Record> 8 | 9 | protected constructor (config, req) { 10 | this._config = config 11 | this._request = req 12 | } 13 | public subscribe (customerToken, productId, emailAddress): Promise { 14 | throw new Error('AbstractContactProxy::subscribe must be implemented for specific platform') 15 | } 16 | } 17 | 18 | export default AbstractStockAlertProxy 19 | -------------------------------------------------------------------------------- /docs/guide/default-modules/extensions.md: -------------------------------------------------------------------------------- 1 | # Extensions 2 | 3 | Storefront API evolved out of [Vue Storefront API](https://github.com/DivanteLtd/vue-storefront-api) which supported the concept of API extensions. Initially, it was just an additional Express.js handler mounted to a specific URL. 4 | 5 | We thought it might be useful for developers to have the same functionality within their modules. We've added a [`registerExtensions`](https://github.com/DivanteLtd/storefront-api/blob/a66222768bf7fb5f54acf268b6a0bb4e0f94a4cf/src/modules/template-module/index.ts#L29) helper that is in charge of loading the modules from `src/module/your-custom-module/api/extensions`. 6 | 7 | -------------------------------------------------------------------------------- /packages/platform-magento1/stock_alert.ts: -------------------------------------------------------------------------------- 1 | import AbstractStockAlertProxy from '@storefront-api/platform-abstract/stock_alert'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class StockAlertProxy extends AbstractStockAlertProxy { 5 | public 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 | public subscribe (customerToken, productId, emailAddress) { 11 | return this.api.stockAlert.subscribe(customerToken, productId, emailAddress); 12 | } 13 | } 14 | 15 | export default StockAlertProxy; 16 | -------------------------------------------------------------------------------- /packages/platform-magento2/review.ts: -------------------------------------------------------------------------------- 1 | import AbstractReviewProxy from '@storefront-api/platform-abstract/review' 2 | import { multiStoreConfig } from './util' 3 | const Magento2Client = require('magento2-rest-client').Magento2Client; 4 | 5 | class ReviewProxy extends AbstractReviewProxy { 6 | public constructor (config, req) { 7 | super(config, req) 8 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 9 | } 10 | 11 | public 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 | export default ReviewProxy 20 | -------------------------------------------------------------------------------- /packages/default-catalog/elasticsearch/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 | -------------------------------------------------------------------------------- /var/catalog_de_taxrule.json: -------------------------------------------------------------------------------- 1 | {"_index":"vue_storefront_catalog_de_taxrule","_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":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":6,"tax_country_id":"DE","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-DE","titles":[]},{"id":7,"tax_country_id":"PL","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-PL","titles":[]}],"tsk":1568837436161}} 2 | -------------------------------------------------------------------------------- /var/catalog_it_taxrule.json: -------------------------------------------------------------------------------- 1 | {"_index":"vue_storefront_catalog_it_taxrule","_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":5,"tax_country_id":"IT","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-IT","titles":[]},{"id":4,"tax_country_id":"US","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23","titles":[]},{"id":6,"tax_country_id":"DE","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-DE","titles":[]},{"id":7,"tax_country_id":"PL","tax_region_id":0,"tax_postcode":"*","rate":23,"code":"VAT23-PL","titles":[]}],"tsk":1568837763644}} 2 | -------------------------------------------------------------------------------- /var/catalog_taxrule.json: -------------------------------------------------------------------------------- 1 | {"_index":"vue_storefront_catalog_taxrule_1568838132","_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":[{"tax_region_id":0,"code":"VAT23","rate":23,"id":4,"tax_country_id":"US","titles":[],"tax_postcode":"*"},{"tax_region_id":0,"code":"VAT23-DE","rate":23,"id":6,"tax_country_id":"DE","titles":[],"tax_postcode":"*"},{"tax_region_id":0,"code":"VAT23-PL","rate":23,"id":7,"tax_country_id":"PL","titles":[],"tax_postcode":"*"},{"tax_region_id":0,"code":"VAT23-IT","rate":23,"id":5,"tax_country_id":"IT","titles":[],"tax_postcode":"*"}],"tsk":1568838056364}} 2 | -------------------------------------------------------------------------------- /packages/platform-magento1/newsletter.ts: -------------------------------------------------------------------------------- 1 | import AbstractNewsletterProxy from '@storefront-api/platform-abstract/newsletter'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class NewsletterProxy extends AbstractNewsletterProxy { 5 | public 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 | public subscribe (emailAddress) { 11 | return this.api.newsletter.subscribe(emailAddress); 12 | } 13 | public unsubscribe (customerToken) { 14 | return this.api.newsletter.unsubscribe(customerToken); 15 | } 16 | } 17 | 18 | export default NewsletterProxy; 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: feature request 5 | 6 | --- 7 | 8 | ## What is the motivation for adding/enhancing this feature? 9 | 10 | 11 | 12 | 13 | ## What are the acceptance criteria 14 | 15 | 16 | - [ ] ... 17 | 18 | ## Can you complete this feature request by yourself? 19 | 20 | - [ ] YES 21 | - [ ] NO 22 | 23 | ## Additional information 24 | 25 | 26 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./packages/core" 5 | }, 6 | { 7 | "path": "./packages/lib" 8 | }, 9 | { 10 | "path": "./packages/hooks" 11 | }, 12 | { 13 | "path": "./packages/default-catalog" 14 | }, 15 | { 16 | "path": "./packages/default-img" 17 | }, 18 | { 19 | "path": "./packages/default-vsf" 20 | }, 21 | { 22 | "path": "./packages/platform" 23 | }, 24 | { 25 | "path": "./packages/platform-abstract" 26 | }, 27 | { 28 | "path": "./packages/platform-magento1" 29 | }, 30 | { 31 | "path": "./packages/platform-magento2" 32 | } 33 | ], 34 | "files": [], 35 | "include": [] 36 | } 37 | -------------------------------------------------------------------------------- /docker/storefront-api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine3.11 2 | 3 | ENV VS_ENV prod 4 | 5 | WORKDIR /var/www 6 | 7 | RUN apk add --no-cache curl git python make g++ 8 | 9 | COPY package.json ./ 10 | COPY yarn.lock ./ 11 | 12 | RUN apk --no-cache --update upgrade musl 13 | RUN apk add --no-cache \ 14 | --repository http://dl-cdn.alpinelinux.org/alpine/edge/community \ 15 | --repository http://dl-cdn.alpinelinux.org/alpine/edge/main \ 16 | --virtual .build-deps \ 17 | python \ 18 | make \ 19 | g++ \ 20 | ca-certificates \ 21 | wget \ 22 | && yarn install --no-cache \ 23 | && apk del .build-deps 24 | 25 | COPY docker/storefront-api/storefront-api.sh /usr/local/bin/ 26 | 27 | CMD ["storefront-api.sh"] 28 | -------------------------------------------------------------------------------- /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 | '/packages/**/test/unit/**/*.spec.(js|ts)' 11 | ], 12 | transform: { 13 | '^.+\\.js$': '/node_modules/ts-jest', 14 | '^.+\\.ts$': '/node_modules/ts-jest' 15 | }, 16 | coverageDirectory: '/test/unit/coverage', 17 | collectCoverageFrom: [ 18 | 'src/**/*.{js,ts}', 19 | '!src/**/types/*.{js,ts}' 20 | ], 21 | moduleNameMapper: { 22 | '^src(.*)$': '/src$1' 23 | }, 24 | transformIgnorePatterns: [ 25 | '/node_modules/(?!lodash)' 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/lib/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'redis' 2 | import { RedisClient } from 'redis'; 3 | 4 | let redisClient: RedisClient|boolean = false; 5 | 6 | /** 7 | * Return Redis Client 8 | * @param {config} config 9 | */ 10 | export function getClient (config): RedisClient { 11 | if (redisClient instanceof RedisClient) { 12 | return redisClient 13 | } 14 | 15 | redisClient = Redis.createClient(config.redis); // redis client 16 | 17 | redisClient.on('error', (err) => { // workaround for https://github.com/NodeRedis/node_redis/issues/713 18 | redisClient = Redis.createClient(config.redis); // redis client 19 | }); 20 | 21 | if (config.redis.auth) { 22 | redisClient.auth(config.redis.auth); 23 | } 24 | 25 | return redisClient 26 | } 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/guide/default-modules/introduction.md: -------------------------------------------------------------------------------- 1 | # Default Gateway 2 | 3 | The Storefront API architecture [is modular](../modules/introduction.md). You can create custom modules to support different GraphQL and Elastic schemas along with custom REST endpoints. 4 | 5 | The default modules we provide are compatible with [Vue Storefront](https://vuestorefront.io) eCommerce PWA and are pretty much similar to Magento interfaces. The essential advantage here is that we already have [integrations](../integration/integration.md) for eCommerce platforms like Magento1, Magento2, and Spree and these integrations are compatible with the default data formats and APIs. 6 | 7 | You can easily integrate your custom platfrom by [following our integration tutorial](../integration/integration.md). 8 | -------------------------------------------------------------------------------- /integration-sdk/sample-data/fetch_demo_categories.sh: -------------------------------------------------------------------------------- 1 | # This command requires "jq" -> https://stedolan.github.io/jq/ 2 | if ! [ -x "$(command -v jq)" ]; then 3 | echo 'Error: jq is not installed. Please download it from https://stedolan.github.io/jq/' >&2 4 | exit 1 5 | fi 6 | 7 | curl -sS "https://demo.storefrontcloud.io/api/catalog/vue_storefront_catalog/category/_search?size=2500&from=0" | jq ".hits.hits[]._source | { id, parent_id, name, url_key, path, url_path, is_active, position, level, product_count, children_data: [ .children_data[] | { id, children_data: [ .children_data[] | { id, children_data: [ .children_data[] | { id, children_data: [ .children_data[] | { id } ] } ] } ] } ] }" | jq -s -M \ > categories.json 8 | 9 | echo "Categories dumped into 'categories.json'" -------------------------------------------------------------------------------- /packages/platform-abstract/newsletter.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractNewsletterProxy { 5 | protected _request: Request 6 | protected _config: IConfig 7 | public api: Record> 8 | 9 | protected constructor (config, req) { 10 | this._config = config 11 | this._request = req 12 | } 13 | 14 | public subscribe (emailAddress): Promise { throw new Error('AbstractNewsletterProxy::subscribe must be implemented for specific platform') } 15 | 16 | public unsubscribe (customerToken): Promise { throw new Error('AbstractNewsletterProxy::unsubscribe must be implemented for specific platform') } 17 | } 18 | 19 | export default AbstractNewsletterProxy 20 | -------------------------------------------------------------------------------- /test/integration/jest.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: '../../', 3 | moduleFileExtensions: [ 4 | 'js', 5 | 'ts', 6 | 'json' 7 | ], 8 | testMatch: [ 9 | '/src/**/test/integration/**/*.spec.(js|ts)', 10 | '/packages/**/test/integration/**/*.spec.(js|ts)' 11 | ], 12 | transform: { 13 | '^.+\\.js$': '/node_modules/ts-jest', 14 | '^.+\\.ts$': '/node_modules/ts-jest' 15 | }, 16 | coverageDirectory: '/test/integration/coverage', 17 | collectCoverageFrom: [ 18 | 'src/**/*.{js,ts}', 19 | '!src/**/types/*.{js,ts}' 20 | ], 21 | moduleNameMapper: { 22 | '^src(.*)$': '/src$1' 23 | }, 24 | transformIgnorePatterns: [ 25 | '/node_modules/(?!lodash)' 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /packages/default-vsf/api/sync.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '@storefront-api/lib/util'; 2 | import { Router } from 'express'; 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 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Related issues 2 | 3 | 4 | closes # 5 | 6 | ### Short description and why it's useful 7 | 8 | 9 | 10 | 11 | ### Screenshots of visual changes before/after (if there are any) 12 | 13 | 14 | 15 | **IMPORTANT NOTICE** - Remember to update `CHANGELOG.md` with a description of your change 16 | 17 | ### Contribution and currently important rules acceptance 18 | 19 | 20 | - [ ] I read and followed [contribution rules](https://github.com/DivanteLtd/storefront-api/blob/develop/CONTRIBUTING.md) 21 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { DefaultVuestorefrontApiModule } from '@storefront-api/default-vsf' 2 | import { DefaultCatalogModule } from '@storefront-api/default-catalog' 3 | import { DefaultImgModule } from '@storefront-api/default-img' 4 | import { SampleApiModule } from './sample-api' 5 | import { TemplateModule } from './template-module' 6 | import {StorefrontApiModule} from '@storefront-api/lib/module' 7 | import * as magento2 from '@storefront-api/platform-magento2' 8 | 9 | export let modules: StorefrontApiModule[] = [ 10 | DefaultVuestorefrontApiModule({ 11 | platform: { 12 | name: 'magento2', 13 | platformImplementation: magento2 14 | } 15 | }), 16 | DefaultCatalogModule(), 17 | DefaultImgModule(), 18 | SampleApiModule, 19 | TemplateModule 20 | ] 21 | 22 | export default modules 23 | -------------------------------------------------------------------------------- /src/modules/template-module/api/version.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '@storefront-api/lib/util' 2 | import { Router } from 'express' 3 | import { version } from '../../../../package.json' 4 | import { ExtensionAPIFunction, ExtensionAPIFunctionParameter } from '@storefront-api/lib/module'; 5 | 6 | /** 7 | * Here you can see an other way to export it 8 | * 9 | * const api: ExtensionAPIFunction = ({ config, db }) => { 10 | * let cartApi = Router(); 11 | * cartApi.get('/version', (req, res) => { 12 | * apiStatus(res, { version }, 200); 13 | * }); 14 | * return cartApi; 15 | }* } 16 | * export default api 17 | */ 18 | 19 | export default ({ config, db }: ExtensionAPIFunctionParameter) => { 20 | let cartApi = Router(); 21 | cartApi.get('/version', (req, res) => { 22 | apiStatus(res, { version }, 200); 23 | }); 24 | return cartApi; 25 | } 26 | -------------------------------------------------------------------------------- /packages/platform-magento1/wishlist.ts: -------------------------------------------------------------------------------- 1 | import AbstractWishlistProxy from '@storefront-api/platform-abstract/wishlist'; 2 | import { multiStoreConfig } from './util'; 3 | 4 | class WishlistProxy extends AbstractWishlistProxy { 5 | public 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 | public pull (customerToken) { 11 | return this.api.wishlist.pull(customerToken); 12 | } 13 | public update (customerToken, wishListItem) { 14 | return this.api.wishlist.update(customerToken, wishListItem); 15 | } 16 | public delete (customerToken, wishListItem) { 17 | return this.api.wishlist.delete(customerToken, wishListItem); 18 | } 19 | } 20 | 21 | export default WishlistProxy; 22 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/product/processor.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import ProcessorFactory from '../../../processor/factory' 3 | import Logger from '@storefront-api/lib/logger' 4 | 5 | export default function esResultsProcessor (response, esRequest, entityType, indexName) { 6 | return new Promise((resolve, reject) => { 7 | const factory = new ProcessorFactory(config) 8 | let resultProcessor = factory.getAdapter(entityType, indexName, esRequest, response) 9 | 10 | if (!resultProcessor) { 11 | resultProcessor = factory.getAdapter('default', indexName, esRequest, response) // get the default processor 12 | } 13 | 14 | resultProcessor.process(response.hits.hits) 15 | .then((result) => { 16 | resolve(result) 17 | }) 18 | .catch((err) => { 19 | Logger.error(err) 20 | }) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/mapping.ts: -------------------------------------------------------------------------------- 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.get('elasticsearch.indices')[0] 7 | 8 | if (urlParts.length >= 1 && urlParts[0] !== '' && urlParts[0] !== '?') { 9 | esIndex = config.get('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.get('entities')[entityType].filterFieldMapping !== 'undefined') { 20 | mapping = config.get('entities')[entityType].filterFieldMapping 21 | } 22 | 23 | if (typeof mapping[attribute] !== 'undefined') { 24 | return mapping[attribute] 25 | } 26 | 27 | return attribute 28 | } 29 | -------------------------------------------------------------------------------- /packages/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 | -------------------------------------------------------------------------------- /packages/default-catalog/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: search !== null ? search : '' }) 15 | queryChain = elasticsearch.applySort({ sort, queryChain }); 16 | queryChain = queryChain.from((currentPage > 0 ? (currentPage - 1) : 0) * 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 | -------------------------------------------------------------------------------- /packages/platform-abstract/wishlist.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractWishlistProxy { 5 | protected _request: Request 6 | protected _config: IConfig 7 | public api: Record> 8 | 9 | protected constructor (config, req) { 10 | this._config = config 11 | this._request = req 12 | } 13 | 14 | public pull (customerToken): Promise { 15 | throw new Error('AbstractWishlistProxy::pull must be implemented for specific platform') 16 | } 17 | public update (customerToken, wishListItem): Promise { 18 | throw new Error('AbstractWishlistProxy::update must be implemented for specific platform') 19 | } 20 | public delete (customerToken, wishListItem) { 21 | throw new Error('AbstractWishlistProxy::delete must be implemented for specific platform') 22 | } 23 | } 24 | 25 | export default AbstractWishlistProxy 26 | -------------------------------------------------------------------------------- /packages/default-vsf/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 | -------------------------------------------------------------------------------- /packages/platform-abstract/tax.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractTaxProxy { 5 | protected _config: IConfig 6 | public api: Record> 7 | public _entityType: any 8 | public _indexName: any 9 | public _sourcePriceInclTax: any 10 | public _finalPriceInclTax: any 11 | public _userGroupId: any 12 | public _storeConfigTax: any 13 | 14 | protected constructor (config) { 15 | this._config = config 16 | } 17 | 18 | public taxFor (product, args): Promise|any { 19 | throw new Error('TaxProxy::taxFor must be implemented for specific platform') 20 | } 21 | 22 | /** 23 | * @param Array productList 24 | * @returns Promise 25 | */ 26 | public process (productList, groupId = null): Promise { 27 | throw new Error('TaxProxy::process must be implemented for specific platform') 28 | } 29 | } 30 | 31 | export default AbstractTaxProxy 32 | -------------------------------------------------------------------------------- /packages/platform-magento1/address.ts: -------------------------------------------------------------------------------- 1 | import AbstractAddressProxy from '@storefront-api/platform-abstract/address' 2 | import {multiStoreConfig} from './util'; 3 | 4 | class AddressProxy extends AbstractAddressProxy { 5 | public 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 | public list (customerToken) { 12 | return this.api.address.list(customerToken) 13 | } 14 | 15 | public update (customerToken, addressData) { 16 | return this.api.address.update(customerToken, addressData); 17 | } 18 | 19 | public get (customerToken, addressId) { 20 | return this.api.address.get(customerToken, addressId) 21 | } 22 | 23 | public delete (customerToken, addressData) { 24 | return this.api.address.delete(customerToken, addressData) 25 | } 26 | } 27 | 28 | export default AddressProxy 29 | -------------------------------------------------------------------------------- /integration-sdk/sample-data/import.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // This is just an example app to import the files to Vue Storefront default index 3 | const elasticsearch = require('elasticsearch'); 4 | const fs = require('fs'); 5 | const client = new elasticsearch.Client({ 6 | hosts: [ 'http://localhost:9200'], 7 | apiVersion: '5.6' 8 | }); 9 | 10 | const fileName = process.argv[2] 11 | const entityType = process.argv[3] 12 | const indexName = process.argv[4] 13 | 14 | if (!fileName || !entityType) { 15 | console.error('Please run `node import.js [fileName] [product|attribute|category] [indexName]') 16 | } 17 | 18 | const records = JSON.parse(fs.readFileSync(fileName)) 19 | for (const record of records) { 20 | console.log(`Importing ${entityType}`, record) 21 | client.index({ 22 | index: indexName, 23 | id: record.id, 24 | type: entityType, 25 | body: record 26 | }, function(err, resp, status) { 27 | console.log(resp); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /packages/hooks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/hooks", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront API hooks implementation", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "prepublishOnly": "tsc --build" 10 | }, 11 | "files": [ 12 | "**/*.d.ts", 13 | "*.d.ts", 14 | "**/*.js", 15 | "*.js" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 20 | }, 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "author": "Piotr Karwatka ", 25 | "bugs": { 26 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 27 | }, 28 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 29 | "keywords": [ 30 | "storefront", 31 | "rest", 32 | "api", 33 | "nodejs" 34 | ], 35 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 36 | } 37 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/root.graphqls: -------------------------------------------------------------------------------- 1 | directive @deprecated( 2 | reason: String 3 | ) on FIELD_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION | INPUT_OBJECT | OBJECT | ARGUMENT_DEFINITION | INTERFACE 4 | 5 | 6 | # This is the root Query object - an entry point for `product`, `categories`, `reviews` and other queries 7 | type Query { 8 | version: String 9 | } 10 | 11 | # The classic ElasticSearch result interface 12 | interface ESResponseInterface { 13 | 14 | # Document returned by this search query hits 15 | hits: JSON 16 | 17 | # Search suggest feature 18 | suggest: JSON 19 | 20 | # Aggregations returned when filter have been applied 21 | aggregations: JSON 22 | } 23 | 24 | type ESResponse implements ESResponseInterface { 25 | 26 | # Document returned by this search query hits 27 | hits: JSON 28 | 29 | # Search suggest feature 30 | suggest: JSON 31 | 32 | # Aggregations returned when filter have been applied 33 | aggregations: JSON 34 | } -------------------------------------------------------------------------------- /packages/default-catalog/api/extensions/example-processor/processors/my-product-processor.ts: -------------------------------------------------------------------------------- 1 | class MyProductProcessor { 2 | private _request: any 3 | private _config: any 4 | public constructor (config, request) { 5 | this._request = request 6 | this._config = config 7 | } 8 | 9 | public process (productList) { 10 | // Product search results can be modified here. 11 | // For example, the following would add a paragraph to the short description of every product 12 | // 13 | // for (const prod of productList) { 14 | // prod._source.short_description = prod._source.short_description + '

Free Shipping Today Only!

' 15 | // } 16 | // 17 | // For a real-life example processor, see src/platform/magento2/tax.js 18 | // For more details and another example, see https://docs.vuestorefront.io/guide/extensions/extensions-to-modify-results.html 19 | return productList 20 | } 21 | } 22 | 23 | module.exports = MyProductProcessor 24 | -------------------------------------------------------------------------------- /packages/platform-magento2/order.ts: -------------------------------------------------------------------------------- 1 | import AbstractOrderProxy from '@storefront-api/platform-abstract/order' 2 | import Logger from '@storefront-api/lib/logger' 3 | import { multiStoreConfig } from './util' 4 | import { processSingleOrder } from './o2m' 5 | 6 | class OrderProxy extends AbstractOrderProxy { 7 | public constructor (config, req) { 8 | const Magento2Client = require('magento2-rest-client').Magento2Client; 9 | super(config, req) 10 | this._config = config 11 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 12 | } 13 | 14 | public create (orderData) { 15 | const inst = this 16 | return new Promise((resolve, reject) => { 17 | try { 18 | processSingleOrder(orderData, inst._config, null, (error, result) => { 19 | Logger.error(error) 20 | if (error) reject(error) 21 | resolve(result) 22 | }) 23 | } catch (e) { 24 | reject(e) 25 | } 26 | }) 27 | } 28 | } 29 | 30 | export default OrderProxy 31 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/attribute/aggregations.ts: -------------------------------------------------------------------------------- 1 | import AttributeService from '../../../api/attribute/service' 2 | import { IConfig } from 'config' 3 | 4 | export async function aggregationsToAttributes (response: { aggregations: any, attribute_metadata: { 5 | is_visible_on_front: any, 6 | is_visible: any, 7 | default_frontend_label: any, 8 | attribute_id: any, 9 | entity_type_id: any, 10 | id: any, 11 | is_user_defined: any, 12 | is_comparable: any, 13 | attribute_code: any, 14 | slug: any, 15 | options: any 16 | }[] 17 | }, config: IConfig, esIndex: string) { 18 | if (response.aggregations && config.get('entities.attribute.loadByAttributeMetadata')) { 19 | const attributeListParam = AttributeService.transformAggsToAttributeListParam(response.aggregations) 20 | const attributeList = await AttributeService.list(attributeListParam, config, esIndex) 21 | response.attribute_metadata = attributeList.map(AttributeService.transformToMetadata) 22 | } 23 | return response 24 | } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/lib/helpers/graphql.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileLoader } from 'merge-graphql-schemas'; 3 | 4 | export function loadModuleResolversArray (graphQlPath: string, extensionsPath?: string) { 5 | const rootResolvers = fileLoader(path.join(graphQlPath, `*.resolver.{js,ts}`)) 6 | const coreResolvers = fileLoader( 7 | path.join(graphQlPath, `/**/resolver.{js,ts}`) 8 | ); 9 | const extensionsResolvers = extensionsPath ? fileLoader( 10 | path.join(extensionsPath, `/**/resolver.{js,ts}`) 11 | ) : []; 12 | return rootResolvers.concat(coreResolvers).concat(extensionsResolvers) 13 | } 14 | 15 | export function loadModuleSchemaArray (graphQlPath: string, extensionsPath?: string) { 16 | const rootSchemas = fileLoader(path.join(graphQlPath, `*.graphqls`)) 17 | const coreSchemas = fileLoader( 18 | path.join(graphQlPath, `/**/*.graphqls`) 19 | ); 20 | const extensionsSchemas = extensionsPath ? fileLoader( 21 | path.join(extensionsPath, `/**/*.graphqls`) 22 | ) : [] 23 | return rootSchemas.concat(coreSchemas).concat(extensionsSchemas) 24 | } 25 | -------------------------------------------------------------------------------- /packages/default-img/image/cache/abstract/index.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express' 2 | import { IConfig } from 'config'; 3 | 4 | export interface ImageCacheConstructor { 5 | new(config, req: Request): Cache 6 | 7 | } 8 | 9 | export default abstract class ImageCache implements Cache { 10 | public image: Buffer 11 | public config 12 | public req: Request 13 | public key: string 14 | 15 | public constructor (config, req) { 16 | this.config = config 17 | this.req = req 18 | this.key = this.createKey() 19 | } 20 | 21 | abstract getImageFromCache() 22 | 23 | abstract save() 24 | 25 | abstract check() 26 | 27 | abstract createKey(): string 28 | 29 | abstract isValidFor(type: string): boolean 30 | } 31 | 32 | interface Cache { 33 | image: Buffer, 34 | config: any, 35 | req: Request, 36 | key: string, 37 | getImageFromCache(): void, 38 | save(): void, 39 | check(): void, 40 | createKey(): string, 41 | isValidFor(type: string): boolean 42 | } 43 | 44 | export { 45 | // eslint-disable-next-line no-undef 46 | Cache, 47 | ImageCache 48 | } 49 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/taxrule/resolver.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery, getResponseObject } from '@storefront-api/lib/elastic' 6 | 7 | async function taxrule ({ filter, context, rootValue }) { 8 | let query = buildQuery({ filter, pageSize: 150, type: 'taxrule' }); 9 | 10 | const response = getResponseObject(await client.search(adjustQuery({ 11 | index: getIndexName(context.req.url), 12 | body: query 13 | }, 'taxrule', config))) 14 | 15 | // Process hits 16 | response.items = [] 17 | response.hits.hits.forEach(hit => { 18 | let item = hit._source 19 | item._score = hit._score 20 | response.items.push(item) 21 | }); 22 | 23 | response.total_count = response.hits.total 24 | return response; 25 | } 26 | 27 | const resolver = { 28 | Query: { 29 | taxrule: (_, { filter }, context, rootValue) => taxrule({ filter, context, rootValue }) 30 | } 31 | }; 32 | 33 | export default resolver; 34 | -------------------------------------------------------------------------------- /packages/platform-abstract/address.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractAddressProxy { 5 | protected _request: Request 6 | protected _config: IConfig 7 | public api: Record> 8 | 9 | protected constructor (config, req) { 10 | this._config = config 11 | this._request = req 12 | } 13 | 14 | public list (customerToken): Promise { 15 | throw new Error('AbstractAddressProxy::list must be implemented for specific platform') 16 | } 17 | public update (customerToken, addressData): Promise { 18 | throw new Error('AbstractAddressProxy::update must be implemented for specific platform') 19 | } 20 | public get (customerToken, addressId): Promise { 21 | throw new Error('AbstractAddressProxy::get must be implemented for specific platform') 22 | } 23 | public delete (customerToken, addressData): Promise { 24 | throw new Error('AbstractAddressProxy::delete must be implemented for specific platform') 25 | } 26 | } 27 | 28 | export default AbstractAddressProxy 29 | -------------------------------------------------------------------------------- /packages/platform-magento2/stock.ts: -------------------------------------------------------------------------------- 1 | import AbstractUserProxy from '@storefront-api/platform-abstract/user' 2 | import { multiStoreConfig } from './util' 3 | 4 | class StockProxy extends AbstractUserProxy { 5 | public 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 | public check ({sku, stockId = 0}) { 12 | return this.api.stockItems.list(sku).then((result) => { 13 | if (this._config.get('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 | export default StockProxy 31 | -------------------------------------------------------------------------------- /src/modules/template-module/graphql/hello/resolver.ts: -------------------------------------------------------------------------------- 1 | import { StoreFrontResloverContext } from '@storefront-api/lib/module/types'; 2 | import { IResolvers } from 'apollo-server-express'; 3 | import es from '@storefront-api/lib/elastic' 4 | import bodybuilder from 'bodybuilder' 5 | 6 | const resolver: IResolvers = { 7 | Query: { 8 | sayHello: (_, { name }, context, rootValue) => { 9 | return `Hello ${name}!` 10 | }, 11 | testElastic: async (_, { sku }, { db, config }, rootValue) => { 12 | const client = db.getElasticClient() 13 | const esQuery = es.adjustQuery({ 14 | index: 'vue_storefront_catalog', // current index name 15 | type: 'product', 16 | body: bodybuilder().filter('terms', 'visibility', [2, 3, 4]).andFilter('term', 'status', 1).andFilter('terms', 'sku', sku).build() 17 | }, 'product', config) 18 | const response = es.getResponseObject(await client.search(esQuery)).hits.hits.map(el => { return el._source }) 19 | if (response.length > 0) return response[0]; else return null 20 | } 21 | } 22 | }; 23 | 24 | export default resolver; 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/platform-magento2/util.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import { getCurrentStoreCode } from '@storefront-api/lib/util' 3 | import Logger from '@storefront-api/lib/logger' 4 | 5 | /** 6 | * Adjust the config provided to the current store selected via request params 7 | * @param Object config configuration 8 | * @param Express request req 9 | */ 10 | export function multiStoreConfig (apiConfig, req) { 11 | let confCopy = Object.assign({}, apiConfig) 12 | let storeCode = getCurrentStoreCode(req) 13 | 14 | const availableStores = config.get('availableStores') 15 | const magento2Config = config.get>('magento2') 16 | 17 | if (storeCode && availableStores.indexOf(storeCode) >= 0) { 18 | if (magento2Config['api_' + storeCode]) { 19 | confCopy = Object.assign({}, magento2Config['api_' + storeCode]) // we're to use the specific api configuration - maybe even separate magento instance 20 | } 21 | confCopy.url = confCopy.url + '/' + storeCode 22 | } else { 23 | if (storeCode) { 24 | Logger.error('Unavailable store code', storeCode) 25 | } 26 | } 27 | 28 | return confCopy 29 | } 30 | -------------------------------------------------------------------------------- /packages/default-catalog/helper/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.modules.defaultCatalog.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 | -------------------------------------------------------------------------------- /scripts/kue.ts: -------------------------------------------------------------------------------- 1 | import program from 'commander' 2 | import config from 'config' 3 | import kue from '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.get('redis'), 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 | -------------------------------------------------------------------------------- /packages/default-img/image/cache/file/index.ts: -------------------------------------------------------------------------------- 1 | import ImageCache from '../abstract' 2 | import fs from 'fs-extra' 3 | import { createHash } from 'crypto' 4 | import Logger from '@storefront-api/lib/logger' 5 | 6 | export default class FileImageCache extends ImageCache { 7 | public async getImageFromCache () { 8 | this.image = await fs.readFile( 9 | `${this.config.imageable.caching.file.path}/${this.path}` 10 | ) 11 | } 12 | 13 | public async save () { 14 | await fs.outputFile( 15 | `${this.config.imageable.caching.file.path}/${this.path}`, 16 | this.image 17 | ) 18 | } 19 | 20 | public async check () { 21 | const response = await fs.pathExists(`${this.config.imageable.caching.file.path}/${this.path}`) 22 | return response 23 | } 24 | 25 | private get path (): string { 26 | return `${this.key.substring(0, 2)}/${this.key.substring(2, 4)}/${this.key}` 27 | } 28 | 29 | public createKey (): string { 30 | Logger.info(createHash('md5').update(this.req.url).digest('hex')) 31 | return createHash('md5').update(this.req.url).digest('hex') 32 | } 33 | 34 | public isValidFor (type) { 35 | return type === 'file' 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: If you are not sure how something works or want discuss something just describe your doubts. 4 | labels: question 5 | --- 6 | 7 | ## Prerequisites 8 | 9 | Please answer the following questions for yourself before submitting an issue. 10 | - [ ] I am running the latest version 11 | - [ ] I checked the documentation and found no answer 12 | - [ ] I checked to make sure that this issue has not already been filed 13 | 14 | ## Check our forum: 15 | https://forum.vuestorefront.io/ 16 | 17 | # Is there something you don't understand? What is it? Describe it. 18 | 19 | 20 | # Can't find what you're looking for? 21 | 22 | 23 | 24 | 25 | ## Keep the problem description concise and include: 26 | - [ ] A brief description of the problem, 27 | - [ ] Where the problem is occurring, 28 | - [ ] The length of time the problem has been occurring, 29 | - [ ] The size of the problem. 30 | 31 | 32 | ## Additional information 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /packages/platform-magento2/product.ts: -------------------------------------------------------------------------------- 1 | import AbstractProductProxy from '@storefront-api/platform-abstract/product' 2 | import { multiStoreConfig } from './util' 3 | 4 | class ProductProxy extends AbstractProductProxy { 5 | public 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 | public 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 | public 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 | export default ProductProxy 27 | -------------------------------------------------------------------------------- /.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 | yarn prebuild:packages 30 | env: 31 | CI: true 32 | - name: yarn unit test 33 | run: | 34 | yarn test:unit 35 | env: 36 | CI: true 37 | - name: Start Elasticsearch and Redis 38 | run: | 39 | docker-compose up -d redis es7 40 | env: 41 | CI: true 42 | - name: Restore main Index for testing 43 | run: | 44 | yarn restore7 45 | env: 46 | CI: true 47 | - name: yarn integration test 48 | run: | 49 | yarn test:integration 50 | env: 51 | CI: true 52 | -------------------------------------------------------------------------------- /integration-sdk/sample-data/fetch_demo_attributes.sh: -------------------------------------------------------------------------------- 1 | # This command requires "jq" -> https://stedolan.github.io/jq/ 2 | if ! [ -x "$(command -v jq)" ]; then 3 | echo 'Error: jq is not installed. Please download it from https://stedolan.github.io/jq/' >&2 4 | exit 1 5 | fi 6 | 7 | curl -sS "https://demo.storefrontcloud.io/api/catalog/vue_storefront_catalog/attribute/_search?size=50&from=0&sort=&_source_include=attribute_code%2Cid%2Centity_type_id%2Coptions%2Cdefault_value%2Cis_user_defined%2Cfrontend_label%2Cattribute_id%2Cdefault_frontend_label%2Cis_visible_on_front%2Cis_visible%2Cis_comparable%2Ctier_prices%2Cfrontend_input&request=%7B%22query%22%3A%7B%22bool%22%3A%7B%22filter%22%3A%7B%22bool%22%3A%7B%22must%22%3A%5B%7B%22terms%22%3A%7B%22attribute_code%22%3A%5B%22pattern%22%2C%22eco_collection%22%2C%22new%22%2C%22climate%22%2C%22style_bottom%22%2C%22size%22%2C%22color%22%2C%22performance_fabric%22%2C%22sale%22%2C%22material%22%5D%7D%7D%2C%7B%22terms%22%3A%7B%22is_user_defined%22%3A%5Btrue%5D%7D%7D%2C%7B%22terms%22%3A%7B%22is_visible%22%3A%5Btrue%5D%7D%7D%5D%7D%7D%7D%7D%7D" | jq ".hits.hits[]._source | { id, is_user_defined, is_visible, frontend_input, attribute_code, default_value, options, default_frontend_label }" | jq -s -M \ > attributes.json 8 | 9 | echo "Attributes dumped into 'attributes.json'" -------------------------------------------------------------------------------- /packages/platform-magento1/util.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import { getCurrentStoreCode } from '@storefront-api/lib/util' 3 | import Logger from '@storefront-api/lib/logger' 4 | 5 | /** 6 | * Adjust the config provided to the current store selected via request params 7 | * @param Object config configuration 8 | * @param Express request req 9 | */ 10 | export function multiStoreConfig (apiConfig, req) { 11 | let confCopy = Object.assign({}, apiConfig) 12 | let storeCode = getCurrentStoreCode(req) 13 | const availableStores = config.get('availableStores') 14 | const magento1Config = config.get>('magento1') 15 | if (storeCode && availableStores.indexOf(storeCode) >= 0) { 16 | if (magento1Config['api_' + storeCode]) { 17 | confCopy = Object.assign({}, magento1Config['api_' + storeCode]) // we're to use the specific api configuration - maybe even separate magento instance 18 | } else { 19 | if (new RegExp('(/' + availableStores.join('|') + '/)', 'gm').exec(confCopy.url) === null) { 20 | confCopy.url = (confCopy.url).replace(/(vsbridge)/gm, `${storeCode}/$1`); 21 | } 22 | } 23 | } else { 24 | if (storeCode) { 25 | Logger.error('Unavailable store code', storeCode) 26 | } 27 | } 28 | 29 | return confCopy 30 | } 31 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/category/resolver.ts: -------------------------------------------------------------------------------- 1 | import { list as listProducts } from '../product/service' 2 | import { list, listSingleCategory, listBreadcrumbs } from './service' 3 | 4 | const resolver = { 5 | Query: { 6 | categories: (_, { search, filter, currentPage, pageSize, sort, _sourceIncludes }, context, rootValue) => 7 | list({ search, filter, currentPage, pageSize, sort, context, rootValue, _sourceIncludes }), 8 | category: (_, { id, url_path, _sourceIncludes, _sourceExcludes }, context, rootValue) => 9 | listSingleCategory({ id, url_path, context, rootValue, _sourceIncludes, _sourceExcludes }) 10 | }, 11 | Category: { 12 | products: (_, { search, filter, currentPage, pageSize, sort, _sourceIncludes, _sourceExcludes }, context, rootValue) => { 13 | return listProducts({ filter: Object.assign({}, filter, { category_ids: { in: _.id } }), sort, currentPage, pageSize, search, context, rootValue, _sourceIncludes, _sourceExcludes }) 14 | }, 15 | children: (_, { search }, context, rootValue) => 16 | _.children_data, 17 | breadcrumbs: async (_, { search }, context) => { 18 | const breadcrumbs = await listBreadcrumbs({ category: _, context }) 19 | 20 | return breadcrumbs 21 | } 22 | } 23 | }; 24 | 25 | export default resolver; 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | 6 | --- 7 | 8 | ## Current behavior 9 | 10 | 11 | 12 | 13 | ## Expected behavior 14 | 15 | 16 | 17 | 18 | ## Steps to reproduce the issue 19 | 23 | 24 | 25 | 26 | ## Repository 27 | 28 | 29 | 30 | 31 | ## Can you handle fixing this bug by yourself? 32 | 33 | - [ ] YES 34 | - [ ] NO 35 | 36 | 37 | ## Environment details 38 | 39 | - Browser: 40 | - OS: 41 | - Node: 42 | - Code Version: 43 | 44 | ## Additional information 45 | 46 | 47 | -------------------------------------------------------------------------------- /packages/default-vsf/api/extensions/example-magento-api/index.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '@storefront-api/lib/util'; 2 | import { Router } from 'express'; 3 | import Logger from '@storefront-api/lib/logger' 4 | const Magento2Client = require('magento2-rest-client').Magento2Client 5 | 6 | module.exports = ({ config, db }) => { 7 | let mcApi = Router(); 8 | 9 | /** 10 | * This is just an example on how to extend magento2 api client and get the cms blocks 11 | * https://devdocs.magento.com/swagger/#!/cmsBlockRepositoryV1/cmsBlockRepositoryV1GetListGet 12 | * 13 | * NOTICE: storefront-api should be platform agnostic. This is just for the customization example 14 | */ 15 | mcApi.get('/cmsBlock', (req, res) => { 16 | const client = Magento2Client(config.magento2.api); 17 | client.addMethods('cmsBlock', (restClient) => { 18 | var module: Record = {}; 19 | 20 | module.search = function () { 21 | return restClient.get('/cmsPage/search'); 22 | } 23 | return module; 24 | }) 25 | Logger.info(client) 26 | client.cmsBlock.search().then((result) => { 27 | apiStatus(res, result, 200); // just dump it to the browser, result = JSON object 28 | }).catch(err => { 29 | apiStatus(res, err, 500); 30 | }) 31 | }) 32 | 33 | return mcApi 34 | } 35 | -------------------------------------------------------------------------------- /packages/default-vsf/api/review.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus, apiError } from '@storefront-api/lib/util'; 2 | import { Router } from 'express'; 3 | import PlatformFactory from '@storefront-api/platform/factory' 4 | import AbstractReviewProxy from '@storefront-api/platform-abstract/review'; 5 | 6 | const Ajv = require('ajv'); // json validator 7 | 8 | export default ({config, db}) => { 9 | const reviewApi = Router(); 10 | 11 | const _getProxy = (req): AbstractReviewProxy => { 12 | const platform = config.platform 13 | const factory = new PlatformFactory(config, req) 14 | return factory.getAdapter(platform, 'review') 15 | }; 16 | 17 | reviewApi.post('/create', (req, res) => { 18 | const ajv = new Ajv(); 19 | const reviewProxy = _getProxy(req) 20 | const reviewSchema = require('../models/review.schema') 21 | const validate = ajv.compile(reviewSchema) 22 | 23 | req.body.review.review_status = config.review.defaultReviewStatus 24 | 25 | if (!validate(req.body)) { 26 | console.dir(validate.errors); 27 | apiStatus(res, validate.errors, 400); 28 | return; 29 | } 30 | 31 | reviewProxy.create(req.body.review).then((result) => { 32 | apiStatus(res, result, 200); 33 | }).catch(err => { 34 | apiError(res, err); 35 | }) 36 | }) 37 | 38 | return reviewApi 39 | } 40 | -------------------------------------------------------------------------------- /packages/default-img/image/cache/factory.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Request } from 'express' 4 | import { IConfig } from 'config' 5 | import { Cache, ImageCacheConstructor } from './abstract' 6 | 7 | export default class CacheFactory { 8 | private request: Request 9 | private config: IConfig 10 | public static adapter: Record = {} 11 | 12 | public constructor (app_config: IConfig, req: Request) { 13 | this.config = app_config 14 | this.request = req 15 | } 16 | 17 | public static addAdapter (type: string, object: Record): any { 18 | CacheFactory.adapter[type] = object 19 | } 20 | 21 | public getAdapter (type: string, ...constructorParams): any { 22 | let AdapterClass: ImageCacheConstructor|undefined = CacheFactory.adapter[type] 23 | 24 | if (!AdapterClass) { 25 | AdapterClass = require(`./${type}`).default 26 | } 27 | 28 | if (!AdapterClass) { 29 | throw new Error(`Invalid adapter ${type}`) 30 | } else { 31 | const adapterInstance: Cache = new AdapterClass(this.config, this.request) 32 | if ((typeof adapterInstance.isValidFor === 'function') && !adapterInstance.isValidFor(type)) { throw new Error(`Not valid adapter class or adapter is not valid for ${type}`) } 33 | return adapterInstance 34 | } 35 | } 36 | } 37 | 38 | export { 39 | CacheFactory 40 | } 41 | -------------------------------------------------------------------------------- /packages/default-catalog/processor/default.ts: -------------------------------------------------------------------------------- 1 | import {Request, Response} from 'express'; 2 | import {IConfig} from 'config'; 3 | 4 | const jwa = require('jwa'); 5 | const hmac = jwa('HS256'); 6 | 7 | export abstract class ProcessorAbstract { 8 | protected _config: IConfig 9 | protected _entityType: any 10 | protected _indexName: any 11 | protected _req: Request 12 | protected _res: Response 13 | 14 | public constructor (config, entityType, indexName, req, res) { 15 | this._config = config 16 | this._entityType = entityType 17 | this._indexName = indexName 18 | this._req = req 19 | this._res = res 20 | } 21 | 22 | public abstract process(items: any[], args?: any): Promise 23 | } 24 | 25 | export default class HmacProcessor extends ProcessorAbstract { 26 | public process (items): Promise { 27 | const processorChain = [] 28 | return new Promise((resolve, reject) => { 29 | const rs = items.map((item) => { 30 | if (this._req.query._source_exclude && (this._req.query._source_exclude as string).indexOf('sgn') < 0) { 31 | 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 32 | } 33 | return item 34 | }) 35 | 36 | // return first resultSet 37 | resolve(rs) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/lib/module/types.ts: -------------------------------------------------------------------------------- 1 | import { IConfig } from 'config' 2 | import { Express } from 'express'; 3 | import { RedisClient } from 'redis'; 4 | import { Client } from '@elastic/elasticsearch' 5 | import { ExpressContext } from 'apollo-server-express/dist/ApolloServer' 6 | import {BaseLogger} from '../logger'; 7 | 8 | export interface DbContext { 9 | getElasticClient(): Client, 10 | getRedisClient(): RedisClient 11 | } 12 | 13 | export interface StorefrontApiContext { 14 | config?: IConfig, 15 | app: Express, 16 | db: DbContext, 17 | logger: BaseLogger 18 | } 19 | 20 | export interface GraphqlConfiguration { 21 | schema: any[], 22 | resolvers: any[], 23 | hasGraphqlSupport: boolean 24 | } 25 | 26 | export interface ElasticSearchMappings { 27 | schemas: any 28 | } 29 | export interface StorefrontApiModuleConfig { 30 | key: string, 31 | initMiddleware?: (context: StorefrontApiContext) => void, 32 | initApi?: (context: StorefrontApiContext) => void, 33 | initGraphql?: (context: StorefrontApiContext) => GraphqlConfiguration, 34 | loadMappings?: (context: StorefrontApiContext) => ElasticSearchMappings, 35 | beforeRegistration?: (context: StorefrontApiContext) => void, 36 | afterRegistration?: (context: StorefrontApiContext) => void 37 | } 38 | 39 | export interface StoreFrontResloverContext extends ExpressContext { 40 | config?: IConfig, 41 | db: DbContext, 42 | logger: BaseLogger 43 | } 44 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/core", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront API core", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "prepublishOnly": "tsc --build" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "files": [ 15 | "*.d.ts", 16 | "*.js" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 21 | }, 22 | "author": "Piotr Karwatka ", 23 | "dependencies": { 24 | "@storefront-api/lib": "^1.0.0-rc.3", 25 | "apollo-server-express": "^2.9.12", 26 | "body-parser": "^1.18.2", 27 | "config": "^1.30.0", 28 | "cors": "^2.8.5", 29 | "express": "^4.17.1", 30 | "lodash": "^4.17.15", 31 | "merge-graphql-schemas": "^1.7.3", 32 | "morgan": "^1.9.1" 33 | }, 34 | "devDependencies": { 35 | "@types/body-parser": "^1.17.0", 36 | "@types/config": "^0.0.34", 37 | "@types/cors": "^2.8.6", 38 | "@types/lodash": "^4.14.149" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 42 | }, 43 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 44 | "keywords": [ 45 | "storefront", 46 | "rest", 47 | "api", 48 | "nodejs" 49 | ], 50 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 51 | } 52 | -------------------------------------------------------------------------------- /packages/platform/factory.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Request } from 'express'; 4 | import { IConfig } from 'config'; 5 | 6 | export interface Platform { 7 | name: string, 8 | platformImplementation: Record 9 | } 10 | 11 | class PlatformFactory { 12 | private readonly request: Request 13 | private readonly config: IConfig 14 | 15 | private static platform: Record = {} 16 | private static platformName: string = '' 17 | 18 | public constructor (app_config: IConfig, req: Request|null = null) { 19 | this.config = app_config; 20 | this.request = req 21 | } 22 | 23 | public static addPlatform ({ name, platformImplementation }: Platform) { 24 | PlatformFactory.platform = platformImplementation 25 | PlatformFactory.platformName = name 26 | } 27 | 28 | public getAdapter (platform: string, type: string, ...constructorParams): any { 29 | let AdapterClass = PlatformFactory.platform[type]; 30 | if (!AdapterClass) { 31 | throw new Error(`Invalid adapter ${PlatformFactory.platformName} / ${type}`); 32 | } else { 33 | let adapter_instance = new AdapterClass(this.config, this.request, ...constructorParams); 34 | if ((typeof adapter_instance.isValidFor === 'function') && !adapter_instance.isValidFor(type)) { throw new Error(`Not valid adapter class or adapter is not valid for ${type}`); } 35 | return adapter_instance; 36 | } 37 | } 38 | } 39 | 40 | export default PlatformFactory 41 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/json_type/resolver.ts: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /packages/default-img/image/action/factory.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { NextFunction, Request, Response } from 'express' 4 | import { IConfig } from 'config'; 5 | import { ImageActionConstructor } from './abstract'; 6 | 7 | export default class ActionFactory { 8 | public request: Request 9 | public next: NextFunction 10 | public response: Response 11 | public config: IConfig 12 | public static adapter: Record = {} 13 | 14 | public constructor (req: Request, res, next, app_config) { 15 | this.request = req 16 | this.response = res 17 | this.next = next 18 | this.config = app_config; 19 | } 20 | 21 | public static addAdapter (type: string, object: Record): any { 22 | ActionFactory.adapter[type] = object 23 | } 24 | 25 | public getAdapter (type: string): any { 26 | let AdapterClass: ImageActionConstructor|undefined = ActionFactory.adapter[type] 27 | 28 | if (!AdapterClass) { 29 | AdapterClass = require(`./${type}`).default 30 | } 31 | 32 | if (!AdapterClass) { 33 | throw new Error(`Invalid adapter ${type}`); 34 | } else { 35 | let adapter_instance = new AdapterClass(this.request, this.response, this.next, this.config); 36 | if ((typeof adapter_instance.isValidFor === 'function') && !adapter_instance.isValidFor(type)) { throw new Error(`Not valid adapter class or adapter is not valid for ${type}`); } 37 | return adapter_instance; 38 | } 39 | } 40 | } 41 | 42 | export { 43 | ActionFactory 44 | }; 45 | -------------------------------------------------------------------------------- /docs/guide/default-modules/platforms.md: -------------------------------------------------------------------------------- 1 | # Platform concept 2 | 3 | Our default modules (`default-catalog`, `default-img`, `default-vsf`) implement the abstract platform concept. Here is how it works: 4 | 5 | ## Select platform in the configuration 6 | 7 | The `local.json` and `default.json` files include the platform property: 8 | 9 | ```json 10 | "platform": "magento2", 11 | ``` 12 | 13 | The config file is also prepared for storing some additional platform-related information ([example](https://github.com/DivanteLtd/storefront-api/blob/a66222768bf7fb5f54acf268b6a0bb4e0f94a4cf/config/default.json#L201)). 14 | 15 | 16 | ## Create a driver 17 | 18 | Based on this setting, [Storefront API Platform Factory](https://github.com/DivanteLtd/storefront-api/blob/develop/src/platform/factory.ts) selects the [backend platform driver](https://github.com/DivanteLtd/storefront-api/blob/a66222768bf7fb5f54acf268b6a0bb4e0f94a4cf/src/modules/default-vsf/api/cart.js#L10). 19 | 20 | Then, the Express.js API handler ([example](https://github.com/DivanteLtd/storefront-api/blob/a66222768bf7fb5f54acf268b6a0bb4e0f94a4cf/src/modules/default-vsf/api/cart.js#L20)) calls the proper request handler. 21 | 22 | **Note:** The platform driver is in charge of adapting the input and output data from the normalized (platform agnostic) data formats to the platform specific format. It usually calls the [platform specific API](https://github.com/DivanteLtd/storefront-api/blob/a66222768bf7fb5f54acf268b6a0bb4e0f94a4cf/src/platform/magento2/order.js#L10) inside. 23 | -------------------------------------------------------------------------------- /packages/platform-magento2/user.ts: -------------------------------------------------------------------------------- 1 | import AbstractUserProxy from '@storefront-api/platform-abstract/user' 2 | import { multiStoreConfig } from './util' 3 | import Logger from '@storefront-api/lib/logger' 4 | 5 | class UserProxy extends AbstractUserProxy { 6 | public constructor (config, req) { 7 | const Magento2Client = require('magento2-rest-client').Magento2Client; 8 | super(config, req) 9 | this.api = Magento2Client(multiStoreConfig(config.magento2.api, req)); 10 | } 11 | 12 | public register (userData) { 13 | return this.api.customers.create(userData) 14 | } 15 | 16 | public login (userData) { 17 | return this.api.customers.token(userData) 18 | } 19 | 20 | public me (requestToken) { 21 | Logger.info(this.api.customers.me(requestToken)); 22 | 23 | return this.api.customers.me(requestToken) 24 | } 25 | public orderHistory (requestToken, pageSize = 20, currentPage = 1) { 26 | return this.api.customers.orderHistory(requestToken, pageSize, currentPage) 27 | } 28 | public resetPassword (emailData) { 29 | return this.api.customers.resetPassword(emailData) 30 | } 31 | 32 | public update (userData) { 33 | return this.api.customers.update(userData) 34 | } 35 | 36 | public changePassword (passwordData) { 37 | return this.api.customers.changePassword(passwordData) 38 | } 39 | 40 | public resetPasswordUsingResetToken (resetData) { 41 | return this.api.customers.resetPasswordUsingResetToken(resetData) 42 | } 43 | } 44 | 45 | export default UserProxy 46 | -------------------------------------------------------------------------------- /src/modules/template-module/index.ts: -------------------------------------------------------------------------------- 1 | import { StorefrontApiModule, registerExtensions } from '@storefront-api/lib/module' 2 | import { StorefrontApiContext, GraphqlConfiguration, ElasticSearchMappings } from '@storefront-api/lib/module/types' 3 | import { Router } from 'express' 4 | import resolvers from './graphql/resolvers' 5 | import schema from './graphql/schema' 6 | import path from 'path' 7 | import version from './api/version' 8 | import { loadSchema } from '@storefront-api/lib/elastic' 9 | 10 | export const TemplateModule: StorefrontApiModule = new StorefrontApiModule({ 11 | key: 'template-module', 12 | 13 | initGraphql: ({ config, db, app }: StorefrontApiContext): GraphqlConfiguration => { 14 | return { 15 | resolvers, 16 | schema, 17 | hasGraphqlSupport: true 18 | } 19 | }, 20 | 21 | loadMappings: ({ config, db, app }: StorefrontApiContext): ElasticSearchMappings => { 22 | return { 23 | schemas: { 24 | 'custom': loadSchema(path.join(__dirname, 'elasticsearch'), 'custom', config.get('elasticsearch.apiVersion')) 25 | }} 26 | }, 27 | 28 | initApi: ({ config, db, app }: StorefrontApiContext): void => { 29 | let api = Router(); 30 | 31 | // mount the order resource 32 | api.use('/version', version({ config, db })); 33 | registerExtensions({ app, config, db, registeredExtensions: config.get('modules.templateModule.registeredExtensions'), rootPath: path.join(__dirname, 'api', 'extensions') }) 34 | 35 | // api router 36 | app.use('/template', api); 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /scripts/cache.ts: -------------------------------------------------------------------------------- 1 | import program from 'commander' 2 | import config from 'config' 3 | import cache from 'packages/lib/cache-instance' 4 | 5 | program 6 | .command('clear') 7 | .option('-t|--tag ', 'tag name, available tags: ' + config.get('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.get('server.availableCacheTags') 17 | } else { 18 | tags = cmd.tag.split(',') 19 | } 20 | const subPromises = [] 21 | tags.forEach(tag => { 22 | if (config.get('server.availableCacheTags').indexOf(tag) >= 0 || config.get('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 | -------------------------------------------------------------------------------- /packages/platform-magento1/user.ts: -------------------------------------------------------------------------------- 1 | import AbstractUserProxy from '@storefront-api/platform-abstract/user' 2 | import { multiStoreConfig } from './util' 3 | 4 | class UserProxy extends AbstractUserProxy { 5 | public 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 | public register (userData) { 11 | return this.api.user.create(userData) 12 | } 13 | public login (userData) { 14 | return this.api.user.login(userData) 15 | } 16 | public me (customerToken) { 17 | return this.api.user.me(customerToken) 18 | } 19 | public orderHistory (customerToken, page, pageSize) { 20 | return this.api.user.orderHistory(customerToken, page, pageSize) 21 | } 22 | public creditValue (customerToken) { 23 | return this.api.user.creditValue(customerToken) 24 | } 25 | public refillCredit (customerToken, creditCode) { 26 | return this.api.user.refillCredit(customerToken, creditCode) 27 | } 28 | public resetPassword (emailData) { 29 | return this.api.user.resetPassword(emailData) 30 | } 31 | public update (userData) { 32 | return this.api.user.update(userData) 33 | } 34 | public changePassword (passwordData) { 35 | return this.api.user.changePassword(passwordData) 36 | } 37 | public resetPasswordUsingResetToken (resetData) { 38 | return this.api.user.resetPasswordUsingResetToken(resetData) 39 | } 40 | } 41 | 42 | export default UserProxy 43 | -------------------------------------------------------------------------------- /kubernetes/storefront-graphql-api-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: storefront-api 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: storefront-api 9 | template: 10 | metadata: 11 | labels: 12 | app: storefront-api 13 | spec: 14 | containers: 15 | - name: storefront-api 16 | image: divante/storefront-api:latest 17 | envFrom: 18 | - configMapRef: 19 | name: 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/storefront-api" 52 | - name: dist 53 | emptyDir: 54 | medium: Memory 55 | -------------------------------------------------------------------------------- /packages/platform/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/platform", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront GraphQL API", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "test": "eslint src", 10 | "test:unit": "jest -c test/unit/jest.conf.js", 11 | "lint": "eslint --ext .ts", 12 | "prepublishOnly": "tsc --build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 17 | }, 18 | "files": [ 19 | "**/*.d.ts", 20 | "*.d.ts", 21 | "**/*.js", 22 | "*.js" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "author": "Piotr Karwatka ", 28 | "dependencies": { 29 | "config": "^1.30.0", 30 | "typescript": "3.7.*" 31 | }, 32 | "devDependencies": { 33 | "@types/config": "^0.0.34", 34 | "@types/express": "^4.16.1", 35 | "@types/jest": "^24.0.11", 36 | "@types/jsonfile": "^5.0.0", 37 | "@types/lodash": "^4.14.149", 38 | "@types/node": "^11.13.4", 39 | "jest": "^24.8.0", 40 | "nodemon": "^1.18.7", 41 | "ts-jest": "^24.0.2", 42 | "tslib": "^1.9.3" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 46 | }, 47 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 48 | "keywords": [ 49 | "storefront", 50 | "rest", 51 | "api", 52 | "nodejs" 53 | ], 54 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 55 | } 56 | -------------------------------------------------------------------------------- /packages/platform-abstract/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/platform-abstract", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront GraphQL API", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "test": "eslint src", 10 | "test:unit": "jest -c test/unit/jest.conf.js", 11 | "lint": "eslint --ext .ts", 12 | "prepublishOnly": "tsc --build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 17 | }, 18 | "files": [ 19 | "**/*.d.ts", 20 | "*.d.ts", 21 | "**/*.js", 22 | "*.js" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "author": "Piotr Karwatka ", 28 | "dependencies": { 29 | "config": "^1.30.0", 30 | "typescript": "3.7.*" 31 | }, 32 | "devDependencies": { 33 | "@types/config": "^0.0.34", 34 | "@types/express": "^4.16.1", 35 | "@types/jest": "^24.0.11", 36 | "@types/jsonfile": "^5.0.0", 37 | "@types/lodash": "^4.14.149", 38 | "@types/node": "^11.13.4", 39 | "jest": "^24.8.0", 40 | "nodemon": "^1.18.7", 41 | "ts-jest": "^24.0.2", 42 | "tslib": "^1.9.3" 43 | }, 44 | "bugs": { 45 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 46 | }, 47 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 48 | "keywords": [ 49 | "storefront", 50 | "rest", 51 | "api", 52 | "nodejs" 53 | ], 54 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 55 | } 56 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/cms/resolver.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery, getResponseObject } from '@storefront-api/lib/elastic' 6 | 7 | async function list ({ filter, currentPage, pageSize = 200, context, _source_include, type }) { 8 | let query = buildQuery({ filter, currentPage, pageSize, type }); 9 | 10 | const response = getResponseObject(await client.search(adjustQuery({ 11 | index: getIndexName(context.req.url), 12 | body: query, 13 | _source_include 14 | }, type, config))); 15 | 16 | return buildItems(response) 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, _sourceIncludes, type = 'cms_page' }, context) => 33 | list({ filter, currentPage, pageSize, _source_include: _sourceIncludes, type, context }), 34 | cmsBlocks: (_, { filter, currentPage, pageSize, _sourceIncludes, type = 'cms_block' }, context) => 35 | list({ filter, currentPage, pageSize, _source_include: _sourceIncludes, type, context }), 36 | cmsHierarchies: (_, { filter, currentPage, pageSize, _sourceIncludes, type = 'cms_hierarchy' }, context) => 37 | list({ filter, currentPage, pageSize, _source_include: _sourceIncludes, type, context }) 38 | } 39 | }; 40 | 41 | export default resolver; 42 | -------------------------------------------------------------------------------- /packages/default-catalog/processor/factory.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import Logger from '@storefront-api/lib/logger' 3 | 4 | /** 5 | * Check if the module exists 6 | * @param module name name 7 | */ 8 | function module_exists (name) { 9 | try { return require.resolve(name) } catch (e) { return false } 10 | } 11 | 12 | export class ProcessorFactory { 13 | private config: any 14 | public static processors: Record = {} 15 | public constructor (app_config) { 16 | this.config = app_config; 17 | } 18 | 19 | public static addAdapter (name: string, object: Record) { 20 | ProcessorFactory.processors[name] = object 21 | } 22 | 23 | public getAdapter (entityType, indexName, req, res) { 24 | let AdapterClass 25 | if (ProcessorFactory.processors[entityType]) { 26 | AdapterClass = ProcessorFactory.processors[entityType] 27 | } else { 28 | const moduleName = './' + entityType 29 | 30 | if (!module_exists(moduleName)) { 31 | Logger.info('No additional data adapter for ' + entityType) 32 | return null 33 | } 34 | 35 | AdapterClass = require(moduleName); 36 | } 37 | 38 | if (!AdapterClass) { 39 | Logger.info('No additional data adapter for ' + entityType) 40 | return null 41 | } else { 42 | let adapter_instance = new AdapterClass(this.config, entityType, indexName, req, res); 43 | 44 | if ((typeof adapter_instance.isValidFor === 'function') && !adapter_instance.isValidFor(entityType)) { throw new Error('Not valid adapter class or adapter is not valid for ' + entityType); } 45 | 46 | return adapter_instance; 47 | } 48 | } 49 | } 50 | 51 | export default ProcessorFactory; 52 | -------------------------------------------------------------------------------- /packages/default-vsf/worker/order_to_magento2.ts: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | services: 3 | es7: 4 | container_name: sfa_elasticsearch 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 | container_name: sfa_redis 23 | image: 'redis:4-alpine' 24 | ports: 25 | - '6379:6379' 26 | 27 | app: 28 | # image: divante/storefront-api:latest 29 | build: 30 | context: . 31 | dockerfile: docker/storefront-api/Dockerfile 32 | depends_on: 33 | - es7 34 | - redis 35 | env_file: docker/storefront-api/default.env 36 | environment: 37 | VS_ENV: dev 38 | volumes: 39 | - './config:/var/www/config' 40 | - './ecosystem.json:/var/www/ecosystem.json' 41 | - './migrations:/var/www/migrations' 42 | - './package.json:/var/www/package.json' 43 | - './babel.config.js:/var/www/babel.config.js' 44 | - './tsconfig.json:/var/www/tsconfig.json' 45 | - './nodemon.json:/var/www/nodemon.json' 46 | - './graphql-schema-linter.config.js:/var/www/graphql-schema-linter.config.js' 47 | - './scripts:/var/www/scripts' 48 | - './src:/var/www/src' 49 | - './var:/var/www/var' 50 | - './packages:/var/www/packages' 51 | tmpfs: 52 | - /var/www/dist 53 | ports: 54 | - '8080:8080' 55 | -------------------------------------------------------------------------------- /packages/default-img/index.ts: -------------------------------------------------------------------------------- 1 | import { StorefrontApiModule } from '@storefront-api/lib/module' 2 | import { StorefrontApiContext } from '@storefront-api/lib/module/types' 3 | import img from './api/img' 4 | import CacheFactory from './image/cache/factory'; 5 | import LocalImageAction from './image/action/local'; 6 | import FileImageCache from './image/cache/file'; 7 | import ActionFactory from './image/action/factory'; 8 | import { Cache } from './image/cache/abstract'; 9 | import { Action } from './image/action/abstract'; 10 | import Logger from '@storefront-api/lib/logger' 11 | 12 | export const module: StorefrontApiModule = new StorefrontApiModule({ 13 | key: 'default-img', 14 | initApi: ({ config, db, app }: StorefrontApiContext): void => { 15 | app.use('/img', img({ config, db })); 16 | app.use('/img/:width/:height/:action/:image', (req, res, next) => { 17 | Logger.info(req.params) 18 | }); 19 | } 20 | }) 21 | 22 | interface imageActions { 23 | name: string, 24 | class: Action 25 | } 26 | 27 | interface cacheActions { 28 | name: string, 29 | class: Cache 30 | } 31 | 32 | export const DefaultImgModule = ({ imageActions, cacheActions }: { imageActions?: imageActions[], cacheActions?: cacheActions[] } = {}): StorefrontApiModule => { 33 | ActionFactory.addAdapter('local', LocalImageAction) 34 | CacheFactory.addAdapter('file', FileImageCache) 35 | if (Array.isArray(imageActions)) { 36 | imageActions.forEach((action) => { 37 | ActionFactory.addAdapter(action.name, action.class) 38 | }) 39 | } 40 | 41 | if (Array.isArray(imageActions)) { 42 | cacheActions.forEach((cache) => { 43 | CacheFactory.addAdapter(cache.name, cache.class) 44 | }) 45 | } 46 | 47 | return module 48 | } 49 | -------------------------------------------------------------------------------- /packages/lib/helpers/priceTiers.ts: -------------------------------------------------------------------------------- 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: number) { 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?: number|null) => { 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/modules/sample-api/index.ts: -------------------------------------------------------------------------------- 1 | import { StorefrontApiModule, registerExtensions } from '@storefront-api/lib/module' 2 | import { StorefrontApiContext } from '@storefront-api/lib/module/types' 3 | import { Router } from 'express'; 4 | import order from './api/order'; 5 | import user from './api/user'; 6 | import stock from './api/stock'; 7 | import cart from './api/cart'; 8 | import catalog from './api/catalog' 9 | import img from './api/img' 10 | import { version } from '../../../package.json'; 11 | import path from 'path' 12 | 13 | export const SampleApiModule: StorefrontApiModule = new StorefrontApiModule({ 14 | key: 'sample-api', 15 | 16 | initApi: ({ config, db, app }: StorefrontApiContext): void => { 17 | let api = Router(); 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 cart resource 29 | api.use('/cart', cart({ config, db })); 30 | 31 | // mount the catalog resource 32 | api.use('/catalog', catalog({ config, db })) 33 | 34 | api.use('/img', img({ config, db })); 35 | api.use('/img/:width/:height/:action/:image', (req, res, next) => { 36 | console.log(req.params) 37 | }); 38 | 39 | // perhaps expose some API metadata at the root 40 | api.get('/', (req, res) => { 41 | res.json({ version }); 42 | }); 43 | 44 | registerExtensions({ app, config, db, registeredExtensions: config.get('modules.sampleApi.registeredExtensions'), rootPath: path.join(__dirname, 'api', 'extensions') }) 45 | 46 | // api router 47 | app.use('/sample-api', api); 48 | } 49 | 50 | }) 51 | -------------------------------------------------------------------------------- /packages/platform-magento2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/platform-magento2", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront GraphQL API", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "test": "eslint src", 10 | "test:unit": "jest -c test/unit/jest.conf.js", 11 | "lint": "eslint --ext .ts", 12 | "prepublishOnly": "tsc --build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "files": [ 22 | "**/*.d.ts", 23 | "*.d.ts", 24 | "**/*.js", 25 | "*.js" 26 | ], 27 | "author": "Piotr Karwatka ", 28 | "dependencies": { 29 | "@storefront-api/lib": "^1.0.0-rc.3", 30 | "@storefront-api/platform-abstract": "^1.0.0-rc.3", 31 | "bodybuilder": "^2.2.21", 32 | "config": "^1.30.0", 33 | "magento2-rest-client": "github:DivanteLtd/magento2-rest-client", 34 | "typescript": "3.7.*" 35 | }, 36 | "devDependencies": { 37 | "@types/config": "^0.0.34", 38 | "@types/express": "^4.16.1", 39 | "@types/jest": "^24.0.11", 40 | "@types/jsonfile": "^5.0.0", 41 | "@types/lodash": "^4.14.149", 42 | "@types/node": "^11.13.4", 43 | "jest": "^24.8.0", 44 | "nodemon": "^1.18.7", 45 | "ts-jest": "^24.0.2", 46 | "tslib": "^1.9.3" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 50 | }, 51 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 52 | "keywords": [ 53 | "storefront", 54 | "rest", 55 | "api", 56 | "nodejs" 57 | ], 58 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 59 | } 60 | -------------------------------------------------------------------------------- /packages/platform-magento1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/platform-magento1", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront GraphQL API", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "test": "eslint src", 10 | "test:unit": "jest -c test/unit/jest.conf.js", 11 | "lint": "eslint --ext .ts", 12 | "prepublishOnly": "tsc --build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "files": [ 22 | "**/*.d.ts", 23 | "*.d.ts", 24 | "**/*.js", 25 | "*.js" 26 | ], 27 | "author": "Piotr Karwatka ", 28 | "dependencies": { 29 | "@storefront-api/lib": "^1.0.0-rc.3", 30 | "@storefront-api/platform-abstract": "^1.0.0-rc.3", 31 | "bodybuilder": "^2.2.21", 32 | "config": "^1.30.0", 33 | "magento1-vsbridge-client": "github:DivanteLtd/magento1-vsbridge-client", 34 | "typescript": "3.7.*" 35 | }, 36 | "devDependencies": { 37 | "@types/config": "^0.0.34", 38 | "@types/express": "^4.16.1", 39 | "@types/jest": "^24.0.11", 40 | "@types/jsonfile": "^5.0.0", 41 | "@types/lodash": "^4.14.149", 42 | "@types/node": "^11.13.4", 43 | "jest": "^24.8.0", 44 | "nodemon": "^1.18.7", 45 | "ts-jest": "^24.0.2", 46 | "tslib": "^1.9.3" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 50 | }, 51 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 52 | "keywords": [ 53 | "storefront", 54 | "rest", 55 | "api", 56 | "nodejs" 57 | ], 58 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 59 | } 60 | -------------------------------------------------------------------------------- /packages/default-img/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 | -------------------------------------------------------------------------------- /packages/default-img/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/default-img", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront GraphQL API", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "test": "eslint src", 10 | "test:unit": "jest -c test/unit/jest.conf.js", 11 | "lint": "eslint --ext .ts", 12 | "prepublishOnly": "tsc --build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "files": [ 22 | "**/*.d.ts", 23 | "*.d.ts", 24 | "**/*.js", 25 | "*.js" 26 | ], 27 | "author": "Piotr Karwatka ", 28 | "dependencies": { 29 | "@storefront-api/lib": "^1.0.0-rc.3", 30 | "bodybuilder": "^2.2.21", 31 | "config": "^1.30.0", 32 | "fs-extra": "^8.1.0", 33 | "graphql": "^14.5.8", 34 | "js-sha3": "^0.8.0", 35 | "jwt-simple": "^0.5.1", 36 | "lodash": "^4.17.10", 37 | "request": "^2.85.0", 38 | "typescript": "3.7.*" 39 | }, 40 | "devDependencies": { 41 | "@types/config": "^0.0.34", 42 | "@types/express": "^4.16.1", 43 | "@types/jest": "^24.0.11", 44 | "@types/jsonfile": "^5.0.0", 45 | "@types/lodash": "^4.14.149", 46 | "@types/node": "^11.13.4", 47 | "jest": "^24.8.0", 48 | "nodemon": "^1.18.7", 49 | "ts-jest": "^24.0.2", 50 | "tslib": "^1.9.3" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 54 | }, 55 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 56 | "keywords": [ 57 | "storefront", 58 | "rest", 59 | "api", 60 | "nodejs" 61 | ], 62 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 63 | } 64 | -------------------------------------------------------------------------------- /packages/hooks/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | Listener hook just fires functions passed to hook function when executor is invoked. 4 | e. g. We want to listen for onAppInit event in various places of the application. 5 | Functions passed to this hook will be invoked only when executor function is executed. 6 | Usually we want to use hook in app/modules and executor in core. 7 | @return hook: a hook function to use in modules 8 | @return executor: a function that will run all the collected hooks 9 | */ 10 | export function createListenerHook () { 11 | const functionsToRun: ((arg: T) => void)[] = [] 12 | 13 | function hook (fn: (arg?: T) => void) { 14 | functionsToRun.push(fn) 15 | } 16 | 17 | function executor (args: T = null): void { 18 | functionsToRun.forEach(fn => fn(args)) 19 | } 20 | 21 | return { 22 | hook, 23 | executor 24 | } 25 | } 26 | 27 | /** 28 | Mutators work like listeners except they can modify passed value in hooks. 29 | e.g we can apply the hook mutator to object order that is returned before placing order 30 | now you can access and modify this value from hook returned by this function 31 | @return hook: a hook function to use in modules 32 | @return executor: a function that will apply all hooks on a given value 33 | */ 34 | export function createMutatorHook () { 35 | const mutators: ((arg: T) => R)[] = [] 36 | 37 | function hook (mutator: (arg: T) => R) { 38 | mutators.push(mutator) 39 | } 40 | 41 | function executor (rawOutput: T): T | R { 42 | if (mutators.length > 0) { 43 | let modifiedOutput: R = null 44 | mutators.forEach(fn => { 45 | modifiedOutput = fn(rawOutput) 46 | }) 47 | return modifiedOutput 48 | } else { 49 | return rawOutput 50 | } 51 | } 52 | 53 | return { 54 | hook, 55 | executor 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/platform-abstract/stock.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractStockProxy { 5 | protected _request: Request 6 | protected _config: IConfig 7 | public api: Record> 8 | 9 | protected constructor (config, req) { 10 | this._config = config 11 | this._request = req 12 | } 13 | 14 | /* 15 | GET /api/stock/check/:sku 16 | This method is used to check the stock item for specified product sku 17 | https://sfa-docs.now.sh/guide/default-modules/api.html#get-api-stock-check-sku 18 | 19 | #RESPONSE BODY: 20 | { 21 | "code": 200, 22 | "result": { 23 | "item_id": 580, 24 | "product_id": 580, 25 | "stock_id": 1, 26 | "qty": 53, 27 | "is_in_stock": true, 28 | "is_qty_decimal": false, 29 | "show_default_notification_message": false, 30 | "use_config_min_qty": true, 31 | "min_qty": 0, 32 | "use_config_min_sale_qty": 1, 33 | "min_sale_qty": 1, 34 | "use_config_max_sale_qty": true, 35 | "max_sale_qty": 10000, 36 | "use_config_backorders": true, 37 | "backorders": 0, 38 | "use_config_notify_stock_qty": true, 39 | "notify_stock_qty": 1, 40 | "use_config_qty_increments": true, 41 | "qty_increments": 0, 42 | "use_config_enable_qty_inc": true, 43 | "enable_qty_increments": false, 44 | "use_config_manage_stock": true, 45 | "manage_stock": true, 46 | "low_stock_date": null, 47 | "is_decimal_divided": false, 48 | "stock_status_changed_auto": 0 49 | } 50 | } 51 | */ 52 | public check (sku): Promise { 53 | throw new Error('UserProxy::check must be implemented for specific platform') 54 | } 55 | } 56 | 57 | export default AbstractStockProxy 58 | -------------------------------------------------------------------------------- /packages/default-catalog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/default-catalog", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront GraphQL API", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "test": "eslint src", 10 | "test:unit": "jest -c test/unit/jest.conf.js", 11 | "lint": "eslint --ext .ts", 12 | "prepublishOnly": "tsc --build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "files": [ 22 | "**/*.d.ts", 23 | "*.d.ts", 24 | "**/*.js", 25 | "*.js", 26 | "**/*.json", 27 | "**/*.graphqls" 28 | ], 29 | "author": "Piotr Karwatka ", 30 | "dependencies": { 31 | "@storefront-api/lib": "^1.0.0-rc.3", 32 | "bodybuilder": "^2.2.21", 33 | "config": "^1.30.0", 34 | "graphql": "^14.5.8", 35 | "js-sha3": "^0.8.0", 36 | "jwt-simple": "^0.5.1", 37 | "lodash": "^4.17.10", 38 | "request": "^2.85.0", 39 | "storefront-query-builder": "^1.0.0", 40 | "typescript": "3.7.*" 41 | }, 42 | "devDependencies": { 43 | "@types/config": "^0.0.34", 44 | "@types/express": "^4.16.1", 45 | "@types/jest": "^24.0.11", 46 | "@types/jsonfile": "^5.0.0", 47 | "@types/lodash": "^4.14.149", 48 | "@types/node": "^11.13.4", 49 | "jest": "^24.8.0", 50 | "nodemon": "^1.18.7", 51 | "ts-jest": "^24.0.2", 52 | "tslib": "^1.9.3" 53 | }, 54 | "bugs": { 55 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 56 | }, 57 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 58 | "keywords": [ 59 | "storefront", 60 | "rest", 61 | "api", 62 | "nodejs" 63 | ], 64 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 65 | } 66 | -------------------------------------------------------------------------------- /packages/lib/image.ts: -------------------------------------------------------------------------------- 1 | import sharp from 'sharp'; 2 | import rp from 'request-promise-native'; 3 | import config from 'config'; 4 | import Logger from '@storefront-api/lib/logger' 5 | 6 | sharp.cache(config.get('imageable.cache')); 7 | sharp.concurrency(config.get('imageable.concurrency')); 8 | // @ts-ignore 9 | sharp.counters(config.get('imageable.counters')); 10 | sharp.simd(config.get('imageable.simd')); 11 | 12 | export async function downloadImage (url) { 13 | const response = await rp.get(url, { encoding: null }); 14 | return response 15 | } 16 | 17 | export async function identify (buffer) { 18 | try { 19 | const transformer = sharp(buffer); 20 | 21 | return transformer.metadata(); 22 | } catch (err) { 23 | Logger.info(err); 24 | } 25 | } 26 | 27 | export async function resize (buffer, width, height) { 28 | try { 29 | const transformer = sharp(buffer); 30 | 31 | if (width || height) { 32 | const options = { 33 | withoutEnlargement: true, 34 | fit: sharp.fit.inside 35 | } 36 | transformer.resize(width, height, options) 37 | } 38 | 39 | return transformer.toBuffer(); 40 | } catch (err) { 41 | Logger.info(err); 42 | } 43 | } 44 | 45 | export async function fit (buffer, width, height) { 46 | try { 47 | const transformer = sharp(buffer); 48 | 49 | if (width || height) { 50 | transformer.resize(width, height, { fit: 'cover' }); 51 | } 52 | 53 | return transformer.toBuffer(); 54 | } catch (err) { 55 | Logger.info(err); 56 | } 57 | } 58 | 59 | export async function crop (buffer, width, height, x, y) { 60 | try { 61 | const transformer = sharp(buffer); 62 | 63 | if (width || height || x || y) { 64 | transformer.extract({ left: x, top: y, width, height }); 65 | } 66 | 67 | return transformer.toBuffer(); 68 | } catch (err) { 69 | Logger.info(err); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/lib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/lib", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront GraphQL API", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "test": "eslint src", 10 | "test:unit": "jest -c test/unit/jest.conf.js", 11 | "lint": "eslint --ext .ts", 12 | "prepublishOnly": "tsc --build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 17 | }, 18 | "files": [ 19 | "**/*.d.ts", 20 | "*.d.ts", 21 | "**/*.js", 22 | "*.js" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "author": "Piotr Karwatka ", 28 | "dependencies": { 29 | "@elastic/elasticsearch": "^7.3.0", 30 | "cli-color": "^2.0.0", 31 | "config": "^1.30.0", 32 | "jsonfile": "^4.0.0", 33 | "lodash": "^4.17.10", 34 | "query-string": "^6.8.3", 35 | "redis": "^3.0.2", 36 | "redis-tag-cache": "^1.2.1", 37 | "request": "^2.85.0", 38 | "request-promise-native": "^1.0.5", 39 | "semver": "^6.3.0", 40 | "sharp": "^0.23.4", 41 | "syswide-cas": "^5.3.0", 42 | "typescript": "3.7.*" 43 | }, 44 | "devDependencies": { 45 | "@types/cli-color": "^0.3.30", 46 | "@types/config": "^0.0.34", 47 | "@types/express": "^4.16.1", 48 | "@types/jest": "^24.0.11", 49 | "@types/jsonfile": "^5.0.0", 50 | "@types/lodash": "^4.14.149", 51 | "@types/node": "^11.13.4", 52 | "@types/redis": "^2.8.14", 53 | "@types/semver": "^6.2.0", 54 | "@types/sharp": "^0.23.1", 55 | "jest": "^24.8.0", 56 | "ts-jest": "^24.0.2" 57 | }, 58 | "bugs": { 59 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 60 | }, 61 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 62 | "keywords": [ 63 | "storefront", 64 | "rest", 65 | "api", 66 | "nodejs" 67 | ], 68 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 69 | } 70 | -------------------------------------------------------------------------------- /packages/default-img/api/img.ts: -------------------------------------------------------------------------------- 1 | import CacheFactory from '../image/cache/factory'; 2 | import ActionFactory from '../image/action/factory'; 3 | 4 | const asyncMiddleware = fn => (req, res, next) => { 5 | Promise.resolve(fn(req, res, next)).catch(next); 6 | }; 7 | 8 | /** 9 | * Image resizer 10 | * 11 | * ```bash 12 | * curl https://your-domain.example.com/img/310/300/resize/w/p/wp07-black_main.jpg 13 | * ``` 14 | * 15 | * or 16 | * 17 | * ```bash 18 | * curl http://localhost:8080/sample-api/img/600/744/resize/m/p/mp10-black_main.jpg 19 | * ``` 20 | * 21 | * Details: https://sfa-docs.now.sh/guide/default-modules/api.html#img 22 | */ 23 | export default ({ config, db }) => 24 | asyncMiddleware(async (req, res, next) => { 25 | if (!(req.method === 'GET')) { 26 | res.set('Allow', 'GET'); 27 | return res.status(405).send('Method Not Allowed'); 28 | } 29 | const cacheFactory = new CacheFactory(config, req) 30 | 31 | req.socket.setMaxListeners(config.imageable.maxListeners || 50); 32 | 33 | let imageBuffer 34 | 35 | const actionFactory = new ActionFactory(req, res, next, config) 36 | const imageAction = actionFactory.getAdapter(config.imageable.action.type) 37 | imageAction.getOption() 38 | imageAction.validateOptions() 39 | imageAction.isImageSourceHostAllowed() 40 | imageAction.validateMIMEType() 41 | 42 | const cache = cacheFactory.getAdapter(config.imageable.caching.type) 43 | 44 | if (config.imageable.caching.active && await cache.check()) { 45 | await cache.getImageFromCache() 46 | imageBuffer = cache.image 47 | } else { 48 | await imageAction.processImage() 49 | 50 | if (config.imageable.caching.active) { 51 | cache.image = imageAction.imageBuffer 52 | await cache.save() 53 | } 54 | 55 | imageBuffer = imageAction.imageBuffer 56 | } 57 | 58 | return res 59 | .type(imageAction.mimeType) 60 | .set({ 'Cache-Control': `max-age=${imageAction.maxAgeForResponse}` }) 61 | .send(imageBuffer); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/review/resolver.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery, getResponseObject } from '@storefront-api/lib/elastic' 6 | import { aggregationsToAttributes } from '../attribute/aggregations' 7 | 8 | export async function list ({ search, filter, currentPage, pageSize = 200, sort, context, rootValue, _sourceIncludes, _sourceExcludes }) { 9 | let query = buildQuery({ search, filter, currentPage, pageSize, sort, type: 'review' }); 10 | 11 | const esIndex = getIndexName(context.req.url) 12 | let response = getResponseObject(await client.search(adjustQuery({ 13 | index: esIndex, 14 | body: query, 15 | _source_includes: _sourceIncludes, 16 | _source_excludes: _sourceExcludes 17 | }, 'review', config))); 18 | 19 | // Process hits 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 | response = await aggregationsToAttributes(response, config, esIndex) 28 | response.total_count = response.hits.total 29 | 30 | // Process sort 31 | let sortOptions = [] 32 | for (var sortAttribute in sort) { 33 | sortOptions.push( 34 | { 35 | label: sortAttribute, 36 | value: sortAttribute 37 | } 38 | ) 39 | } 40 | 41 | response.sort_fields = {} 42 | if (sortOptions.length > 0) { 43 | response.sort_fields.options = sortOptions 44 | } 45 | 46 | response.page_info = { 47 | page_size: pageSize, 48 | current_page: currentPage 49 | } 50 | return response; 51 | } 52 | 53 | const resolver = { 54 | Query: { 55 | reviews: (_, { search, filter, currentPage, pageSize, sort, _sourceIncludes, _sourceExcludes }, context, rootValue) => 56 | list({ search, filter, currentPage, pageSize, sort, context, rootValue, _sourceIncludes, _sourceExcludes }) 57 | } 58 | }; 59 | 60 | export default resolver; 61 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | base: '/', 4 | dest: 'docs/public', 5 | port: 8090, 6 | markdown: { 7 | toc: { 8 | includeLevel: [2] 9 | } 10 | }, 11 | head: [['link', { rel: 'icon', href: '/favicon.png' }]], 12 | themeConfig: { 13 | repo: 'DivanteLtd/storefront-api', 14 | docsDir: 'docs', 15 | editLinks: false, 16 | sidebarDepth: 3, 17 | nav: [ 18 | { 19 | text: 'YouTube', 20 | link: 'https://www.youtube.com/channel/UCkm1F3Cglty3CE1QwKQUhhg', 21 | }, 22 | { 23 | text: 'Medium', 24 | link: 'https://medium.com/the-vue-storefront-journal', 25 | }, 26 | ], 27 | sidebar: { 28 | '/guide/': [ 29 | { 30 | title : 'General Information', 31 | collapsable: false, 32 | children: [ 33 | 'general/introduction', 34 | 'general/installation', 35 | 'general/config' 36 | ] 37 | }, 38 | { 39 | title: 'Default gateway', 40 | collapsable: false, 41 | children: [ 42 | 'default-modules/introduction', 43 | 'default-modules/api', 44 | 'default-modules/graphql', 45 | 'default-modules/platforms', 46 | 'default-modules/extensions' 47 | ], 48 | }, 49 | { 50 | title: 'Modules', 51 | collapsable: false, 52 | children: [ 53 | 'modules/introduction', 54 | 'modules/tutorial' 55 | ], 56 | }, 57 | { 58 | title: 'Integrations', 59 | collapsable: false, 60 | children: [ 61 | 'integration/integration', 62 | 'integration/prices-how-to', 63 | 'integration/format-product', 64 | 'integration/format-category', 65 | 'integration/format-attribute', 66 | 'integration/database-tools' 67 | ], 68 | } 69 | ], 70 | }, 71 | }, 72 | title: 'Storefront API', 73 | description: 'Storefront API Gateway', 74 | }; 75 | -------------------------------------------------------------------------------- /packages/default-vsf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@storefront-api/default-vsf", 3 | "version": "1.0.0-rc.3", 4 | "description": "Storefront GraphQL API", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "tsc --build -w", 8 | "build": "tsc --build", 9 | "test": "eslint src", 10 | "test:unit": "jest -c test/unit/jest.conf.js", 11 | "lint": "eslint --ext .ts", 12 | "prepublishOnly": "tsc --build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/DivanteLtd/storefront-api.git" 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "files": [ 22 | "**/*.d.ts", 23 | "*.d.ts", 24 | "**/*.js", 25 | "*.js", 26 | "**/*.json" 27 | ], 28 | "author": "Piotr Karwatka ", 29 | "dependencies": { 30 | "@storefront-api/lib": "^1.0.0-rc.3", 31 | "@storefront-api/platform": "^1.0.0-rc.3", 32 | "bodybuilder": "^2.2.21", 33 | "config": "^1.30.0", 34 | "email-check": "^1.1.0", 35 | "express": "^4.16.3", 36 | "graphql": "^14.5.8", 37 | "js-sha3": "^0.8.0", 38 | "jwa": "^1.1.5", 39 | "jwt-simple": "^0.5.1", 40 | "kue": "^0.11.6", 41 | "lodash": "^4.17.10", 42 | "md5": "^2.2.1", 43 | "nodemailer": "^4.6.8", 44 | "request": "^2.85.0", 45 | "resource-router-middleware": "^0.6.0", 46 | "typescript": "3.7.*", 47 | "winston": "^3.2.0" 48 | }, 49 | "devDependencies": { 50 | "@types/config": "^0.0.34", 51 | "@types/express": "^4.16.1", 52 | "@types/jest": "^24.0.11", 53 | "@types/jsonfile": "^5.0.0", 54 | "@types/lodash": "^4.14.149", 55 | "@types/node": "^11.13.4", 56 | "jest": "^24.8.0", 57 | "nodemon": "^1.18.7", 58 | "ts-jest": "^24.0.2", 59 | "tslib": "^1.9.3" 60 | }, 61 | "bugs": { 62 | "url": "https://github.com/DivanteLtd/storefront-api/issues" 63 | }, 64 | "homepage": "https://github.com/DivanteLtd/storefront-api/", 65 | "keywords": [ 66 | "storefront", 67 | "rest", 68 | "api", 69 | "nodejs" 70 | ], 71 | "gitHead": "1466d94a04a47f4125267d0fb8fc4c1204ad2951" 72 | } 73 | -------------------------------------------------------------------------------- /packages/platform-magento2/cart.ts: -------------------------------------------------------------------------------- 1 | import AbstractCartProxy from '@storefront-api/platform-abstract/cart' 2 | import { multiStoreConfig } from './util' 3 | 4 | class CartProxy extends AbstractCartProxy { 5 | public 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 | public create (customerToken) { 12 | return this.api.cart.create(customerToken) 13 | } 14 | 15 | public update (customerToken, cartId, cartItem) { 16 | return this.api.cart.update(customerToken, cartId, cartItem) 17 | } 18 | 19 | public delete (customerToken, cartId, cartItem) { 20 | return this.api.cart.delete(customerToken, cartId, cartItem) 21 | } 22 | 23 | public pull (customerToken, cartId, params) { 24 | return this.api.cart.pull(customerToken, cartId, params) 25 | } 26 | 27 | public totals (customerToken, cartId, params) { 28 | return this.api.cart.totals(customerToken, cartId, params) 29 | } 30 | 31 | public getShippingMethods (customerToken, cartId, address) { 32 | return this.api.cart.shippingMethods(customerToken, cartId, address) 33 | } 34 | 35 | public getPaymentMethods (customerToken, cartId) { 36 | return this.api.cart.paymentMethods(customerToken, cartId) 37 | } 38 | 39 | public setShippingInformation (customerToken, cartId, address) { 40 | return this.api.cart.shippingInformation(customerToken, cartId, address) 41 | } 42 | 43 | public collectTotals (customerToken, cartId, shippingMethod) { 44 | return this.api.cart.collectTotals(customerToken, cartId, shippingMethod) 45 | } 46 | 47 | public applyCoupon (customerToken, cartId, coupon) { 48 | return this.api.cart.applyCoupon(customerToken, cartId, coupon) 49 | } 50 | 51 | public deleteCoupon (customerToken, cartId) { 52 | return this.api.cart.deleteCoupon(customerToken, cartId) 53 | } 54 | 55 | public getCoupon (customerToken, cartId) { 56 | return this.api.cart.getCoupon(customerToken, cartId) 57 | } 58 | } 59 | 60 | export default CartProxy 61 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/attribute/resolver.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder'; 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery, getResponseObject } from '@storefront-api/lib/elastic' 6 | 7 | async function listAttributes ({ attributes = null, filter = null, context, rootValue, _sourceIncludes, _sourceExcludes }) { 8 | let query = buildQuery({ filter: filter || attributes, pageSize: 150, type: 'attribute' }); 9 | 10 | const esQuery = { 11 | index: getIndexName(context.req.url), 12 | body: query, 13 | _source_includes: _sourceIncludes, 14 | _source_excludes: _sourceExcludes 15 | } 16 | 17 | const response = getResponseObject(await client.search(adjustQuery(esQuery, 'attribute', config))); 18 | 19 | response.items = [] 20 | response.total_count = response.hits.total 21 | response.hits.hits.forEach(hit => { 22 | let item = hit._source 23 | item._score = hit._score 24 | response.items.push(item) 25 | }); 26 | return response; 27 | } 28 | 29 | export async function listSingleAttribute ({ attribute_id, attribute_code, context, rootValue, _sourceIncludes, _sourceExcludes }) { 30 | const filter = {} 31 | if (attribute_id) filter['attribute_id'] = { eq: attribute_id } 32 | if (attribute_code) filter['attribute_code'] = { eq: attribute_code } 33 | const attrList = await listAttributes({ filter, context, rootValue, _sourceIncludes, _sourceExcludes }) 34 | if (attrList && attrList.items.length > 0) { 35 | return attrList.items[0] 36 | } else { 37 | return null 38 | } 39 | } 40 | 41 | const resolver = { 42 | Query: { 43 | customAttributeMetadata: (_, { attributes, _sourceIncludes, _sourceExcludes }, context, rootValue) => listAttributes({ attributes, context, rootValue, _sourceIncludes, _sourceExcludes }), 44 | attribute: (_, { attribute_id, attribute_code, _sourceIncludes, _sourceExcludes }, context, rootValue) => listSingleAttribute({ attribute_id, attribute_code, _sourceIncludes, _sourceExcludes, context, rootValue }) 45 | } 46 | }; 47 | 48 | export default resolver; 49 | -------------------------------------------------------------------------------- /packages/platform-magento1/cart.ts: -------------------------------------------------------------------------------- 1 | import AbstractCartProxy from '@storefront-api/platform-abstract/cart'; 2 | import {multiStoreConfig} from './util'; 3 | 4 | class CartProxy extends AbstractCartProxy { 5 | public 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 | public create (customerToken) { 12 | return this.api.cart.create(customerToken); 13 | } 14 | 15 | public update (customerToken, cartId, cartItem) { 16 | return this.api.cart.update(customerToken, cartId, cartItem); 17 | } 18 | 19 | public delete (customerToken, cartId, cartItem) { 20 | return this.api.cart.delete(customerToken, cartId, cartItem); 21 | } 22 | 23 | public pull (customerToken, cartId, params) { 24 | return this.api.cart.pull(customerToken, cartId, params); 25 | } 26 | 27 | public totals (customerToken, cartId, params) { 28 | return this.api.cart.totals(customerToken, cartId, params); 29 | } 30 | 31 | public getShippingMethods (customerToken, cartId, address) { 32 | return this.api.cart.shippingMethods(customerToken, cartId, address); 33 | } 34 | 35 | public getPaymentMethods (customerToken, cartId) { 36 | return this.api.cart.paymentMethods(customerToken, cartId); 37 | } 38 | 39 | public setShippingInformation (customerToken, cartId, address) { 40 | return this.api.cart.shippingInformation(customerToken, cartId, address); 41 | } 42 | 43 | public collectTotals (customerToken, cartId, shippingMethod) { 44 | return this.api.cart.collectTotals(customerToken, cartId, shippingMethod); 45 | } 46 | 47 | public applyCoupon (customerToken, cartId, coupon) { 48 | return this.api.cart.applyCoupon(customerToken, cartId, coupon); 49 | } 50 | 51 | public deleteCoupon (customerToken, cartId) { 52 | return this.api.cart.deleteCoupon(customerToken, cartId); 53 | } 54 | 55 | public getCoupon (customerToken, cartId) { 56 | return this.api.cart.getCoupon(customerToken, cartId); 57 | } 58 | } 59 | 60 | export default CartProxy; 61 | -------------------------------------------------------------------------------- /docs/guide/integration/format-category.md: -------------------------------------------------------------------------------- 1 | ## Category entity 2 | 3 | Please check the [sample-data/categories.json](https://github.com/DivanteLtd/storefront-api/blob/develop/integration-sdk/sample-data/categories.json) to be sure which fields are critical for Storefront API to work. 4 | 5 | Remember - you can add any properties you like to the JSON objects to consume them on the Storefront API level. Please make sure you added the new property names to [the proper `includeFields` list for queries](https://github.com/DivanteLtd/vue-storefront/blob/bb6f8e70b5587ed73c457d382c7ac93bd14db413/config/default.json#L151). 6 | 7 | Here we present the core purpose of the product properties: 8 | 9 | ```json 10 | "id": 24, 11 | ``` 12 | The type is undefined (it can be anything) - but must be unique. The category identifier is used mostly for caching purposes (as a key). 13 | 14 | ```json 15 | "parent_id": 21, 16 | ``` 17 | 18 | If this is a child category, please set the parent category id in here. This field is used for building up Breadcrumbs. 19 | 20 | ```json 21 | "path": "1/2/29", 22 | ``` 23 | 24 | This is a string, a list of IDs of the parent categories. It's used to build Breadcrumbs more easily. 25 | 26 | ```json 27 | "name": "Hoodies & Sweatshirts", 28 | ``` 29 | 30 | This is just a category name. 31 | 32 | ```json 33 | "url_key": "hoodies-and-sweatshirts-24", 34 | "url_path": "women/tops-women/hoodies-and-sweatshirts-women/hoodies-and-sweatshirts-24", 35 | ``` 36 | 37 | ```json 38 | "is_active": true, 39 | ``` 40 | 41 | If it's false, the category won't be displayed. 42 | 43 | ```json 44 | "position": 2, 45 | ``` 46 | 47 | Sorting position of the category on its level. 48 | 49 | ```json 50 | "level": 4, 51 | ``` 52 | 53 | The category level in the tree. By default, Storefront API displays categories with `level: 2` in the main menu. 54 | 55 | ```json 56 | "product_count": 182, 57 | ``` 58 | 59 | If it's false, the category won't be displayed. 60 | 61 | ```json 62 | "children_data": [ 63 | { 64 | "id": 27, 65 | "children_data": [] 66 | }, 67 | { 68 | "id": 28, 69 | "children_data": [] 70 | } 71 | ] 72 | ``` 73 | 74 | Here is the children structure. It's used for constructing queries to get the child products. 75 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/taxrule/schema.graphqls: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | # List tax rules by filter 3 | taxrule(filter: TaxRuleInput): TaxRules 4 | } 5 | 6 | # Tax rules collection 7 | type TaxRules implements ESResponseInterface { 8 | 9 | # The number of tax rules returned 10 | total_count: Int 11 | 12 | # An array of tax rules that match the specified search criteria 13 | items: [TaxRule] 14 | 15 | # Document returned by this search query hits 16 | hits: JSON 17 | 18 | # Search suggest feature 19 | suggest: JSON 20 | 21 | # Aggregations returned when filter have been applied 22 | aggregations: JSON 23 | } 24 | 25 | # Tax rate - a part of Tax Rule 26 | type TaxRate { 27 | 28 | # Tax region assigned to this rate 29 | tax_region_id: Int 30 | 31 | # Tax country assigned to this rate 32 | tax_country_id: Int 33 | 34 | # Tax rule titles 35 | titles: [String] 36 | 37 | # Post code assigned to this rule 38 | tax_postcode: String 39 | 40 | # Code/name of this rule 41 | code: String 42 | 43 | # Percentage tax rate 44 | rate: Int 45 | 46 | # Rule id 47 | id: ID 48 | } 49 | 50 | # Product review - related product review 51 | type TaxRule { 52 | 53 | # TaxRule id 54 | id: ID 55 | 56 | # Tax rule code 57 | code: String 58 | 59 | # Tax rates assigned to this rule 60 | rates: [TaxRate] 61 | 62 | # Customer tax class ids assigned to this rule 63 | customer_tax_class_ids: [ID] 64 | 65 | # Product tax class ids assigned to this rule 66 | product_tax_class_ids: [ID] 67 | 68 | # Tax rate ids assigned with this rule 69 | tax_rate_ids: [ID] 70 | } 71 | 72 | # TaxRuleInput specifies the tax rules information to search 73 | input TaxRuleInput { 74 | 75 | # An ID that uniquely identifies the tax rule 76 | id: FilterTypeInput 77 | 78 | # The unique identifier for an tax rule. This value should be in lowercase letters without spaces. 79 | code: FilterTypeInput 80 | 81 | # Cunstomer tax class ids of the tax rule 82 | customer_tax_class_ids: FilterTypeInput 83 | 84 | # Products tax class ids of the tax rule 85 | product_tax_class_ids: FilterTypeInput 86 | 87 | # Tax rates ids of the tax rule 88 | tax_rate_ids: FilterTypeInput 89 | } 90 | -------------------------------------------------------------------------------- /.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': 1, 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': 'off' 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 | -------------------------------------------------------------------------------- /src/test/integration/main.spec.ts: -------------------------------------------------------------------------------- 1 | import { Server } from '@storefront-api/core' 2 | import request from 'supertest'; 3 | import { getClient } from '@storefront-api/lib/redis'; 4 | import { getClient as esgetClient } from '@storefront-api/lib/elastic'; 5 | import config from 'config' 6 | 7 | import { DefaultVuestorefrontApiModule } from '@storefront-api/default-vsf' 8 | import { DefaultCatalogModule } from '@storefront-api/default-catalog' 9 | import { DefaultImgModule } from '@storefront-api/default-img' 10 | import {StorefrontApiModule} from '@storefront-api/lib/module' 11 | import magento2 from '@storefront-api/platform-magento2' 12 | 13 | export let modules: StorefrontApiModule[] = [ 14 | DefaultVuestorefrontApiModule({ 15 | platform: { 16 | name: 'magento2', 17 | platformImplementation: magento2 18 | } 19 | }), 20 | DefaultCatalogModule(), 21 | DefaultImgModule() 22 | ] 23 | 24 | const server = new Server({ 25 | modules 26 | }) 27 | 28 | describe('GET /user', () => { 29 | afterAll(() => { 30 | const redis = getClient(config.get('redis')) 31 | if (redis) { 32 | redis.end() 33 | } 34 | const esClint = esgetClient(config) 35 | if (esClint) { 36 | esClint.close() 37 | } 38 | }) 39 | 40 | it('Check that the default api endpoint responses', () => { 41 | return request(server.express) 42 | .get('/api') 43 | .set('Accept', 'application/json') 44 | .expect('Content-Type', /json/) 45 | .expect(200) 46 | }); 47 | 48 | it('Check the default response form product endpoint', () => { 49 | return request(server.express) 50 | .get('/api/catalog/vue_storefront_catalog/product/_search') 51 | .set('Accept', 'application/json') 52 | .expect('Content-Type', /json/) 53 | .expect(200) 54 | .then(response => { 55 | expect(response.body.hits.hits.length).toEqual(10) 56 | }); 57 | }); 58 | 59 | it('Check that the size parameter works ', () => { 60 | return request(server.express) 61 | .get('/api/catalog/vue_storefront_catalog/product/_search') 62 | .query({ 63 | size: 50 64 | }) 65 | .set('Accept', 'application/json') 66 | .expect('Content-Type', /json/) 67 | .expect(200) 68 | .then(response => { 69 | expect(response.body.hits.hits.length).toEqual(50) 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /integration-sdk/sample-data/fetch_demo_products.sh: -------------------------------------------------------------------------------- 1 | # This command requires "jq" -> https://stedolan.github.io/jq/ 2 | if ! [ -x "$(command -v jq)" ]; then 3 | echo 'Error: jq is not installed. Please download it from https://stedolan.github.io/jq/' >&2 4 | exit 1 5 | fi 6 | 7 | curl -sS "https://demo.storefrontcloud.io/api/catalog/vue_storefront_catalog/product/_search?size=50&from=0&sort=updated_at%3Adesc&request=%7B%22query%22%3A%7B%22bool%22%3A%7B%22filter%22%3A%7B%22bool%22%3A%7B%22must%22%3A%5B%7B%22terms%22%3A%7B%22visibility%22%3A%5B2%2C3%2C4%5D%7D%7D%2C%7B%22terms%22%3A%7B%22status%22%3A%5B0%2C1%5D%7D%7D%2C%7B%22terms%22%3A%7B%22stock.is_in_stock%22%3A%5Btrue%5D%7D%7D%2C%7B%22terms%22%3A%7B%22category_ids%22%3A%5B20%2C21%2C23%2C24%2C25%2C26%2C22%2C27%2C28%5D%7D%7D%5D%7D%7D%7D%7D%2C%22aggs%22%3A%7B%22agg_terms_color%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22color%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_color_options%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22color_options%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_size%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22size%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_size_options%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22size_options%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_price%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22price%22%7D%7D%2C%22agg_range_price%22%3A%7B%22range%22%3A%7B%22field%22%3A%22price%22%2C%22ranges%22%3A%5B%7B%22from%22%3A0%2C%22to%22%3A50%7D%2C%7B%22from%22%3A50%2C%22to%22%3A100%7D%2C%7B%22from%22%3A100%2C%22to%22%3A150%7D%2C%7B%22from%22%3A150%7D%5D%7D%7D%2C%22agg_terms_erin_recommends%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22erin_recommends%22%2C%22size%22%3A10%7D%7D%2C%22agg_terms_erin_recommends_options%22%3A%7B%22terms%22%3A%7B%22field%22%3A%22erin_recommends_options%22%2C%22size%22%3A10%7D%7D%7D%7D" | jq ".hits.hits[]._source | { id, name, image, sku, url_key, url_path, type_id, price, special_price, price_incl_tax, special_price_incl_tax, special_to_date, special_from_date, name, status, visibility, size, color, size_options, color_options, category_ids, category, media_gallery, configurable_options, stock: [ .stock | { is_in_stock, qty } ], configurable_children: [ .configurable_children[] | { type_id, sku, special_price, special_to_date, special_from_date, name, price, price_incl_tax, special_price_incl_tax, id, image, url_key, url_path, status, size, color } ] }" | jq -s -M \ > products.json 8 | 9 | echo "Products dumped into 'products.json'" -------------------------------------------------------------------------------- /docs/guide/default-modules/graphql.md: -------------------------------------------------------------------------------- 1 | # GraphQL Interfaces 2 | 3 | Storefront supports GraphQL to get data for: `products`, `categories`, `attributes` and `taxerules`. 4 | The default GraphQL resolvers fetch the data from Elasticsearch. You can easily create custom schemas and resolvers for Search Engines / Third Party APIs. 5 | 6 | ## GraphQL Schema 7 | 8 | Our default GraphQL schema is defined the within the [modules](../modules/introduction.md). By default, we provide the `default-catalog` module. It contains a pretty comprehensive eCommerce schema that can be easily modified to suit your needs. Check the [Default schema documentation](https://divanteltd.github.io/storefront-graphql-api-schema/). 9 | 10 | ## GraphQL Playground and queries 11 | 12 | After launching the Storefront API server, there is a GraphQL API interface available at [http://localhost:8080/graphql](http://localhost:8080/graphql). You can use it for testing out some Graph QL queries. 13 | 14 | You can find an example request for querying the products entity below: 15 | 16 | ```graphql 17 | { 18 | products(search: "bag", filter: { 19 | status: { 20 | in: [0, 1], scope: "default" 21 | }, 22 | stock: { 23 | is_in_stock: {eq: true, scope: "default"} 24 | }, 25 | visibility: { 26 | in: [3, 4], scope: "default"} 27 | }, 28 | sort: { 29 | updated_at: DESC 30 | } 31 | ) { 32 | items 33 | total_count 34 | aggregations 35 | sort_fields { 36 | options { 37 | value 38 | } 39 | } 40 | page_info { 41 | page_size 42 | current_page 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | GraphQL Playground is included 49 | This is a screen showing the GraphQL Playground on storefront-api schema. Check the schema docs. It can be 100% customized. 50 | 51 | ## Extending GraphQL schema 52 | 53 | The default schema can be easily modified by just tweaking the `modules/default-catalog/graphql/*` definitions. You can add your own custom [modules](../modules/introduction.md) that define custom GraphQL schema. In the same way, you can disable the `default-catalog` module if you don't want to use our default entities. 54 | 55 | [Check the tutorial](../modules/tutorial.md) on how to extend GraphQL schema. 56 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/review/schema.graphqls: -------------------------------------------------------------------------------- 1 | extend type Query { 2 | 3 | # List reviews based on search full text query or structure filter 4 | reviews ( 5 | 6 | # Performs a full-text search using the specified key words. 7 | search: String 8 | 9 | # An array of categories that match the specified search criteria 10 | filter: ReviewFilterInput 11 | 12 | # Specifies the maximum number of results to return at once. This attribute is optional. 13 | pageSize: Int = 20 14 | 15 | # Specifies which page of results to return. The default value is 1. 16 | currentPage: Int = 1 17 | 18 | # Specifies which attribute we include in result. 19 | _sourceIncludes: [String] 20 | 21 | # Specifies which attribute we exclude in result. 22 | _sourceExcludes: [String] 23 | ): Reviews 24 | } 25 | 26 | # Product reviews - collection; revierws related to specific products 27 | type Reviews implements ESResponseInterface { 28 | 29 | # An object that includes the page_info and currentPage values specified in the query 30 | page_info: SearchResultPageInfo 31 | 32 | # The number of reviews returned 33 | total_count: Int 34 | 35 | # An array of reviews that match the specified search criteria 36 | items: [Review] 37 | 38 | # Document returned by this search query hits 39 | hits: JSON 40 | 41 | # Search suggest feature 42 | suggest: JSON 43 | 44 | # Aggregations returned when filter have been applied 45 | aggregations: JSON 46 | } 47 | 48 | # Product review - related product review 49 | type Review { 50 | 51 | # Review id 52 | id: ID 53 | 54 | # Review title 55 | title: String 56 | 57 | # Review content 58 | detail: String 59 | 60 | # Review author nickname 61 | nickname: String 62 | 63 | # Review entity type - product, order ... 64 | review_entity: String 65 | 66 | # Review type 67 | review_type: Int 68 | 69 | # Review status 70 | review_status: Int 71 | 72 | # Review date of creation 73 | created_at: DateTime 74 | } 75 | 76 | # 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. 77 | input ReviewFilterInput { 78 | 79 | # Review satus 80 | review_status: FilterTypeInput 81 | 82 | # An ID that uniquely identifies the parent product 83 | product_id: FilterTypeInput 84 | } 85 | -------------------------------------------------------------------------------- /packages/default-vsf/index.ts: -------------------------------------------------------------------------------- 1 | import { StorefrontApiModule, registerExtensions } from '@storefront-api/lib/module/index' 2 | import { StorefrontApiContext } from '@storefront-api/lib/module/types' 3 | import { Router } from 'express'; 4 | import order from './api/order'; 5 | import user from './api/user'; 6 | import stock from './api/stock'; 7 | import review from './api/review'; 8 | import cart from './api/cart'; 9 | import product from './api/product'; 10 | import sync from './api/sync'; 11 | import middleware from './middleware' 12 | import url from './api/url' 13 | import { version } from './package.json'; 14 | import path from 'path' 15 | import PlatformFactory, { Platform } from '@storefront-api/platform/factory'; 16 | 17 | interface DefaultVuestorefrontApiOptions { 18 | platform: Platform 19 | } 20 | 21 | export const DefaultVuestorefrontApiModule = (options: DefaultVuestorefrontApiOptions): StorefrontApiModule => { 22 | PlatformFactory.addPlatform(options.platform) 23 | 24 | return new StorefrontApiModule({ 25 | key: 'default-vsf', 26 | initMiddleware: ({ config, db, app }: StorefrontApiContext): void => { 27 | app.use(middleware({ config, db })); 28 | }, 29 | initApi: ({ config, db, app }: StorefrontApiContext): void => { 30 | let api = Router(); 31 | 32 | // mount the order resource 33 | api.use('/order', order({ config, db })); 34 | 35 | // mount the user resource 36 | api.use('/user', user({ config, db })); 37 | 38 | // mount the stock resource 39 | api.use('/stock', stock({ config, db })); 40 | 41 | // mount the review resource 42 | api.use('/review', review({ config, db })); 43 | 44 | // mount the cart resource 45 | api.use('/cart', cart({ config, db })); 46 | 47 | // mount the product resource 48 | api.use('/product', product({ config, db })) 49 | 50 | // mount the sync resource 51 | api.use('/sync', sync({ config, db })) 52 | 53 | // mount the url resource 54 | api.use('/url', url({ config, db })) 55 | 56 | // perhaps expose some API metadata at the root 57 | api.get('/', (req, res) => { 58 | res.json({ version }); 59 | }); 60 | 61 | registerExtensions({ app, config, db, registeredExtensions: config.get('modules.defaultVsf.registeredExtensions'), rootPath: path.join(__dirname, 'api', 'extensions') }) 62 | 63 | // api router 64 | app.use('/api', api); 65 | } 66 | 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /src/modules/sample-api/api/order.js: -------------------------------------------------------------------------------- 1 | import resource from 'resource-router-middleware'; 2 | 3 | export default ({ config, db }) => 4 | resource({ 5 | /** Property name to store preloaded entity on `request`. */ 6 | id: 'order', 7 | 8 | /** 9 | * POST create an order 10 | * 11 | * Request body: 12 | * 13 | * { 14 | * "user_id": "", 15 | * "cart_id": "d90e9869fbfe3357281a67e3717e3524", 16 | * "products": [ 17 | * { 18 | * "sku": "WT08-XS-Yellow", 19 | * "qty": 1 20 | * } 21 | * ], 22 | * "addressInformation": { 23 | * "shippingAddress": { 24 | * "region": "", 25 | * "region_id": 0, 26 | * "country_id": "PL", 27 | * "street": [ 28 | * "Example", 29 | * "12" 30 | * ], 31 | * "company": "NA", 32 | * "telephone": "", 33 | * "postcode": "50-201", 34 | * "city": "Wroclaw", 35 | * "firstname": "Piotr", 36 | * "lastname": "Karwatka", 37 | * "email": "pkarwatka30@divante.pl", 38 | * "region_code": "" 39 | * }, 40 | * "billingAddress": { 41 | * "region": "", 42 | * "region_id": 0, 43 | * "country_id": "PL", 44 | * "street": [ 45 | * "Example", 46 | * "12" 47 | * ], 48 | * "company": "Company name", 49 | * "telephone": "", 50 | * "postcode": "50-201", 51 | * "city": "Wroclaw", 52 | * "firstname": "Piotr", 53 | * "lastname": "Karwatka", 54 | * "email": "pkarwatka30@divante.pl", 55 | * "region_code": "", 56 | * "vat_id": "PL88182881112" 57 | * }, 58 | * "shipping_method_code": "flatrate", 59 | * "shipping_carrier_code": "flatrate", 60 | * "payment_method_code": "cashondelivery", 61 | * "payment_method_additional": {} 62 | * }, 63 | * "order_id": "1522811662622-d3736c94-49a5-cd34-724c-87a3a57c2750", 64 | * "transmited": false, 65 | * "created_at": "2018-04-04T03:14:22.622Z", 66 | * "updated_at": "2018-04-04T03:14:22.622Z" 67 | * } 68 | */ 69 | create (req, res) { 70 | res.json({ 71 | code: 200, 72 | result: 'OK' 73 | }); 74 | } 75 | }); 76 | -------------------------------------------------------------------------------- /packages/default-img/image/action/abstract/index.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from 'express' 2 | import URL from 'url' 3 | 4 | export interface Action { 5 | SUPPORTED_ACTIONS: string[], 6 | SUPPORTED_MIMETYPES: string[], 7 | req: Request, 8 | res: Response, 9 | next: NextFunction, 10 | options: any, 11 | mimeType: string, 12 | getOption(): void, 13 | validateOptions(): void, 14 | getImageURL(): string, 15 | whitelistDomain: string[], 16 | validateMIMEType(): void, 17 | processImage(): void, 18 | isImageSourceHostAllowed(), 19 | _isUrlWhitelisted(url: string, whitelistType: string, defaultValue: any, whitelist: string[]), 20 | isValidFor?(string): boolean 21 | } 22 | 23 | export interface ImageActionConstructor { 24 | new (req: any, res: Response, next: NextFunction, options: any): Action 25 | } 26 | 27 | export default abstract class ImageAction implements Action { 28 | public readonly SUPPORTED_ACTIONS = ['fit', 'resize', 'identify'] 29 | public readonly SUPPORTED_MIMETYPES 30 | 31 | public req: Request 32 | public res: Response 33 | public next: NextFunction 34 | public options 35 | public mimeType: string 36 | 37 | public constructor (req: any, res: Response, next: NextFunction, options: any) { 38 | this.req = req 39 | this.res = res 40 | this.next = next 41 | this.options = options 42 | } 43 | 44 | abstract getOption(): void 45 | 46 | abstract validateOptions(): void 47 | 48 | abstract getImageURL(): string 49 | 50 | abstract get whitelistDomain(): string[] 51 | 52 | abstract validateMIMEType(): void 53 | 54 | abstract processImage(): void 55 | 56 | public isImageSourceHostAllowed () { 57 | if (!this._isUrlWhitelisted(this.getImageURL(), 'allowedHosts', true, this.whitelistDomain)) { 58 | return this.res.status(400).send({ 59 | code: 400, 60 | result: `Host is not allowed` 61 | }) 62 | } 63 | } 64 | 65 | public _isUrlWhitelisted (url, whitelistType, defaultValue, whitelist) { 66 | if (arguments.length !== 4) throw new Error('params are not optional!') 67 | 68 | if (whitelist && whitelist.hasOwnProperty(whitelistType)) { 69 | const requestedHost = URL.parse(url).host 70 | 71 | const matches = whitelist[whitelistType].map(allowedHost => { 72 | allowedHost = allowedHost instanceof RegExp ? allowedHost : new RegExp(allowedHost) 73 | return !!requestedHost.match(allowedHost) 74 | }) 75 | return matches.indexOf(true) > -1 76 | } else { 77 | return defaultValue 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/guide/integration/prices-how-to.md: -------------------------------------------------------------------------------- 1 | # Prices how-to 2 | 3 | To calculate prices and taxes, we first need to get the proper tax rate. It's based on the [`taxrate`](./integration.md#taxrate-entity) entity, and stored in the Elastic. Each product can have the property `product.tax_class_id` fixed. According to its value, Storefront API applies the `taxrate`. It also applies the country and region to the filter. 4 | 5 | **Note:** We currently do not support searching the tax rules by `customer_tax_class_id` or by the `tax_postcode` fields of the `taxrate` entity. Pull requests are more than welcome ;) 6 | 7 | After getting the right tax rate, we can calculate the prices. 8 | 9 | We've got the following price fields priority in VS: 10 | - `final_price` - if set, depending on the `config.tax.finalPriceIncludesTax`, it's taken as the final price or Net final price. 11 | - `special_price` - if set and lower than the `price`, it will replace the `price` and the `price` value will be set into the `original_price` property. 12 | - `price` - if set, depending on the `config.tax.sourcePriceIncludesTax`, it's taken as the final price or Net final price. 13 | 14 | Depending on the `config.tax.finalPriceIncludesTax` and `config.tax.sourcePriceIncludesTax` settings, Vue Storefront calculates the prices and stores them into the following fields: 15 | 16 | Product Special price: 17 | - `special_price` - optional - if set, it's always the Net price. 18 | - `special_price_incl_tax` - optional - if set, it's always the price after taxes. 19 | - `special_price_tax` - optional - if set, it's the tax amount. 20 | 21 | Product Regular price: 22 | - `price` - required - if set, it's always the Net price. 23 | - `price_incl_tax` - required - if set, it's always the price after taxes. 24 | - `price_tax` - required - if set, it's the tax amount. 25 | 26 | Product Final price: 27 | - `final_price` - optional - if set, it's always the Net price. 28 | - `final_price_incl_tax` - optional - if set, it's always the price after taxes. 29 | - `final_price_tax` - optional - if set, it's the tax amount. 30 | 31 | Product Original price (set only if `final_price` or `special_price` are lower than `price`): 32 | - `original_price` - optional - if set, it's always the Net price. 33 | - `original_price_incl_tax` - optional - if set, it's always the price after taxes. 34 | - `original_price_tax` - optional - if set, it's the tax amount. 35 | 36 | **Note:** The prices are set for all `configurable_children` with the same format. 37 | **Note:** If any of the `configurable_children` has a price lower than the main product price, the main product price will be updated accordingly. 38 | -------------------------------------------------------------------------------- /packages/default-catalog/index.ts: -------------------------------------------------------------------------------- 1 | import { StorefrontApiModule, registerExtensions } from '@storefront-api/lib/module/index' 2 | import { StorefrontApiContext, GraphqlConfiguration, ElasticSearchMappings } from '@storefront-api/lib/module/types' 3 | import invalidateCache from './api/invalidate' 4 | import catalog from './api/catalog'; 5 | 6 | import defaultProcessor from './processor/default' 7 | import productProcessor from './processor/product' 8 | 9 | import resolvers from './graphql/resolvers' 10 | import schema from './graphql/schema' 11 | import { loadSchema } from '@storefront-api/lib/elastic' 12 | import path from 'path' 13 | import ProcessorFactory from './processor/factory'; 14 | 15 | const catalogModule: StorefrontApiModule = new StorefrontApiModule({ 16 | key: 'default-catalog', 17 | loadMappings: ({ config, db, app }: StorefrontApiContext): ElasticSearchMappings => { 18 | return { 19 | schemas: { 20 | 'product': loadSchema(path.join(__dirname, 'elasticsearch'), 'product', config.get('elasticsearch.apiVersion')), 21 | 'attribute': loadSchema(path.join(__dirname, 'elasticsearch'), 'attribute', config.get('elasticsearch.apiVersion')), 22 | 'category': loadSchema(path.join(__dirname, 'elasticsearch'), 'category', config.get('elasticsearch.apiVersion')), 23 | 'cms_block': loadSchema(path.join(__dirname, 'elasticsearch'), 'cms_block', config.get('elasticsearch.apiVersion')), 24 | 'cms_page': loadSchema(path.join(__dirname, 'elasticsearch'), 'cms_page', config.get('elasticsearch.apiVersion')), 25 | 'taxrule': loadSchema(path.join(__dirname, 'elasticsearch'), 'cms_block', config.get('elasticsearch.apiVersion')) 26 | }} 27 | }, 28 | initGraphql: ({ config, db, app }: StorefrontApiContext): GraphqlConfiguration => { 29 | return { 30 | resolvers, 31 | schema, 32 | hasGraphqlSupport: true 33 | } 34 | }, 35 | initApi: ({ config, db, app }: StorefrontApiContext): void => { 36 | registerExtensions({ app, config, db, registeredExtensions: config.get('modules.defaultCatalog.registeredExtensions'), rootPath: path.join(__dirname, 'api', 'extensions') }) 37 | 38 | // mount the catalog resource 39 | app.use('/api/catalog', catalog({ config, db })) 40 | app.post('/api/invalidate', invalidateCache) 41 | app.get('/api/invalidate', invalidateCache) 42 | } 43 | 44 | }) 45 | 46 | export const DefaultCatalogModule = (processors = []): StorefrontApiModule => { 47 | ProcessorFactory.addAdapter('default', defaultProcessor) 48 | ProcessorFactory.addAdapter('product', productProcessor) 49 | processors.forEach(processor => { 50 | ProcessorFactory.addAdapter(processor.name, processor.class) 51 | }) 52 | return catalogModule 53 | } 54 | -------------------------------------------------------------------------------- /packages/default-catalog/api/invalidate.ts: -------------------------------------------------------------------------------- 1 | import config from 'config' 2 | import { apiStatus } from '@storefront-api/lib/util' 3 | import cache from '@storefront-api/lib/cache-instance' 4 | import request from 'request' 5 | import Logger from '@storefront-api/lib/logger' 6 | 7 | function invalidateCache (req, res) { 8 | if (config.get('server.useOutputCache')) { 9 | if (req.query.tag && req.query.key) { // clear cache pages for specific query tag 10 | if (req.query.key !== config.get('server.invalidateCacheKey')) { 11 | Logger.error('Invalid cache invalidation key') 12 | apiStatus(res, 'Invalid cache invalidation key', 400) 13 | return 14 | } 15 | Logger.info(`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').indexOf(tag) >= 0 || config.get('server.availableCacheTags').find(t => { 25 | return tag.indexOf(t) === 0 26 | })) { 27 | subPromises.push(cache.invalidate(tag).then(() => { 28 | Logger.info(`Tags invalidated successfully for [${tag}]`) 29 | })) 30 | } else { 31 | Logger.error(`Invalid tag name ${tag}`) 32 | } 33 | }) 34 | Promise.all(subPromises).then(r => { 35 | apiStatus(res, `Tags invalidated successfully [${req.query.tag}]`, 200) 36 | }).catch(error => { 37 | apiStatus(res, error, 500) 38 | Logger.error(error) 39 | }) 40 | if (config.get('server.invalidateCacheForwarding')) { // forward invalidate request to the next server in the chain 41 | if (!req.query.forwardedFrom && config.get('server.invalidateCacheForwardUrl')) { // don't forward forwarded requests 42 | request(config.get('server.invalidateCacheForwardUrl') + req.query.tag + '&forwardedFrom=vs', {}, (err, res, body) => { 43 | if (err) { Logger.error(err); } 44 | try { 45 | if (body && JSON.parse(body).code !== 200) Logger.info(body); 46 | } catch (e) { 47 | Logger.error('Invalid Cache Invalidation response format', e) 48 | } 49 | }); 50 | } 51 | } 52 | } else { 53 | apiStatus(res, 'Invalid parameters for Clear cache request', 400) 54 | Logger.error('Invalid parameters for Clear cache request') 55 | } 56 | } else { 57 | apiStatus(res, 'Cache invalidation is not required, output cache is disabled', 200) 58 | } 59 | } 60 | 61 | export default invalidateCache 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | Already a JS developer? Pick an issue, push a PR and instantly become a member of `storefront-api` contributors community. 4 | We've marked some issues as "Good first issue" to make it easier for newcomers to begin! 5 | 6 | Thank you for your interest in, and engagement! 7 | 8 | # Branches 9 | 10 | You should fork the project or create a branch for new features. 11 | The main branches used by the core team are: 12 | 13 | - master - where we store the stable release of the app (that can be deployed to our demo instances), 14 | - develop - the most recent version of the app - kind of "nightly" build. 15 | - RC-x (`x` is current version) - release candidate branch with features that will land in next version. 16 | 17 | Please use "develop" or "RC" for development purposes as the "master" can be merged just as the new release is coming out (about once a month)! 18 | 19 | ## Issue reporting guidelines: 20 | 21 | Always define the type of issue: 22 | * Bug report 23 | * Feature request 24 | 25 | While writing issues, be as specific as possible 26 | All requests regarding support with implementation or application setup should be sent to contributors@vuestorefront.io 27 | 28 | **Tag your issues properly**. If you found a bug tag it with `bug` label. If you're requesting new feature tag it with `feature request` label. 29 | 30 | ## Pull request Checklist 31 | 32 | Here’s how to submit a pull request. Pull request that don't meet these requirements will not be merged. 33 | 34 | **ALWAYS** use [Pull Request template](https://github.com/DivanteLtd/storefront-api/blob/develop/PULL_REQUEST_TEMPLATE.md) it's automatically added to each PR. 35 | 1. Fork the repository and clone it locally fro the 'develop' branch. Make sure it's up to date with current `develop` branch 36 | 2. Create a branch for your edits. Use the following branch naming conventions: 37 | * bugfix/task-title 38 | * feature/task-name 39 | 3. Use Pull Request template and fill as much fields as possible to describe your solution. 40 | 4. Reference any relevant issues or supporting documentation in your PR (ex. “Issue: 39. Issue title.”). 41 | 5. Test your changes! Run your changes against any existing tests and create new ones when needed. Make sure your changes don’t break the existing project. Make sure that your branch is passing Travis CI build. 42 | 6. If you have found a potential security vulnerability, please DO NOT report it on the public issue tracker. Instead, send it to us at contributors@vuestorefront.io. We will work with you to verify and fix it as soon as possible. 43 | 44 | ## Acceptance criteria 45 | 46 | Your pull request will be merged after meeting following criteria: 47 | - Everything from "Pull Request Checklist" 48 | - PR is proposed to appropiate branch 49 | - There are at least two approvals from core team members -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/product/service.ts: -------------------------------------------------------------------------------- 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, getResponseObject } from '@storefront-api/lib/elastic' 7 | import { aggregationsToAttributes } from '../attribute/aggregations' 8 | 9 | export async function list ({ filter, sort = null, currentPage = null, pageSize, search = null, context, rootValue, _sourceIncludes = null, _sourceExcludes = null }) { 10 | let _req = { 11 | query: { 12 | _source_excludes: _sourceExcludes, 13 | _source_includes: _sourceIncludes 14 | } 15 | } 16 | 17 | let query = buildQuery({ 18 | filter: filter, 19 | sort: sort, 20 | currentPage: currentPage, 21 | pageSize: pageSize, 22 | search: search, 23 | type: 'product' 24 | }); 25 | 26 | let esIndex = getIndexName(context.req.url) 27 | 28 | let response = getResponseObject(await client.search(adjustQuery({ 29 | index: esIndex, 30 | type: 'product', 31 | body: query, 32 | _source_includes: _sourceIncludes, 33 | _source_excludes: _sourceExcludes 34 | }, 'product', config))); 35 | if (response && response.hits && response.hits.hits) { 36 | // process response result (caluclate taxes etc...) 37 | response.hits.hits = await esResultsProcessor(response, _req, 'product', esIndex); 38 | } 39 | 40 | // Process hits 41 | response.items = [] 42 | response.hits.hits.forEach(hit => { 43 | let item = hit._source 44 | item._score = hit._score 45 | response.items.push(item) 46 | }); 47 | 48 | response = await aggregationsToAttributes(response, config, esIndex) 49 | response.total_count = response.hits.total 50 | 51 | // Process sort 52 | let sortOptions = [] 53 | for (var sortAttribute in sort) { 54 | sortOptions.push( 55 | { 56 | label: sortAttribute, 57 | value: sortAttribute 58 | } 59 | ) 60 | } 61 | response.sort_fields = {} 62 | if (sortOptions.length > 0) { 63 | response.sort_fields.options = sortOptions 64 | } 65 | 66 | response.page_info = { 67 | page_size: pageSize, 68 | current_page: currentPage 69 | } 70 | 71 | return response; 72 | } 73 | 74 | export async function listSingleProduct ({ sku, id = null, url_path = null, context, rootValue, _sourceIncludes = null, _sourceExcludes = null }) { 75 | const filter = {} 76 | if (sku) filter['sku'] = { eq: sku } 77 | if (id) filter['id'] = { eq: id } 78 | if (url_path) filter['url_path'] = { eq: url_path } 79 | const productList = await list({ filter, pageSize: 1, context, rootValue, _sourceIncludes, _sourceExcludes }) 80 | if (productList && productList.items.length > 0) { 81 | return productList.items[0] 82 | } else { 83 | return null 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/default-vsf/api/extensions/cms-data/index.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '@storefront-api/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: Record = {}; 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: Record = {}; 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: Record = {}; 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: Record = {}; 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 | -------------------------------------------------------------------------------- /packages/default-catalog/api/extensions/elastic-stock/index.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus, getCurrentStoreView, getCurrentStoreCode } from '@storefront-api/lib/util'; 2 | import { getClient as getElasticClient, adjustQuery, getHits } from '@storefront-api/lib/elastic' 3 | import { Router } from 'express'; 4 | import Logger from '@storefront-api/lib/logger' 5 | 6 | const bodybuilder = require('bodybuilder') 7 | 8 | module.exports = ({ 9 | config, 10 | db 11 | }) => { 12 | let api = Router(); 13 | 14 | const getStockList = (storeCode, skus) => { 15 | let storeView = getCurrentStoreView(storeCode) 16 | const esQuery = adjustQuery({ 17 | index: storeView.elasticsearch.index, // current index name 18 | type: 'product', 19 | _source_includes: ['stock'], 20 | body: bodybuilder().filter('term', 'status', 1).andFilter('terms', 'sku', skus).build() 21 | }, 'product', config) 22 | return getElasticClient(config).search(esQuery).then((products) => { // we're always trying to populate cache - when online 23 | return getHits(products).map(el => { return el._source.stock }) 24 | }).catch(err => { 25 | Logger.error(err) 26 | }) 27 | } 28 | 29 | /** 30 | * GET get stock item 31 | */ 32 | api.get('/check/:sku', (req, res) => { 33 | if (!req.params.sku) { 34 | return apiStatus(res, 'sku parameter is required', 400); 35 | } 36 | 37 | getStockList(getCurrentStoreCode(req), [req.params.sku]).then((result) => { 38 | if (result && result.length > 0) { 39 | apiStatus(res, result[0], 200); 40 | } else { 41 | apiStatus(res, 'No stock information for given sku', 500); 42 | } 43 | }).catch(err => { 44 | apiStatus(res, err, 500); 45 | }) 46 | }) 47 | 48 | /** 49 | * GET get stock item - 2nd version with the query url parameter 50 | */ 51 | api.get('/check', (req, res) => { 52 | if (!req.query.sku) { 53 | return apiStatus(res, 'sku parameter is required', 400); 54 | } 55 | getStockList(getCurrentStoreCode(req), [req.query.sku]).then((result) => { 56 | if (result && result.length > 0) { 57 | apiStatus(res, result[0], 200); 58 | } else { 59 | apiStatus(res, 'No stock information for given sku', 500); 60 | } 61 | }).catch(err => { 62 | apiStatus(res, err, 500); 63 | }) 64 | }) 65 | 66 | /** 67 | * GET get stock item list by skus (comma separated) 68 | */ 69 | api.get('/list', (req, res) => { 70 | if (!req.query.skus) { 71 | return apiStatus(res, 'skus parameter is required', 400); 72 | } 73 | const skuArray = (req.query.skus as string).split(',') 74 | getStockList(getCurrentStoreCode(req), skuArray).then((result) => { 75 | if (result && result.length > 0) { 76 | apiStatus(res, result, 200); 77 | } else { 78 | apiStatus(res, 'No stock information for given sku', 500); 79 | } 80 | }).catch(err => { 81 | apiStatus(res, err, 500); 82 | }) 83 | }) 84 | 85 | return api 86 | } 87 | -------------------------------------------------------------------------------- /packages/default-vsf/api/extensions/mail-service/index.ts: -------------------------------------------------------------------------------- 1 | import { apiStatus } from '@storefront-api/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!', 401) 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!', 400) 30 | } 31 | if (!userData.sourceAddress) { 32 | apiStatus(res, 'Source email address is not provided!', 400) 33 | return 34 | } 35 | if (!userData.targetAddress) { 36 | apiStatus(res, 'Target email address is not provided!', 400) 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!`, 400) 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!', 400) 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 | -------------------------------------------------------------------------------- /packages/platform-abstract/order.ts: -------------------------------------------------------------------------------- 1 | import { Request } from 'express'; 2 | import { IConfig } from 'config'; 3 | 4 | class AbstractOrderProxy { 5 | protected _request: Request 6 | protected _config: IConfig 7 | public api: Record> 8 | 9 | protected constructor (config, req) { 10 | this._config = config 11 | this._request = req 12 | } 13 | /* 14 | POST '/api/order/create` 15 | Queue the order into the order queue which will be asynchronously submitted to the eCommerce backend. 16 | 17 | #REQUEST BODY: 18 | The user_id field is a numeric user id as returned in api/user/me. The cart_id is a guest or authorized users quote id (You can mix guest cart with authroized user as well) 19 | 20 | { 21 | "user_id": "", 22 | "cart_id": "d90e9869fbfe3357281a67e3717e3524", 23 | "products": [ 24 | { 25 | "sku": "WT08-XS-Yellow", 26 | "qty": 1 27 | } 28 | ], 29 | "addressInformation": { 30 | "shippingAddress": { 31 | "region": "", 32 | "region_id": 0, 33 | "country_id": "PL", 34 | "street": [ 35 | "Example", 36 | "12" 37 | ], 38 | "company": "NA", 39 | "telephone": "", 40 | "postcode": "50-201", 41 | "city": "Wroclaw", 42 | "firstname": "Piotr", 43 | "lastname": "Karwatka", 44 | "email": "pkarwatka30@divante.pl", 45 | "region_code": "" 46 | }, 47 | "billingAddress": { 48 | "region": "", 49 | "region_id": 0, 50 | "country_id": "PL", 51 | "street": [ 52 | "Example", 53 | "12" 54 | ], 55 | "company": "Company name", 56 | "telephone": "", 57 | "postcode": "50-201", 58 | "city": "Wroclaw", 59 | "firstname": "Piotr", 60 | "lastname": "Karwatka", 61 | "email": "pkarwatka30@divante.pl", 62 | "region_code": "", 63 | "vat_id": "PL88182881112" 64 | }, 65 | "shipping_method_code": "flatrate", 66 | "shipping_carrier_code": "flatrate", 67 | "payment_method_code": "cashondelivery", 68 | "payment_method_additional": {} 69 | }, 70 | "order_id": "1522811662622-d3736c94-49a5-cd34-724c-87a3a57c2750", 71 | "transmited": false, 72 | "created_at": "2018-04-04T03:14:22.622Z", 73 | "updated_at": "2018-04-04T03:14:22.622Z" 74 | } 75 | #RESPONSE BODY: 76 | { 77 | "code":200, 78 | "result":"OK" 79 | } 80 | In case of the JSON validation error, the validation errors will be returned inside the result object. 81 | */ 82 | public create (orderData): Promise { throw new Error('AbstractOrderProxy::create must be implemented for specific platform') } 83 | } 84 | 85 | export default AbstractOrderProxy 86 | -------------------------------------------------------------------------------- /packages/default-catalog/graphql/elasticsearch/category/service.ts: -------------------------------------------------------------------------------- 1 | import config from 'config'; 2 | import client from '../client'; 3 | import { buildQuery } from '../queryBuilder' 4 | import { getIndexName } from '../mapping' 5 | import { adjustQuery, getResponseObject } from '@storefront-api/lib/elastic' 6 | import { aggregationsToAttributes } from '../attribute/aggregations' 7 | 8 | export async function list ({ search, filter, currentPage, pageSize = 200, sort, context, rootValue = null, _sourceIncludes, _sourceExcludes = null }) { 9 | let query = buildQuery({ search, filter, currentPage, pageSize, sort, type: 'category' }); 10 | 11 | const esIndex = getIndexName(context.req.url) 12 | let response = getResponseObject(await client.search(adjustQuery({ 13 | index: esIndex, 14 | body: query, 15 | _source_excludes: _sourceExcludes, 16 | _source_includes: _sourceIncludes 17 | }, 'category', config))); 18 | 19 | // Process hits 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 | response = await aggregationsToAttributes(response, config, esIndex) 28 | response.total_count = response.hits.total 29 | 30 | // Process sort 31 | let sortOptions = [] 32 | for (var sortAttribute in sort) { 33 | sortOptions.push( 34 | { 35 | label: sortAttribute, 36 | value: sortAttribute 37 | } 38 | ) 39 | } 40 | 41 | response.sort_fields = {} 42 | if (sortOptions.length > 0) { 43 | response.sort_fields.options = sortOptions 44 | } 45 | 46 | response.page_info = { 47 | page_size: pageSize, 48 | current_page: currentPage 49 | } 50 | return response; 51 | } 52 | 53 | export async function listSingleCategory ({ id, url_path, context, rootValue, _sourceIncludes, _sourceExcludes }) { 54 | const filter = {} 55 | if (id) filter['id'] = { eq: id } 56 | if (url_path) filter['url_path'] = { eq: url_path } 57 | const categoryList = await list({ search: '', filter, currentPage: 0, pageSize: 1, sort: null, context, rootValue, _sourceIncludes, _sourceExcludes }) 58 | if (categoryList && categoryList.items.length > 0) { 59 | return categoryList.items[0] 60 | } else { 61 | return null 62 | } 63 | } 64 | 65 | export async function listBreadcrumbs ({ category, context, addCurrentCategory = false }) { 66 | const ids = (category.parent_ids || (category.path && category.path.split('/')) || []) 67 | .filter((id) => String(id) !== String(category.id)) 68 | const _sourceIncludes = ['id', 'name', 'slug', 'path', 'level'] 69 | const filter = { 70 | id: { in: ids } 71 | } 72 | const response = await list({ 73 | search: '', 74 | filter, 75 | currentPage: 0, 76 | pageSize: 200, 77 | sort: null, 78 | context, 79 | _sourceIncludes 80 | }) 81 | const validResponse = (response && response.items) || [] 82 | const categoryList = (addCurrentCategory ? [...validResponse, category] : validResponse) 83 | .sort((a, b) => a.level - b.level) 84 | .map(({ id, level, ...rest }) => ({category_id: id, ...rest})) 85 | 86 | return categoryList 87 | } 88 | --------------------------------------------------------------------------------