From f67e39288591fd3c93cd13a0be90bb77d34efe85 Mon Sep 17 00:00:00 2001
From: petruki <31597636+petruki@users.noreply.github.com>
Date: Sat, 18 Nov 2023 14:55:56 -0800
Subject: [PATCH] Sync up reources - added API docs
---
.github/workflows/master.yml | 48 +-
README.md | 7 +-
docker-compose.yml | 39 ++
package.json | 19 +-
...cher Resolver Node.postman_collection.json | 504 ++++++++++++++++++
src/api-docs/paths/common.js | 28 +
src/api-docs/paths/path-client.js | 201 +++++++
src/api-docs/schemas/common.js | 19 +
src/api-docs/schemas/config-strategy.js | 72 +++
src/api-docs/swagger-document.js | 43 ++
src/api-docs/swagger-info.js | 14 +
src/app.js | 13 +-
src/client/configuration-type.js | 18 +-
src/client/resolvers.js | 17 +-
src/client/schema.js | 11 +-
src/middleware/auth.js | 10 +
tests/app.test.js | 38 ++
17 files changed, 1027 insertions(+), 74 deletions(-)
create mode 100644 docker-compose.yml
create mode 100644 requests/Switcher Resolver Node.postman_collection.json
create mode 100644 src/api-docs/paths/common.js
create mode 100644 src/api-docs/paths/path-client.js
create mode 100644 src/api-docs/schemas/common.js
create mode 100644 src/api-docs/schemas/config-strategy.js
create mode 100644 src/api-docs/swagger-document.js
create mode 100644 src/api-docs/swagger-info.js
create mode 100644 tests/app.test.js
diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml
index 47411ee..948827d 100644
--- a/.github/workflows/master.yml
+++ b/.github/workflows/master.yml
@@ -54,35 +54,35 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- # docker:
- # name: Publish Docker Image
- # needs: [ build-test ]
- # runs-on: ubuntu-latest
- # if: success() && github.ref == 'refs/heads/master'
+ docker:
+ name: Publish Docker Image
+ needs: [ build-test ]
+ runs-on: ubuntu-latest
+ if: success() && github.ref == 'refs/heads/master'
- # outputs:
- # digest: ${{ steps.docker_build.outputs.digest }}
+ outputs:
+ digest: ${{ steps.docker_build.outputs.digest }}
- # steps:
- # - name: Set up QEMU
- # uses: docker/setup-qemu-action@v2
+ steps:
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
- # - name: Set up Docker Buildx
- # uses: docker/setup-buildx-action@v2
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
- # - name: Login to DockerHub
- # uses: docker/login-action@v2
- # with:
- # username: ${{ secrets.DOCKERHUB_USERNAME }}
- # password: ${{ secrets.DOCKERHUB_TOKEN }}
+ - name: Login to DockerHub
+ uses: docker/login-action@v2
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
- # - name: Build and push
- # id: docker_build
- # uses: docker/build-push-action@v3
- # with:
- # push: true
- # platforms: linux/amd64,linux/arm64
- # tags: trackerforce/switcher-resolver-node:latest
+ - name: Build and push
+ id: docker_build
+ uses: docker/build-push-action@v3
+ with:
+ push: true
+ platforms: linux/amd64,linux/arm64
+ tags: trackerforce/switcher-resolver-node:latest
# update-kustomize:
# name: Deploy
diff --git a/README.md b/README.md
index 80fd37a..2d49e65 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,9 @@ Resolver Node for Component Switchers
+[](https://github.com/switcherapi/switcher-resolver-node/actions/workflows/master.yml)
+[](https://sonarcloud.io/dashboard?id=switcherapi_switcher-resolver-node)
+[](https://snyk.io/test/github/switcherapi/switcher-resolver-node)
[](https://opensource.org/licenses/MIT)
[](https://switcher-hq.slack.com/)
@@ -18,7 +21,7 @@ Resolver Node for Component Switchers
# About
-**Switcher Resolver Node** is the Feature Flag resolver that runs the Switcher evaluation engine.
+**Switcher Resolver Node** is the Feature Flag resolver that runs the Switcher evaluation criteria engine.
* * *
@@ -30,4 +33,4 @@ Resolver Node for Component Switchers
# Quick start
Open Swagger UI by accessing the URL: http://localhost:3000/api-docs
-Or use Postman by importing either the OpenAPI json from http://localhost:3000/swagger.json or Postman Collection from "requests/Switcher API*"
\ No newline at end of file
+Or use Postman by importing either the OpenAPI json from http://localhost:3000/swagger.json or Postman Collection from "requests/Switcher Resolver Node*"
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..86d3fbc
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,39 @@
+version: '3.8'
+
+networks:
+ backend:
+ driver: bridge
+
+services:
+ switcher_resolver_node:
+ image: trackerforce/switcher-resolver-node
+ container_name: switcher-resolver-node
+ ports:
+ - 3001:3001
+ networks:
+ - backend
+ environment:
+ - NODE_ENV=development
+ - PORT=3001
+ - ENV=${ENV}
+ - SSL_KEY=${SSL_KEY}
+ - SSL_CERT=${SSL_CERT}
+
+ - MONGODB_URI=${MONGODB_URI}
+ - RESOURCE_SECRET=${RESOURCE_SECRET}
+ - JWT_SECRET=${JWT_SECRET}
+ - JWT_CLIENT_TOKEN_EXP_TIME=${JWT_CLIENT_TOKEN_EXP_TIME}
+ - RELAY_BYPASS_HTTPS=${RELAY_BYPASS_HTTPS}
+ - RELAY_BYPASS_VERIFICATION=${RELAY_BYPASS_VERIFICATION}
+ - METRICS_ACTIVATED=${METRICS_ACTIVATED}
+ - METRICS_MAX_PAGE=${METRICS_MAX_PAGE}
+ - REGEX_MAX_TIMEOUT=${REGEX_MAX_TIMEOUT}
+ - REGEX_MAX_BLACLIST=${REGEX_MAX_BLACLIST}
+ - MAX_REQUEST_PER_MINUTE=${MAX_REQUEST_PER_MINUTE}
+
+ - SWITCHER_API_LOGGER=${SWITCHER_API_LOGGER}
+ - SWITCHER_API_ENABLE=${SWITCHER_API_ENABLE}
+ - SWITCHER_API_URL=${SWITCHER_API_URL}
+ - SWITCHER_API_KEY=${SWITCHER_API_KEY}
+ - SWITCHER_API_DOMAIN=${SWITCHER_API_DOMAIN}
+ - SWITCHER_API_ENVIRONMENT=${SWITCHER_API_ENVIRONMENT}
\ No newline at end of file
diff --git a/package.json b/package.json
index 2351630..6d25ae8 100644
--- a/package.json
+++ b/package.json
@@ -27,21 +27,22 @@
],
"license": "MIT",
"dependencies": {
- "axios": "^1.6.1",
+ "axios": "^1.6.2",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.18.2",
+ "express-basic-auth": "^1.2.1",
"express-rate-limit": "^7.1.4",
"express-validator": "^7.0.1",
"graphql": "^16.8.1",
"graphql-http": "^1.22.0",
"graphql-tag": "^2.12.6",
- "helmet": "^7.0.0",
+ "helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"moment": "^2.29.4",
- "mongodb": "^6.2.0",
- "mongoose": "^8.0.0",
- "pino": "^8.16.1",
+ "mongodb": "^6.3.0",
+ "mongoose": "^8.0.1",
+ "pino": "^8.16.2",
"pino-pretty": "^10.2.3",
"swagger-ui-express": "^5.0.0",
"switcher-client": "^3.2.0",
@@ -49,19 +50,19 @@
},
"devDependencies": {
"@babel/cli": "^7.23.0",
- "@babel/core": "^7.23.2",
+ "@babel/core": "^7.23.3",
"@babel/node": "^7.22.19",
- "@babel/preset-env": "^7.23.2",
+ "@babel/preset-env": "^7.23.3",
"@babel/register": "^7.22.15",
"babel-jest": "^29.7.0",
"babel-polyfill": "^6.26.0",
"env-cmd": "^10.1.0",
- "eslint": "^8.52.0",
+ "eslint": "^8.54.0",
"jest": "^29.7.0",
"jest-sonar-reporter": "^2.0.0",
"node-notifier": "^10.0.1",
"nodemon": "^3.0.1",
- "sinon": "^17.0.0",
+ "sinon": "^17.0.1",
"supertest": "^6.3.3"
},
"repository": {
diff --git a/requests/Switcher Resolver Node.postman_collection.json b/requests/Switcher Resolver Node.postman_collection.json
new file mode 100644
index 0000000..f547735
--- /dev/null
+++ b/requests/Switcher Resolver Node.postman_collection.json
@@ -0,0 +1,504 @@
+{
+ "info": {
+ "_postman_id": "2987131f-2c31-440d-8a8f-0f43f49483c3",
+ "name": "Switcher Resolver Node",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "9436108"
+ },
+ "item": [
+ {
+ "name": "REST",
+ "item": [
+ {
+ "name": "Auth component",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "if (pm.response.code === 200) {",
+ " pm.environment.set('authClientToken', pm.response.json().token)",
+ "}"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "auth": {
+ "type": "apikey",
+ "apikey": [
+ {
+ "key": "value",
+ "value": "{{apiKey}}",
+ "type": "string"
+ },
+ {
+ "key": "key",
+ "value": "switcher-api-key",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\t\"domain\": \"Playground\",\n\t\"component\": \"switcher-playground\",\n\t\"environment\": \"default\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{url}}/criteria/auth",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "criteria",
+ "auth"
+ ],
+ "query": [
+ {
+ "key": "showReason",
+ "value": "true",
+ "disabled": true
+ },
+ {
+ "key": "showStrategy",
+ "value": "true",
+ "disabled": true
+ },
+ {
+ "key": "bypassMetric",
+ "value": "true",
+ "disabled": true
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Resolve criteria - with input",
+ "request": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{authClientToken}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "value": "application/json",
+ "type": "text"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n\t\"entry\": [\n\t\t{\n\t\t\t\"strategy\": \"PAYLOAD_VALIDATION\",\n\t\t\t\"input\": \"{ \\\"status\\\": \\\"ready\\\" }\"\n\t\t}]\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{url}}/criteria?showReason=true&showStrategy=true&key=MY_SWITCHER",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "criteria"
+ ],
+ "query": [
+ {
+ "key": "showReason",
+ "value": "true"
+ },
+ {
+ "key": "showStrategy",
+ "value": "true"
+ },
+ {
+ "key": "bypassMetric",
+ "value": "true",
+ "disabled": true
+ },
+ {
+ "key": "key",
+ "value": "MY_SWITCHER"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Resolve criteria",
+ "request": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{authClientToken}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{url}}/criteria?key=MY_SWITCHER",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "criteria"
+ ],
+ "query": [
+ {
+ "key": "showReason",
+ "value": "true",
+ "disabled": true
+ },
+ {
+ "key": "showStrategy",
+ "value": "true",
+ "disabled": true
+ },
+ {
+ "key": "bypassMetric",
+ "value": "true",
+ "disabled": true
+ },
+ {
+ "key": "key",
+ "value": "MY_SWITCHER"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Check Snapshot",
+ "protocolProfileBehavior": {
+ "disableBodyPruning": true
+ },
+ "request": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{authClientToken}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "GET",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{url}}/criteria/snapshot_check/:version",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "criteria",
+ "snapshot_check",
+ ":version"
+ ],
+ "variable": [
+ {
+ "key": "version",
+ "value": "1588557288037"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Check Switchers",
+ "request": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{authClientToken}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "POST",
+ "header": [
+ {
+ "key": "Content-Type",
+ "name": "Content-Type",
+ "type": "text",
+ "value": "application/json"
+ }
+ ],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"switchers\": [\r\n \"FEATURE2020\",\r\n \"RELAY_1\"\r\n ]\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{url}}/criteria/switchers_check",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "criteria",
+ "switchers_check"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "GraphqQL",
+ "item": [
+ {
+ "name": "Resolve criteria - complex",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{authClientToken}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "graphql",
+ "graphql": {
+ "query": "{\r\n criteria(\r\n key: \"MY_SWITCHER\",\r\n # bypassMetric: false,\r\n entry: [\r\n {\r\n strategy: \"VALUE_VALIDATION\", \r\n input: \"Roger\"\r\n }\r\n ]\r\n ) {\r\n key\r\n activated\r\n response {\r\n result\r\n reason\r\n domain {\r\n name\r\n activated\r\n description\r\n group {\r\n name\r\n activated\r\n description\r\n config {\r\n key\r\n activated\r\n description\r\n strategies {\r\n strategy\r\n activated\r\n operation\r\n values\r\n }\r\n }\r\n }\r\n }\r\n group {\r\n name\r\n activated\r\n description\r\n }\r\n strategies {\r\n strategy\r\n activated\r\n operation\r\n values\r\n }\r\n }\r\n }\r\n}",
+ "variables": ""
+ }
+ },
+ "url": {
+ "raw": "{{url}}/graphql",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "graphql"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Resolve criteria - simple",
+ "request": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{authClientToken}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "graphql",
+ "graphql": {
+ "query": "{\r\n criteria(\r\n key: \"MY_SWITCHER\", \r\n entry: [\r\n {\r\n strategy: \"VALUE_VALIDATION\", \r\n input: \"Roger\"\r\n },\r\n {\r\n strategy: \"NETWORK_VALIDATION\", \r\n input: \"192.168.0.2\"\r\n }\r\n ]\r\n ) {\r\n response {\r\n result\r\n reason\r\n }\r\n }\r\n}",
+ "variables": ""
+ }
+ },
+ "url": {
+ "raw": "{{url}}/graphql",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "graphql"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Retrieve Snapshot",
+ "request": {
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{authClientToken}}",
+ "type": "string"
+ }
+ ]
+ },
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "graphql",
+ "graphql": {
+ "query": "{\r\n domain(activated: true) {\r\n name\r\n version\r\n description\r\n activated\r\n group(activated: true) {\r\n name\r\n description\r\n activated\r\n config(activated: true) {\r\n key\r\n description\r\n activated\r\n strategies(activated: true) {\r\n strategy\r\n activated\r\n operation\r\n values\r\n }\r\n components\r\n }\r\n }\r\n }\r\n}",
+ "variables": ""
+ }
+ },
+ "url": {
+ "raw": "{{url}}/graphql",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "graphql"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "API Check",
+ "event": [
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "auth": {
+ "type": "noauth"
+ },
+ "method": "GET",
+ "header": [
+ {
+ "key": "Authorization",
+ "type": "text",
+ "value": "Bearer",
+ "disabled": true
+ }
+ ],
+ "url": {
+ "raw": "{{url}}/check",
+ "host": [
+ "{{url}}"
+ ],
+ "path": [
+ "check"
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "auth": {
+ "type": "bearer",
+ "bearer": [
+ {
+ "key": "token",
+ "value": "{{authToken}}",
+ "type": "string"
+ }
+ ]
+ },
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/api-docs/paths/common.js b/src/api-docs/paths/common.js
new file mode 100644
index 0000000..39ac5e8
--- /dev/null
+++ b/src/api-docs/paths/common.js
@@ -0,0 +1,28 @@
+export const commonSchemaContent = (ref) => ({
+ 'application/json': {
+ schema: {
+ $ref: `#/components/schemas/${ref}`
+ }
+ }
+});
+
+export const commonArraySchemaContent = (ref) => ({
+ 'application/json': {
+ schema: {
+ type: 'array',
+ items: {
+ $ref: `#/components/schemas/${ref}`
+ }
+ }
+ }
+});
+
+export const commonOneOfSchemaContent = (refs) => ({
+ 'application/json': {
+ schema: {
+ oneOf: refs.map((ref) => ({
+ $ref: `#/components/schemas/${ref}`
+ }))
+ }
+ }
+});
\ No newline at end of file
diff --git a/src/api-docs/paths/path-client.js b/src/api-docs/paths/path-client.js
new file mode 100644
index 0000000..66b9e08
--- /dev/null
+++ b/src/api-docs/paths/path-client.js
@@ -0,0 +1,201 @@
+import { StrategiesType } from '../../models/config-strategy';
+import { pathParameter, queryParameter } from '../schemas/common';
+import configStrategy from '../schemas/config-strategy';
+
+export default {
+ '/criteria': {
+ post: {
+ tags: ['Client API'],
+ description: 'Execute criteria query against the API settings',
+ security: [{ appAuth: [] }],
+ parameters: [
+ queryParameter('key', 'string', 'Switcher Key', true),
+ queryParameter('showReason', 'boolean', 'Show criteria execution reason (default: true)', false),
+ queryParameter('showStrategy', 'boolean', 'Show criteria execution strategy (default: true)', false),
+ queryParameter('bypassMetric', 'boolean', 'Bypass metric check (default: true)', false)
+ ],
+ requestBody: {
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ entry: {
+ type: 'array',
+ items: {
+ type: 'object',
+ properties: {
+ strategy: {
+ type: 'string',
+ enum: Object.values(StrategiesType)
+ },
+ input: {
+ type: 'string'
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ responses: {
+ 200: {
+ description: 'Success',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ result: {
+ type: 'boolean'
+ },
+ reason: {
+ type: 'string'
+ },
+ strategies: {
+ type: 'array',
+ items: configStrategy
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ '/criteria/snapshot_check/:version': {
+ get: {
+ tags: ['Client API'],
+ description: 'Check if snapshot version is up to date',
+ security: [{ appAuth: [] }],
+ parameters: [
+ pathParameter('version', 'string', 'Snapshot version', true)
+ ],
+ responses: {
+ 200: {
+ description: 'Success',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ status: {
+ type: 'boolean',
+ description: 'true if snapshot version is up to date'
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ '/criteria/switchers_check': {
+ post: {
+ tags: ['Client API'],
+ description: 'Check if switcher keys are valid',
+ security: [{ appAuth: [] }],
+ requestBody: {
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ switchers: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ responses: {
+ 200: {
+ description: 'Success',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ not_found: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ '/criteria/auth': {
+ post: {
+ tags: ['Client API'],
+ description: 'Authenticate component',
+ security: [{ apiKey: [] }],
+ requestBody: {
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ domain: {
+ type: 'string',
+ description: 'Domain name'
+ },
+ component: {
+ type: 'string',
+ description: 'Component name'
+ },
+ enviroment: {
+ type: 'string',
+ description: 'Enviroment name'
+ }
+ }
+ }
+ }
+ }
+ },
+ responses: {
+ 200: {
+ description: 'Success',
+ content: {
+ 'application/json': {
+ schema: {
+ type: 'object',
+ properties: {
+ token: {
+ type: 'string',
+ description: 'Authentication token'
+ },
+ exp: {
+ type: 'number',
+ description: 'Expiration time'
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+};
+
+
+
+
+
+
+
+
diff --git a/src/api-docs/schemas/common.js b/src/api-docs/schemas/common.js
new file mode 100644
index 0000000..9295121
--- /dev/null
+++ b/src/api-docs/schemas/common.js
@@ -0,0 +1,19 @@
+export const queryParameter = (name, description, required, type) => ({
+ in: 'query',
+ name,
+ description,
+ required,
+ schema: {
+ type: type || 'string'
+ }
+});
+
+export const pathParameter = (name, description, required) => ({
+ in: 'path',
+ name,
+ description,
+ required,
+ schema: {
+ type: 'string'
+ }
+});
diff --git a/src/api-docs/schemas/config-strategy.js b/src/api-docs/schemas/config-strategy.js
new file mode 100644
index 0000000..076e81b
--- /dev/null
+++ b/src/api-docs/schemas/config-strategy.js
@@ -0,0 +1,72 @@
+import { OperationsType, StrategiesType } from '../../models/config-strategy';
+
+export const configStrategy = {
+ type: 'object',
+ properties: {
+ _id: {
+ type: 'string',
+ description: 'The unique identifier of the config strategy'
+ },
+ description: {
+ type: 'string',
+ description: 'The description of the config strategy'
+ },
+ activated: {
+ type: 'object',
+ additionalProperties: {
+ type: 'boolean',
+ description: 'The environment status'
+ }
+ },
+ strategy: {
+ type: 'string',
+ enum: Object.values(StrategiesType)
+ },
+ values: [{
+ type: 'string'
+ }],
+ operation: {
+ type: 'string',
+ enum: Object.values(OperationsType)
+ },
+ config: {
+ type: 'string',
+ description: 'The config ID parent of the config strategy',
+ format: 'uuid'
+ },
+ domain: {
+ type: 'string',
+ description: 'The domain ID parent of the config strategy',
+ format: 'uuid'
+ },
+ owner: {
+ type: 'uuid',
+ description: 'The owner id of the config strategy'
+ },
+ admin: {
+ type: 'object',
+ properties: {
+ _id: {
+ type: 'uuid',
+ description: 'The unique identifier of the admin'
+ },
+ name: {
+ type: 'string',
+ description: 'The name of the admin who created the config strategy'
+ }
+ }
+ },
+ createdAt: {
+ type: 'string',
+ description: 'The date when the config strategy was created'
+ },
+ updatedAt: {
+ type: 'string',
+ description: 'The date when the config strategy was updated'
+ }
+ }
+};
+
+export default {
+ ConfigStrategy: configStrategy
+};
\ No newline at end of file
diff --git a/src/api-docs/swagger-document.js b/src/api-docs/swagger-document.js
new file mode 100644
index 0000000..b7457c6
--- /dev/null
+++ b/src/api-docs/swagger-document.js
@@ -0,0 +1,43 @@
+import pathClient from './paths/path-client';
+
+import { commonSchema } from './schemas/common';
+import configStrategySchema from './schemas/config-strategy';
+import info from './swagger-info';
+
+export default {
+ openapi: '3.0.1',
+ info,
+ servers: [
+ {
+ url: 'http://localhost:3000',
+ description: 'Local'
+ },
+ {
+ url: 'https://api.switcherapi.com',
+ description: 'Cloud API'
+ }
+ ],
+ consumes: ['application/json'],
+ produces: ['application/json'],
+ components: {
+ securitySchemes: {
+ appAuth: {
+ type: 'http',
+ scheme: 'bearer',
+ name: 'JWT'
+ },
+ apiKey: {
+ type: 'apiKey',
+ in: 'header',
+ name: 'switcher-api-key'
+ }
+ },
+ schemas: {
+ ...commonSchema,
+ ...configStrategySchema
+ }
+ },
+ paths: {
+ ...pathClient
+ }
+};
\ No newline at end of file
diff --git a/src/api-docs/swagger-info.js b/src/api-docs/swagger-info.js
new file mode 100644
index 0000000..fcbbf3d
--- /dev/null
+++ b/src/api-docs/swagger-info.js
@@ -0,0 +1,14 @@
+export default {
+ title: 'Switcher Resolver Node',
+ version: 'v1.0.0',
+ description: 'Resolver Node for Component Switchers.',
+ contact: {
+ name: 'Roger Floriano (petruki)',
+ email: 'switcher.project@gmail.com',
+ url: 'https://github.com/petruki'
+ },
+ license: {
+ name: 'MIT',
+ url: 'https://github.com/switcherapi/switcher-resolver-node/blob/master/LICENSE'
+ }
+};
\ No newline at end of file
diff --git a/src/app.js b/src/app.js
index 6274186..f9e30b6 100644
--- a/src/app.js
+++ b/src/app.js
@@ -1,4 +1,5 @@
import express from 'express';
+import swaggerUi from 'swagger-ui-express';
import { createHandler } from 'graphql-http/lib/use/express';
import cors from 'cors';
import helmet from 'helmet';
@@ -6,9 +7,10 @@ import helmet from 'helmet';
require('./db/mongoose');
import mongoose from 'mongoose';
+import swaggerDocument from './api-docs/swagger-document';
import clientApiRouter from './routers/client-api';
import schema from './client/schema';
-import { appAuth } from './middleware/auth';
+import { appAuth, resourcesAuth } from './middleware/auth';
import { clientLimiter, defaultLimiter } from './middleware/limiter';
import { createServer } from './app-server';
@@ -41,6 +43,15 @@ app.use('/graphql', appAuth, clientLimiter, handler);
* API Docs and Health Check
*/
+app.use('/api-docs', resourcesAuth(),
+ swaggerUi.serve,
+ swaggerUi.setup(swaggerDocument)
+);
+
+app.get('/swagger.json', resourcesAuth(), (_req, res) => {
+ res.status(200).send(swaggerDocument);
+});
+
app.get('/check', defaultLimiter, (req, res) => {
const showDetails = req.query.details === '1';
const response = {
diff --git a/src/client/configuration-type.js b/src/client/configuration-type.js
index 673178d..0d4106a 100644
--- a/src/client/configuration-type.js
+++ b/src/client/configuration-type.js
@@ -86,11 +86,7 @@ export const configType = new GraphQLObjectType({
type: GraphQLString
},
activated: {
- type: GraphQLBoolean,
- resolve: (source, _args, { environment }) => {
- return source.activated[`${environment}`] === undefined ?
- source.activated[`${EnvType.DEFAULT}`] : source.activated[`${environment}`];
- }
+ type: GraphQLBoolean
}
},
resolve: async (source, { _id, strategy, operation, activated }, context) => {
@@ -141,11 +137,7 @@ export const groupConfigType = new GraphQLObjectType({
type: GraphQLString
},
activated: {
- type: GraphQLBoolean,
- resolve: (source, _args, { environment }) => {
- return source.activated[`${environment}`] === undefined ?
- source.activated[`${EnvType.DEFAULT}`] : source.activated[`${environment}`];
- }
+ type: GraphQLBoolean
}
},
resolve: async (source, { _id, key, activated }, context) => {
@@ -210,11 +202,7 @@ export const domainType = new GraphQLObjectType({
type: GraphQLString
},
activated: {
- type: GraphQLBoolean,
- resolve: (source, _args, { environment }) => {
- return source.activated[`${environment}`] === undefined ?
- source.activated[`${EnvType.DEFAULT}`] : source.activated[`${environment}`];
- }
+ type: GraphQLBoolean
}
},
resolve: async (source, { _id, name, activated }, context) => {
diff --git a/src/client/resolvers.js b/src/client/resolvers.js
index 929cf0b..0ab1f2c 100644
--- a/src/client/resolvers.js
+++ b/src/client/resolvers.js
@@ -74,21 +74,8 @@ export async function resolveGroupConfig(source, _id, name, activated, context)
return resolveComponentsFirst(source, context, groups);
}
-export async function resolveDomain(_id, name, activated, context) {
- const args = {};
-
- if (_id) {
- args._id = _id;
- } else if (context.domain) {
- args._id = context.domain;
- }
-
- if (name && context.admin) {
- args.name = name;
- args.owner = context.admin._id;
- }
-
- let domain = await Domain.findOne({ ...args }).lean().exec();
+export async function resolveDomain(activated, context) {
+ let domain = await Domain.findOne({ _id: context.domain }).lean().exec();
if (activated !== undefined) {
if (domain.activated[context.environment] !== activated) {
return null;
diff --git a/src/client/schema.js b/src/client/schema.js
index 1bc4db7..7545982 100644
--- a/src/client/schema.js
+++ b/src/client/schema.js
@@ -1,6 +1,5 @@
import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLList, GraphQLBoolean, GraphQLNonNull } from 'graphql';
import { domainType } from './configuration-type';
-import { EnvType } from '../models/environment';
import { strategyInputType, criteriaType } from './criteria-type';
import { resolveConfigByKey, resolveDomain } from './resolvers';
@@ -36,11 +35,7 @@ const queryType = new GraphQLObjectType({
type: GraphQLString
},
activated: {
- type: GraphQLBoolean,
- resolve: (source, _args, { environment }) => {
- return source.activated.get(environment) === undefined ?
- source.activated.get(EnvType.DEFAULT) : source.activated.get(environment);
- }
+ type: GraphQLBoolean
},
environment: {
type: GraphQLString
@@ -49,10 +44,10 @@ const queryType = new GraphQLObjectType({
type: GraphQLString
}
},
- resolve: async (_source, { _id, name, activated, environment, _component }, context) => {
+ resolve: async (_source, { activated, environment, _component }, context) => {
if (environment) context.environment = environment;
if (_component) context._component = _component;
- return resolveDomain(_id, name, activated, context);
+ return resolveDomain(activated, context);
}
}
}
diff --git a/src/middleware/auth.js b/src/middleware/auth.js
index 9581b7c..4843f92 100644
--- a/src/middleware/auth.js
+++ b/src/middleware/auth.js
@@ -1,9 +1,19 @@
+import basicAuth from 'express-basic-auth';
import jwt from 'jsonwebtoken';
import { getComponentById } from '../services/component';
import { responseExceptionSilent } from '../exceptions';
import Component from '../models/component';
import { getRateLimit } from '../external/switcher-api-facade';
+export function resourcesAuth() {
+ return basicAuth({
+ users: {
+ admin: process.env.RESOURCE_SECRET || 'admin',
+ },
+ challenge: true,
+ });
+}
+
export async function appAuth(req, res, next) {
try {
const token = req.header('Authorization').replace('Bearer ', '');
diff --git a/tests/app.test.js b/tests/app.test.js
new file mode 100644
index 0000000..d5e1414
--- /dev/null
+++ b/tests/app.test.js
@@ -0,0 +1,38 @@
+import mongoose from 'mongoose';
+import app from '../src/app';
+import request from 'supertest';
+
+afterAll(async () => {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ await mongoose.disconnect();
+});
+
+describe('Testing app [REST] ', () => {
+ test('APP_SUITE - Should return success on a health check request', async () => {
+ const req = await request(app)
+ .get('/check')
+ .expect(200);
+
+ expect(req.statusCode).toBe(200);
+ expect(req.body.status).toEqual('UP');
+ });
+
+ test('APP_SUITE - Should return success on a health check request with details', async () => {
+ const req = await request(app)
+ .get('/check?details=1')
+ .expect(200);
+
+ expect(req.statusCode).toBe(200);
+ expect(req.body.status).toEqual('UP');
+ expect(req.body.attributes).toBeDefined();
+ });
+
+ test('APP_SUITE - Should return 404 - Operation not found', async () => {
+ const req = await request(app)
+ .get('/not-found')
+ .expect(404);
+
+ expect(req.statusCode).toBe(404);
+ expect(req.body.error).toEqual('Operation not found');
+ });
+});