Nodejs express js packages update (#5675)

* Updated to new nodejs packages, depending heavily on express-openapi-validator. Requires quite a change in code.
Updated the business-logic in the controllers/Controller.js file.
Logger now records also timestamp of events.
Files are uploaded according to definition in config.js file

* Removed commented-out code; Changed openApi document extensions to suit new express-openapi-validator definition; multipart and file uploading is supported now; Automatic response returns the values the were sent in the request

* fixed README documentation, fixed a mistage in package.json/mustache

* added generated files that were created when running the ./bin/test file
This commit is contained in:
YishTish
2020-03-24 11:37:18 +02:00
committed by GitHub
parent eac18a779d
commit 9d96ab0983
27 changed files with 1080 additions and 1688 deletions

View File

@@ -269,7 +269,7 @@ public class NodeJSExpressServerCodegen extends DefaultCodegen implements Codege
@SuppressWarnings("unchecked")
private static List<Map<String, Object>> getOperations(Map<String, Object> objs) {
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
List<Map<String, Object>> result = new ArrayList<>();
Map<String, Object> apiInfo = (Map<String, Object>) objs.get("apiInfo");
List<Map<String, Object>> apis = (List<Map<String, Object>>) apiInfo.get("apis");
for (Map<String, Object> api : apis) {
@@ -285,9 +285,9 @@ public class NodeJSExpressServerCodegen extends DefaultCodegen implements Codege
opsByPath.put(op.path, op);
}
List<Map<String, Object>> opsByPathList = new ArrayList<Map<String, Object>>();
List<Map<String, Object>> opsByPathList = new ArrayList<>();
for (Entry<String, Collection<CodegenOperation>> entry : opsByPath.asMap().entrySet()) {
Map<String, Object> opsByPathEntry = new HashMap<String, Object>();
Map<String, Object> opsByPathEntry = new HashMap<>();
opsByPathList.add(opsByPathEntry);
opsByPathEntry.put("path", entry.getKey());
opsByPathEntry.put("operation", entry.getValue());
@@ -369,14 +369,18 @@ public class NodeJSExpressServerCodegen extends DefaultCodegen implements Codege
operation.setOperationId(getOrGenerateOperationId(operation, pathname, method.toString()));
}
// add x-openapi-router-controller
// if (operation.getExtensions() == null ||
// operation.getExtensions().get("x-openapi-router-controller") == null) {
// operation.addExtension("x-openapi-router-controller", sanitizeTag(tag) + "Controller");
// }
// // add x-openapi-router-service
// if (operation.getExtensions() == null ||
// operation.getExtensions().get("x-openapi-router-service") == null) {
// operation.addExtension("x-openapi-router-service", sanitizeTag(tag) + "Service");
// }
if (operation.getExtensions() == null ||
operation.getExtensions().get("x-openapi-router-controller") == null) {
operation.addExtension("x-openapi-router-controller", sanitizeTag(tag) + "Controller");
}
// add x-openapi-router-service
if (operation.getExtensions() == null ||
operation.getExtensions().get("x-openapi-router-service") == null) {
operation.addExtension("x-openapi-router-service", sanitizeTag(tag) + "Service");
operation.getExtensions().get("x-eov-operation-handler") == null) {
operation.addExtension("x-eov-operation-handler", "controllers/" + sanitizeTag(tag) + "Controller");
}
}
}

View File

@@ -1,37 +1,58 @@
{{=<% %>=}}
# OpenAPI Generated JavaScript/Express Server
## Overview
This server was generated using the [OpenAPI Generator](https://openapi-generator.tech) project. The code generator, and it's generated code allows you to develop your system with an API-First attitude, where the API contract is the anchor and definer of your project, and your code and business-logic aims to complete and comply to the terms in the API contract.
### prerequisites
- NodeJS >= 10.4
- NodeJS >= 10.6
- NPM >= 6.10.0
The code was written on a mac, so assuming all should work smoothly on Linux-based computers. However, there is no reason not to run this library on Windows-based machines. If you find an OS-related problem, please open an issue and it will be resolved.
### Running the server
To run the server, run:
```
npm start
```
### View and test the API
You can see the API documentation, and check the available endpoints by going to http://localhost:3000/api-docs/. Endpoints that require security need to have security handlers configured before they can return a successful response. At this point they will return [ a response code of 401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401).
##### At this stage the server does not support document body sent in xml format. Forms will be supported in the near future.
### Running the server
#### This is a long read, but there's a lot to understand. Please take the time to go through this.
1. Use the OpenAPI Generator to generate your application:
Assuming you have Java (1.8+), and [have the jar](https://github.com/openapitools/openapi-generator#13---download-jar) to generate the application, run:
```java -jar {path_to_jar_file} generate -g nodejs-express-server -i {openapi yaml/json file} -o {target_directory_where_the_app_will_be_installed} ```
If you do not have the jar, or do not want to run Java from your local machine, follow instructions on the [OpenAPITools page](https://github.com/openapitools/openapi-generator). You can run the script online, on docker, and various other ways.
2. Go to the generated directory you defined. There's a fully working NodeJS-ExpressJs server waiting for you. This is important - the code is yours to change and update! Look at config.js and see that the settings there are ok with you - the server will run on port 3000, and files will be uploaded to a new directory 'uploaded_files'.
3. The server will base itself on an openapi.yaml file which is located under /api/openapi.yaml. This is not exactly the same file that you used to generate the app:
I. If you have `application/json` contentBody that was defined inside the path object - the generate will have moved it to the components/schemas section of the openapi document.
II. Every process has a new element added to it - `x-eov-operation-handler: controllers/PetController` which directs the call to that file.
III. We have a Java application that translates the operationId to a method, and a nodeJS script that does the same process to call that method. Both are converting the method to `camelCase`, but might have discrepancy. Please pay attention to the operationID names, and see that they are represented in the `controllers` and `services` directories.
4. Take the time to understand the structure of the application. There might be bugs, and there might be settings and business-logic that does not meet your expectation. Instead of dumping this solution and looking for something else - see if you can make the generated code work for you.
To keep the explanation short (a more detailed explanation will follow): Application starts with a call to index.js (this is where you will plug in the db later). It calls expressServer.js which is where the express.js and openapi-validator kick in. This is an important file. Learn it. All calls to endpoints that were configured in the openapi.yaml document go to `controllers/{name_of_tag_which_the_operation_was_associated_with}.js`, which is a very small method. All the business-logic lies in `controllers/Controller.js`, and from there - to `services/{name_of_tag_which_the_operation_was_associated_with}.js`.
### Node version and guidelines
The code was written using Node version 10.6, and complies to the [Airbnb .eslint guiding rules](https://github.com/airbnb/javascript).
5. Once you've understood what is *going* to happen, launch the app and ensure everything is working as expected:
```
npm start
```
### Tests
Unfortunately, I have not written any unit-tests. Those will come in the future. However, the package does come with all that is needed to write and run tests - mocha and sinon and the related libraries are included in the package.js and will be installed upon npm install command
### View and test the API
(Assuming no changes were made to config.js)
1. API documentation, and to check the available endpoints:
http://localhost:3000/api-docs/. To
2. Download the oepnapi.yaml document: http://localhost:3000/openapi.
3. Every call to an endpoint that was defined in the openapi document will return a 200 and a list of all the parameters and objects that were sent in the request.
4. Endpoints that require security need to have security handlers configured before they can return a successful response. At this point they will return [ a response code of 401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401).
5. ##### At this stage the server does not support document body sent in xml format.
### Node version and guidelines
The code was written using Node version 10.6, and complies to the [Airbnb .eslint guiding rules](https://github.com/airbnb/javascript).
### Project Files
#### Root Directory:
In the root directory we have (besides package.json, config.js, and log files):
- **logger.js** - where we define the logger for the project. The project uses winston, but the purpose of this file is to enable users to change and modify their own logger behavior.
- **index.js** - This is the project's 'main' file, and from here we launch the application. This is a very short and concise file, and the idea behind launching from this short file is to allow use-cases of launching the server with different parameters (changing config and/or logger) without affecting the rest of the code.
- **index.js** - This is the project's 'main' file, and from here we launch the application. This is a very short and concise file, and the idea behind launching from this short file is to allow use-cases of launching the server with different parameters (changing config and/or logger) without affecting the rest of the code.
- **expressServer.js** - The core of the Express.js server. This is where the express server is initialized, together with the OpenAPI validator, OpenAPI UI, and other libraries needed to start our server. If we want to add external links, that's where they would go. Our project uses the [express-openapi-validator](https://www.npmjs.com/package/express-openapi-validator) library that acts as a first step in the routing process - requests that are directed to paths defined in the `openapi.yaml` file are caught by this process, and it's parameters and bodyContent are validated against the schema. A successful result of this validation will be a new 'openapi' object added to the request. If the path requested is not part of the openapi.yaml file, the validator ignores the request and passes it on, as is, down the flow of the Express server.
#### api/
- **openapi.yaml** - This is the OpenAPI contract to which this server will comply. The file was generated using the codegen, and should contain everything needed to run the API Gateway - no references to external models/schemas.
- **openapi.yaml** - This is the OpenAPI contract to which this server will comply. The file was generated using the codegen, and should contain everything needed to run the API Gateway - no references to external models/schemas.
#### utils/
Currently a single file:
@@ -39,11 +60,11 @@ Currently a single file:
- **openapiRouter.js** - This is where the routing to our back-end code happens. If the request object includes an ```openapi``` object, it picks up the following values (that are part of the ```openapi.yaml``` file): 'x-openapi-router-controller', and 'x-openapi-router-service'. These variables are names of files/classes in the controllers and services directories respectively. The operationId of the request is also extracted. The operationId is a method in the controller and the service that was generated as part of the codegen process. The routing process sends the request and response objects to the controller, which will extract the expected variables from the request, and send it to be processed by the service, returning the response from the service to the caller.
#### controllers/
After validating the request, and ensuring this belongs to our API gateway, we send the request to a `controller`, where the variables and parameters are extracted from the request and sent to the relevant `service` for processing. The `controller` handles the response from the `service` and builds the appropriate HTTP response to be sent back to the user.
After validating the request, and ensuring this belongs to our API gateway, we send the request to a `controller`, where the variables and parameters are extracted from the request and sent to the relevant `service` for processing. The `controller` handles the response from the `service` and builds the appropriate HTTP response to be sent back to the user.
- **index.js** - load all the controllers that were generated for this project, and export them to be used dynamically by the `openapiRouter.js`. If you would like to customize your controller, it is advised that you link to your controller here, and ensure that the codegen does not rewrite this file.
- **Controller.js** - The core processor of the generated controllers. The generated controllers are designed to be as slim and generic as possible, referencing to the `Controller.js` for the business logic of parsing the needed variables and arguments from the request, and for building the HTTP response which will be sent back. The `Controller.js` is a class with static methods.
- **Controller.js** - The core processor of the generated controllers. The generated controllers are designed to be as slim and generic as possible, referencing to the `Controller.js` for the business logic of parsing the needed variables and arguments from the request, and for building the HTTP response which will be sent back. The `Controller.js` is a class with static methods.
- **{{x-openapi-router-controller}}.js** - auto-generated code, processing all the operations. The Controller is a class that is constructed with the service class it will be sending the request to. Every request defined by the `openapi.yaml` has an operationId. The operationId is the name of the method that will be called. Every method receives the request and response, and calls the `Controller.js` to process the request and response, adding the service method that should be called for the actual business-logic processing.
@@ -67,6 +88,3 @@ Future tests should be written to ensure that the response of every request sent
#### models/
Currently a concept awaiting feedback. The idea is to have the objects defined in the openapi.yaml act as models which are passed between the different modules. This will conform the programmers to interact using defined objects, rather than loosley-defined JSON objects. Given the nature of JavaScript progrmmers, who want to work with their own bootstrapped parameters, this concept might not work. Keeping this here for future discussion and feedback.

View File

@@ -6,7 +6,10 @@ const config = {
URL_PATH: 'http://localhost',
BASE_VERSION: 'v2',
CONTROLLER_DIRECTORY: path.join(__dirname, 'controllers'),
PROJECT_DIR: __dirname,
};
config.OPENAPI_YAML = path.join(config.ROOT_DIR, 'api', 'openapi.yaml');
config.FULL_PATH = `${config.URL_PATH}:${config.URL_PORT}/${config.BASE_VERSION}`;
config.FILE_UPLOAD_PATH = path.join(config.PROJECT_DIR, 'uploaded_files');
module.exports = config;

View File

@@ -1,18 +1,26 @@
/**
* The {{{classname}}}Controller file is a very simple one, which does not need to be changed manually,
* unless there's a case where business logic reoutes the request to an entity which is not
* the service.
* The heavy lifting of the Controller item is done in Request.js - that is where request
* parameters are extracted and sent to the service, and where response is handled.
*/
const Controller = require('./Controller');
class {{{classname}}}Controller {
constructor(Service) {
this.service = Service;
}
const service = require('../services/{{{classname}}}Service');
{{#operations}}
{{#operation}}
async {{operationId}}(request, response) {
await Controller.handleRequest(request, response, this.service.{{operationId}});
}
const {{operationId}} = async (request, response) => {
await Controller.handleRequest(request, response, service.{{operationId}});
};
{{/operation}}
}
module.exports = {{classname}}Controller;
{{/operations}}
module.exports = {
{{#operations}}
{{#operation}}
{{operationId}},
{{/operation}}
{{/operations}}
};

View File

@@ -1,3 +1,6 @@
const fs = require('fs');
const path = require('path');
const config = require('../config');
const logger = require('../logger');
class Controller {
@@ -25,35 +28,99 @@ class Controller {
}
}
static collectFiles(request) {
logger.info('Checking if files are expected in schema');
if (request.openapi.schema.requestBody !== undefined) {
const [contentType] = request.headers['content-type'].split(';');
if (contentType === 'multipart/form-data') {
const contentSchema = request.openapi.schema.requestBody.content[contentType].schema;
Object.entries(contentSchema.properties).forEach(([name, property]) => {
if (property.type === 'string' && ['binary', 'base64'].indexOf(property.format) > -1) {
request.body[name] = request.files.find(file => file.fieldname === name);
}
});
} else if (request.openapi.schema.requestBody.content[contentType] !== undefined
&& request.files !== undefined) {
[request.body] = request.files;
/**
* Files have been uploaded to the directory defined by config.js as upload directory
* Files have a temporary name, that was saved as 'filename' of the file object that is
* referenced in reuquest.files array.
* This method finds the file and changes it to the file name that was originally called
* when it was uploaded. To prevent files from being overwritten, a timestamp is added between
* the filename and its extension
* @param request
* @param fieldName
* @returns {string}
*/
static collectFile(request, fieldName) {
let uploadedFileName = '';
if (request.files && request.files.length > 0) {
const fileObject = request.files.find(file => file.fieldname === fieldName);
if (fileObject) {
const fileArray = fileObject.originalname.split('.');
const extension = fileArray.pop();
fileArray.push(`_${Date.now()}`);
uploadedFileName = `${fileArray.join('')}.${extension}`;
fs.renameSync(path.join(config.FILE_UPLOAD_PATH, fileObject.filename),
path.join(config.FILE_UPLOAD_PATH, uploadedFileName));
}
}
return uploadedFileName;
}
// static collectFiles(request) {
// logger.info('Checking if files are expected in schema');
// const requestFiles = {};
// if (request.openapi.schema.requestBody !== undefined) {
// const [contentType] = request.headers['content-type'].split(';');
// if (contentType === 'multipart/form-data') {
// const contentSchema = request.openapi.schema.requestBody.content[contentType].schema;
// Object.entries(contentSchema.properties).forEach(([name, property]) => {
// if (property.type === 'string' && ['binary', 'base64'].indexOf(property.format) > -1) {
// const fileObject = request.files.find(file => file.fieldname === name);
// const fileArray = fileObject.originalname.split('.');
// const extension = fileArray.pop();
// fileArray.push(`_${Date.now()}`);
// const uploadedFileName = `${fileArray.join('')}.${extension}`;
// fs.renameSync(path.join(config.FILE_UPLOAD_PATH, fileObject.filename),
// path.join(config.FILE_UPLOAD_PATH, uploadedFileName));
// requestFiles[name] = uploadedFileName;
// }
// });
// } else if (request.openapi.schema.requestBody.content[contentType] !== undefined
// && request.files !== undefined) {
// [request.body] = request.files;
// }
// }
// return requestFiles;
// }
static collectRequestParams(request) {
this.collectFiles(request);
const requestParams = {};
if (request.openapi.schema.requestBody !== undefined) {
requestParams.body = request.body;
const { content } = request.openapi.schema.requestBody;
if (content['application/json'] !== undefined) {
const schema = request.openapi.schema.requestBody.content['application/json'];
if (schema.$ref) {
requestParams[schema.$ref.substr(schema.$ref.lastIndexOf('.'))] = request.body;
} else {
requestParams.body = request.body;
}
} else if (content['multipart/form-data'] !== undefined) {
Object.keys(content['multipart/form-data'].schema.properties).forEach(
(property) => {
const propertyObject = content['multipart/form-data'].schema.properties[property];
if (propertyObject.format !== undefined && propertyObject.format === 'binary') {
requestParams[property] = this.collectFile(request, property);
} else {
requestParams[property] = request.body[property];
}
},
);
}
}
// if (request.openapi.schema.requestBody.content['application/json'] !== undefined) {
// const schema = request.openapi.schema.requestBody.content['application/json'];
// if (schema.$ref) {
// requestParams[schema.$ref.substr(schema.$ref.lastIndexOf('.'))] = request.body;
// } else {
// requestParams.body = request.body;
// }
// }
request.openapi.schema.parameters.forEach((param) => {
if (param.in === 'path') {
requestParams[param.name] = request.openapi.pathParams[param.name];
} else if (param.in === 'query') {
requestParams[param.name] = request.query[param.name];
} else if (param.in === 'header') {
requestParams[param.name] = request.headers[param.name];
}
});
return requestParams;

View File

@@ -1,35 +1,43 @@
// const { Middleware } = require('swagger-express-middleware');
const http = require('http');
const fs = require('fs');
const path = require('path');
const swaggerUI = require('swagger-ui-express');
const yamljs = require('yamljs');
const jsYaml = require('js-yaml');
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const { OpenApiValidator } = require('express-openapi-validator');
const openapiRouter = require('./utils/openapiRouter');
const logger = require('./logger');
const config = require('./config');
class ExpressServer {
constructor(port, openApiYaml) {
this.port = port;
this.app = express();
this.openApiPath = openApiYaml;
this.schema = yamljs.load(openApiYaml);
try {
this.schema = jsYaml.safeLoad(fs.readFileSync(openApiYaml));
} catch (e) {
logger.error('failed to start Express Server', e.message);
}
this.setupMiddleware();
}
setupMiddleware() {
// this.setupAllowedMedia();
this.app.use(cors());
this.app.use(bodyParser.json());
this.app.use(bodyParser.json({ limit: '14MB' }));
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: false }));
this.app.use(cookieParser());
this.app.use('/spec', express.static(path.join(__dirname, 'api')));
this.app.get('/hello', (req, res) => res.send('Hello World. path: '+this.openApiPath));
// this.app.get('/spec', express.static(this.openApiPath));
this.app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(this.schema));
//Simple test to see that the server is up and responding
this.app.get('/hello', (req, res) => res.send(`Hello World. path: ${this.openApiPath}`));
//Send the openapi document *AS GENERATED BY THE GENERATOR*
this.app.get('/openapi', (req, res) => res.sendFile((path.join(__dirname, 'api', 'openapi.yaml'))));
//View the openapi document in a visual interface. Should be able to test from this page
this.app.use('/api-doc', swaggerUI.serve, swaggerUI.setup(this.schema));
this.app.get('/login-redirect', (req, res) => {
res.status(200);
res.json(req.query);
@@ -38,50 +46,31 @@ class ExpressServer {
res.status(200);
res.json(req.query);
});
}
launch() {
new OpenApiValidator({
apiSpecPath: this.openApiPath,
}).install(this.app);
this.app.use(openapiRouter());
this.app.get('/', (req, res) => {
res.status(200);
res.end('Hello World');
});
}
addErrorHandler() {
this.app.use('*', (req, res) => {
res.status(404);
res.send(JSON.stringify({ error: `path ${req.baseUrl} doesn't exist` }));
});
/**
* suppressed eslint rule: The next variable is required here, even though it's not used.
*
** */
// eslint-disable-next-line no-unused-vars
this.app.use((error, req, res, next) => {
const errorResponse = error.error || error.errors || error.message || 'Unknown error';
res.status(error.status || 500);
res.type('json');
res.json({ error: errorResponse });
});
}
async launch() {
return new Promise(
async (resolve, reject) => {
try {
this.addErrorHandler();
this.server = await this.app.listen(this.port, () => {
console.log(`server running on port ${this.port}`);
resolve(this.server);
apiSpec: this.openApiPath,
operationHandlers: path.join(__dirname),
fileUploader: { dest: config.FILE_UPLOAD_PATH },
}).install(this.app)
.catch(e => console.log(e))
.then(() => {
// eslint-disable-next-line no-unused-vars
this.app.use((err, req, res, next) => {
// format errors
res.status(err.status || 500).json({
message: err.message || err,
errors: err.errors || '',
});
} catch (error) {
reject(error);
}
},
);
});
http.createServer(this.app).listen(this.port);
console.log(`Listening on port ${this.port}`);
});
}
async close() {
if (this.server !== undefined) {
await this.server.close();

View File

@@ -1,26 +1,14 @@
const config = require('./config');
const logger = require('./logger');
const ExpressServer = require('./expressServer');
// const App = require('./app');
// const app = new App(config);
// app.launch()
// .then(() => {
// logger.info('Server launched');
// })
// .catch((error) => {
// logger.error('found error, shutting down server');
// app.close()
// .catch(closeError => logger.error(closeError))
// .finally(() => logger.error(error));
// });
const launchServer = async () => {
try {
this.expressServer = new ExpressServer(config.URL_PORT, config.OPENAPI_YAML);
await this.expressServer.launch();
this.expressServer.launch();
logger.info('Express server running');
} catch (error) {
logger.error(error);
logger.error('Express Server failure', error.message);
await this.close();
}
};

View File

@@ -1,17 +1,21 @@
const winston = require('winston');
const { transports, createLogger, format } = require('winston');
const logger = winston.createLogger({
const logger = createLogger({
level: 'info',
format: winston.format.json(),
format: format.combine(
format.timestamp(),
format.json(),
),
defaultMeta: { service: 'user-service' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new transports.Console(),
new transports.File({ filename: 'error.log', level: 'error', timestamp: true }),
new transports.File({ filename: 'combined.log', timestamp: true }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({ format: winston.format.simple() }));
logger.add(new transports.Console({ format: format.simple() }));
}
module.exports = logger;

View File

@@ -1,5 +1,5 @@
{
"name": "{{projectName}}",
"name": "{{projectName}}",
"version": "{{appVersion}}",
"description": "{{{appDescription}}}",
"main": "index.js",
@@ -15,28 +15,25 @@
"private": true,
"dependencies": {
"body-parser": "^1.19.0",
"connect": "^3.2.0",
"camelcase": "^5.3.1",
"cookie-parser": "^1.4.4",
"cors": "^2.8.5",
"express": "^4.16.4",
"express-openapi-validator": "^1.0.0",
"express-openapi-validator": "^3.9.1",
"js-yaml": "^3.3.0",
"jstoxml": "^1.5.0",
"ono": "^5.0.1",
"openapi-sampler": "^1.0.0-beta.15",
"swagger-express-middleware": "^2.0.2",
"swagger-tools": "^0.10.4",
"swagger-ui-express": "^4.0.2",
"winston": "^3.2.1",
"yamljs": "^0.3.0",
"mocha": "^6.1.4",
"winston": "^3.2.1"
},
"devDependencies": {
"axios": "^0.19.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.17.2",
"form-data": "^2.3.3"
"mocha": "^7.1.1"
},
"eslintConfig": {
"env": {

View File

@@ -1,45 +1,49 @@
/* eslint-disable no-unused-vars */
const Service = require('./Service');
class {{{classname}}}Service {
{{#operations}}
{{#operation}}
/**
{{#summary}}
* {{{summary}}}
{{/summary}}
{{#notes}}
* {{{notes}}}
{{/notes}}
*
{{#allParams}}
* {{paramName}} {{{dataType}}} {{{description}}}{{^required}} (optional){{/required}}
{{/allParams}}
{{^returnType}}
* no response value expected for this operation
{{/returnType}}
{{#returnType}}
* returns {{{returnType}}}
{{/returnType}}
**/
static {{{operationId}}}({{#allParams}}{{#-first}}{ {{/-first}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{#-last}} }{{/-last}}{{/allParams}}) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
{{#summary}}
* {{{summary}}}
{{/summary}}
{{#notes}}
* {{{notes}}}
{{/notes}}
*
{{#allParams}}
* {{paramName}} {{{dataType}}} {{{description}}}{{^required}} (optional){{/required}}
{{/allParams}}
{{^returnType}}
* no response value expected for this operation
{{/returnType}}
{{#returnType}}
* returns {{{returnType}}}
{{/returnType}}
* */
const {{{operationId}}} = ({{#allParams}}{{#-first}}{ {{/-first}}{{paramName}}{{#hasMore}}, {{/hasMore}}{{#-last}} }{{/-last}}{{/allParams}}) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
{{#allParams}}
{{paramName}},
{{/allParams}}
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
{{/operation}}
}
module.exports = {{{classname}}}Service;
{{/operations}}
module.exports = {
{{#operations}}
{{#operation}}
{{operationId}},
{{/operation}}
{{/operations}}
};

View File

@@ -1 +1 @@
4.1.0-SNAPSHOT
unset

View File

@@ -1,36 +1,58 @@
# OpenAPI Generated JavaScript/Express Server
## Overview
This server was generated using the [OpenAPI Generator](https://openapi-generator.tech) project. The code generator, and it's generated code allows you to develop your system with an API-First attitude, where the API contract is the anchor and definer of your project, and your code and business-logic aims to complete and comply to the terms in the API contract.
### prerequisites
- NodeJS >= 10.4
- NodeJS >= 10.6
- NPM >= 6.10.0
The code was written on a mac, so assuming all should work smoothly on Linux-based computers. However, there is no reason not to run this library on Windows-based machines. If you find an OS-related problem, please open an issue and it will be resolved.
### Running the server
To run the server, run:
```
npm start
```
### View and test the API
You can see the API documentation, and check the available endpoints by going to http://localhost:3000/api-docs/. Endpoints that require security need to have security handlers configured before they can return a successful response. At this point they will return [ a response code of 401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401).
##### At this stage the server does not support document body sent in xml format. Forms will be supported in the near future.
### Running the server
#### This is a long read, but there's a lot to understand. Please take the time to go through this.
1. Use the OpenAPI Generator to generate your application:
Assuming you have Java (1.8+), and [have the jar](https://github.com/openapitools/openapi-generator#13---download-jar) to generate the application, run:
```java -jar {path_to_jar_file} generate -g nodejs-express-server -i {openapi yaml/json file} -o {target_directory_where_the_app_will_be_installed} ```
If you do not have the jar, or do not want to run Java from your local machine, follow instructions on the [OpenAPITools page](https://github.com/openapitools/openapi-generator). You can run the script online, on docker, and various other ways.
2. Go to the generated directory you defined. There's a fully working NodeJS-ExpressJs server waiting for you. This is important - the code is yours to change and update! Look at config.js and see that the settings there are ok with you - the server will run on port 3000, and files will be uploaded to a new directory 'uploaded_files'.
3. The server will base itself on an openapi.yaml file which is located under /api/openapi.yaml. This is not exactly the same file that you used to generate the app:
I. If you have `application/json` contentBody that was defined inside the path object - the generate will have moved it to the components/schemas section of the openapi document.
II. Every process has a new element added to it - `x-eov-operation-handler: controllers/PetController` which directs the call to that file.
III. We have a Java application that translates the operationId to a method, and a nodeJS script that does the same process to call that method. Both are converting the method to `camelCase`, but might have discrepancy. Please pay attention to the operationID names, and see that they are represented in the `controllers` and `services` directories.
4. Take the time to understand the structure of the application. There might be bugs, and there might be settings and business-logic that does not meet your expectation. Instead of dumping this solution and looking for something else - see if you can make the generated code work for you.
To keep the explanation short (a more detailed explanation will follow): Application starts with a call to index.js (this is where you will plug in the db later). It calls expressServer.js which is where the express.js and openapi-validator kick in. This is an important file. Learn it. All calls to endpoints that were configured in the openapi.yaml document go to `controllers/{name_of_tag_which_the_operation_was_associated_with}.js`, which is a very small method. All the business-logic lies in `controllers/Controller.js`, and from there - to `services/{name_of_tag_which_the_operation_was_associated_with}.js`.
### Node version and guidelines
The code was written using Node version 10.6, and complies to the [Airbnb .eslint guiding rules](https://github.com/airbnb/javascript).
5. Once you've understood what is *going* to happen, launch the app and ensure everything is working as expected:
```
npm start
```
### Tests
Unfortunately, I have not written any unit-tests. Those will come in the future. However, the package does come with all that is needed to write and run tests - mocha and sinon and the related libraries are included in the package.js and will be installed upon npm install command
### View and test the API
(Assuming no changes were made to config.js)
1. API documentation, and to check the available endpoints:
http://localhost:3000/api-docs/. To
2. Download the oepnapi.yaml document: http://localhost:3000/openapi.
3. Every call to an endpoint that was defined in the openapi document will return a 200 and a list of all the parameters and objects that were sent in the request.
4. Endpoints that require security need to have security handlers configured before they can return a successful response. At this point they will return [ a response code of 401](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401).
5. ##### At this stage the server does not support document body sent in xml format.
### Node version and guidelines
The code was written using Node version 10.6, and complies to the [Airbnb .eslint guiding rules](https://github.com/airbnb/javascript).
### Project Files
#### Root Directory:
In the root directory we have (besides package.json, config.js, and log files):
- **logger.js** - where we define the logger for the project. The project uses winston, but the purpose of this file is to enable users to change and modify their own logger behavior.
- **index.js** - This is the project's 'main' file, and from here we launch the application. This is a very short and concise file, and the idea behind launching from this short file is to allow use-cases of launching the server with different parameters (changing config and/or logger) without affecting the rest of the code.
- **index.js** - This is the project's 'main' file, and from here we launch the application. This is a very short and concise file, and the idea behind launching from this short file is to allow use-cases of launching the server with different parameters (changing config and/or logger) without affecting the rest of the code.
- **expressServer.js** - The core of the Express.js server. This is where the express server is initialized, together with the OpenAPI validator, OpenAPI UI, and other libraries needed to start our server. If we want to add external links, that's where they would go. Our project uses the [express-openapi-validator](https://www.npmjs.com/package/express-openapi-validator) library that acts as a first step in the routing process - requests that are directed to paths defined in the `openapi.yaml` file are caught by this process, and it's parameters and bodyContent are validated against the schema. A successful result of this validation will be a new 'openapi' object added to the request. If the path requested is not part of the openapi.yaml file, the validator ignores the request and passes it on, as is, down the flow of the Express server.
#### api/
- **openapi.yaml** - This is the OpenAPI contract to which this server will comply. The file was generated using the codegen, and should contain everything needed to run the API Gateway - no references to external models/schemas.
- **openapi.yaml** - This is the OpenAPI contract to which this server will comply. The file was generated using the codegen, and should contain everything needed to run the API Gateway - no references to external models/schemas.
#### utils/
Currently a single file:
@@ -38,13 +60,13 @@ Currently a single file:
- **openapiRouter.js** - This is where the routing to our back-end code happens. If the request object includes an ```openapi``` object, it picks up the following values (that are part of the ```openapi.yaml``` file): 'x-openapi-router-controller', and 'x-openapi-router-service'. These variables are names of files/classes in the controllers and services directories respectively. The operationId of the request is also extracted. The operationId is a method in the controller and the service that was generated as part of the codegen process. The routing process sends the request and response objects to the controller, which will extract the expected variables from the request, and send it to be processed by the service, returning the response from the service to the caller.
#### controllers/
After validating the request, and ensuring this belongs to our API gateway, we send the request to a `controller`, where the variables and parameters are extracted from the request and sent to the relevant `service` for processing. The `controller` handles the response from the `service` and builds the appropriate HTTP response to be sent back to the user.
After validating the request, and ensuring this belongs to our API gateway, we send the request to a `controller`, where the variables and parameters are extracted from the request and sent to the relevant `service` for processing. The `controller` handles the response from the `service` and builds the appropriate HTTP response to be sent back to the user.
- **index.js** - load all the controllers that were generated for this project, and export them to be used dynamically by the `openapiRouter.js`. If you would like to customize your controller, it is advised that you link to your controller here, and ensure that the codegen does not rewrite this file.
- **Controller.js** - The core processor of the generated controllers. The generated controllers are designed to be as slim and generic as possible, referencing to the `Controller.js` for the business logic of parsing the needed variables and arguments from the request, and for building the HTTP response which will be sent back. The `Controller.js` is a class with static methods.
- **Controller.js** - The core processor of the generated controllers. The generated controllers are designed to be as slim and generic as possible, referencing to the `Controller.js` for the business logic of parsing the needed variables and arguments from the request, and for building the HTTP response which will be sent back. The `Controller.js` is a class with static methods.
- **{{x-openapi-router-controller}}.js** - auto-generated code, processing all the operations. The Controller is a class that is constructed with the service class it will be sending the request to. Every request defined by the `openapi.yaml` has an operationId. The operationId is the name of the method that will be called. Every method receives the request and response, and calls the `Controller.js` to process the request and response, adding the service method that should be called for the actual business-logic processing.
- **.js** - auto-generated code, processing all the operations. The Controller is a class that is constructed with the service class it will be sending the request to. Every request defined by the `openapi.yaml` has an operationId. The operationId is the name of the method that will be called. Every method receives the request and response, and calls the `Controller.js` to process the request and response, adding the service method that should be called for the actual business-logic processing.
#### services/
This is where the API Gateway ends, and the unique business-logic of your application kicks in. Every endpoint in the `openapi.yaml` has a variable 'x-openapi-router-service', which is the name of the service class that is generated. The operationID of the endpoint is the name of the method that will be called. The generated code provides a simple promise with a try/catch clause. A successful operation ends with a call to the generic `Service.js` to build a successful response (payload and response code), and a failure will call the generic `Service.js` to build a response with an error object and the relevant response code. It is recommended to have the services be generated automatically once, and after the initial build add methods manually.
@@ -53,7 +75,7 @@ This is where the API Gateway ends, and the unique business-logic of your applic
- **Service.js** - A utility class, very simple and thin at this point, with two static methods for building a response object for successful and failed results in the service operation. The default response code is 200 for success and 500 for failure. It is recommended to send more accurate response codes and override these defaults when relevant.
- **{{x-openapi-router-service}}.js** - auto-generated code, providing a stub Promise for each operationId defined in the `openapi.yaml`. Each method receives the variables that were defined in the `openapi.yaml` file, and wraps a Promise in a try/catch clause. The Promise resolves both success and failure in a call to the `Service.js` utility class for building the appropriate response that will be sent back to the Controller and then to the caller of this endpoint.
- **.js** - auto-generated code, providing a stub Promise for each operationId defined in the `openapi.yaml`. Each method receives the variables that were defined in the `openapi.yaml` file, and wraps a Promise in a try/catch clause. The Promise resolves both success and failure in a call to the `Service.js` utility class for building the appropriate response that will be sent back to the Controller and then to the caller of this endpoint.
#### tests/
- **serverTests.js** - basic server validation tests, checking that the server is up, that a call to an endpoint within the scope of the `openapi.yaml` file returns 200, that a call to a path outside that scope returns 200 if it exists and a 404 if not.
@@ -66,6 +88,3 @@ Future tests should be written to ensure that the response of every request sent
#### models/
Currently a concept awaiting feedback. The idea is to have the objects defined in the openapi.yaml act as models which are passed between the different modules. This will conform the programmers to interact using defined objects, rather than loosley-defined JSON objects. Given the nature of JavaScript progrmmers, who want to work with their own bootstrapped parameters, this concept might not work. Keeping this here for future discussion and feedback.

View File

@@ -31,7 +31,7 @@ paths:
description: Pet object that needs to be added to the store
required: true
responses:
405:
"405":
content: {}
description: Invalid input
security:
@@ -42,8 +42,7 @@ paths:
tags:
- pet
x-codegen-request-body-name: body
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
x-eov-operation-handler: controllers/PetController
put:
operationId: updatePet
requestBody:
@@ -57,13 +56,13 @@ paths:
description: Pet object that needs to be added to the store
required: true
responses:
400:
"400":
content: {}
description: Invalid ID supplied
404:
"404":
content: {}
description: Pet not found
405:
"405":
content: {}
description: Validation exception
security:
@@ -74,8 +73,7 @@ paths:
tags:
- pet
x-codegen-request-body-name: body
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
x-eov-operation-handler: controllers/PetController
/pet/findByStatus:
get:
description: Multiple status values can be provided with comma separated strings
@@ -97,7 +95,7 @@ paths:
type: array
style: form
responses:
200:
"200":
content:
application/xml:
schema:
@@ -110,7 +108,7 @@ paths:
$ref: '#/components/schemas/Pet'
type: array
description: successful operation
400:
"400":
content: {}
description: Invalid status value
security:
@@ -120,8 +118,7 @@ paths:
summary: Finds Pets by status
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
x-eov-operation-handler: controllers/PetController
/pet/findByTags:
get:
deprecated: true
@@ -140,7 +137,7 @@ paths:
type: array
style: form
responses:
200:
"200":
content:
application/xml:
schema:
@@ -153,7 +150,7 @@ paths:
$ref: '#/components/schemas/Pet'
type: array
description: successful operation
400:
"400":
content: {}
description: Invalid tag value
security:
@@ -163,8 +160,7 @@ paths:
summary: Finds Pets by tags
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
x-eov-operation-handler: controllers/PetController
/pet/{petId}:
delete:
operationId: deletePet
@@ -181,7 +177,7 @@ paths:
format: int64
type: integer
responses:
400:
"400":
content: {}
description: Invalid pet value
security:
@@ -191,8 +187,7 @@ paths:
summary: Deletes a pet
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
x-eov-operation-handler: controllers/PetController
get:
description: Returns a single pet
operationId: getPetById
@@ -205,7 +200,7 @@ paths:
format: int64
type: integer
responses:
200:
"200":
content:
application/xml:
schema:
@@ -214,10 +209,10 @@ paths:
schema:
$ref: '#/components/schemas/Pet'
description: successful operation
400:
"400":
content: {}
description: Invalid ID supplied
404:
"404":
content: {}
description: Pet not found
security:
@@ -225,8 +220,7 @@ paths:
summary: Find pet by ID
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
x-eov-operation-handler: controllers/PetController
post:
operationId: updatePetWithForm
parameters:
@@ -249,7 +243,7 @@ paths:
description: Updated status of the pet
type: string
responses:
405:
"405":
content: {}
description: Invalid input
security:
@@ -259,8 +253,7 @@ paths:
summary: Updates a pet in the store with form data
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
x-eov-operation-handler: controllers/PetController
/pet/{petId}/uploadImage:
post:
operationId: uploadFile
@@ -285,7 +278,7 @@ paths:
format: binary
type: string
responses:
200:
"200":
content:
application/json:
schema:
@@ -298,14 +291,13 @@ paths:
summary: uploads an image
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
x-eov-operation-handler: controllers/PetController
/store/inventory:
get:
description: Returns a map of status codes to quantities
operationId: getInventory
responses:
200:
"200":
content:
application/json:
schema:
@@ -319,8 +311,7 @@ paths:
summary: Returns pet inventories by status
tags:
- store
x-openapi-router-controller: StoreController
x-openapi-router-service: StoreService
x-eov-operation-handler: controllers/StoreController
/store/order:
post:
operationId: placeOrder
@@ -332,7 +323,7 @@ paths:
description: order placed for purchasing the pet
required: true
responses:
200:
"200":
content:
application/xml:
schema:
@@ -341,15 +332,14 @@ paths:
schema:
$ref: '#/components/schemas/Order'
description: successful operation
400:
"400":
content: {}
description: Invalid Order
summary: Place an order for a pet
tags:
- store
x-codegen-request-body-name: body
x-openapi-router-controller: StoreController
x-openapi-router-service: StoreService
x-eov-operation-handler: controllers/StoreController
/store/order/{orderId}:
delete:
description: For valid response try integer IDs with value < 1000. Anything
@@ -363,17 +353,16 @@ paths:
schema:
type: string
responses:
400:
"400":
content: {}
description: Invalid ID supplied
404:
"404":
content: {}
description: Order not found
summary: Delete purchase order by ID
tags:
- store
x-openapi-router-controller: StoreController
x-openapi-router-service: StoreService
x-eov-operation-handler: controllers/StoreController
get:
description: For valid response try integer IDs with value <= 5 or > 10. Other
values will generated exceptions
@@ -389,7 +378,7 @@ paths:
minimum: 1
type: integer
responses:
200:
"200":
content:
application/xml:
schema:
@@ -398,17 +387,16 @@ paths:
schema:
$ref: '#/components/schemas/Order'
description: successful operation
400:
"400":
content: {}
description: Invalid ID supplied
404:
"404":
content: {}
description: Order not found
summary: Find purchase order by ID
tags:
- store
x-openapi-router-controller: StoreController
x-openapi-router-service: StoreService
x-eov-operation-handler: controllers/StoreController
/user:
post:
description: This can only be done by the logged in user.
@@ -428,8 +416,7 @@ paths:
tags:
- user
x-codegen-request-body-name: body
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
x-eov-operation-handler: controllers/UserController
/user/createWithArray:
post:
operationId: createUsersWithArrayInput
@@ -450,8 +437,7 @@ paths:
tags:
- user
x-codegen-request-body-name: body
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
x-eov-operation-handler: controllers/UserController
/user/createWithList:
post:
operationId: createUsersWithListInput
@@ -472,8 +458,7 @@ paths:
tags:
- user
x-codegen-request-body-name: body
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
x-eov-operation-handler: controllers/UserController
/user/login:
get:
operationId: loginUser
@@ -491,7 +476,7 @@ paths:
schema:
type: string
responses:
200:
"200":
content:
application/xml:
schema:
@@ -511,14 +496,13 @@ paths:
schema:
format: date-time
type: string
400:
"400":
content: {}
description: Invalid username/password supplied
summary: Logs user into the system
tags:
- user
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
x-eov-operation-handler: controllers/UserController
/user/logout:
get:
operationId: logoutUser
@@ -529,8 +513,7 @@ paths:
summary: Logs out current logged in user session
tags:
- user
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
x-eov-operation-handler: controllers/UserController
/user/{username}:
delete:
description: This can only be done by the logged in user.
@@ -543,17 +526,16 @@ paths:
schema:
type: string
responses:
400:
"400":
content: {}
description: Invalid username supplied
404:
"404":
content: {}
description: User not found
summary: Delete user
tags:
- user
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
x-eov-operation-handler: controllers/UserController
get:
operationId: getUserByName
parameters:
@@ -564,7 +546,7 @@ paths:
schema:
type: string
responses:
200:
"200":
content:
application/xml:
schema:
@@ -573,17 +555,16 @@ paths:
schema:
$ref: '#/components/schemas/User'
description: successful operation
400:
"400":
content: {}
description: Invalid username supplied
404:
"404":
content: {}
description: User not found
summary: Get user by user name
tags:
- user
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
x-eov-operation-handler: controllers/UserController
put:
description: This can only be done by the logged in user.
operationId: updateUser
@@ -602,18 +583,17 @@ paths:
description: Updated user object
required: true
responses:
400:
"400":
content: {}
description: Invalid user supplied
404:
"404":
content: {}
description: User not found
summary: Updated user
tags:
- user
x-codegen-request-body-name: body
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
x-eov-operation-handler: controllers/UserController
components:
schemas:
Order:

View File

@@ -1,795 +0,0 @@
---
swagger: "2.0"
info:
description: |
"This is a sample server Petstore server. You can find out more about\
\ Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).\
\ For this sample, you can use the api key `special-key` to test the authorization\
\ filters."
version: "1.0.0"
title: "Swagger Petstore"
termsOfService: "http://swagger.io/terms/"
contact:
email: "apiteam@swagger.io"
license:
name: "Apache-2.0"
url: "https://www.apache.org/licenses/LICENSE-2.0.html"
basePath: "/v2"
tags:
- name: "pet"
description: "Everything about your Pets"
externalDocs:
description: "Find out more"
url: "http://swagger.io"
- name: "store"
description: "Access to Petstore orders"
- name: "user"
description: "Operations about user"
externalDocs:
description: "Find out more about our store"
url: "http://swagger.io"
schemes:
- "http"
paths:
/pet:
post:
tags:
- "pet"
summary: "Add a new pet to the store"
description: ""
operationId: "addPet"
consumes:
- "application/json"
- "application/xml"
produces:
- "application/xml"
- "application/json"
parameters:
- in: "body"
name: "body"
description: "Pet object that needs to be added to the store"
required: true
schema:
$ref: "#/definitions/Pet"
responses:
405:
description: "Invalid input"
security:
- petstore_auth:
- "write:pets"
- "read:pets"
x-swagger-router-controller: "Pet"
put:
tags:
- "pet"
summary: "Update an existing pet"
description: ""
operationId: "updatePet"
consumes:
- "application/json"
- "application/xml"
produces:
- "application/xml"
- "application/json"
parameters:
- in: "body"
name: "body"
description: "Pet object that needs to be added to the store"
required: true
schema:
$ref: "#/definitions/Pet"
responses:
400:
description: "Invalid ID supplied"
404:
description: "Pet not found"
405:
description: "Validation exception"
security:
- petstore_auth:
- "write:pets"
- "read:pets"
x-swagger-router-controller: "Pet"
/pet/findByStatus:
get:
tags:
- "pet"
summary: "Finds Pets by status"
description: "Multiple status values can be provided with comma separated strings"
operationId: "findPetsByStatus"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "status"
in: "query"
description: "Status values that need to be considered for filter"
required: true
type: "array"
items:
type: "string"
default: "available"
enum:
- "available"
- "pending"
- "sold"
collectionFormat: "csv"
responses:
200:
description: "successful operation"
schema:
type: "array"
items:
$ref: "#/definitions/Pet"
400:
description: "Invalid status value"
security:
- petstore_auth:
- "write:pets"
- "read:pets"
x-swagger-router-controller: "Pet"
/pet/findByTags:
get:
tags:
- "pet"
summary: "Finds Pets by tags"
description: |
"Multiple tags can be provided with comma separated strings. Use\
\ tag1, tag2, tag3 for testing."
operationId: "findPetsByTags"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "tags"
in: "query"
description: "Tags to filter by"
required: true
type: "array"
items:
type: "string"
collectionFormat: "csv"
responses:
200:
description: "successful operation"
schema:
type: "array"
items:
$ref: "#/definitions/Pet"
400:
description: "Invalid tag value"
security:
- petstore_auth:
- "write:pets"
- "read:pets"
deprecated: true
x-swagger-router-controller: "Pet"
/pet/{petId}:
get:
tags:
- "pet"
summary: "Find pet by ID"
description: "Returns a single pet"
operationId: "getPetById"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "petId"
in: "path"
description: "ID of pet to return"
required: true
type: "integer"
format: "int64"
responses:
200:
description: "successful operation"
schema:
$ref: "#/definitions/Pet"
400:
description: "Invalid ID supplied"
404:
description: "Pet not found"
security:
- api_key: []
x-swagger-router-controller: "Pet"
post:
tags:
- "pet"
summary: "Updates a pet in the store with form data"
description: ""
operationId: "updatePetWithForm"
consumes:
- "application/x-www-form-urlencoded"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "petId"
in: "path"
description: "ID of pet that needs to be updated"
required: true
type: "integer"
format: "int64"
- name: "name"
in: "formData"
description: "Updated name of the pet"
required: false
type: "string"
- name: "status"
in: "formData"
description: "Updated status of the pet"
required: false
type: "string"
responses:
405:
description: "Invalid input"
security:
- petstore_auth:
- "write:pets"
- "read:pets"
x-swagger-router-controller: "Pet"
delete:
tags:
- "pet"
summary: "Deletes a pet"
description: ""
operationId: "deletePet"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "api_key"
in: "header"
required: false
type: "string"
- name: "petId"
in: "path"
description: "Pet id to delete"
required: true
type: "integer"
format: "int64"
responses:
400:
description: "Invalid pet value"
security:
- petstore_auth:
- "write:pets"
- "read:pets"
x-swagger-router-controller: "Pet"
/pet/{petId}/uploadImage:
post:
tags:
- "pet"
summary: "uploads an image"
description: ""
operationId: "uploadFile"
consumes:
- "multipart/form-data"
produces:
- "application/json"
parameters:
- name: "petId"
in: "path"
description: "ID of pet to update"
required: true
type: "integer"
format: "int64"
- name: "additionalMetadata"
in: "formData"
description: "Additional data to pass to server"
required: false
type: "string"
- name: "file"
in: "formData"
description: "file to upload"
required: false
type: "file"
responses:
200:
description: "successful operation"
schema:
$ref: "#/definitions/ApiResponse"
security:
- petstore_auth:
- "write:pets"
- "read:pets"
x-swagger-router-controller: "Pet"
/store/inventory:
get:
tags:
- "store"
summary: "Returns pet inventories by status"
description: "Returns a map of status codes to quantities"
operationId: "getInventory"
produces:
- "application/json"
parameters: []
responses:
200:
description: "successful operation"
schema:
type: "object"
additionalProperties:
type: "integer"
format: "int32"
security:
- api_key: []
x-swagger-router-controller: "Store"
/store/order:
post:
tags:
- "store"
summary: "Place an order for a pet"
description: ""
operationId: "placeOrder"
produces:
- "application/xml"
- "application/json"
parameters:
- in: "body"
name: "body"
description: "order placed for purchasing the pet"
required: true
schema:
$ref: "#/definitions/Order"
responses:
200:
description: "successful operation"
schema:
$ref: "#/definitions/Order"
400:
description: "Invalid Order"
x-swagger-router-controller: "Store"
/store/order/{orderId}:
get:
tags:
- "store"
summary: "Find purchase order by ID"
description: |
"For valid response try integer IDs with value <= 5 or > 10. Other\
\ values will generated exceptions"
operationId: "getOrderById"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "orderId"
in: "path"
description: "ID of pet that needs to be fetched"
required: true
type: "integer"
maximum: 5
minimum: 1
format: "int64"
responses:
200:
description: "successful operation"
schema:
$ref: "#/definitions/Order"
400:
description: "Invalid ID supplied"
404:
description: "Order not found"
x-swagger-router-controller: "Store"
delete:
tags:
- "store"
summary: "Delete purchase order by ID"
description: |
"For valid response try integer IDs with value < 1000. Anything\
\ above 1000 or nonintegers will generate API errors"
operationId: "deleteOrder"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "orderId"
in: "path"
description: "ID of the order that needs to be deleted"
required: true
type: "string"
responses:
400:
description: "Invalid ID supplied"
404:
description: "Order not found"
x-swagger-router-controller: "Store"
/user:
post:
tags:
- "user"
summary: "Create user"
description: "This can only be done by the logged in user."
operationId: "createUser"
produces:
- "application/xml"
- "application/json"
parameters:
- in: "body"
name: "body"
description: "Created user object"
required: true
schema:
$ref: "#/definitions/User"
responses:
default:
description: "successful operation"
x-swagger-router-controller: "User"
/user/createWithArray:
post:
tags:
- "user"
summary: "Creates list of users with given input array"
description: ""
operationId: "createUsersWithArrayInput"
produces:
- "application/xml"
- "application/json"
parameters:
- in: "body"
name: "body"
description: "List of user object"
required: true
schema:
type: "array"
items:
$ref: "#/definitions/User"
responses:
default:
description: "successful operation"
x-swagger-router-controller: "User"
/user/createWithList:
post:
tags:
- "user"
summary: "Creates list of users with given input array"
description: ""
operationId: "createUsersWithListInput"
produces:
- "application/xml"
- "application/json"
parameters:
- in: "body"
name: "body"
description: "List of user object"
required: true
schema:
type: "array"
items:
$ref: "#/definitions/User"
responses:
default:
description: "successful operation"
x-swagger-router-controller: "User"
/user/login:
get:
tags:
- "user"
summary: "Logs user into the system"
description: ""
operationId: "loginUser"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "username"
in: "query"
description: "The user name for login"
required: true
type: "string"
- name: "password"
in: "query"
description: "The password for login in clear text"
required: true
type: "string"
responses:
200:
description: "successful operation"
schema:
type: "string"
headers:
X-Rate-Limit:
type: "integer"
format: "int32"
description: "calls per hour allowed by the user"
X-Expires-After:
type: "string"
format: "date-time"
description: "date in UTC when toekn expires"
400:
description: "Invalid username/password supplied"
x-swagger-router-controller: "User"
/user/logout:
get:
tags:
- "user"
summary: "Logs out current logged in user session"
description: ""
operationId: "logoutUser"
produces:
- "application/xml"
- "application/json"
parameters: []
responses:
default:
description: "successful operation"
x-swagger-router-controller: "User"
/user/{username}:
get:
tags:
- "user"
summary: "Get user by user name"
description: ""
operationId: "getUserByName"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "username"
in: "path"
description: "The name that needs to be fetched. Use user1 for testing."
required: true
type: "string"
responses:
200:
description: "successful operation"
schema:
$ref: "#/definitions/User"
400:
description: "Invalid username supplied"
404:
description: "User not found"
x-swagger-router-controller: "User"
put:
tags:
- "user"
summary: "Updated user"
description: "This can only be done by the logged in user."
operationId: "updateUser"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "username"
in: "path"
description: "name that need to be deleted"
required: true
type: "string"
- in: "body"
name: "body"
description: "Updated user object"
required: true
schema:
$ref: "#/definitions/User"
responses:
400:
description: "Invalid user supplied"
404:
description: "User not found"
x-swagger-router-controller: "User"
delete:
tags:
- "user"
summary: "Delete user"
description: "This can only be done by the logged in user."
operationId: "deleteUser"
produces:
- "application/xml"
- "application/json"
parameters:
- name: "username"
in: "path"
description: "The name that needs to be deleted"
required: true
type: "string"
responses:
400:
description: "Invalid username supplied"
404:
description: "User not found"
x-swagger-router-controller: "User"
securityDefinitions:
petstore_auth:
type: "oauth2"
authorizationUrl: "http://petstore.swagger.io/api/oauth/dialog"
flow: "implicit"
scopes:
write:pets: "modify pets in your account"
read:pets: "read your pets"
api_key:
type: "apiKey"
name: "api_key"
in: "header"
definitions:
Order:
type: "object"
properties:
id:
type: "integer"
format: "int64"
petId:
type: "integer"
format: "int64"
quantity:
type: "integer"
format: "int32"
shipDate:
type: "string"
format: "date-time"
status:
type: "string"
description: "Order Status"
enum:
- "placed"
- "approved"
- "delivered"
complete:
type: "boolean"
default: false
title: "Pet Order"
description: "An order for a pets from the pet store"
example:
petId: 6
quantity: 1
id: 0
shipDate: "2000-01-23T04:56:07.000+00:00"
complete: false
status: "placed"
xml:
name: "Order"
Category:
type: "object"
properties:
id:
type: "integer"
format: "int64"
name:
type: "string"
title: "Pet category"
description: "A category for a pet"
example:
name: "name"
id: 6
xml:
name: "Category"
User:
type: "object"
properties:
id:
type: "integer"
format: "int64"
username:
type: "string"
firstName:
type: "string"
lastName:
type: "string"
email:
type: "string"
password:
type: "string"
phone:
type: "string"
userStatus:
type: "integer"
format: "int32"
description: "User Status"
title: "a User"
description: "A User who is purchasing from the pet store"
example:
firstName: "firstName"
lastName: "lastName"
password: "password"
userStatus: 6
phone: "phone"
id: 0
email: "email"
username: "username"
xml:
name: "User"
Tag:
type: "object"
properties:
id:
type: "integer"
format: "int64"
name:
type: "string"
title: "Pet Tag"
description: "A tag for a pet"
example:
name: "name"
id: 1
xml:
name: "Tag"
Pet:
type: "object"
required:
- "name"
- "photoUrls"
properties:
id:
type: "integer"
format: "int64"
category:
$ref: "#/definitions/Category"
name:
type: "string"
example: "doggie"
photoUrls:
type: "array"
xml:
name: "photoUrl"
wrapped: true
items:
type: "string"
tags:
type: "array"
xml:
name: "tag"
wrapped: true
items:
$ref: "#/definitions/Tag"
status:
type: "string"
description: "pet status in the store"
enum:
- "available"
- "pending"
- "sold"
title: "a Pet"
description: "A pet for sale in the pet store"
example:
photoUrls:
- "photoUrls"
- "photoUrls"
name: "doggie"
id: 0
category:
name: "name"
id: 6
tags:
- name: "name"
id: 1
- name: "name"
id: 1
status: "available"
xml:
name: "Pet"
ApiResponse:
type: "object"
properties:
code:
type: "integer"
format: "int32"
type:
type: "string"
message:
type: "string"
title: "An uploaded response"
description: "Describes the result of uploading an image resource"
example:
code: 0
type: "type"
message: "message"
testItem:
type: object
properties:
id:
type: integer
name:
type: string
descrtiption:
type: string
version:
type: number
example:
id: 1
name: "testItem"
description: "An item which means very little, as it's only a test"
version: 2.3
externalDocs:
description: "Find out more about Swagger"
url: "http://swagger.io"

View File

@@ -0,0 +1,30 @@
const ExpressServer = require('./expressServer');
const logger = require('./logger');
// const swaggerRouter = require('./utils/swaggerRouter');
class App {
constructor(config) {
this.config = config;
}
async launch() {
try {
this.expressServer = new ExpressServer(this.config.URL_PORT, this.config.OPENAPI_YAML);
// this.expressServer.app.use(swaggerRouter());
await this.expressServer.launch();
logger.info('Express server running');
} catch (error) {
logger.error(error);
await this.close();
}
}
async close() {
if (this.expressServer !== undefined) {
await this.expressServer.close();
logger.info(`Server shut down on port ${this.config.URL_PORT}`);
}
}
}
module.exports = App;

View File

@@ -6,7 +6,10 @@ const config = {
URL_PATH: 'http://localhost',
BASE_VERSION: 'v2',
CONTROLLER_DIRECTORY: path.join(__dirname, 'controllers'),
PROJECT_DIR: __dirname,
};
config.OPENAPI_YAML = path.join(config.ROOT_DIR, 'api', 'openapi.yaml');
config.FULL_PATH = `${config.URL_PATH}:${config.URL_PORT}/${config.BASE_VERSION}`;
config.FILE_UPLOAD_PATH = path.join(config.PROJECT_DIR, 'uploaded_files');
module.exports = config;

View File

@@ -1,3 +1,6 @@
const fs = require('fs');
const path = require('path');
const config = require('../config');
const logger = require('../logger');
class Controller {
@@ -25,35 +28,99 @@ class Controller {
}
}
static collectFiles(request) {
logger.info('Checking if files are expected in schema');
if (request.openapi.schema.requestBody !== undefined) {
const [contentType] = request.headers['content-type'].split(';');
if (contentType === 'multipart/form-data') {
const contentSchema = request.openapi.schema.requestBody.content[contentType].schema;
Object.entries(contentSchema.properties).forEach(([name, property]) => {
if (property.type === 'string' && ['binary', 'base64'].indexOf(property.format) > -1) {
request.body[name] = request.files.find(file => file.fieldname === name);
}
});
} else if (request.openapi.schema.requestBody.content[contentType] !== undefined
&& request.files !== undefined) {
[request.body] = request.files;
/**
* Files have been uploaded to the directory defined by config.js as upload directory
* Files have a temporary name, that was saved as 'filename' of the file object that is
* referenced in reuquest.files array.
* This method finds the file and changes it to the file name that was originally called
* when it was uploaded. To prevent files from being overwritten, a timestamp is added between
* the filename and its extension
* @param request
* @param fieldName
* @returns {string}
*/
static collectFile(request, fieldName) {
let uploadedFileName = '';
if (request.files && request.files.length > 0) {
const fileObject = request.files.find(file => file.fieldname === fieldName);
if (fileObject) {
const fileArray = fileObject.originalname.split('.');
const extension = fileArray.pop();
fileArray.push(`_${Date.now()}`);
uploadedFileName = `${fileArray.join('')}.${extension}`;
fs.renameSync(path.join(config.FILE_UPLOAD_PATH, fileObject.filename),
path.join(config.FILE_UPLOAD_PATH, uploadedFileName));
}
}
return uploadedFileName;
}
// static collectFiles(request) {
// logger.info('Checking if files are expected in schema');
// const requestFiles = {};
// if (request.openapi.schema.requestBody !== undefined) {
// const [contentType] = request.headers['content-type'].split(';');
// if (contentType === 'multipart/form-data') {
// const contentSchema = request.openapi.schema.requestBody.content[contentType].schema;
// Object.entries(contentSchema.properties).forEach(([name, property]) => {
// if (property.type === 'string' && ['binary', 'base64'].indexOf(property.format) > -1) {
// const fileObject = request.files.find(file => file.fieldname === name);
// const fileArray = fileObject.originalname.split('.');
// const extension = fileArray.pop();
// fileArray.push(`_${Date.now()}`);
// const uploadedFileName = `${fileArray.join('')}.${extension}`;
// fs.renameSync(path.join(config.FILE_UPLOAD_PATH, fileObject.filename),
// path.join(config.FILE_UPLOAD_PATH, uploadedFileName));
// requestFiles[name] = uploadedFileName;
// }
// });
// } else if (request.openapi.schema.requestBody.content[contentType] !== undefined
// && request.files !== undefined) {
// [request.body] = request.files;
// }
// }
// return requestFiles;
// }
static collectRequestParams(request) {
this.collectFiles(request);
const requestParams = {};
if (request.openapi.schema.requestBody !== undefined) {
requestParams.body = request.body;
const { content } = request.openapi.schema.requestBody;
if (content['application/json'] !== undefined) {
const schema = request.openapi.schema.requestBody.content['application/json'];
if (schema.$ref) {
requestParams[schema.$ref.substr(schema.$ref.lastIndexOf('.'))] = request.body;
} else {
requestParams.body = request.body;
}
} else if (content['multipart/form-data'] !== undefined) {
Object.keys(content['multipart/form-data'].schema.properties).forEach(
(property) => {
const propertyObject = content['multipart/form-data'].schema.properties[property];
if (propertyObject.format !== undefined && propertyObject.format === 'binary') {
requestParams[property] = this.collectFile(request, property);
} else {
requestParams[property] = request.body[property];
}
},
);
}
}
// if (request.openapi.schema.requestBody.content['application/json'] !== undefined) {
// const schema = request.openapi.schema.requestBody.content['application/json'];
// if (schema.$ref) {
// requestParams[schema.$ref.substr(schema.$ref.lastIndexOf('.'))] = request.body;
// } else {
// requestParams.body = request.body;
// }
// }
request.openapi.schema.parameters.forEach((param) => {
if (param.in === 'path') {
requestParams[param.name] = request.openapi.pathParams[param.name];
} else if (param.in === 'query') {
requestParams[param.name] = request.query[param.name];
} else if (param.in === 'header') {
requestParams[param.name] = request.headers[param.name];
}
});
return requestParams;

View File

@@ -1,42 +1,53 @@
/**
* The PetController file is a very simple one, which does not need to be changed manually,
* unless there's a case where business logic reoutes the request to an entity which is not
* the service.
* The heavy lifting of the Controller item is done in Request.js - that is where request
* parameters are extracted and sent to the service, and where response is handled.
*/
const Controller = require('./Controller');
const service = require('../services/PetService');
const addPet = async (request, response) => {
await Controller.handleRequest(request, response, service.addPet);
};
class PetController {
constructor(Service) {
this.service = Service;
}
const deletePet = async (request, response) => {
await Controller.handleRequest(request, response, service.deletePet);
};
async addPet(request, response) {
await Controller.handleRequest(request, response, this.service.addPet);
}
const findPetsByStatus = async (request, response) => {
await Controller.handleRequest(request, response, service.findPetsByStatus);
};
async deletePet(request, response) {
await Controller.handleRequest(request, response, this.service.deletePet);
}
const findPetsByTags = async (request, response) => {
await Controller.handleRequest(request, response, service.findPetsByTags);
};
async findPetsByStatus(request, response) {
await Controller.handleRequest(request, response, this.service.findPetsByStatus);
}
const getPetById = async (request, response) => {
await Controller.handleRequest(request, response, service.getPetById);
};
async findPetsByTags(request, response) {
await Controller.handleRequest(request, response, this.service.findPetsByTags);
}
const updatePet = async (request, response) => {
await Controller.handleRequest(request, response, service.updatePet);
};
async getPetById(request, response) {
await Controller.handleRequest(request, response, this.service.getPetById);
}
const updatePetWithForm = async (request, response) => {
await Controller.handleRequest(request, response, service.updatePetWithForm);
};
async updatePet(request, response) {
await Controller.handleRequest(request, response, this.service.updatePet);
}
const uploadFile = async (request, response) => {
await Controller.handleRequest(request, response, service.uploadFile);
};
async updatePetWithForm(request, response) {
await Controller.handleRequest(request, response, this.service.updatePetWithForm);
}
async uploadFile(request, response) {
await Controller.handleRequest(request, response, this.service.uploadFile);
}
}
module.exports = PetController;
module.exports = {
addPet,
deletePet,
findPetsByStatus,
findPetsByTags,
getPetById,
updatePet,
updatePetWithForm,
uploadFile,
};

View File

@@ -1,26 +1,33 @@
/**
* The StoreController file is a very simple one, which does not need to be changed manually,
* unless there's a case where business logic reoutes the request to an entity which is not
* the service.
* The heavy lifting of the Controller item is done in Request.js - that is where request
* parameters are extracted and sent to the service, and where response is handled.
*/
const Controller = require('./Controller');
const service = require('../services/StoreService');
const deleteOrder = async (request, response) => {
await Controller.handleRequest(request, response, service.deleteOrder);
};
class StoreController {
constructor(Service) {
this.service = Service;
}
const getInventory = async (request, response) => {
await Controller.handleRequest(request, response, service.getInventory);
};
async deleteOrder(request, response) {
await Controller.handleRequest(request, response, this.service.deleteOrder);
}
const getOrderById = async (request, response) => {
await Controller.handleRequest(request, response, service.getOrderById);
};
async getInventory(request, response) {
await Controller.handleRequest(request, response, this.service.getInventory);
}
const placeOrder = async (request, response) => {
await Controller.handleRequest(request, response, service.placeOrder);
};
async getOrderById(request, response) {
await Controller.handleRequest(request, response, this.service.getOrderById);
}
async placeOrder(request, response) {
await Controller.handleRequest(request, response, this.service.placeOrder);
}
}
module.exports = StoreController;
module.exports = {
deleteOrder,
getInventory,
getOrderById,
placeOrder,
};

View File

@@ -1,42 +1,53 @@
/**
* The UserController file is a very simple one, which does not need to be changed manually,
* unless there's a case where business logic reoutes the request to an entity which is not
* the service.
* The heavy lifting of the Controller item is done in Request.js - that is where request
* parameters are extracted and sent to the service, and where response is handled.
*/
const Controller = require('./Controller');
const service = require('../services/UserService');
const createUser = async (request, response) => {
await Controller.handleRequest(request, response, service.createUser);
};
class UserController {
constructor(Service) {
this.service = Service;
}
const createUsersWithArrayInput = async (request, response) => {
await Controller.handleRequest(request, response, service.createUsersWithArrayInput);
};
async createUser(request, response) {
await Controller.handleRequest(request, response, this.service.createUser);
}
const createUsersWithListInput = async (request, response) => {
await Controller.handleRequest(request, response, service.createUsersWithListInput);
};
async createUsersWithArrayInput(request, response) {
await Controller.handleRequest(request, response, this.service.createUsersWithArrayInput);
}
const deleteUser = async (request, response) => {
await Controller.handleRequest(request, response, service.deleteUser);
};
async createUsersWithListInput(request, response) {
await Controller.handleRequest(request, response, this.service.createUsersWithListInput);
}
const getUserByName = async (request, response) => {
await Controller.handleRequest(request, response, service.getUserByName);
};
async deleteUser(request, response) {
await Controller.handleRequest(request, response, this.service.deleteUser);
}
const loginUser = async (request, response) => {
await Controller.handleRequest(request, response, service.loginUser);
};
async getUserByName(request, response) {
await Controller.handleRequest(request, response, this.service.getUserByName);
}
const logoutUser = async (request, response) => {
await Controller.handleRequest(request, response, service.logoutUser);
};
async loginUser(request, response) {
await Controller.handleRequest(request, response, this.service.loginUser);
}
const updateUser = async (request, response) => {
await Controller.handleRequest(request, response, service.updateUser);
};
async logoutUser(request, response) {
await Controller.handleRequest(request, response, this.service.logoutUser);
}
async updateUser(request, response) {
await Controller.handleRequest(request, response, this.service.updateUser);
}
}
module.exports = UserController;
module.exports = {
createUser,
createUsersWithArrayInput,
createUsersWithListInput,
deleteUser,
getUserByName,
loginUser,
logoutUser,
updateUser,
};

View File

@@ -1,35 +1,43 @@
// const { Middleware } = require('swagger-express-middleware');
const http = require('http');
const fs = require('fs');
const path = require('path');
const swaggerUI = require('swagger-ui-express');
const yamljs = require('yamljs');
const jsYaml = require('js-yaml');
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const { OpenApiValidator } = require('express-openapi-validator');
const openapiRouter = require('./utils/openapiRouter');
const logger = require('./logger');
const config = require('./config');
class ExpressServer {
constructor(port, openApiYaml) {
this.port = port;
this.app = express();
this.openApiPath = openApiYaml;
this.schema = yamljs.load(openApiYaml);
try {
this.schema = jsYaml.safeLoad(fs.readFileSync(openApiYaml));
} catch (e) {
logger.error('failed to start Express Server', e.message);
}
this.setupMiddleware();
}
setupMiddleware() {
// this.setupAllowedMedia();
this.app.use(cors());
this.app.use(bodyParser.json());
this.app.use(bodyParser.json({ limit: '14MB' }));
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: false }));
this.app.use(cookieParser());
this.app.use('/spec', express.static(path.join(__dirname, 'api')));
this.app.get('/hello', (req, res) => res.send('Hello World. path: '+this.openApiPath));
// this.app.get('/spec', express.static(this.openApiPath));
this.app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(this.schema));
//Simple test to see that the server is up and responding
this.app.get('/hello', (req, res) => res.send(`Hello World. path: ${this.openApiPath}`));
//Send the openapi document *AS GENERATED BY THE GENERATOR*
this.app.get('/openapi', (req, res) => res.sendFile((path.join(__dirname, 'api', 'openapi.yaml'))));
//View the openapi document in a visual interface. Should be able to test from this page
this.app.use('/api-doc', swaggerUI.serve, swaggerUI.setup(this.schema));
this.app.get('/login-redirect', (req, res) => {
res.status(200);
res.json(req.query);
@@ -38,50 +46,31 @@ class ExpressServer {
res.status(200);
res.json(req.query);
});
}
launch() {
new OpenApiValidator({
apiSpecPath: this.openApiPath,
}).install(this.app);
this.app.use(openapiRouter());
this.app.get('/', (req, res) => {
res.status(200);
res.end('Hello World');
});
}
addErrorHandler() {
this.app.use('*', (req, res) => {
res.status(404);
res.send(JSON.stringify({ error: `path ${req.baseUrl} doesn't exist` }));
});
/**
* suppressed eslint rule: The next variable is required here, even though it's not used.
*
** */
// eslint-disable-next-line no-unused-vars
this.app.use((error, req, res, next) => {
const errorResponse = error.error || error.errors || error.message || 'Unknown error';
res.status(error.status || 500);
res.type('json');
res.json({ error: errorResponse });
});
}
async launch() {
return new Promise(
async (resolve, reject) => {
try {
this.addErrorHandler();
this.server = await this.app.listen(this.port, () => {
console.log(`server running on port ${this.port}`);
resolve(this.server);
apiSpec: this.openApiPath,
operationHandlers: path.join(__dirname),
fileUploader: { dest: config.FILE_UPLOAD_PATH },
}).install(this.app)
.catch(e => console.log(e))
.then(() => {
// eslint-disable-next-line no-unused-vars
this.app.use((err, req, res, next) => {
// format errors
res.status(err.status || 500).json({
message: err.message || err,
errors: err.errors || '',
});
} catch (error) {
reject(error);
}
},
);
});
http.createServer(this.app).listen(this.port);
console.log(`Listening on port ${this.port}`);
});
}
async close() {
if (this.server !== undefined) {
await this.server.close();

View File

@@ -1,26 +1,14 @@
const config = require('./config');
const logger = require('./logger');
const ExpressServer = require('./expressServer');
// const App = require('./app');
// const app = new App(config);
// app.launch()
// .then(() => {
// logger.info('Server launched');
// })
// .catch((error) => {
// logger.error('found error, shutting down server');
// app.close()
// .catch(closeError => logger.error(closeError))
// .finally(() => logger.error(error));
// });
const launchServer = async () => {
try {
this.expressServer = new ExpressServer(config.URL_PORT, config.OPENAPI_YAML);
await this.expressServer.launch();
this.expressServer.launch();
logger.info('Express server running');
} catch (error) {
logger.error(error);
logger.error('Express Server failure', error.message);
await this.close();
}
};

View File

@@ -1,17 +1,21 @@
const winston = require('winston');
const { transports, createLogger, format } = require('winston');
const logger = winston.createLogger({
const logger = createLogger({
level: 'info',
format: winston.format.json(),
format: format.combine(
format.timestamp(),
format.json(),
),
defaultMeta: { service: 'user-service' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new transports.Console(),
new transports.File({ filename: 'error.log', level: 'error', timestamp: true }),
new transports.File({ filename: 'combined.log', timestamp: true }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({ format: winston.format.simple() }));
logger.add(new transports.Console({ format: format.simple() }));
}
module.exports = logger;

View File

@@ -1,5 +1,5 @@
{
"name": "openapi-petstore",
"name": "openapi-petstore",
"version": "1.0.0",
"description": "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.",
"main": "index.js",
@@ -15,28 +15,25 @@
"private": true,
"dependencies": {
"body-parser": "^1.19.0",
"connect": "^3.2.0",
"camelcase": "^5.3.1",
"cookie-parser": "^1.4.4",
"cors": "^2.8.5",
"express": "^4.16.4",
"express-openapi-validator": "^1.0.0",
"express-openapi-validator": "^3.9.1",
"js-yaml": "^3.3.0",
"jstoxml": "^1.5.0",
"ono": "^5.0.1",
"openapi-sampler": "^1.0.0-beta.15",
"swagger-express-middleware": "^2.0.2",
"swagger-tools": "^0.10.4",
"swagger-ui-express": "^4.0.2",
"winston": "^3.2.1",
"yamljs": "^0.3.0",
"mocha": "^6.1.4",
"winston": "^3.2.1"
},
"devDependencies": {
"axios": "^0.19.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"eslint": "^5.16.0",
"eslint-config-airbnb-base": "^13.1.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-plugin-import": "^2.17.2",
"form-data": "^2.3.3"
"mocha": "^7.1.1"
},
"eslintConfig": {
"env": {

View File

@@ -1,184 +1,187 @@
/* eslint-disable no-unused-vars */
const Service = require('./Service');
class PetService {
/**
* Add a new pet to the store
*
* body Pet Pet object that needs to be added to the store
* no response value expected for this operation
* */
const addPet = ({ body }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
body,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Deletes a pet
*
* petId Long Pet id to delete
* apiUnderscorekey String (optional)
* no response value expected for this operation
* */
const deletePet = ({ petId, apiUnderscorekey }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
petId,
apiUnderscorekey,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Finds Pets by status
* Multiple status values can be provided with comma separated strings
*
* status List Status values that need to be considered for filter
* returns List
* */
const findPetsByStatus = ({ status }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
status,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Finds Pets by tags
* Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
*
* tags List Tags to filter by
* returns List
* */
const findPetsByTags = ({ tags }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
tags,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Find pet by ID
* Returns a single pet
*
* petId Long ID of pet to return
* returns Pet
* */
const getPetById = ({ petId }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
petId,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Update an existing pet
*
* body Pet Pet object that needs to be added to the store
* no response value expected for this operation
* */
const updatePet = ({ body }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
body,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Updates a pet in the store with form data
*
* petId Long ID of pet that needs to be updated
* name String Updated name of the pet (optional)
* status String Updated status of the pet (optional)
* no response value expected for this operation
* */
const updatePetWithForm = ({ petId, name, status }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
petId,
name,
status,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* uploads an image
*
* petId Long ID of pet to update
* additionalMetadata String Additional data to pass to server (optional)
* file File file to upload (optional)
* returns ApiResponse
* */
const uploadFile = ({ petId, additionalMetadata, file }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
petId,
additionalMetadata,
file,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Add a new pet to the store
*
* body Pet Pet object that needs to be added to the store
* no response value expected for this operation
**/
static addPet({ body }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Deletes a pet
*
* petId Long Pet id to delete
* apiUnderscorekey String (optional)
* no response value expected for this operation
**/
static deletePet({ petId, apiUnderscorekey }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Finds Pets by status
* Multiple status values can be provided with comma separated strings
*
* status List Status values that need to be considered for filter
* returns List
**/
static findPetsByStatus({ status }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Finds Pets by tags
* Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
*
* tags List Tags to filter by
* returns List
**/
static findPetsByTags({ tags }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Find pet by ID
* Returns a single pet
*
* petId Long ID of pet to return
* returns Pet
**/
static getPetById({ petId }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Update an existing pet
*
* body Pet Pet object that needs to be added to the store
* no response value expected for this operation
**/
static updatePet({ body }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Updates a pet in the store with form data
*
* petId Long ID of pet that needs to be updated
* name String Updated name of the pet (optional)
* status String Updated status of the pet (optional)
* no response value expected for this operation
**/
static updatePetWithForm({ petId, name, status }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* uploads an image
*
* petId Long ID of pet to update
* additionalMetadata String Additional data to pass to server (optional)
* file File file to upload (optional)
* returns ApiResponse
**/
static uploadFile({ petId, additionalMetadata, file }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
}
module.exports = PetService;
module.exports = {
addPet,
deletePet,
findPetsByStatus,
findPetsByTags,
getPetById,
updatePet,
updatePetWithForm,
uploadFile,
};

View File

@@ -1,94 +1,91 @@
/* eslint-disable no-unused-vars */
const Service = require('./Service');
class StoreService {
/**
* Delete purchase order by ID
* For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
*
* orderId String ID of the order that needs to be deleted
* no response value expected for this operation
* */
const deleteOrder = ({ orderId }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
orderId,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Returns pet inventories by status
* Returns a map of status codes to quantities
*
* returns Map
* */
const getInventory = () => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Find purchase order by ID
* For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions
*
* orderId Long ID of pet that needs to be fetched
* returns Order
* */
const getOrderById = ({ orderId }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
orderId,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Place an order for a pet
*
* body Order order placed for purchasing the pet
* returns Order
* */
const placeOrder = ({ body }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
body,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Delete purchase order by ID
* For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
*
* orderId String ID of the order that needs to be deleted
* no response value expected for this operation
**/
static deleteOrder({ orderId }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Returns pet inventories by status
* Returns a map of status codes to quantities
*
* returns Map
**/
static getInventory() {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Find purchase order by ID
* For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions
*
* orderId Long ID of pet that needs to be fetched
* returns Order
**/
static getOrderById({ orderId }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Place an order for a pet
*
* body Order order placed for purchasing the pet
* returns Order
**/
static placeOrder({ body }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
}
module.exports = StoreService;
module.exports = {
deleteOrder,
getInventory,
getOrderById,
placeOrder,
};

View File

@@ -1,180 +1,179 @@
/* eslint-disable no-unused-vars */
const Service = require('./Service');
class UserService {
/**
* Create user
* This can only be done by the logged in user.
*
* body User Created user object
* no response value expected for this operation
* */
const createUser = ({ body }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
body,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Creates list of users with given input array
*
* body List List of user object
* no response value expected for this operation
* */
const createUsersWithArrayInput = ({ body }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
body,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Creates list of users with given input array
*
* body List List of user object
* no response value expected for this operation
* */
const createUsersWithListInput = ({ body }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
body,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Delete user
* This can only be done by the logged in user.
*
* username String The name that needs to be deleted
* no response value expected for this operation
* */
const deleteUser = ({ username }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
username,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Get user by user name
*
* username String The name that needs to be fetched. Use user1 for testing.
* returns User
* */
const getUserByName = ({ username }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
username,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Logs user into the system
*
* username String The user name for login
* password String The password for login in clear text
* returns String
* */
const loginUser = ({ username, password }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
username,
password,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Logs out current logged in user session
*
* no response value expected for this operation
* */
const logoutUser = () => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Updated user
* This can only be done by the logged in user.
*
* username String name that need to be deleted
* body User Updated user object
* no response value expected for this operation
* */
const updateUser = ({ username, body }) => new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse({
username,
body,
}));
} catch (e) {
reject(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
/**
* Create user
* This can only be done by the logged in user.
*
* body User Created user object
* no response value expected for this operation
**/
static createUser({ body }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Creates list of users with given input array
*
* body List List of user object
* no response value expected for this operation
**/
static createUsersWithArrayInput({ body }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Creates list of users with given input array
*
* body List List of user object
* no response value expected for this operation
**/
static createUsersWithListInput({ body }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Delete user
* This can only be done by the logged in user.
*
* username String The name that needs to be deleted
* no response value expected for this operation
**/
static deleteUser({ username }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Get user by user name
*
* username String The name that needs to be fetched. Use user1 for testing.
* returns User
**/
static getUserByName({ username }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Logs user into the system
*
* username String The user name for login
* password String The password for login in clear text
* returns String
**/
static loginUser({ username, password }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Logs out current logged in user session
*
* no response value expected for this operation
**/
static logoutUser() {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
/**
* Updated user
* This can only be done by the logged in user.
*
* username String name that need to be deleted
* body User Updated user object
* no response value expected for this operation
**/
static updateUser({ username, body }) {
return new Promise(
async (resolve) => {
try {
resolve(Service.successResponse(''));
} catch (e) {
resolve(Service.rejectResponse(
e.message || 'Invalid input',
e.status || 405,
));
}
},
);
}
}
module.exports = UserService;
module.exports = {
createUser,
createUsersWithArrayInput,
createUsersWithListInput,
deleteUser,
getUserByName,
loginUser,
logoutUser,
updateUser,
};