diff --git a/.env.sample b/.env.sample index dd802034..44d0963a 100644 --- a/.env.sample +++ b/.env.sample @@ -1,3 +1,3 @@ -S3FILE=db.json -S3BUCKET=json-serverless-dev -READONLY=false \ No newline at end of file +S3File=db.json +S3Bucket=json-serverless-dev +readOnly=false \ No newline at end of file diff --git a/README.md b/README.md index cb998a05..ae7e45e7 100644 --- a/README.md +++ b/README.md @@ -9,38 +9,45 @@ - [Develop locally with cloud resources](#develop-locally-with-cloud-resources) - [Diagnose issues](#diagnose-issues) +## Architecture + + + ## Features -- Development: - - Easily setup routes and resources for the REST Api via json file. [(via json-server)](https://github.com/typicode/json-server) - - This solution written in **NodeJS** can be easily extended for additional enhanced scenarios - - adding user authentication - - own custom domain - - additional routes etc. - - Develop and test solution locally in Visual Studio Code -- Security: This Api is secured via API Key and https by default. +- Easily setup routes and resources for the REST Api via json file. [(via json-server)](https://github.com/typicode/json-server) +- **New:** Added Swagger UI support - Deployment: - Deployed in AWS cloud within Minutes by a single command - Almost **zero costs** (First million requests for Lambda are free) - Less maintenance as the deployed solution runs **serverless** +- Security: + - Secured with https by default. + - Optional: Use a generated API Key +- Customization: + - This solution written in **NodeJS** can be easily extended for additional enhanced scenarios + - adding user authentication + - own custom domain + - additional routes etc. + - Develop and debug solution locally in Visual Studio Code ## Quickstart -##### 1. Clone Solution +### 1. Clone Solution ```bash -git clone https://github.com/pharindoko/json-serverless.git +git clone https://github.com/pharindoko/json-serverless.git cd json-serverless ``` -##### 2. Install dependencies +### 2. Install dependencies ```bash npm install -g serverless npm i ``` -##### 3. Verify AWS Access / Credentials +### 3. Verify AWS Access / Credentials => You need to have access to AWS to upload the solution. @@ -48,7 +55,7 @@ npm i aws sts get-caller-identity ``` -##### 4. Update db.json file in root directory +### 4. Update db.json file in root directory - Childproperties are the REST endpoints you create - Samplefile: Routes marked **bold** @@ -61,8 +68,7 @@ aws sts get-caller-identity } -##### 5. Deploy via Serverless Framework - +### 5. Deploy via Serverless Framework ```bash # set --stage parameter for different stages @@ -72,7 +78,7 @@ serverless deploy --stage dev - serverless-webpack is used - the build will be triggered automatically -##### 6. When the deployment with serverless framework was successful you can see following output +### 6. When the deployment with serverless framework was successful you can see following output
@@ -82,9 +88,9 @@ stage: dev
region: eu-central-1
stack: serverless-json-server-dev
api keys:
- serverless-json-server.dev: {API - KEY}
+ serverless-json-server.dev: {API-KEY}
endpoints:
- ANY - https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/
+ ANY - https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/ <== {ENDPOINTURL}
ANY - https://xxxxxxx.eu-central-1.amazonaws.com/dev/{proxy+}
functions:
app: serverless-json-server-dev-app
@@ -93,45 +99,40 @@ layers:
Serverless: Removing old service artifacts from S3...
-##### 7. Test your Api
+### 7. Test your Api
+
+#### With Swagger
+
+Open the {ENDPOINTURL}: https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/ that you received as output
-##### With Curl
+**MIND**: If you have set enableApiKeyAuth to true => [SwaggerUI](#Cannot-use-Swagger-UI-when-enableApiKeyAuth-is-true)
+)
+
+#### With Curl
1. replace the url with the url provided by serverless (see above)
-2. replace the {API - KEY} with the key you get from serverless (see above)
+2. replace the {API-KEY} with the key you get from serverless (see above)
3. replace {route} at the end of the url e.g. with posts (default value)
Default Schema:
```bash
Default route is posts: (see db.json)
-curl -H "x-api-key: {API - KEY}" -H "Content-Type: application/json" https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/posts
+curl -H "Content-Type: application/json" https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/api/posts
-#or another route given in db.json file
-curl -H "x-api-key: {API - KEY}" -H "Content-Type: application/json" https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/{route}
-```
+# or another route given in db.json file
+curl -H "Content-Type: application/json" https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/api/{route}
-##### With Postman
+# with enableApiKeyAuth=true
+curl -H "x-api-key: {API-KEY}" -H "Content-Type: application/json" https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/api/{route}
-- Create a new GET Request and add these values to the header section
-
- |Key| Value|
- |---|---|
- |x-api-key | {API - KEY}|
- |Content-Type | application/json|
-
-- Enter as Url the endpoints url
-
-```bash
- https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/{route}
- # e.g. default value: https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/posts
```
What`s my {route} ? -> see [json-server documentation](https://github.com/typicode/json-server)
## Customization
-#### Update content of db.json
+### Update content of db.json
1. update local db.json file in root directory with new values
2. re-deploy the stack via serverless framework
@@ -141,10 +142,15 @@ What`s my {route} ? -> see [json-server documentation](https://github.com/typico
```
3. delete db.json file in S3 Bucket
-4. Make a GET request against the root url https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/
+4. Make a GET request against the root url https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/api
```bash
-curl -H "x-api-key: {API - KEY}" -H "Content-Type: application/json" https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev
+curl -H "Content-Type: application/json" https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/api
+
+# with enableApiKeyAuth=true
+curl -H "x-api-key: {API-KEY}" -H "Content-Type: application/json" https://xxxxxx.execute-api.eu-central-1.amazonaws.com/dev/api/{route}
+
+
```
=> With the next request a new db.json file will be created in the S3 Bucket
@@ -153,13 +159,13 @@ curl -H "x-api-key: {API - KEY}" -H "Content-Type: application/json" https://xxx
[edit service property in serverless.yml (in root directory)](https://github.com/pharindoko/json-server-less-lambda/blob/66756961d960c44cf317ca307b097f595799a890/serverless.yml#L8)
-#### Adapt settings in config/servleressconfig.yml file
+#### Adapt settings in config/appconfig.yml file
| Attribute | Description | Type | Default |
|---|---|---|---|
-| S3FILE | JSON file used as db to read and write (will be created with a default json value - customize in db.json) | string |db.json |
-| S3BUCKET | S3-Bucket - this bucket must already exist in AWS | string | json-server-less-lambda-dev |
-| READONLY | all API - write operations are forbidden (http 403)) | boolean | false |
+| readOnly | Make API readonly - all API - write operations are forbidden (http 403)) | string |false |
+| enableSwagger | Enable swagger and swagger UI support | string | true |
+| enableApiKeyAuth | Make your routes private by using an additional ApiKey | boolean | false |
## Used Packages
@@ -170,7 +176,7 @@ curl -H "x-api-key: {API - KEY}" -H "Content-Type: application/json" https://xxx
## Components
-- [NodeJS 8.10](https://nodejs.org/en/about/)
+- [NodeJS 8.10](https://nodejs.org/en/about/)
- [AWS API Gateway](https://aws.amazon.com/api-gateway/)
- [AWS Lambda](https://aws.amazon.com/lambda/features/)
- [AWS S3](https://aws.amazon.com/s3/)
@@ -179,13 +185,14 @@ curl -H "x-api-key: {API - KEY}" -H "Content-Type: application/json" https://xxx
db.json file will be loaded directly from your local filesystem. No AWS access is needed.
-#### Start solution
+### Start solution
```bash
npm run start
```
-#### Debug solution
+### Debug solution
+
If you want to debug locally in VS Code everything is already setup (using webpack with sourcemap support)
```bash
@@ -194,13 +201,24 @@ npm run debug
#### 2. Test your API
-To test you can use e.g. [Postman](https://www.getpostman.com/)
+#### With Swagger
-- Open Postman
-- Enter as Url the endpoints url
+Open the {ENDPOINTURL}: http://localhost:3000/ that you received as output
+
+#### With Curl
+
+1. replace the url with the url provided by serverless (see above)
+2. replace the {API - KEY} with the key you get from serverless (see above)
+3. replace {route} at the end of the url e.g. with posts (default value)
+
+Default Schema:
```bash
- https://localhost:3000/{route} #e.g. default value: https://localhost:3000/posts/
+Default route is posts: (see db.json)
+curl -H "Content-Type: application/json" http://localhost:3000/api/posts
+
+#or another route given in db.json file
+curl -H "Content-Type: application/json" http://localhost:3000/api/{route}
```
What`s my {route} ? -> see [json-server documentation](https://github.com/typicode/json-server)
@@ -209,7 +227,7 @@ What`s my {route} ? -> see [json-server documentation](https://github.com/typico
Use same componentes (S3, LowDB) as the lambda does but have code executed locally.
-#### 1. Add .env file to root folder
+### 1. Add .env file to root folder
**Mind:** If you haven`t deployed the solution yet, please create a private S3-Bucket and .json - file manually or deploy the solution first to AWS via serverless framework
-curl -H "x-api-key: {API-KEY}" -H "Content-Type: application/json" http://localhost:3000/{route}
-
\ No newline at end of file
+curl -H "x-api-key: {API-KEY}" -H "Content-Type: application/json" http://localhost:3000/api/{route}
+
+
+## FAQ
+
+### How can I change the lambda region or stack name
+
+Please have a look to the serverless guideline: https://serverless.com/framework/docs/providers/aws/guide/deploying/
+
+### Cannot use Swagger UI when enableApiKeyAuth is true
+
+The apiKey is set in AWS API Gateway. This means all requests (even the standard route) need to use the API-KEY.
+
+If you want to see the Swagger UI you need to add a plugin e.g. ModHeader to Chrome and add the needed headers:
+- Content-Type: application/json
+- x-api-key: {provided by sls info in the output after deployment}
+
+
+
+### I forgot the API-KEY I have set
+
+Ensure you have credentials for AWS set.
+
+```bash
+sls info
+```
+
+### Destroy the stack in the cloud
+
+```bash
+sls remove
+```
+
+### I deployed the solution but I get back a http 500 error
+
+Check Cloudwatch Logs in AWS - the issue should be describe there. Log has the same name as the stack that has been created.
diff --git a/config/appconfig.json b/config/appconfig.json
new file mode 100644
index 00000000..aeffe4b1
--- /dev/null
+++ b/config/appconfig.json
@@ -0,0 +1,6 @@
+{
+ "readOnly": false,
+ "enableSwagger": true,
+ "enableApiKeyAuth": false
+ }
+
\ No newline at end of file
diff --git a/config/serverlessconfig.json b/config/serverlessconfig.json
new file mode 100644
index 00000000..59968c05
--- /dev/null
+++ b/config/serverlessconfig.json
@@ -0,0 +1,5 @@
+{
+ "S3File": "db.json",
+ "S3Bucket": "${self:service}-${self:provider.stage}",
+ "basePath": "/${self:provider.stage}"
+}
diff --git a/config/serverlessconfig.yml b/config/serverlessconfig.yml
deleted file mode 100644
index c00ffc0a..00000000
--- a/config/serverlessconfig.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-S3FILE: "db.json"
-S3BUCKET: "${self:service}-${self:provider.stage}"
-READONLY: false
\ No newline at end of file
diff --git a/docs/header.png b/docs/header.png
new file mode 100644
index 00000000..f8c573c0
Binary files /dev/null and b/docs/header.png differ
diff --git a/docs/json-serverless.png b/docs/json-serverless.png
new file mode 100644
index 00000000..c2c82ab5
Binary files /dev/null and b/docs/json-serverless.png differ
diff --git a/package-lock.json b/package-lock.json
index 6a034963..eefe2cf1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3193,6 +3193,12 @@
"integrity": "sha512-VvxA0xhNqIIfg0V9AmJkDg91DaJwryutH5rVEZAhcNi4iJFj9f+QxmAjgK1LT9I8OgToX27fypX6/MeCXVbBjQ==",
"dev": true
},
+ "call-me-maybe": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz",
+ "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=",
+ "dev": true
+ },
"caller-callsite": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz",
@@ -4415,6 +4421,26 @@
"esutils": "^2.0.2"
}
},
+ "doctrine-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/doctrine-file/-/doctrine-file-1.0.3.tgz",
+ "integrity": "sha512-OK37HbZtNmIMn84riibVXRmcEGUIf6BNfYMcbXg20ejP+LEsf4tnk8QfYy3EmQs4KzZFhTl3zwoKqVwARxpBgA==",
+ "dev": true,
+ "requires": {
+ "doctrine": "^2.0.0"
+ },
+ "dependencies": {
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ }
+ }
+ },
"domain-browser": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
@@ -5140,6 +5166,45 @@
"vary": "~1.1.2"
}
},
+ "express-list-endpoints": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/express-list-endpoints/-/express-list-endpoints-4.0.1.tgz",
+ "integrity": "sha512-KjY7frYk72/Jwk2VgqyvuXlTPslEkWkzjUXPUMCUguVmAWqd6fh60VHr+sEfqJgMAOE3hKhUjm/7tLASVaE2Qg=="
+ },
+ "express-swagger-generator": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/express-swagger-generator/-/express-swagger-generator-1.1.15.tgz",
+ "integrity": "sha512-dOqXj+amehXMyPxGnkHDdGDGSxQy5fBrZ74d3L7noLfGDC2b6pb+uziZ/QcBth/w6AwtxpUWlRI9K0hyoIQRvQ==",
+ "dev": true,
+ "requires": {
+ "doctrine": "^2.0.0",
+ "doctrine-file": "^1.0.2",
+ "express-swaggerize-ui": "^1.0.3",
+ "glob": "^7.0.3",
+ "recursive-iterator": "^2.0.3",
+ "swagger-parser": "^5.0.5"
+ },
+ "dependencies": {
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ }
+ }
+ },
+ "express-swaggerize-ui": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/express-swaggerize-ui/-/express-swaggerize-ui-1.1.0.tgz",
+ "integrity": "sha512-dDJuWV/GlISNYyKvFMa3EDr6sYzMgMrVRCt9o1kQxaIIKnmK1NJvaTzGbRIokIlGGHriIT6E2ztorRyRxLuOzA==",
+ "dev": true,
+ "requires": {
+ "express": "^4.13.3"
+ }
+ },
"express-urlrewrite": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/express-urlrewrite/-/express-urlrewrite-1.2.0.tgz",
@@ -5596,6 +5661,12 @@
"mime-types": "^2.1.12"
}
},
+ "format-util": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.3.tgz",
+ "integrity": "sha1-Ay3KShFiYqEsQ/TD7IVmQWxbLZU=",
+ "dev": true
+ },
"formidable": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.1.tgz",
@@ -7958,6 +8029,35 @@
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
},
+ "json-schema-ref-parser": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/json-schema-ref-parser/-/json-schema-ref-parser-5.1.3.tgz",
+ "integrity": "sha512-CpDFlBwz/6la78hZxyB9FECVKGYjIIl3Ms3KLqFj99W7IIb7D00/RDgc++IGB4BBALl0QRhh5m4q5WNSopvLtQ==",
+ "dev": true,
+ "requires": {
+ "call-me-maybe": "^1.0.1",
+ "debug": "^3.1.0",
+ "js-yaml": "^3.12.0",
+ "ono": "^4.0.6"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ }
+ }
+ },
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -8035,6 +8135,18 @@
"integrity": "sha512-CXQJ/tsgFogKYBuCRmnlChIw66JBXp8kAkT+R4mSB2cuzCSBi88lx2A+vHvo27RY4Wtj5xVVGu2/2O7NwZ79mg==",
"dev": true
},
+ "jsonschema": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz",
+ "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==",
+ "dev": true
+ },
+ "jsonschema-draft4": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz",
+ "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=",
+ "dev": true
+ },
"jsonwebtoken": {
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz",
@@ -8357,6 +8469,12 @@
"integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=",
"dev": true
},
+ "lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
+ "dev": true
+ },
"lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
@@ -9893,6 +10011,26 @@
"mimic-fn": "^2.1.0"
}
},
+ "ono": {
+ "version": "4.0.11",
+ "resolved": "https://registry.npmjs.org/ono/-/ono-4.0.11.tgz",
+ "integrity": "sha512-jQ31cORBFE6td25deYeD80wxKBMj+zBmHTrVxnc6CKhx8gho6ipmWM5zj/oeoqioZ99yqBls9Z/9Nss7J26G2g==",
+ "dev": true,
+ "requires": {
+ "format-util": "^1.0.3"
+ }
+ },
+ "openapi-schema-validation": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz",
+ "integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==",
+ "dev": true,
+ "requires": {
+ "jsonschema": "1.2.4",
+ "jsonschema-draft4": "^1.0.0",
+ "swagger-schema-official": "2.0.0-bab6bed"
+ }
+ },
"opn": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
@@ -10815,6 +10953,12 @@
"resolve": "^1.1.6"
}
},
+ "recursive-iterator": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/recursive-iterator/-/recursive-iterator-2.0.3.tgz",
+ "integrity": "sha1-0ODSx+eoMQnXMJHPBD/FCeWnbcM=",
+ "dev": true
+ },
"regenerate": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@@ -12747,6 +12891,64 @@
"has-flag": "^3.0.0"
}
},
+ "swagger-methods": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/swagger-methods/-/swagger-methods-1.0.8.tgz",
+ "integrity": "sha512-G6baCwuHA+C5jf4FNOrosE4XlmGsdjbOjdBK4yuiDDj/ro9uR4Srj3OR84oQMT8F3qKp00tYNv0YN730oTHPZA==",
+ "dev": true
+ },
+ "swagger-parser": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-5.0.6.tgz",
+ "integrity": "sha512-FdzCYFK11iGgrOpojlqUluU6SKThtzmu+5Get+6ValJR2TFwTnES1x4Fdfgy3C4/8VVXk4Va/WsqGlbyY/Os+A==",
+ "dev": true,
+ "requires": {
+ "call-me-maybe": "^1.0.1",
+ "debug": "^3.1.0",
+ "json-schema-ref-parser": "^5.1.3",
+ "ono": "^4.0.6",
+ "openapi-schema-validation": "^0.4.2",
+ "swagger-methods": "^1.0.4",
+ "swagger-schema-official": "2.0.0-bab6bed",
+ "z-schema": "^3.23.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ }
+ }
+ },
+ "swagger-schema-official": {
+ "version": "2.0.0-bab6bed",
+ "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz",
+ "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=",
+ "dev": true
+ },
+ "swagger-ui-dist": {
+ "version": "3.23.5",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.23.5.tgz",
+ "integrity": "sha512-fsCF3wR0kBF5G7yF8uD5kALSbo2TjpfdheXWnrmxD2/d/8bgKDlUVoqO4gMNrZRQOEbyXCexZiU9geMNspWWyA=="
+ },
+ "swagger-ui-express": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.0.7.tgz",
+ "integrity": "sha512-ipXe53qDMjB2GlFcWARof15fMxX0n0wkwUturBpdovfJLaqod3WAqimwQGFXjwpWKA6hnxEPrd31yOzaYkP++A==",
+ "requires": {
+ "swagger-ui-dist": "^3.18.1"
+ }
+ },
"symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -13586,6 +13788,12 @@
"spdx-expression-parse": "^3.0.0"
}
},
+ "validator": {
+ "version": "10.11.0",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz",
+ "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==",
+ "dev": true
+ },
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -14137,6 +14345,19 @@
"integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=",
"dev": true
},
+ "z-schema": {
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-3.25.1.tgz",
+ "integrity": "sha512-7tDlwhrBG+oYFdXNOjILSurpfQyuVgkRe3hB2q8TEssamDHB7BbLWYkYO98nTn0FibfdFroFKDjndbgufAgS/Q==",
+ "dev": true,
+ "requires": {
+ "commander": "^2.7.1",
+ "core-js": "^2.5.7",
+ "lodash.get": "^4.0.0",
+ "lodash.isequal": "^4.0.0",
+ "validator": "^10.0.0"
+ }
+ },
"zip-stream": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-1.2.0.tgz",
diff --git a/package.json b/package.json
index 61b38716..9c1a1561 100644
--- a/package.json
+++ b/package.json
@@ -30,14 +30,18 @@
"@babel/runtime": "^7.5.5",
"babel-loader": "^8.0.6",
"dotenv": "^8.0.0",
+ "express": "^4.17.1",
+ "express-list-endpoints": "^4.0.1",
"json-server": "^0.15.0",
+ "lodash": "^4.17.15",
"lowdb": "^1.0.0",
"lowdb-adapter-aws-s3": "^1.1.2",
"pino": "^5.10.6",
"pino-pretty": "^3.2.0",
"serverless-http": "^2.0.0",
"snyk": "^1.198.0",
- "supertest": "^4.0.2"
+ "supertest": "^4.0.2",
+ "swagger-ui-express": "^4.0.7"
},
"devDependencies": {
"@babel/cli": "7.5.5",
@@ -51,6 +55,7 @@
"eslint": "6.1.0",
"eslint-config-airbnb-base": "14.0.0",
"eslint-plugin-import": "2.18.2",
+ "express-swagger-generator": "^1.1.15",
"jest": "24.9.0",
"node-env-webpack-plugin": "1.1.0",
"nodemon": "1.19.1",
diff --git a/serverless.yml b/serverless.yml
index ed704e55..902437aa 100644
--- a/serverless.yml
+++ b/serverless.yml
@@ -13,7 +13,8 @@ package:
individually: false
excludeDevDependencies: true
custom:
- file: ${file(./config/serverlessconfig.yml)}
+ file: ${file(./config/serverlessconfig.json)}
+ appconfig: ${file(./config/appconfig.json)}
custom:
serverless-offline:
resourceRoutes: true
@@ -32,10 +33,12 @@ provider:
runtime: nodejs10.x
region: ${opt:region, 'eu-central-1'}
environment:
- S3BUCKET: ${self:custom.file.S3BUCKET}
- S3FILE: ${self:custom.file.S3FILE}
- READONLY: ${self:custom.file.READONLY}
-
+ S3Bucket: ${self:custom.file.S3Bucket}
+ S3File: ${self:custom.file.S3File}
+ basePath: ${self:custom.file.basePath}
+ apiGateway: # Optional API Gateway global config
+ binaryMediaTypes: # Optional binary media types the API might return
+ - '*/*'
apiKeys:
- ${self:service}.${self:provider.stage}
iamRoleStatements:
@@ -47,7 +50,7 @@ provider:
Action:
- "s3:*"
Resource:
- - "arn:aws:s3:::${self:custom.file.S3BUCKET}/*"
+ - "arn:aws:s3:::${self:custom.file.S3Bucket}/*"
# The `functions` block defines what code to deploy
functions:
@@ -59,15 +62,15 @@ functions:
path: /
method: ANY
cors: true
- private: true
+ private: ${self:custom.appconfig.enableApiKeyAuth}
- http:
path: "{proxy+}"
method: ANY
cors: true
- private: true
+ private: ${self:custom.appconfig.enableApiKeyAuth}
resources:
Resources:
S3BucketStorage:
Type: AWS::S3::Bucket
Properties:
- BucketName: ${self:custom.file.S3BUCKET}
\ No newline at end of file
+ BucketName: ${self:custom.file.S3Bucket}
\ No newline at end of file
diff --git a/src/handler.js b/src/handler.js
index c6f48512..b3f9adca 100644
--- a/src/handler.js
+++ b/src/handler.js
@@ -1,8 +1,6 @@
require('@babel/polyfill');
const serverless = require('serverless-http');
-const logger = require('pino')({
- prettyPrint: true,
-}, process.stderr);
+const { logger } = require('./logger');
const app = require('./utils');
function start(server, port) {
diff --git a/src/logger.js b/src/logger.js
new file mode 100644
index 00000000..b8b402ad
--- /dev/null
+++ b/src/logger.js
@@ -0,0 +1,5 @@
+const logger = require('pino')({
+ prettyPrint: true,
+}, process.stderr);
+
+module.exports.logger = logger;
diff --git a/src/swagger/swagdefgen.js b/src/swagger/swagdefgen.js
new file mode 100644
index 00000000..efa43952
--- /dev/null
+++ b/src/swagger/swagdefgen.js
@@ -0,0 +1,171 @@
+/* eslint-disable no-use-before-define */
+
+let outSwagger;
+let tabCount;
+let indentator;
+const nullType = 'string';
+// ---- Functions definitions ----
+function changeIndentation(count) {
+ /*
+ Assign 'indentator' a string beginning with newline and followed by 'count' tabs
+ Updates variable 'tabCount' with the number of tabs used
+ Global variables updated:
+ -identator
+ -tabcount
+ */
+
+ let i;
+ if (count >= tabCount) {
+ i = tabCount;
+ } else {
+ i = 0;
+ indentator = '\n';
+ }
+ for (; i < count; i += 1) {
+ indentator += '\t';
+ }
+ // Update tabCount
+ tabCount = count;
+}
+
+function isFloatNumber(num) {
+ return Number(num) === num && num % 1 !== 0;
+}
+function convertNumber(num) {
+ /*
+ Append to 'outSwagger' string with Swagger schema attributes relative to given number
+ Global variables updated:
+ -outSwagger
+ */
+
+
+ if (Number.isInteger(num)) {
+ outSwagger += `${indentator}"type": "integer",`;
+ if (num < 2147483647 && num > -2147483647) {
+ outSwagger += `${indentator}"format": "int32"`;
+ } else if (Number.isSafeInteger(num)) {
+ outSwagger += `${indentator}"format": "int64"`;
+ } else {
+ outSwagger += `${indentator}"format": "unsafe"`;
+ }
+ } else if (isFloatNumber(num)) {
+ outSwagger += `${indentator}"format": "double"`;
+ } else {
+ outSwagger += `${indentator}"format": "unsafe"`;
+ }
+ outSwagger += `,${indentator}"example": "${num}"`;
+}
+
+// date is ISO8601 format - https://xml2rfc.tools.ietf.org/public/rfc/html/rfc3339.html#anchor14
+function convertString(str) {
+ /*
+ Append to 'outSwagger' string with Swagger schema attributes relative to given string
+ Global variables updated:
+ -outSwagger
+ */
+
+ const regxDate = /^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
+ const regxDateTime = /^(19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]).([0-1][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](\.[0-9]{1,2})?(Z|(\+|-)([0-1][0-9]|2[0-3]):[0-5][0-9])$/;
+
+ outSwagger += `${indentator}"type": "string"`;
+ if (regxDateTime.test(str)) {
+ outSwagger += ',';
+ outSwagger += `${indentator}"format": "date-time"`;
+ } else if (regxDate.test(str)) {
+ outSwagger += ',';
+ outSwagger += `${indentator}"format": "date"`;
+ }
+
+ outSwagger += `,${indentator}"example": "${str}"`;
+}
+
+function convertArray(obj) {
+ /*
+ Append to 'outSwagger' string with Swagger schema attributes relative to given array
+ Global variables updated:
+ -outSwagger
+ */
+
+ outSwagger += `${indentator}"type": "array",`;
+ // ---- Begin items scope ----
+ outSwagger += `${indentator}"items": {`;
+ conversorSelection(obj);
+ outSwagger += `${indentator}}`;
+ // ---- End items scope ----
+}
+
+function convertObject(obj) {
+ /*
+ Append to 'outSwagger' string with Swagger schema attributes relative to given object
+ Global variables updated:
+ -outSwagger
+ */
+
+ // Convert null attributes to given type
+ if (obj === null) {
+ outSwagger += `${indentator}"type": "${nullType}",`;
+ outSwagger += `${indentator}"format": "nullable"`;
+ return;
+ }
+ // ---- Begin properties scope ----
+ outSwagger += `${indentator}"type": "object",`;
+ outSwagger += `${indentator}"properties": {`;
+ changeIndentation(tabCount + 1);
+ // For each attribute inside that object
+ Object.keys(obj).forEach((prop) => {
+ // ---- Begin property type scope ----
+ outSwagger += `${indentator}"${prop}": {`;
+ conversorSelection(obj[prop]);
+ outSwagger += `${indentator}},`;
+ // ---- End property type scope ----
+ });
+
+
+ changeIndentation(tabCount - 1);
+ if (Object.keys(obj).length > 0) { // At least 1 property inserted
+ outSwagger = outSwagger.substring(0, outSwagger.length - 1); // Remove last comma
+ outSwagger += `${indentator}}`;
+ } else { // No property inserted
+ outSwagger += ' }';
+ }
+}
+
+function conversorSelection(obj) {
+ changeIndentation(tabCount + 1);
+ if (typeof obj === 'number') { // attribute is a number
+ convertNumber(obj);
+ } else if (Object.prototype.toString.call(obj) === '[object Array]') { // attribute is an array
+ convertArray(obj[0]);
+ } else if (typeof obj === 'object') { // attribute is an object
+ convertObject(obj);
+ } else if (typeof obj === 'string') { // attribute is a string
+ convertString(obj);
+ } else if (typeof obj === 'boolean') { // attribute is a boolean
+ outSwagger += `${indentator}"type": "boolean"`;
+ } else { // not a valid Swagger type
+ throw new Error(`Property type "${typeof obj}" not valid for Swagger definitions`);
+ }
+ changeIndentation(tabCount - 1);
+}
+
+module.exports.generateDefinitions = (json) => {
+ tabCount = 0;
+ indentator = '\n';
+ // ---- Begin definitions ----
+ outSwagger = '{"definitions": {';
+ changeIndentation(1);
+ // For each object inside the JSON
+ Object.keys(json).forEach((obj) => {
+ outSwagger += `${indentator}"${obj}": {`;
+ conversorSelection(json[obj]);
+ outSwagger += `${indentator}},`;
+ });
+
+ // Remove last comma
+ outSwagger = outSwagger.substring(0, outSwagger.length - 1);
+ // ---- End definitions ----
+ changeIndentation(tabCount - 1);
+ outSwagger += `${indentator}}}`;
+ const jsonDefinition = JSON.parse(outSwagger);
+ return jsonDefinition;
+};
diff --git a/src/swagger/swagger.js b/src/swagger/swagger.js
new file mode 100644
index 00000000..2634f14b
--- /dev/null
+++ b/src/swagger/swagger.js
@@ -0,0 +1,31 @@
+
+const swaggerUi = require('swagger-ui-express');
+const swaggerSpec = require('./swaggerspec');
+const swaggerDefGen = require('./swagdefgen');
+const { logger } = require('../logger');
+
+module.exports.generateSwagger = (server, json, config) => {
+ logger.info('init Swagger');
+ const swaggerSchemaDefinitions = swaggerDefGen.generateDefinitions(json);
+ const spec = swaggerSpec.getSpec(server, {}, config.readOnly);
+ const auth = {
+ securityDefinitions: {
+ apiKeyHeader: {
+ type: 'apiKey',
+ in: 'header',
+ name: 'x-api-key',
+ description: 'All requests must include the `x-api-key` header containing your account ID.',
+ },
+ },
+ };
+ if (config.enableApiKeyAuth) {
+ swaggerSpec.addAuthentication(spec, auth);
+ }
+ swaggerSpec.addSchemaDefitions(spec, swaggerSchemaDefinitions);
+
+ server.use('/api-spec', (req, res) => {
+ res.setHeader('Content-Type', 'application/json');
+ res.send(spec, null, 2);
+ });
+ server.use('/', swaggerUi.serve, swaggerUi.setup(spec));
+};
diff --git a/src/swagger/swaggerspec.js b/src/swagger/swaggerspec.js
new file mode 100644
index 00000000..351c5ebc
--- /dev/null
+++ b/src/swagger/swaggerspec.js
@@ -0,0 +1,326 @@
+const _ = require('lodash');
+const fs = require('fs');
+
+const listEndpoints = require('express-list-endpoints');
+
+const packageJsonPath = `${process.cwd()}/package.json`;
+let packageInfo;
+let app;
+let predefinedSpec = {};
+
+
+function updateSpecFromPackage(currentSpec) {
+ const spec = currentSpec;
+ /* eslint global-require : off */
+ // eslint-disable-next-line import/no-dynamic-require
+ packageInfo = fs.existsSync(packageJsonPath) ? JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) : {};
+
+ spec.info = spec.info || {};
+
+ if (packageInfo.name) {
+ spec.info.title = packageInfo.name;
+ }
+ if (packageInfo.version) {
+ spec.info.version = packageInfo.version;
+ }
+ if (packageInfo.license) {
+ spec.info.license = { name: packageInfo.license };
+ }
+
+ if (exports.getPackageInfo()) {
+ spec.info.description = `[Specification JSON](${exports.getPackageInfo()}/api-spec)`;
+ } else {
+ spec.info.description = '[Specification JSON](/api-spec)';
+ }
+
+ if (packageInfo.description) {
+ spec.info.description += `\n\n${packageInfo.description}`;
+ }
+
+ if (exports.getPackageInfo()) {
+ spec.basePath = exports.getPackageInfo();
+ } else {
+ spec.basePath = '';
+ }
+
+ return spec;
+}
+function sortObject(o) {
+ const sorted = {};
+ let key;
+ const a = Object.keys(o);
+ a.sort();
+ for (key = 0; key < a.length; key += 1) {
+ sorted[a[key]] = o[a[key]];
+ }
+ return sorted;
+}
+
+
+function init(readOnly) {
+ let spec = { swagger: '2.0', paths: {} };
+ const excludedRoutes = ['/api/:resource/:id/:nested', '/api/db'];
+ const endpoints = listEndpoints(app);
+ endpoints.forEach((endpoint) => {
+ if (readOnly) {
+ for (let i = 0; i < endpoint.methods.length; i += 1) {
+ if (endpoint.methods[i] !== 'GET') {
+ endpoint.methods.splice(i, 1);
+ i -= 1;
+ }
+ }
+ }
+ if (!excludedRoutes.includes(endpoint.path)) {
+ const params = [];
+ let { path } = endpoint;
+ const matches = path.match(/:([^/]+)/g);
+ if (matches) {
+ matches.forEach((found) => {
+ const paramName = found.substr(1);
+ path = path.replace(found, `{${paramName}}`);
+ params.push(paramName);
+ });
+ }
+
+ if (!spec.paths[path]) {
+ spec.paths[path] = {};
+ }
+ endpoint.methods.forEach((m) => {
+ spec.paths[path][m.toLowerCase()] = {
+ summary: path,
+ consumes: ['application/json'],
+ parameters: params.map((p) => ({
+ name: p,
+ in: 'path',
+ required: true,
+ type: 'integer',
+ })) || [],
+ responses: {},
+ };
+ });
+ }
+ });
+
+ spec = updateSpecFromPackage(spec);
+
+ spec = sortObject(_.merge(spec, predefinedSpec || {}));
+ return spec;
+}
+
+
+module.exports.getSpec = (App, PredefinedSpec, ReadOnly) => {
+ app = App;
+ predefinedSpec = PredefinedSpec;
+ const spec = init(ReadOnly);
+ return spec;
+};
+
+
+module.exports.getPackageInfo = () => (process.env.basePath ? process.env.basePath : '');
+function setSchemaReference(spec, definition) {
+ let schemaDef = null;
+ if (spec.definitions[definition].type === 'array') {
+ schemaDef = { $ref: `#/definitions/${definition}/items` };
+ } else if (spec.definitions[definition].type === 'object') {
+ schemaDef = { $ref: `#/definitions/${definition}` };
+ }
+ return schemaDef;
+}
+
+function getDefaultParameterSchema(schemaDef, definition) {
+ return {
+ schema: schemaDef,
+ in: 'body',
+ name: 'body',
+ description: definition,
+ required: true,
+ };
+}
+
+function getQueryParameterSchema() {
+ return [{
+ name: '_page',
+ in: 'query',
+ required: false,
+ type: 'integer',
+ description: 'parameter to return paginated data',
+ },
+ {
+ name: '_limit',
+ in: 'query',
+ required: false,
+ type: 'integer',
+ description: 'parameter to limit paginated data',
+ },
+ {
+ name: '_sort',
+ in: 'query',
+ required: false,
+ type: 'string',
+ description: 'sort by attributes',
+ },
+ {
+ name: '_order',
+ in: 'query',
+ required: false,
+ type: 'string',
+ description: 'order ascending or descending',
+ enum: ['asc', 'desc'],
+ },
+ {
+ name: '_start',
+ in: 'query',
+ required: false,
+ type: 'integer',
+ description: 'parameter to set start sliced data',
+ },
+ {
+ name: '_end',
+ in: 'query',
+ required: false,
+ type: 'integer',
+ description: 'parameter to set start sliced data',
+ },
+ {
+ name: 'q',
+ in: 'query',
+ required: false,
+ type: 'string',
+ description: 'full text search',
+ },
+ {
+ name: '_embed',
+ in: 'query',
+ required: false,
+ type: 'string',
+ description: 'include children resources',
+ },
+ {
+ name: '_expand',
+ in: 'query',
+ required: false,
+ type: 'string',
+ description: 'include parent resource',
+ },
+ ];
+}
+
+
+function getDefaultPostResponses(definition) {
+ return {
+ responses: {
+ 200: {
+ description: 'successful operation',
+ schema: {
+ $ref: `#/definitions/${definition}`,
+ },
+ },
+ 400: {
+ description: `Invalid ${definition}`,
+ },
+ },
+ };
+}
+
+function getDefaultPutResponses(definition) {
+ return {
+ responses: {
+ 400: {
+ description: 'Invalid ID supplied',
+ },
+ 404: {
+ description: `${definition} not found`,
+ },
+ 405: {
+ description: 'Validation exception',
+ },
+ },
+ };
+}
+
+function getDefaultDeleteResponses(definition) {
+ return {
+ responses: {
+ 400: {
+ description: 'Invalid ID supplied',
+ },
+ 404: {
+ description: `${definition} not found`,
+ },
+ },
+ };
+}
+
+
+function getDefaultSchemaProperties(definition) {
+ return {
+ produces: ['application/json'],
+ tags: [
+ definition,
+ ],
+ };
+}
+
+module.exports.addSchemaDefitions = (Spec, SchemaDefinitons) => {
+ const spec = Object.assign(Spec, SchemaDefinitons);
+ Object.keys(spec.paths).forEach((path) => {
+ Object.keys(spec.definitions).forEach((definition) => {
+ const schemaDef = setSchemaReference(spec, definition);
+ if (path.endsWith(definition)) {
+ if (spec.paths[path].get) {
+ Object.assign(spec.paths[path].get, (getDefaultSchemaProperties(definition)));
+ spec.paths[path].get.responses[200] = { schema: { $ref: `#/definitions/${definition}` }, description: 'successful operation' };
+ spec.paths[path].get.parameters = getQueryParameterSchema();
+ }
+ if (spec.paths[path].post) {
+ Object.assign(spec.paths[path].post, (getDefaultSchemaProperties(definition)));
+ Object.assign(spec.paths[path].post, (getDefaultPostResponses(definition)));
+ spec.paths[path].post.parameters.push(getDefaultParameterSchema(schemaDef, definition));
+ }
+ if (spec.paths[path].put) {
+ Object.assign(spec.paths[path].put, (getDefaultSchemaProperties(definition)));
+ Object.assign(spec.paths[path].put, (getDefaultPutResponses(definition)));
+ spec.paths[path].put.parameters.push(getDefaultParameterSchema(schemaDef, definition));
+ }
+ if (spec.paths[path].patch) {
+ Object.assign(spec.paths[path].patch, (getDefaultSchemaProperties(definition)));
+ Object.assign(spec.paths[path].patch, (getDefaultPutResponses(definition)));
+ spec.paths[path].patch.parameters.push(getDefaultParameterSchema(schemaDef, definition));
+ }
+ }
+ if (path.endsWith(`${definition}/{id}`)) {
+ if (spec.paths[path].get) {
+ Object.assign(spec.paths[path].get, (getDefaultSchemaProperties(definition)));
+ spec.paths[path].get.responses[200] = { schema: { $ref: `#/definitions/${definition}` }, description: 'successful operation' };
+ }
+ if (spec.paths[path].delete) {
+ Object.assign(spec.paths[path].delete, (getDefaultSchemaProperties(definition)));
+ Object.assign(spec.paths[path].delete, (getDefaultDeleteResponses(definition)));
+ }
+
+ if (spec.paths[path].put) {
+ Object.assign(spec.paths[path].put, (getDefaultSchemaProperties(definition)));
+ Object.assign(spec.paths[path].put, (getDefaultPutResponses(definition)));
+ spec.paths[path].put.parameters.push(getDefaultParameterSchema(schemaDef, definition));
+ }
+ if (spec.paths[path].patch) {
+ Object.assign(spec.paths[path].patch, (getDefaultSchemaProperties(definition)));
+ Object.assign(spec.paths[path].patch, (getDefaultPutResponses(definition)));
+ spec.paths[path].patch.parameters.push(getDefaultParameterSchema(schemaDef, definition));
+ }
+ }
+ });
+ });
+ return spec;
+};
+
+module.exports.addAuthentication = (Spec, Auth) => {
+ const spec = Object.assign(Spec, Auth);
+ spec.security = [];
+ Object.keys(spec.securityDefinitions).forEach((sec) => {
+ const obj = {};
+ obj[sec] = [];
+ spec.security.push(obj);
+ });
+ return spec;
+};
diff --git a/src/utils.js b/src/utils.js
index 6fdba4bd..526ae0c5 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,32 +1,46 @@
+const express = require('express');
const fs = require('fs');
const low = require('lowdb');
const AwsAdapter = require('lowdb-adapter-aws-s3');
const jsonServer = require('json-server');
+const { logger } = require('./logger');
+const swagger = require('./swagger/swagger');
+
const defaultDB = JSON.parse(fs.readFileSync('./db.json', 'UTF-8'));
-const logger = require('pino')({
- prettyPrint: true,
-}, process.stderr);
+const appConfig = JSON.parse(fs.readFileSync('./config/appconfig.json', 'UTF-8'));
+
+const server = express();
-const server = jsonServer.create();
let storage = null;
-function startLocal() {
- logger.info('start local environment');
- const router = jsonServer.router('db.json');
- const middlewares = jsonServer.defaults();
+function setupServer(middlewares, router) {
+ if (appConfig.enableSwagger) {
+ middlewares.splice(middlewares.findIndex((x) => x.name === 'serveStatic'), 1);
+ }
server.use(middlewares);
- server.use(router);
+ server.use('/api', router);
+ if (appConfig.enableSwagger) {
+ swagger.generateSwagger(server, defaultDB, appConfig);
+ }
+}
+
+function startLocal() {
+ logger.info('start locals environment');
+ const router = jsonServer.router('db.json');
+ const middlewares = jsonServer.defaults({ readOnly: appConfig.readOnly });
+ setupServer(middlewares, router);
}
function startInCloud() {
- logger.info(`S3FILE: ${process.env.S3FILE}`);
- logger.info(`S3BUCKET: ${process.env.S3BUCKET}`);
- logger.info(`READONLY: ${process.env.READONLY}`);
- storage = new AwsAdapter(process.env.S3FILE, {
+ logger.info(`S3File: ${process.env.S3File}`);
+ logger.info(`S3Bucket: ${process.env.S3Bucket}`);
+ logger.info(`readOnly: ${appConfig.readOnly}`);
+ logger.info(`basePath: ${process.env.basePath}`);
+ storage = new AwsAdapter(process.env.S3File, {
defaultValue: defaultDB,
- aws: { bucketName: process.env.S3BUCKET },
+ aws: { bucketName: process.env.S3Bucket },
});
}
@@ -34,9 +48,8 @@ const request = async () => {
try {
const adapter = await low(storage);
const router = jsonServer.router(adapter);
- const middlewares = jsonServer.defaults({ readOnly: process.env.READONLY === 'true' });
- server.use(middlewares);
- server.use(router);
+ const middlewares = jsonServer.defaults({ readOnly: appConfig.readOnly });
+ setupServer(middlewares, router);
} catch (e) {
if (e.code === 'ExpiredToken') {
logger.error(`Please add valid credentials for AWS. Error: ${e.message}`);
diff --git a/tests/test.js b/tests/test.js
index a3257f93..34bc216c 100644
--- a/tests/test.js
+++ b/tests/test.js
@@ -10,9 +10,8 @@ describe('Test the root path', () => {
describe('Test the root path', () => {
test('It should return a default object', async () => {
- const response = await request(app.server).get('/posts');
+ const response = await request(app.server).get('/api/posts');
expect(response.statusCode).toBe(200);
- console.log(JSON.stringify(response.body));
expect(response.body[0].title).toBe('json-server');
});
});
diff --git a/webpack.config.js b/webpack.config.js
index 3c949008..ac4ed816 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -11,6 +11,7 @@ module.exports = {
plugins: [
new CopyPlugin([
{ from: './db.json', to: './db.json' },
+ { from: './config/appconfig.json', to: './config/appconfig.json' },
]),
new NodeEnvPlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
diff --git a/webpack.config.prod.js b/webpack.config.prod.js
index eee01f06..5fb4f3d0 100644
--- a/webpack.config.prod.js
+++ b/webpack.config.prod.js
@@ -1,18 +1,19 @@
-
const nodeExternals = require('webpack-node-externals');
const CopyPlugin = require('copy-webpack-plugin');
-const NodeEnvPlugin = require('node-env-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
+const webpack = require('webpack');
module.exports = {
mode: 'production',
plugins: [
+ new webpack.EnvironmentPlugin({
+ NODE_ENV: 'production', // use 'development' unless process.env.NODE_ENV is defined
+ DEBUG: false,
+ }),
new CopyPlugin([
{ from: './db.json', to: './db.json' },
+ { from: './config/appconfig.json', to: './config/appconfig.json' },
]),
- new NodeEnvPlugin({
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
- }),
],
entry: { 'src/handler': './src/handler.js' },
optimization: {