Started Swagger

This commit is contained in:
fkereki 2018-06-04 23:12:32 -04:00
parent 81e49fc35f
commit b2a06951c7
12 changed files with 1802 additions and 17 deletions

View File

@ -34,7 +34,7 @@ app.post("/gettoken", (req, res) => {
app.use((req, res, next) => {
// First check for the Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
if (!authHeader || !authHeader.startsWith("Bearer")) {
return res.status(401).send("No token specified");
}

View File

@ -75,9 +75,8 @@ const ca = fs.readFileSync(`${keysPath}/modernjsbook.csr`);
const cert = fs.readFileSync(`${keysPath}/modernjsbook.crt`);
const key = fs.readFileSync(`${keysPath}/modernjsbook.key`);
https.createServer({ ca, cert, key }, app).listen(8443);
https.createServer({ ca, cert, key }, app);
and remove the following line if HTTPS is used
*/
app.listen(8080, () =>
console.log("Routing ready at http://localhost:8080")

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,7 @@
"main": "index.js",
"scripts": {
"build": "flow-remove-types src/ -d out/",
"buildWithMaps":
"flow-remove-types src/ -d out/ --pretty --sourcemaps",
"buildWithMaps": "flow-remove-types src/ -d out/ --pretty --sourcemaps",
"start": "npm run build && node out/doroundmath.js",
"start-db": "npm run build && node out/dbaccess.js",
"nodemon": "nodemon --watch src --delay 1 --exec npm start",
@ -16,12 +15,16 @@
"flow-coverage": "flow-coverage-report",
"eslint": "eslint src",
"testNoCoverage": "jest out/",
"test": "jest out/ --coverage --no-cache"
"test": "jest out/ --coverage --no-cache",
"newman": "newman run postman_collection.json"
},
"author": "Federico Kereki",
"license": "ISC",
"babel": {
"presets": ["env", "flow"]
"presets": [
"env",
"flow"
]
},
"eslintConfig": {
"parserOptions": {
@ -34,8 +37,14 @@
"es6": true,
"jest": true
},
"extends": ["eslint:recommended", "plugin:flowtype/recommended"],
"plugins": ["babel", "flowtype"],
"extends": [
"eslint:recommended",
"plugin:flowtype/recommended"
],
"plugins": [
"babel",
"flowtype"
],
"rules": {
"no-console": "off",
"no-var": "error",
@ -43,13 +52,23 @@
"flowtype/no-types-missing-file-annotation": 0
}
},
"eslintIgnore": ["**/out/*.js"],
"eslintIgnore": [
"**/out/*.js"
],
"flow-coverage-report": {
"concurrentFiles": 1,
"excludeGlob": ["node_modules/**"],
"includeGlob": ["src/**/*.js"],
"excludeGlob": [
"node_modules/**"
],
"includeGlob": [
"src/**/*.js"
],
"threshold": 90,
"type": ["text", "html", "json"]
"type": [
"text",
"html",
"json"
]
},
"prettier": {
"tabWidth": 4,
@ -65,6 +84,7 @@
"mariasql": "^0.2.6",
"morgan": "^1.9.0",
"node-mocks-http": "^1.7.0",
"swagger-ui-express": "^3.0.9",
"winston": "^3.0.0-rc6"
},
"devDependencies": {
@ -80,6 +100,7 @@
"flow-remove-types": "^1.2.3",
"flow-typed": "^2.4.0",
"jest": "^23.1.0",
"newman": "^3.9.4",
"nodemon": "^1.17.5",
"prettier": "^1.13.3"
}

View File

@ -0,0 +1,311 @@
{
"info": {
"_postman_id": "056f86ef-903d-4d01-9f6c-9d83998db7c2",
"name": "Restful server testing for regions",
"description": "This test is for /regions ",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Test Delete",
"description": "",
"item": [
{
"name": "Get JWT",
"event": [
{
"listen": "test",
"script": {
"id": "f5ba5984-536c-461b-8ac9-53369abd1386",
"type": "text/javascript",
"exec": [
"pm.test(\"Response is long enough\", () => ",
" pm.expect(pm.response.text()).to.have.lengthOf.above(40)); ",
" ",
"pm.test(\"Response has three parts\", () => ",
" pm.expect(pm.response.text().split(\".\")).to.have.lengthOf(3));",
" ",
"pm.environment.set(\"token\", responseBody); // for later scripts",
""
]
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/x-www-form-urlencoded"
}
],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "user",
"value": "fkereki",
"description": "",
"type": "text"
},
{
"key": "password",
"value": "modernjsbook",
"description": "",
"type": "text"
}
]
},
"url": {
"raw": "localhost:8443/gettoken",
"host": [
"localhost"
],
"port": "8443",
"path": [
"gettoken"
]
},
"description": "The HTTP `GET` request method is meant to retrieve data from a server. The data\nis identified by a unique URI (Uniform Resource Identifier). \n\nA `GET` request can pass parameters to the server using \"Query String \nParameters\". For example, in the following request,\n\n> http://example.com/hi/there?hand=wave\n\nThe parameter \"hand\" has the value \"wave\".\n\nThis endpoint echoes the HTTP headers, request parameters and the complete\nURI requested."
},
"response": []
},
{
"name": "Delete non-existing region",
"event": [
{
"listen": "test",
"script": {
"id": "706fe7f8-7417-48a9-b03d-0677eee4aad9",
"type": "text/javascript",
"exec": [
"pm.test(\"Status code is 404 baby!!\", () => ",
" pm.response.to.have.status(404));"
]
}
}
],
"request": {
"method": "DELETE",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
}
],
"body": {},
"url": {
"raw": "localhost:8443/regions/zz/99",
"host": [
"localhost"
],
"port": "8443",
"path": [
"regions",
"zz",
"99"
]
}
},
"response": []
}
]
},
{
"name": "Test Get",
"description": "",
"item": [
{
"name": "Get JWT",
"event": [
{
"listen": "test",
"script": {
"id": "f5ba5984-536c-461b-8ac9-53369abd1386",
"type": "text/javascript",
"exec": [
"pm.test(\"Response is long enough\", () => ",
" pm.expect(pm.response.text()).to.have.lengthOf.above(40)); ",
" ",
"pm.test(\"Response has three parts\", () => ",
" pm.expect(pm.response.text().split(\".\")).to.have.lengthOf(3));",
" ",
"pm.environment.set(\"token\", responseBody); // for later scripts",
""
]
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/x-www-form-urlencoded"
}
],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "user",
"value": "fkereki",
"description": "",
"type": "text"
},
{
"key": "password",
"value": "modernjsbook",
"description": "",
"type": "text"
}
]
},
"url": {
"raw": "localhost:8443/gettoken",
"host": [
"localhost"
],
"port": "8443",
"path": [
"gettoken"
]
},
"description": "The HTTP `GET` request method is meant to retrieve data from a server. The data\nis identified by a unique URI (Uniform Resource Identifier). \n\nA `GET` request can pass parameters to the server using \"Query String \nParameters\". For example, in the following request,\n\n> http://example.com/hi/there?hand=wave\n\nThe parameter \"hand\" has the value \"wave\".\n\nThis endpoint echoes the HTTP headers, request parameters and the complete\nURI requested."
},
"response": []
},
{
"name": "Get /regions/uy",
"event": [
{
"listen": "test",
"script": {
"id": "4a0fe82d-a640-42c1-b451-f7e6a5e3705e",
"type": "text/javascript",
"exec": [
"pm.test(\"Answer should be JSON\", () => {",
" pm.response.to.be.success;",
" pm.response.to.have.jsonBody(); ",
"});",
" ",
"pm.test(\"Answer should have at least 19 regions\", () => {",
" const regions = JSON.parse(pm.response.text());",
" pm.expect(regions).to.have.lengthOf.at.least(19);",
"});",
""
]
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
},
{
"key": "",
"value": ""
}
],
"body": {},
"url": {
"raw": "localhost:8443/regions/uy",
"host": [
"localhost"
],
"port": "8443",
"path": [
"regions",
"uy"
]
},
"description": "Attempt getting all regions of UY\n"
},
"response": []
},
{
"name": "Get /regions/uy/10",
"event": [
{
"listen": "test",
"script": {
"id": "c8a43643-d266-4e1a-9576-7647a544b796",
"type": "text/javascript",
"exec": [
"pm.test(\"Answer is valid, JSON\", function () {",
" pm.response.to.be.success;",
" pm.response.to.have.jsonBody(); ",
" ",
" const jsonData = pm.response.json();",
" ",
" pm.test(\"Answer has a single region\", ",
" () => pm.expect(jsonData).to.have.lengthOf(1));",
" ",
" pm.test(\"Country code is UY\", ",
" () => pm.expect(jsonData[0].countryCode).to.equal(\"UY\"));",
"",
" pm.test(\"Region code is 11\", ",
" () => pm.expect(jsonData[0].regionCode).to.equal(\"11\"));",
"",
" pm.test(\"Region name is Paysandu\", ",
" () => pm.expect(jsonData[0].regionName).to.equal(\"Paysandu\"));",
"});",
""
]
}
}
],
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
}
],
"body": {},
"url": {
"raw": "localhost:8443/regions/uy/11",
"host": [
"localhost"
],
"port": "8443",
"path": [
"regions",
"uy",
"11"
]
},
"description": "Attempt getting a single region of UY\n"
},
"response": []
}
]
}
],
"event": [
{
"listen": "prerequest",
"script": {
"id": "bd5384f0-4651-4701-a733-e4e2c12aba7a",
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"id": "4af046c3-048a-4a41-b070-32481a04d094",
"type": "text/javascript",
"exec": [
""
]
}
}
]
}

138
chapter05/restful.json Normal file
View File

@ -0,0 +1,138 @@
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore",
"license": {
"name": "MIT"
}
},
"host": "petstore.swagger.io",
"basePath": "/v1",
"schemes": ["http"],
"consumes": ["application/json"],
"produces": ["application/json"],
"paths": {
"/pets": {
"get": {
"summary": "List all pets",
"operationId": "listPets",
"tags": ["pets"],
"parameters": [
{
"name": "limit",
"in": "query",
"description":
"How many items to return at one time (max 100)",
"required": false,
"type": "integer",
"format": "int32"
}
],
"responses": {
"200": {
"description": "An paged array of pets",
"headers": {
"x-next": {
"type": "string",
"description":
"A link to the next page of responses"
}
},
"schema": {
"$ref": "#/definitions/Pets"
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
},
"post": {
"summary": "Create a pet",
"operationId": "createPets",
"tags": ["pets"],
"responses": {
"201": {
"description": "Null response"
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
},
"/pets/{petId}": {
"get": {
"summary": "Info for a specific pet",
"operationId": "showPetById",
"tags": ["pets"],
"parameters": [
{
"name": "petId",
"in": "path",
"required": true,
"description": "The id of the pet to retrieve",
"type": "string"
}
],
"responses": {
"200": {
"description":
"Expected response to a valid request",
"schema": {
"$ref": "#/definitions/Pets"
}
},
"default": {
"description": "unexpected error",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
}
},
"definitions": {
"Pet": {
"required": ["id", "name"],
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"name": {
"type": "string"
},
"tag": {
"type": "string"
}
}
},
"Pets": {
"type": "array",
"items": {
"$ref": "#/definitions/Pet"
}
},
"Error": {
"required": ["code", "message"],
"properties": {
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}

View File

@ -0,0 +1,20 @@
/* @flow */
"use strict";
const mariaSQL = require("mariasql");
const { promisify } = require("util");
const DB_HOST = "127.0.0.1";
const DB_USER = "fkereki";
const DB_PASS = "modernJS!!";
const DB_SCHEMA = "world";
const getDbConnection = (host, user, password, db) => {
const dbConn = new mariaSQL({ host, user, password, db });
dbConn.query = promisify(dbConn.query);
return dbConn;
};
const dbConn = getDbConnection(DB_HOST, DB_USER, DB_PASS, DB_SCHEMA);
module.exports = dbConn;

View File

@ -48,10 +48,11 @@ const getRegion = async (
`;
}
res.set("Connection", "close");
const regions = await dbConn.query(sqlQuery);
if (regions.length > 0 || region === null) {
res
.status(200)
res.status(200)
.set("Content-Type", "application/json")
.send(JSON.stringify(regions));
} else {
@ -69,6 +70,8 @@ const deleteRegion = async (
region: string
) => {
try {
res.set("Connection", "close");
const sqlCities = `
SELECT 1 FROM cities
WHERE countryCode="${country}"
@ -103,6 +106,8 @@ const postRegion = async (
country: string,
name: string
) => {
res.set("Connection", "close");
if (!name) {
return res.status(400).send("Missing name");
}
@ -136,8 +141,7 @@ const postRegion = async (
const result = await dbConn.query(sqlAddRegion);
if (result.info.affectedRows > 0) {
res
.status(201)
res.status(201)
.header("Location", `/regions/${country}/${newId}`)
.send("Region created");
} else {
@ -155,6 +159,8 @@ const putRegion = async (
region: string,
name: string
) => {
res.set("Connection", "close");
if (!name) {
return res.status(400).send("Missing name");
}

View File

@ -0,0 +1,125 @@
/* @flow */
"use strict";
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express();
const bodyParser = require("body-parser");
const cors = require("cors");
const swaggerUi = require("swagger-ui-express");
const swaggerDocument = require("../restful.json");
const dbConn = require("./restful_db.js");
const validateUser = require("./validate_user.js");
const SECRET_JWT_KEY = "modernJSbook";
const https = require("https");
const fs = require("fs");
const path = require("path");
const keysPath = path.join(__dirname, "../../certificates");
const ca = fs.readFileSync(`${keysPath}/modernjsbook.csr`);
const cert = fs.readFileSync(`${keysPath}/modernjsbook.crt`);
const key = fs.readFileSync(`${keysPath}/modernjsbook.key`);
https.createServer({ ca, cert, key }, app);
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerDocument));
app.get("/", (req, res) =>
res
.status(200)
.set("Connection", "close")
.send("Ready")
);
app.post("/gettoken", (req, res) => {
validateUser(req.body.user, req.body.password, (idErr, userid) => {
res.set("Connection", "close");
if (idErr !== null) {
res.status(401).send(idErr);
} else {
jwt.sign(
{ userid },
SECRET_JWT_KEY,
{ algorithm: "HS256", expiresIn: "1h" },
(err, token) => res.status(200).send(token)
);
}
});
});
app.use((req, res, next) => {
res.set("Connection", "close");
// First check for the Authorization header
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer")) {
return res.status(401).send("No token specified");
}
// Now validate the token itself
const token = authHeader.split(" ")[1];
jwt.verify(token, SECRET_JWT_KEY, (err, decoded) => {
if (err) {
// Token bad formed, or expired, or other problem
return res.status(403).send("Token expired or not valid");
} else {
// Token OK; get the user id from it
req.userid = decoded.userid;
// Keep processing the request
next();
}
});
});
const {
getRegion,
deleteRegion,
postRegion,
putRegion
} = require("./restful_regions.js");
app.get("/regions/", (req, res) => getRegion(res, dbConn));
app.get("/regions/:country/", (req, res) =>
getRegion(res, dbConn, req.params.country)
);
app.get("/regions/:country/:region/", (req, res) =>
getRegion(res, dbConn, req.params.country, req.params.region)
);
app.delete("/regions/:country/:region", (req, res) =>
deleteRegion(res, dbConn, req.params.country, req.params.region)
);
app.post("/regions/:country", (req, res) =>
postRegion(res, dbConn, req.params.country, req.body.name)
);
app.put("/regions/:country/:region", (req, res) =>
putRegion(
res,
dbConn,
req.params.country,
req.params.region,
req.body.name
)
);
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
console.error("Error....", err.message);
res.status(500).send("INTERNAL SERVER ERROR");
});
app.listen(8443, () =>
console.log("Server ready at http://localhost:8443")
);

View File

@ -0,0 +1,212 @@
openapi: "3.0.0"
info:
title: World Data RESTful API
description: "This is a RESTful API to access world data, including countries, regions, and cities."
version: "0.0.1"
servers:
- url: http://127.0.0.1:8443
tags:
- name: "token"
description: "Get a JWT for authorization"
- name: "countries"
description: "Access the world countries"
- name: "regions"
description: "Access the regions of countries"
- name: "cities"
description: "Access the world cities"
paths:
/gettoken:
post:
tags:
- "token"
summary: "Get a token to authorize future requests"
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
properties:
user:
type: string
password:
type: string
required:
- user
- password
responses:
200:
description: A valid token to use for other requests
401:
description: "No token provided"
404:
description: "Country not found"
/regions:
get:
tags:
- "regions"
summary: "Get all regions of all countries"
produces:
- application/json
responses:
200:
description: "OK"
401:
description: "No token provided"
post:
tags:
- "regions"
summary: "Add a region to a given country"
consumes:
- "application/x-www-form-urlencoded"
produces:
- text/plain
parameters:
- in: formData
name: name
required: true
type: string
description: The new region's name.
- in: header
name: "Authorization: Bearer"
required: true
type: string
description: Authorization Token
responses:
201:
description: "OK, created"
400:
description: "Name missing, region not created"
401:
description: "No token provided"
403:
description: "Country not found, region not created"
409:
description: "Other failure, region not created"
/regions/{country}:
get:
tags:
- "regions"
summary: "Get all regions of a given country"
produces:
- application/json
parameters:
- name: country
in: path
description: "Country (id) whose regions are required"
type: string
required: true
responses:
200:
description: "OK"
401:
description: "No token provided"
404:
description: "Country not found"
/regions/{country}/{id}:
get:
tags:
- "regions"
summary: "Get a specific region of a given country"
produces:
- application/json
parameters:
- name: country
in: path
description: "Country (id) of the region"
type: string
required: true
- name: id
in: path
description: "Region (id) that is required"
type: string
required: true
responses:
200:
description: "OK"
401:
description: "No token provided"
404:
description: "Country not found"
delete:
tags:
- "regions"
summary: "Delete a specific region of a given country"
description: ""
parameters:
- name: country
in: path
description: "Country (id) of the region"
type: string
required: true
- name: id
in: path
description: "Region (id) that is to be deleted"
type: string
required: true
responses:
204:
description: "OK, region was deleted"
401:
description: "No token provided"
404:
description: "Region does not exist"
405:
description: "Region has cities, cannot be deleted"
put:
tags:
- "regions"
summary: "Update a specific region of a given country"
consumes:
- "application/x-www-form-urlencoded"
produces:
- text/plain
parameters:
- name: country
in: path
description: "Country (id) of the region"
type: string
required: true
- name: id
in: path
description: "Region (id) that is to be deleted"
type: string
required: true
- in: formData
name: name
required: true
type: string
description: The region's new name.
responses:
204:
description: "OK, region was updated"
400:
description: "Name missing, region not updated"
401:
description: "No token provided"
404:
description: "Country not found"
components:
securitySchemes:
JwtAuth:
type: apiKey
in: header
name: Authorization
bearerAuth: # arbitrary name for the security scheme
type: apiKey
in: header
scheme: bearer
bearerFormat: JWT

214
chapter05/swagger.yaml Normal file
View File

@ -0,0 +1,214 @@
openapi: "2.0.0"
info:
description: "This is a RESTful API to access world data, including countries, regions, and cities."
version: "1.0.0"
title: "World Data API"
host: "localhost:8443"
tags:
- name: "token"
description: "Get a JWT for authorization"
- name: "countries"
description: "Access the world countries"
- name: "regions"
description: "Access the regions of countries"
- name: "cities"
description: "Access the world cities"
schemes:
- "http"
paths:
/gettoken:
post:
tags:
- "token"
summary: "Get a token to authorize future requests"
consumes:
- "application/x-www-form-urlencoded"
produces:
- text/plain
parameters:
- in: formData
name: user
required: true
type: string
description: A person's name.
- in: formData
name: password
required: true
type: string
description: A person's favorite number.
responses:
200:
description: A valid token to use for other requests
401:
description: "No token provided"
404:
description: "Country not found"
/regions:
get:
tags:
- "regions"
summary: "Get all regions of all countries"
produces:
- application/json
responses:
200:
description: "OK"
401:
description: "No token provided"
post:
tags:
- "regions"
summary: "Add a region to a given country"
consumes:
- "application/x-www-form-urlencoded"
produces:
- text/plain
parameters:
- in: formData
name: name
required: true
type: string
description: The new region's name.
- in: header
name: "Authorization: Bearer"
required: true
type: string
description: Authorization Token
responses:
201:
description: "OK, created"
400:
description: "Name missing, region not created"
401:
description: "No token provided"
403:
description: "Country not found, region not created"
409:
description: "Other failure, region not created"
/regions/{country}:
get:
tags:
- "regions"
summary: "Get all regions of a given country"
produces:
- application/json
parameters:
- name: country
in: path
description: "Country (id) whose regions are required"
type: string
required: true
responses:
200:
description: "OK"
401:
description: "No token provided"
404:
description: "Country not found"
/regions/{country}/{id}:
get:
tags:
- "regions"
summary: "Get a specific region of a given country"
produces:
- application/json
parameters:
- name: country
in: path
description: "Country (id) of the region"
type: string
required: true
- name: id
in: path
description: "Region (id) that is required"
type: string
required: true
responses:
200:
description: "OK"
401:
description: "No token provided"
404:
description: "Country not found"
delete:
tags:
- "regions"
summary: "Delete a specific region of a given country"
description: ""
parameters:
- name: country
in: path
description: "Country (id) of the region"
type: string
required: true
- name: id
in: path
description: "Region (id) that is to be deleted"
type: string
required: true
responses:
204:
description: "OK, region was deleted"
401:
description: "No token provided"
404:
description: "Region does not exist"
405:
description: "Region has cities, cannot be deleted"
put:
tags:
- "regions"
summary: "Update a specific region of a given country"
consumes:
- "application/x-www-form-urlencoded"
produces:
- text/plain
parameters:
- name: country
in: path
description: "Country (id) of the region"
type: string
required: true
- name: id
in: path
description: "Region (id) that is to be deleted"
type: string
required: true
- in: formData
name: name
required: true
type: string
description: The region's new name.
responses:
204:
description: "OK, region was updated"
400:
description: "Name missing, region not updated"
401:
description: "No token provided"
404:
description: "Country not found"
components:
securitySchemes:
JwtAuth:
type: apiKey
in: header
name: Authorization
bearerAuth: # arbitrary name for the security scheme
type: apiKey
in: header
scheme: bearer
bearerFormat: JWT

3
package-lock.json generated Normal file
View File

@ -0,0 +1,3 @@
{
"lockfileVersion": 1
}