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 + +![Architecture](docs/json-serverless.png) + ## 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
**Mind:** This function requires that you have access to AWS (e.g. via credentials) @@ -224,9 +242,9 @@ cp .env.sample .env | 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 | +| 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 | #### 2. Start solution @@ -236,13 +254,28 @@ npm run dev #### 3. Test your API -To test you can use e.g. [Postman](https://www.getpostman.com/) +#### With Swagger + +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) -- Open Postman -- Enter as Url the endpoints url +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} + +# 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} + ``` What`s my {route} ? -> see [json-server documentation](https://github.com/typicode/json-server) @@ -254,7 +287,7 @@ serverless-offline will help you to troubleshoot issues with the lambda executio **Mind:** The assumption is that the solution has been already deployed
**Mind:** This function requires that you have access to AWS (e.g. via credentials) -#### 1. build sources and execute serverless offline +### 1. build sources and execute serverless offline - sources will be build with babel in advance to test the functionality. - after that sls offline will be started @@ -276,5 +309,39 @@ Serverless: Remember to use x-api-key on the request headers - Replace {route} with the route you want to test e.g. /posts

-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} + +![ModHeader](docs/header.png) + +### 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: {