Add a new NodeJS Express server generator (#3567)

* create nodejs express esrver

* 1st commit of the express.js module. Express server working, api-docs loads properly. No real paths yet

* 1st commit of the express.js module. Express server working, api-docs loads properly. No real paths yet (#2839)

* Working Express server with successful routing to controllers.

* rewrote controllers and services. Haven't tested yet

* controllers and services have passed tests successfully

* Added documentation

* Added documentation

* Support for openApi v3, using 'express-openapi-validator' for parsing and validation, and an internal router to pass arguments to controllers and services. /controllers/Pet.js and /services/PetService.js should be used for reverse engineering for future codegen script

* update generator and template

* update samples

* more update

* update service, controller

* add vendor extensions

* some updates to adapt to changes in the generator (removing references to swager); some work on handling file uploads; some work on tests

* Update NodeJS server generator and templates based on new output (#3261)

* update generator and template

* update samples

* more update

* update service, controller

* add vendor extensions

* update doc

* Changed routing code to follow the following convention:
Each path operation has a 'x-openapi-router-controller' and 'x-openapi-router-service'. Automated files will be placed under /controllers and /services respectively.
Controller file names will end with 'Controller.js'.
Removed swaggerRouter, replaced it with openapiRouter
Routing works and simple tests show a return of 200 to requests.

* [nodejs-express-server] various updates, fixes (#3319)

* various fix

* remove dot from service

* add space

* better method empty argument

* remove test service (#3379)

* add new doc

* 1. routingTests.js runs through all operations described in openapi.yaml and tries calling them, expecting 200 in return. Currently not all tests pass - not supporting xml, and problems with formData
2. Removed old testing files.
3. Added model files - contain data and structure as defined in openapi.yaml. Model.js has static methods relevant to all model files.
4. Changed openapi.yaml to allow running tests easily.

* 1. routingTests.js runs through all operations described in openapi.yaml and tries calling them, expecting 200 in return. Currently not all tests pass - not supporting xml, and problems with formData (#3442)

2. Removed old testing files.
3. Added model files - contain data and structure as defined in openapi.yaml. Model.js has static methods relevant to all model files.
4. Changed openapi.yaml to allow running tests easily.

* added model classes. Currently as a concept only. Seems like won't be in use

* Updated README.md to be a detailed description of the project.
Removed test files that are not needed.
Removed utils/writer.js which is not needed, and the references to it in the codegen files

* Removed redundant file app.js - this file has no benefit at this point. index.js now calls ExpressServer.js directly. Updated files that used to call app.js. Updated README.md accordingly
Added a path to call the openapi.yaml, and a test file for all endpoints that are not in the openapi.yaml, ensuring that they return 200. Updated README.md accordingly

* Remove test controller (#3575)

* remove test controller

* add back changes to templates

* remove app.js

* update wording
This commit is contained in:
William Cheng 2019-08-09 00:30:47 +08:00 committed by GitHub
parent fbb2f1e05a
commit 2d7cc778db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 4257 additions and 0 deletions

View File

@ -730,6 +730,7 @@ Here is a list of template creators:
* JAX-RS RestEasy (JBoss EAP): @jfiala
* Kotlin: @jimschubert [:heart:](https://www.patreon.com/jimschubert)
* Kotlin (Spring Boot): @dr4ke616
* NodeJS Express: @YishTish
* PHP Laravel: @renepardon
* PHP Lumen: @abcsun
* PHP Slim: @jfastnacht

View File

@ -0,0 +1,32 @@
#!/bin/sh
SCRIPT="$0"
echo "# START SCRIPT: $SCRIPT"
while [ -h "$SCRIPT" ] ; do
ls=`ls -ld "$SCRIPT"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
SCRIPT="$link"
else
SCRIPT=`dirname "$SCRIPT"`/"$link"
fi
done
if [ ! -d "${APP_DIR}" ]; then
APP_DIR=`dirname "$SCRIPT"`/..
APP_DIR=`cd "${APP_DIR}"; pwd`
fi
executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar"
if [ ! -f "$executable" ]
then
mvn -B clean package
fi
# if you've executed sbt assembly previously it will use that instead.
export JAVA_OPTS="${JAVA_OPTS} -Xmx1024M -DloggerPath=conf/log4j.properties"
ags="generate -t modules/openapi-generator/src/main/resources/nodejs-express-server -i modules/openapi-generator/src/test/resources/2_0/petstore.yaml -g nodejs-express-server -o samples/server/petstore/nodejs-express-server -Dservice $@"
java $JAVA_OPTS -jar $executable $ags

View File

@ -94,6 +94,7 @@ The following generators are available:
- [jaxrs-spec](generators/jaxrs-spec.md)
- [kotlin-server](generators/kotlin-server.md)
- [kotlin-spring](generators/kotlin-spring.md)
- [nodejs-express-server](generators/nodejs-express-server.md) (beta)
- [nodejs-server-deprecated](generators/nodejs-server-deprecated.md) (deprecated)
- [php-laravel](generators/php-laravel.md)
- [php-lumen](generators/php-lumen.md)

View File

@ -0,0 +1,14 @@
---
id: generator-opts-server-nodejs-express-server
title: Config Options for nodejs-express-server
sidebar_label: nodejs-express-server
---
| Option | Description | Values | Default |
| ------ | ----------- | ------ | ------- |
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false|
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
|serverPort|TCP port to listen on.| |null|

View File

@ -0,0 +1,390 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openapitools.codegen.languages;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.PathItem.HttpMethod;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.info.Info;
import org.openapitools.codegen.*;
import org.openapitools.codegen.meta.GeneratorMetadata;
import org.openapitools.codegen.meta.Stability;
import org.openapitools.codegen.utils.URLPathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.URL;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import static org.openapitools.codegen.utils.StringUtils.*;
public class NodeJSExpressServerCodegen extends DefaultCodegen implements CodegenConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(NodeJSExpressServerCodegen.class);
public static final String EXPORTED_NAME = "exportedName";
public static final String SERVER_PORT = "serverPort";
protected String apiVersion = "1.0.0";
protected String defaultServerPort = "8080";
protected String implFolder = "services";
protected String projectName = "openapi-server";
protected String exportedName;
public NodeJSExpressServerCodegen() {
super();
generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata)
.stability(Stability.BETA)
.build();
outputFolder = "generated-code/nodejs-express-server";
embeddedTemplateDir = templateDir = "nodejs-express-server";
setReservedWordsLowerCase(
Arrays.asList(
"break", "case", "class", "catch", "const", "continue", "debugger",
"default", "delete", "do", "else", "export", "extends", "finally",
"for", "function", "if", "import", "in", "instanceof", "let", "new",
"return", "super", "switch", "this", "throw", "try", "typeof", "var",
"void", "while", "with", "yield")
);
additionalProperties.put("apiVersion", apiVersion);
additionalProperties.put("implFolder", implFolder);
// no model file
modelTemplateFiles.clear();
apiTemplateFiles.put("controller.mustache", ".js");
apiTemplateFiles.put("service.mustache", ".js");
supportingFiles.add(new SupportingFile("openapi.mustache", "api", "openapi.yaml"));
supportingFiles.add(new SupportingFile("config.mustache", "", "config.js"));
supportingFiles.add(new SupportingFile("expressServer.mustache", "", "expressServer.js"));
supportingFiles.add(new SupportingFile("index.mustache", "", "index.js"));
supportingFiles.add(new SupportingFile("logger.mustache", "", "logger.js"));
supportingFiles.add(new SupportingFile("eslintrc.mustache", "", ".eslintrc.json"));
// utils folder
supportingFiles.add(new SupportingFile("utils" + File.separator + "openapiRouter.mustache", "utils", "openapiRouter.js"));
// controllers folder
supportingFiles.add(new SupportingFile("controllers" + File.separator + "index.mustache", "controllers", "index.js"));
supportingFiles.add(new SupportingFile("controllers" + File.separator + "Controller.mustache", "controllers", "Controller.js"));
// service folder
supportingFiles.add(new SupportingFile("services" + File.separator + "index.mustache", "services", "index.js"));
supportingFiles.add(new SupportingFile("services" + File.separator + "Service.mustache", "services", "Service.js"));
// do not overwrite if the file is already present
writeOptional(outputFolder, new SupportingFile("package.mustache", "", "package.json"));
writeOptional(outputFolder, new SupportingFile("README.mustache", "", "README.md"));
cliOptions.add(new CliOption(SERVER_PORT,
"TCP port to listen on."));
}
@Override
public String apiPackage() {
return "controllers";
}
/**
* Configures the type of generator.
*
* @return the CodegenType for this generator
* @see org.openapitools.codegen.CodegenType
*/
@Override
public CodegenType getTag() {
return CodegenType.SERVER;
}
/**
* Configures a friendly name for the generator. This will be used by the generator
* to select the library with the -g flag.
*
* @return the friendly name for the generator
*/
@Override
public String getName() {
return "nodejs-express-server";
}
/**
* Returns human-friendly help for the generator. Provide the consumer with help
* tips, parameters here
*
* @return A string value for the help message
*/
@Override
public String getHelp() {
return "Generates a NodeJS Express server (alpha). IMPORTANT: this generator may subject to breaking changes without further notice).";
}
@Override
public String toApiName(String name) {
if (name.length() == 0) {
return "Default";
}
return camelize(name);
}
@Override
public String toApiFilename(String name) {
return toApiName(name) + "Controller";
}
@Override
public String apiFilename(String templateName, String tag) {
String result = super.apiFilename(templateName, tag);
if (templateName.equals("service.mustache")) {
String stringToMatch = File.separator + "controllers" + File.separator;
String replacement = File.separator + implFolder + File.separator;
result = result.replaceAll(Pattern.quote(stringToMatch), replacement);
stringToMatch = "Controller.js";
replacement = "Service.js";
result = result.replaceAll(Pattern.quote(stringToMatch), replacement);
}
return result;
}
/*
@Override
protected String implFileFolder(String output) {
return outputFolder + File.separator + output + File.separator + apiPackage().replace('.', File.separatorChar);
}
*/
/**
* Escapes a reserved word as defined in the `reservedWords` array. Handle escaping
* those terms here. This logic is only called if a variable matches the reserved words
*
* @return the escaped term
*/
@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "_" + name;
}
/**
* Location to write api files. You can use the apiPackage() as defined when the class is
* instantiated
*/
@Override
public String apiFileFolder() {
return outputFolder + File.separator + apiPackage().replace('.', File.separatorChar);
}
public String getExportedName() {
return exportedName;
}
public void setExportedName(String name) {
exportedName = name;
}
@Override
public Map<String, Object> postProcessOperationsWithModels(Map<String, Object> objs, List<Object> allModels) {
@SuppressWarnings("unchecked")
Map<String, Object> objectMap = (Map<String, Object>) objs.get("operations");
@SuppressWarnings("unchecked")
List<CodegenOperation> operations = (List<CodegenOperation>) objectMap.get("operation");
for (CodegenOperation operation : operations) {
operation.httpMethod = operation.httpMethod.toLowerCase(Locale.ROOT);
List<CodegenParameter> params = operation.allParams;
if (params != null && params.size() == 0) {
operation.allParams = null;
}
List<CodegenResponse> responses = operation.responses;
if (responses != null) {
for (CodegenResponse resp : responses) {
if ("0".equals(resp.code)) {
resp.code = "default";
}
}
}
if (operation.examples != null && !operation.examples.isEmpty()) {
// Leave application/json* items only
for (Iterator<Map<String, String>> it = operation.examples.iterator(); it.hasNext(); ) {
final Map<String, String> example = it.next();
final String contentType = example.get("contentType");
if (contentType == null || !contentType.startsWith("application/json")) {
it.remove();
}
}
}
}
return objs;
}
@SuppressWarnings("unchecked")
private static List<Map<String, Object>> getOperations(Map<String, Object> objs) {
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
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) {
result.add((Map<String, Object>) api.get("operations"));
}
return result;
}
private static List<Map<String, Object>> sortOperationsByPath(List<CodegenOperation> ops) {
Multimap<String, CodegenOperation> opsByPath = ArrayListMultimap.create();
for (CodegenOperation op : ops) {
opsByPath.put(op.path, op);
}
List<Map<String, Object>> opsByPathList = new ArrayList<Map<String, Object>>();
for (Entry<String, Collection<CodegenOperation>> entry : opsByPath.asMap().entrySet()) {
Map<String, Object> opsByPathEntry = new HashMap<String, Object>();
opsByPathList.add(opsByPathEntry);
opsByPathEntry.put("path", entry.getKey());
opsByPathEntry.put("operation", entry.getValue());
List<CodegenOperation> operationsForThisPath = Lists.newArrayList(entry.getValue());
operationsForThisPath.get(operationsForThisPath.size() - 1).hasMore = false;
if (opsByPathList.size() < opsByPath.asMap().size()) {
opsByPathEntry.put("hasMore", "true");
}
}
return opsByPathList;
}
@Override
public void processOpts() {
super.processOpts();
if (additionalProperties.containsKey(EXPORTED_NAME)) {
setExportedName((String) additionalProperties.get(EXPORTED_NAME));
}
/*
* Supporting Files. You can write single files for the generator with the
* entire object tree available. If the input file has a suffix of `.mustache
* it will be processed by the template engine. Otherwise, it will be copied
*/
// supportingFiles.add(new SupportingFile("controller.mustache",
// "controllers",
// "controller.js")
// );
}
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
URL url = URLPathUtils.getServerURL(openAPI);
String host = URLPathUtils.getProtocolAndHost(url);
String port = URLPathUtils.getPort(url, defaultServerPort) ;
String basePath = url.getPath();
if (additionalProperties.containsKey(SERVER_PORT)) {
port = additionalProperties.get(SERVER_PORT).toString();
}
this.additionalProperties.put(SERVER_PORT, port);
if (openAPI.getInfo() != null) {
Info info = openAPI.getInfo();
if (info.getTitle() != null) {
// when info.title is defined, use it for projectName
// used in package.json
projectName = info.getTitle()
.replaceAll("[^a-zA-Z0-9]", "-")
.replaceAll("^[-]*", "")
.replaceAll("[-]*$", "")
.replaceAll("[-]{2,}", "-")
.toLowerCase(Locale.ROOT);
this.additionalProperties.put("projectName", projectName);
}
}
// need vendor extensions
Paths paths = openAPI.getPaths();
if (paths != null) {
for (String pathname : paths.keySet()) {
PathItem path = paths.get(pathname);
Map<HttpMethod, Operation> operationMap = path.readOperationsMap();
if (operationMap != null) {
for (HttpMethod method : operationMap.keySet()) {
Operation operation = operationMap.get(method);
String tag = "default";
if (operation.getTags() != null && operation.getTags().size() > 0) {
tag = toApiName(operation.getTags().get(0));
}
if (operation.getOperationId() == null) {
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");
}
}
}
}
}
}
@Override
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
generateYAMLSpecFile(objs);
for (Map<String, Object> operations : getOperations(objs)) {
@SuppressWarnings("unchecked")
List<CodegenOperation> ops = (List<CodegenOperation>) operations.get("operation");
List<Map<String, Object>> opsByPathList = sortOperationsByPath(ops);
operations.put("operationsByPath", opsByPathList);
}
return super.postProcessSupportingFileData(objs);
}
@Override
public String removeNonNameElementToCamelCase(String name) {
return removeNonNameElementToCamelCase(name, "[-:;#]");
}
@Override
public String escapeUnsafeCharacters(String input) {
return input.replace("*/", "*_/").replace("/*", "/_*");
}
@Override
public String escapeQuotationMark(String input) {
// remove " to avoid code injection
return input.replace("\"", "");
}
}

View File

@ -61,6 +61,7 @@ org.openapitools.codegen.languages.JMeterClientCodegen
org.openapitools.codegen.languages.LuaClientCodegen
org.openapitools.codegen.languages.MysqlSchemaCodegen
org.openapitools.codegen.languages.NodeJSServerCodegen
org.openapitools.codegen.languages.NodeJSExpressServerCodegen
org.openapitools.codegen.languages.ObjcClientCodegen
org.openapitools.codegen.languages.OCamlClientCodegen
org.openapitools.codegen.languages.OpenAPIGenerator

View File

@ -0,0 +1,72 @@
{{=<% %>=}}
# 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
- 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.
### 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.
- **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.
#### utils/
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.
- **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.
- **{{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.
#### 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.
- **index.js** - load all the services that were generated for this project, and export them to be used dynamically by the `openapiRouter.js`. If you would like to customize your service, it is advised that you link to your controller here, and ensure that the codegen does not rewrite this file.
- **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.
#### 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.
- **routingTests.js** - Runs through all the endpoints defined in the `openapi.yaml`, and constructs a dummy request to send to the server. Confirms that the response code is 200. At this point requests containing xml or formData fail - currently they are not supported in the router.
- **additionalEndpointsTests.js** - A test file for all the endpoints that are defined outside the openapi.yaml scope. Confirms that these endpoints return a successful 200 response.
Future tests should be written to ensure that the response of every request sent should conform to the structure defined in the `openapi.yaml`. This test will fail 100% initially, and the job of the development team will be to clear these tests.
#### 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

@ -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

@ -0,0 +1,12 @@
const path = require('path');
const config = {
ROOT_DIR: __dirname,
URL_PORT: 3000,
URL_PATH: 'http://localhost',
BASE_VERSION: 'v2',
CONTROLLER_DIRECTORY: path.join(__dirname, 'controllers'),
};
config.OPENAPI_YAML = path.join(config.ROOT_DIR, 'api', 'openapi.yaml');
config.FULL_PATH = `${config.URL_PATH}:${config.URL_PORT}/${config.BASE_VERSION}`;
module.exports = config;

View File

@ -0,0 +1,18 @@
const Controller = require('./Controller');
class {{{classname}}}Controller {
constructor(Service) {
this.service = Service;
}
{{#operations}}
{{#operation}}
async {{operationId}}(request, response) {
await Controller.handleRequest(request, response, this.service.{{operationId}});
}
{{/operation}}
}
module.exports = {{classname}}Controller;
{{/operations}}

View File

@ -0,0 +1,72 @@
const logger = require('../logger');
class Controller {
static sendResponse(response, payload) {
/**
* The default response-code is 200. We want to allow to change that. in That case,
* payload will be an object consisting of a code and a payload. If not customized
* send 200 and the payload as received in this method.
*/
response.status(payload.code || 200);
const responsePayload = payload.payload !== undefined ? payload.payload : payload;
if (responsePayload instanceof Object) {
response.json(responsePayload);
} else {
response.end(responsePayload);
}
}
static sendError(response, error) {
response.status(error.code || 500);
if (error.error instanceof Object) {
response.json(error.error);
} else {
response.end(error.error || error.message);
}
}
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;
}
}
}
static collectRequestParams(request) {
this.collectFiles(request);
const requestParams = {};
if (request.openapi.schema.requestBody !== undefined) {
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];
}
});
return requestParams;
}
static async handleRequest(request, response, serviceOperation) {
try {
const serviceResponse = await serviceOperation(this.collectRequestParams(request));
Controller.sendResponse(response, serviceResponse);
} catch (error) {
Controller.sendError(response, error);
}
}
}
module.exports = Controller;

View File

@ -0,0 +1,25 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
{{#operation}}
{{#-first}}
const {{classname}}Controller = require('./{{classname}}Controller');
{{/-first}}
{{/operation}}
{{/operations}}
{{/apis}}
{{/apiInfo}}
module.exports = {
{{#apiInfo}}
{{#apis}}
{{#operations}}
{{#operation}}
{{#-first}}
{{classname}}Controller,
{{/-first}}
{{/operation}}
{{/operations}}
{{/apis}}
{{/apiInfo}}
};

View File

@ -0,0 +1,72 @@
const Service = require('../services/Service');
const testItems = require('../tests/testFiles/testItems.json');
class TestService {
static testGetController() {
return new Promise(
async (resolve, reject) => {
try {
resolve(Service.successResponse(
testItems,
200,
));
} catch (e) {
const message = e.getMessage() || 'Could not get items. Server error';
reject(Service.rejectResponse(message, 500));
}
},
);
sendResponse(request, response) {
response.status(200);
const objectToReturn = {};
Object.keys(request.swagger.paramValues).forEach((key) => {
const val = request.swagger.paramValues[key];
if (val instanceof Object) {
objectToReturn[key] = val.originalname || val.name || val;
} else {
objectToReturn[key] = request.swagger.paramValues[key];
}
});
response.json(objectToReturn);
}
confirmRouteGetSingle(request, response) {
this.sendResponse(request, response);
}
confirmRouteGetMany(request, response) {
this.sendResponse(request, response);
}
confirmRoutePost(request, response) {
this.sendResponse(request, response);
}
confirmRoutePut(request, response) {
this.sendResponse(request, response);
}
async testGetController(request, response) {
await Controller.handleRequest(request, response, this.service.testGetController);
}
async testPostController(request, response) {
await Controller.handleRequest(request, response, this.service.testPostController);
}
async testPutController(request, response) {
await Controller.handleRequest(request, response, this.service.testPutController);
}
async testDeleteController(request, response) {
await Controller.handleRequest(request, response, this.service.testDeleteController);
}
async testFindByIdController(request, response) {
await Controller.handleRequest(request, response, this.service.testFindByIdController);
}
}
module.exports = TestController;

View File

@ -0,0 +1,8 @@
// Use this file as a starting point for your project's .eslintrc.
// Copy this file, and add rule overrides as needed.
{
"extends": "airbnb",
"rules": {
"no-console": "off"
}
}

View File

@ -0,0 +1,93 @@
// const { Middleware } = require('swagger-express-middleware');
const path = require('path');
const swaggerUI = require('swagger-ui-express');
const yamljs = require('yamljs');
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');
class ExpressServer {
constructor(port, openApiYaml) {
this.port = port;
this.app = express();
this.openApiPath = openApiYaml;
this.schema = yamljs.load(openApiYaml);
this.setupMiddleware();
}
setupMiddleware() {
// this.setupAllowedMedia();
this.app.use(cors());
this.app.use(bodyParser.json());
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));
this.app.get('/login-redirect', (req, res) => {
res.status(200);
res.json(req.query);
});
this.app.get('/oauth2-redirect.html', (req, res) => {
res.status(200);
res.json(req.query);
});
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);
});
} catch (error) {
reject(error);
}
},
);
}
async close() {
if (this.server !== undefined) {
await this.server.close();
console.log(`Server on port ${this.port} shut down`);
}
}
}
module.exports = ExpressServer;

View File

@ -0,0 +1,28 @@
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();
logger.info('Express server running');
} catch (error) {
logger.error(error);
await this.close();
}
};
launchServer().catch(e => logger.error(e));

View File

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

View File

@ -0,0 +1 @@
{{{openapi-yaml}}}

View File

@ -0,0 +1,46 @@
{
"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",
"scripts": {
"prestart": "npm install",
"start": "node index.js"
},
"keywords": [
"openapi-generator",
"openapi"
],
"license": "Unlicense",
"private": true,
"dependencies": {
"body-parser": "^1.19.0",
"connect": "^3.2.0",
"cookie-parser": "^1.4.4",
"cors": "^2.8.5",
"express": "^4.16.4",
"express-openapi-validator": "^1.0.0",
"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",
"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-plugin-import": "^2.17.2",
"form-data": "^2.3.3"
},
"eslintConfig": {
"env": {
"node": true
}
}
}

View File

@ -0,0 +1,45 @@
/* 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,
));
}
},
);
}
{{/operation}}
}
module.exports = {{{classname}}}Service;
{{/operations}}

View File

@ -0,0 +1,11 @@
class Service {
static rejectResponse(error, code = 500) {
return { error, code };
}
static successResponse(payload, code = 200) {
return { payload, code };
}
}
module.exports = Service;

View File

@ -0,0 +1,25 @@
{{#apiInfo}}
{{#apis}}
{{#operations}}
{{#operation}}
{{#-first}}
const {{classname}}Service = require('./{{classname}}Service');
{{/-first}}
{{/operation}}
{{/operations}}
{{/apis}}
{{/apiInfo}}
module.exports = {
{{#apiInfo}}
{{#apis}}
{{#operations}}
{{#operation}}
{{#-first}}
{{classname}}Service,
{{/-first}}
{{/operation}}
{{/operations}}
{{/apis}}
{{/apiInfo}}
};

View File

@ -0,0 +1,67 @@
const logger = require('../logger');
const controllers = require('../controllers');
const Services = require('../services');
function handleError(err, request, response, next) {
logger.error(err);
const code = err.code || 400;
response.status(code);
response.error = err;
next(JSON.stringify({
code,
error: err,
}));
}
/**
* The purpose of this route is to collect the request variables as defined in the
* OpenAPI document and pass them to the handling controller as another Express
* middleware. All parameters are collected in the requet.swagger.values key-value object
*
* The assumption is that security handlers have already verified and allowed access
* to this path. If the business-logic of a particular path is dependant on authentication
* parameters (e.g. scope checking) - it is recommended to define the authentication header
* as one of the parameters expected in the OpenAPI/Swagger document.
*
* Requests made to paths that are not in the OpernAPI scope
* are passed on to the next middleware handler.
* @returns {Function}
*/
function openApiRouter() {
return async (request, response, next) => {
try {
/**
* This middleware runs after a previous process have applied an openapi object
* to the request.
* If none was applied This is because the path requested is not in the schema.
* If there's no openapi object, we have nothing to do, and pass on to next middleware.
*/
if (request.openapi === undefined
|| request.openapi.schema === undefined
) {
next();
return;
}
// request.swagger.paramValues = {};
// request.swagger.params.forEach((param) => {
// request.swagger.paramValues[param.name] = getValueFromRequest(request, param);
// });
const controllerName = request.openapi.schema['x-openapi-router-controller'];
const serviceName = request.openapi.schema['x-openapi-router-service'];
if (!controllers[controllerName] || controllers[controllerName] === undefined) {
handleError(`request sent to controller '${controllerName}' which has not been defined`,
request, response, next);
} else {
const apiController = new controllers[controllerName](Services[serviceName]);
const controllerOperation = request.openapi.schema.operationId;
await apiController[controllerOperation](request, response, next);
}
} catch (error) {
console.error(error);
const err = { code: 500, error: error.message };
handleError(err, request, response, next);
}
};
}
module.exports = openApiRouter;

View File

@ -0,0 +1,43 @@
var ResponsePayload = function(code, payload) {
this.code = code;
this.payload = payload;
}
exports.respondWithCode = function(code, payload) {
return new ResponsePayload(code, payload);
}
var writeJson = exports.writeJson = function(response, arg1, arg2) {
var code;
var payload;
if(arg1 && arg1 instanceof ResponsePayload) {
writeJson(response, arg1.payload, arg1.code);
return;
}
if(arg2 && Number.isInteger(arg2)) {
code = arg2;
}
else {
if(arg1 && Number.isInteger(arg1)) {
code = arg1;
}
}
if(code && arg1) {
payload = arg1;
}
else if(arg1) {
payload = arg1;
}
if(!code) {
// if no response code given, we default to 200
code = 200;
}
if(typeof payload === 'object') {
payload = JSON.stringify(payload, null, 2);
}
response.writeHead(code, {'Content-Type': 'application/json'});
response.end(payload);
}

View File

@ -0,0 +1,8 @@
// Use this file as a starting point for your project's .eslintrc.
// Copy this file, and add rule overrides as needed.
{
"extends": "airbnb",
"rules": {
"no-console": "off"
}
}

View File

@ -0,0 +1,8 @@
// Use this file as a starting point for your project's .eslintrc.
// Copy this file, and add rule overrides as needed.
{
"extends": "airbnb",
"rules": {
"no-console": "off"
}
}

View File

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@ -0,0 +1 @@
4.1.0-SNAPSHOT

View File

@ -0,0 +1,71 @@
# 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
- 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.
### 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.
- **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.
#### utils/
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.
- **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.
- **{{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.
#### 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.
- **index.js** - load all the services that were generated for this project, and export them to be used dynamically by the `openapiRouter.js`. If you would like to customize your service, it is advised that you link to your controller here, and ensure that the codegen does not rewrite this file.
- **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.
#### 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.
- **routingTests.js** - Runs through all the endpoints defined in the `openapi.yaml`, and constructs a dummy request to send to the server. Confirms that the response code is 200. At this point requests containing xml or formData fail - currently they are not supported in the router.
- **additionalEndpointsTests.js** - A test file for all the endpoints that are defined outside the openapi.yaml scope. Confirms that these endpoints return a successful 200 response.
Future tests should be written to ensure that the response of every request sent should conform to the structure defined in the `openapi.yaml`. This test will fail 100% initially, and the job of the development team will be to clear these tests.
#### 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

@ -0,0 +1,802 @@
openapi: 3.0.1
info:
description: This is a sample server Petstore server. For this sample, you can use
the api key `special-key` to test the authorization filters.
license:
name: Apache-2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
title: OpenAPI Petstore
version: 1.0.0
servers:
- url: http://petstore.swagger.io/v2
tags:
- description: Everything about your Pets
name: pet
- description: Access to Petstore orders
name: store
- description: Operations about user
name: user
paths:
/pet:
post:
operationId: addPet
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
description: Pet object that needs to be added to the store
required: true
responses:
405:
content: {}
description: Invalid input
security:
- petstore_auth:
- write:pets
- read:pets
summary: Add a new pet to the store
tags:
- pet
x-codegen-request-body-name: body
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
put:
operationId: updatePet
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
application/xml:
schema:
$ref: '#/components/schemas/Pet'
description: Pet object that needs to be added to the store
required: true
responses:
400:
content: {}
description: Invalid ID supplied
404:
content: {}
description: Pet not found
405:
content: {}
description: Validation exception
security:
- petstore_auth:
- write:pets
- read:pets
summary: Update an existing pet
tags:
- pet
x-codegen-request-body-name: body
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
/pet/findByStatus:
get:
description: Multiple status values can be provided with comma separated strings
operationId: findPetsByStatus
parameters:
- description: Status values that need to be considered for filter
explode: false
in: query
name: status
required: true
schema:
items:
default: available
enum:
- available
- pending
- sold
type: string
type: array
style: form
responses:
200:
content:
application/xml:
schema:
items:
$ref: '#/components/schemas/Pet'
type: array
application/json:
schema:
items:
$ref: '#/components/schemas/Pet'
type: array
description: successful operation
400:
content: {}
description: Invalid status value
security:
- petstore_auth:
- write:pets
- read:pets
summary: Finds Pets by status
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
/pet/findByTags:
get:
deprecated: true
description: Multiple tags can be provided with comma separated strings. Use
tag1, tag2, tag3 for testing.
operationId: findPetsByTags
parameters:
- description: Tags to filter by
explode: false
in: query
name: tags
required: true
schema:
items:
type: string
type: array
style: form
responses:
200:
content:
application/xml:
schema:
items:
$ref: '#/components/schemas/Pet'
type: array
application/json:
schema:
items:
$ref: '#/components/schemas/Pet'
type: array
description: successful operation
400:
content: {}
description: Invalid tag value
security:
- petstore_auth:
- write:pets
- read:pets
summary: Finds Pets by tags
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
/pet/{petId}:
delete:
operationId: deletePet
parameters:
- in: header
name: api_key
schema:
type: string
- description: Pet id to delete
in: path
name: petId
required: true
schema:
format: int64
type: integer
responses:
400:
content: {}
description: Invalid pet value
security:
- petstore_auth:
- write:pets
- read:pets
summary: Deletes a pet
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
get:
description: Returns a single pet
operationId: getPetById
parameters:
- description: ID of pet to return
in: path
name: petId
required: true
schema:
format: int64
type: integer
responses:
200:
content:
application/xml:
schema:
$ref: '#/components/schemas/Pet'
application/json:
schema:
$ref: '#/components/schemas/Pet'
description: successful operation
400:
content: {}
description: Invalid ID supplied
404:
content: {}
description: Pet not found
security:
- api_key: []
summary: Find pet by ID
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
post:
operationId: updatePetWithForm
parameters:
- description: ID of pet that needs to be updated
in: path
name: petId
required: true
schema:
format: int64
type: integer
requestBody:
content:
application/x-www-form-urlencoded:
schema:
properties:
name:
description: Updated name of the pet
type: string
status:
description: Updated status of the pet
type: string
responses:
405:
content: {}
description: Invalid input
security:
- petstore_auth:
- write:pets
- read:pets
summary: Updates a pet in the store with form data
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
/pet/{petId}/uploadImage:
post:
operationId: uploadFile
parameters:
- description: ID of pet to update
in: path
name: petId
required: true
schema:
format: int64
type: integer
requestBody:
content:
multipart/form-data:
schema:
properties:
additionalMetadata:
description: Additional data to pass to server
type: string
file:
description: file to upload
format: binary
type: string
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
description: successful operation
security:
- petstore_auth:
- write:pets
- read:pets
summary: uploads an image
tags:
- pet
x-openapi-router-controller: PetController
x-openapi-router-service: PetService
/store/inventory:
get:
description: Returns a map of status codes to quantities
operationId: getInventory
responses:
200:
content:
application/json:
schema:
additionalProperties:
format: int32
type: integer
type: object
description: successful operation
security:
- api_key: []
summary: Returns pet inventories by status
tags:
- store
x-openapi-router-controller: StoreController
x-openapi-router-service: StoreService
/store/order:
post:
operationId: placeOrder
requestBody:
content:
'*/*':
schema:
$ref: '#/components/schemas/Order'
description: order placed for purchasing the pet
required: true
responses:
200:
content:
application/xml:
schema:
$ref: '#/components/schemas/Order'
application/json:
schema:
$ref: '#/components/schemas/Order'
description: successful operation
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
/store/order/{orderId}:
delete:
description: For valid response try integer IDs with value < 1000. Anything
above 1000 or nonintegers will generate API errors
operationId: deleteOrder
parameters:
- description: ID of the order that needs to be deleted
in: path
name: orderId
required: true
schema:
type: string
responses:
400:
content: {}
description: Invalid ID supplied
404:
content: {}
description: Order not found
summary: Delete purchase order by ID
tags:
- store
x-openapi-router-controller: StoreController
x-openapi-router-service: StoreService
get:
description: For valid response try integer IDs with value <= 5 or > 10. Other
values will generated exceptions
operationId: getOrderById
parameters:
- description: ID of pet that needs to be fetched
in: path
name: orderId
required: true
schema:
format: int64
maximum: 5
minimum: 1
type: integer
responses:
200:
content:
application/xml:
schema:
$ref: '#/components/schemas/Order'
application/json:
schema:
$ref: '#/components/schemas/Order'
description: successful operation
400:
content: {}
description: Invalid ID supplied
404:
content: {}
description: Order not found
summary: Find purchase order by ID
tags:
- store
x-openapi-router-controller: StoreController
x-openapi-router-service: StoreService
/user:
post:
description: This can only be done by the logged in user.
operationId: createUser
requestBody:
content:
'*/*':
schema:
$ref: '#/components/schemas/User'
description: Created user object
required: true
responses:
default:
content: {}
description: successful operation
summary: Create user
tags:
- user
x-codegen-request-body-name: body
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
/user/createWithArray:
post:
operationId: createUsersWithArrayInput
requestBody:
content:
'*/*':
schema:
items:
$ref: '#/components/schemas/User'
type: array
description: List of user object
required: true
responses:
default:
content: {}
description: successful operation
summary: Creates list of users with given input array
tags:
- user
x-codegen-request-body-name: body
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
/user/createWithList:
post:
operationId: createUsersWithListInput
requestBody:
content:
'*/*':
schema:
items:
$ref: '#/components/schemas/User'
type: array
description: List of user object
required: true
responses:
default:
content: {}
description: successful operation
summary: Creates list of users with given input array
tags:
- user
x-codegen-request-body-name: body
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
/user/login:
get:
operationId: loginUser
parameters:
- description: The user name for login
in: query
name: username
required: true
schema:
type: string
- description: The password for login in clear text
in: query
name: password
required: true
schema:
type: string
responses:
200:
content:
application/xml:
schema:
type: string
application/json:
schema:
type: string
description: successful operation
headers:
X-Rate-Limit:
description: calls per hour allowed by the user
schema:
format: int32
type: integer
X-Expires-After:
description: date in UTC when toekn expires
schema:
format: date-time
type: string
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
/user/logout:
get:
operationId: logoutUser
responses:
default:
content: {}
description: successful operation
summary: Logs out current logged in user session
tags:
- user
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
/user/{username}:
delete:
description: This can only be done by the logged in user.
operationId: deleteUser
parameters:
- description: The name that needs to be deleted
in: path
name: username
required: true
schema:
type: string
responses:
400:
content: {}
description: Invalid username supplied
404:
content: {}
description: User not found
summary: Delete user
tags:
- user
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
get:
operationId: getUserByName
parameters:
- description: The name that needs to be fetched. Use user1 for testing.
in: path
name: username
required: true
schema:
type: string
responses:
200:
content:
application/xml:
schema:
$ref: '#/components/schemas/User'
application/json:
schema:
$ref: '#/components/schemas/User'
description: successful operation
400:
content: {}
description: Invalid username supplied
404:
content: {}
description: User not found
summary: Get user by user name
tags:
- user
x-openapi-router-controller: UserController
x-openapi-router-service: UserService
put:
description: This can only be done by the logged in user.
operationId: updateUser
parameters:
- description: name that need to be deleted
in: path
name: username
required: true
schema:
type: string
requestBody:
content:
'*/*':
schema:
$ref: '#/components/schemas/User'
description: Updated user object
required: true
responses:
400:
content: {}
description: Invalid user supplied
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
components:
schemas:
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
properties:
id:
format: int64
type: integer
petId:
format: int64
type: integer
quantity:
format: int32
type: integer
shipDate:
format: date-time
type: string
status:
description: Order Status
enum:
- placed
- approved
- delivered
type: string
complete:
default: false
type: boolean
title: Pet Order
type: object
xml:
name: Order
Category:
description: A category for a pet
example:
name: name
id: 6
properties:
id:
format: int64
type: integer
name:
type: string
title: Pet category
type: object
xml:
name: Category
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
properties:
id:
format: int64
type: integer
username:
type: string
firstName:
type: string
lastName:
type: string
email:
type: string
password:
type: string
phone:
type: string
userStatus:
description: User Status
format: int32
type: integer
title: a User
type: object
xml:
name: User
Tag:
description: A tag for a pet
example:
name: name
id: 1
properties:
id:
format: int64
type: integer
name:
type: string
title: Pet Tag
type: object
xml:
name: Tag
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
properties:
id:
format: int64
type: integer
category:
$ref: '#/components/schemas/Category'
name:
example: doggie
type: string
photoUrls:
items:
type: string
type: array
xml:
name: photoUrl
wrapped: true
tags:
items:
$ref: '#/components/schemas/Tag'
type: array
xml:
name: tag
wrapped: true
status:
description: pet status in the store
enum:
- available
- pending
- sold
type: string
required:
- name
- photoUrls
title: a Pet
type: object
xml:
name: Pet
ApiResponse:
description: Describes the result of uploading an image resource
example:
code: 0
type: type
message: message
properties:
code:
format: int32
type: integer
type:
type: string
message:
type: string
title: An uploaded response
type: object
securitySchemes:
petstore_auth:
flows:
implicit:
authorizationUrl: http://petstore.swagger.io/api/oauth/dialog
scopes:
write:pets: modify pets in your account
read:pets: read your pets
type: oauth2
api_key:
in: header
name: api_key
type: apiKey

View File

@ -0,0 +1,795 @@
---
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: "http://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,12 @@
const path = require('path');
const config = {
ROOT_DIR: __dirname,
URL_PORT: 3000,
URL_PATH: 'http://localhost',
BASE_VERSION: 'v2',
CONTROLLER_DIRECTORY: path.join(__dirname, 'controllers'),
};
config.OPENAPI_YAML = path.join(config.ROOT_DIR, 'api', 'openapi.yaml');
config.FULL_PATH = `${config.URL_PATH}:${config.URL_PORT}/${config.BASE_VERSION}`;
module.exports = config;

View File

@ -0,0 +1,72 @@
const logger = require('../logger');
class Controller {
static sendResponse(response, payload) {
/**
* The default response-code is 200. We want to allow to change that. in That case,
* payload will be an object consisting of a code and a payload. If not customized
* send 200 and the payload as received in this method.
*/
response.status(payload.code || 200);
const responsePayload = payload.payload !== undefined ? payload.payload : payload;
if (responsePayload instanceof Object) {
response.json(responsePayload);
} else {
response.end(responsePayload);
}
}
static sendError(response, error) {
response.status(error.code || 500);
if (error.error instanceof Object) {
response.json(error.error);
} else {
response.end(error.error || error.message);
}
}
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;
}
}
}
static collectRequestParams(request) {
this.collectFiles(request);
const requestParams = {};
if (request.openapi.schema.requestBody !== undefined) {
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];
}
});
return requestParams;
}
static async handleRequest(request, response, serviceOperation) {
try {
const serviceResponse = await serviceOperation(this.collectRequestParams(request));
Controller.sendResponse(response, serviceResponse);
} catch (error) {
Controller.sendError(response, error);
}
}
}
module.exports = Controller;

View File

@ -0,0 +1,42 @@
const Controller = require('./Controller');
class PetController {
constructor(Service) {
this.service = Service;
}
async addPet(request, response) {
await Controller.handleRequest(request, response, this.service.addPet);
}
async deletePet(request, response) {
await Controller.handleRequest(request, response, this.service.deletePet);
}
async findPetsByStatus(request, response) {
await Controller.handleRequest(request, response, this.service.findPetsByStatus);
}
async findPetsByTags(request, response) {
await Controller.handleRequest(request, response, this.service.findPetsByTags);
}
async getPetById(request, response) {
await Controller.handleRequest(request, response, this.service.getPetById);
}
async updatePet(request, response) {
await Controller.handleRequest(request, response, this.service.updatePet);
}
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;

View File

@ -0,0 +1,26 @@
const Controller = require('./Controller');
class StoreController {
constructor(Service) {
this.service = Service;
}
async deleteOrder(request, response) {
await Controller.handleRequest(request, response, this.service.deleteOrder);
}
async getInventory(request, response) {
await Controller.handleRequest(request, response, this.service.getInventory);
}
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;

View File

@ -0,0 +1,42 @@
const Controller = require('./Controller');
class UserController {
constructor(Service) {
this.service = Service;
}
async createUser(request, response) {
await Controller.handleRequest(request, response, this.service.createUser);
}
async createUsersWithArrayInput(request, response) {
await Controller.handleRequest(request, response, this.service.createUsersWithArrayInput);
}
async createUsersWithListInput(request, response) {
await Controller.handleRequest(request, response, this.service.createUsersWithListInput);
}
async deleteUser(request, response) {
await Controller.handleRequest(request, response, this.service.deleteUser);
}
async getUserByName(request, response) {
await Controller.handleRequest(request, response, this.service.getUserByName);
}
async loginUser(request, response) {
await Controller.handleRequest(request, response, this.service.loginUser);
}
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;

View File

@ -0,0 +1,9 @@
const PetController = require('./PetController');
const StoreController = require('./StoreController');
const UserController = require('./UserController');
module.exports = {
PetController,
StoreController,
UserController,
};

View File

@ -0,0 +1,93 @@
// const { Middleware } = require('swagger-express-middleware');
const path = require('path');
const swaggerUI = require('swagger-ui-express');
const yamljs = require('yamljs');
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');
class ExpressServer {
constructor(port, openApiYaml) {
this.port = port;
this.app = express();
this.openApiPath = openApiYaml;
this.schema = yamljs.load(openApiYaml);
this.setupMiddleware();
}
setupMiddleware() {
// this.setupAllowedMedia();
this.app.use(cors());
this.app.use(bodyParser.json());
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));
this.app.get('/login-redirect', (req, res) => {
res.status(200);
res.json(req.query);
});
this.app.get('/oauth2-redirect.html', (req, res) => {
res.status(200);
res.json(req.query);
});
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);
});
} catch (error) {
reject(error);
}
},
);
}
async close() {
if (this.server !== undefined) {
await this.server.close();
console.log(`Server on port ${this.port} shut down`);
}
}
}
module.exports = ExpressServer;

View File

@ -0,0 +1,28 @@
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();
logger.info('Express server running');
} catch (error) {
logger.error(error);
await this.close();
}
};
launchServer().catch(e => logger.error(e));

View File

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

View File

@ -0,0 +1,23 @@
const ono = require('ono');
const Model = require('./Model');
class Category {
constructor(id, name) {
const validationErrors = (Model.validateModel(Category, { name, id }));
if (validationErrors.length === 0) {
this.id = id;
this.name = name;
} else {
throw ono('Tried to create an invalid Category instance', { errors: validationErrors });
}
}
}
Category.types = {
id: 'integer',
name: 'string',
};
Category.required = [];
module.exports = Category;

View File

@ -0,0 +1,47 @@
class Model {
static validateModel(modelClass, variables) {
const invalidArray = [];
Object.entries(variables).forEach(([key, value]) => {
const typeToCheck = modelClass.types[key];
switch (typeToCheck) {
case 'string':
if (!(typeof value === 'string' || value instanceof String)) {
invalidArray.push({ key, expectedType: typeToCheck, value });
}
break;
case 'number':
case 'integer':
if (!(typeof value === 'number' && !Number.isNaN(value))) {
invalidArray.push({ key, expectedType: typeToCheck, value });
}
break;
case 'array':
if (!(value && typeof value === 'object' && value.constructor === Array)) {
invalidArray.push({ key, expectedType: typeToCheck, value });
}
break;
case 'object':
if (!(value && typeof value === 'object' && value.constructor === Object)) {
invalidArray.push({ key, expectedType: typeToCheck, value });
}
break;
case 'boolean':
if (!(typeof value === 'boolean')) {
invalidArray.push({ key, expectedType: typeToCheck, value });
}
break;
default:
break;
}
});
modelClass.required.forEach((requiredFieldName) => {
if (variables[requiredFieldName] === undefined || variables[requiredFieldName] === '') {
invalidArray.push(
{ field: requiredFieldName, required: true, value: variables[requiredFieldName] },
);
}
});
return invalidArray;
}
}
module.exports = Model;

View File

@ -0,0 +1,35 @@
const ono = require('ono');
const Model = require('./Model');
const Tag = require('./Tag');
const Category = require('./Category');
class Pet {
constructor(photoUrls, name, id, tags, status, category) {
const validationErrors = Model.validateModel(Pet, {
photoUrls, name, id, tags, status, category,
});
if (validationErrors.length === 0) {
this.photoUrls = photoUrls;
this.name = name;
this.id = id;
this.tags = tags.map(t => new Tag(t.id, t.name));
this.status = status;
this.category = new Category(category.id, category.name);
} else {
throw ono('Tried to create an invalid Pet instance', { errors: validationErrors });
}
}
}
Pet.types = {
photoUrls: 'array',
name: 'string',
id: 'integer',
tags: 'array',
status: 'string',
category: 'object',
};
Pet.required = ['name', 'photoUrls'];
module.exports = Pet;

View File

@ -0,0 +1,24 @@
const ono = require('ono');
const Model = require('./Model');
class Tag {
constructor(id, name) {
const validationErrors = Model.validateModel(Tag,
{ name, id });
if (validationErrors.length === 0) {
this.name = name;
this.id = id;
} else {
throw ono('Tried to create an invalid Tag instance', { errors: validationErrors });
}
}
}
Tag.types = {
name: 'string',
id: 'integer',
};
Tag.required = [];
module.exports = Tag;

View File

@ -0,0 +1,25 @@
const ono = require('ono');
const Model = require('./Model');
class UpdatePetWithFormModel {
constructor(name, status) {
const validationErrors = Model.validateModel(UpdatePetWithFormModel, {
name, status,
});
if (validationErrors.length === 0) {
this.name = name;
this.status = status;
} else {
throw ono('Tried to create an invalid UpdatePetWithFormModel instance', { errors: validationErrors });
}
}
}
UpdatePetWithFormModel.types = {
name: 'string',
status: 'string',
};
UpdatePetWithFormModel.required = [];
module.exports = UpdatePetWithFormModel;

View File

@ -0,0 +1,25 @@
const ono = require('ono');
const Model = require('./Model');
class UploadFileModel {
constructor(additionalMetadata, file) {
const validationErrors = Model.validateModel(UploadFileModel, {
additionalMetadata, file,
});
if (validationErrors.length === 0) {
this.additionalMetadata = additionalMetadata;
this.file = file;
} else {
throw ono('Tried to create an invalid UploadFileModel instance', { errors: validationErrors });
}
}
}
UploadFileModel.types = {
additionalMetadata: 'string',
file: 'string',
};
UploadFileModel.required = [];
module.exports = UploadFileModel;

View File

@ -0,0 +1,9 @@
const CategoryModel = require('./Category');
const PetModel = require('./Pet');
const TagModel = require('./Tag');
module.exports = {
CategoryModel,
PetModel,
TagModel,
};

View File

@ -0,0 +1,46 @@
{
"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",
"scripts": {
"prestart": "npm install",
"start": "node index.js"
},
"keywords": [
"openapi-generator",
"openapi"
],
"license": "Unlicense",
"private": true,
"dependencies": {
"body-parser": "^1.19.0",
"connect": "^3.2.0",
"cookie-parser": "^1.4.4",
"cors": "^2.8.5",
"express": "^4.16.4",
"express-openapi-validator": "^1.0.0",
"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",
"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-plugin-import": "^2.17.2",
"form-data": "^2.3.3"
},
"eslintConfig": {
"env": {
"node": true
}
}
}

View File

@ -0,0 +1,184 @@
/* 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
**/
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;

View File

@ -0,0 +1,11 @@
class Service {
static rejectResponse(error, code = 500) {
return { error, code };
}
static successResponse(payload, code = 200) {
return { payload, code };
}
}
module.exports = Service;

View File

@ -0,0 +1,94 @@
/* 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
**/
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;

View File

@ -0,0 +1,180 @@
/* 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
**/
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;

View File

@ -0,0 +1,9 @@
const PetService = require('./PetService');
const StoreService = require('./StoreService');
const UserService = require('./UserService');
module.exports = {
PetService,
StoreService,
UserService,
};

View File

@ -0,0 +1,46 @@
const {
describe, before, after, it,
} = require('mocha');
const assert = require('assert').strict;
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const axios = require('axios');
const logger = require('./logger');
const config = require('./config');
const ExpressServer = require('../expressServer');
const app = new ExpressServer(config.URL_PORT, config.OPENAPI_YAML);
chai.use(chaiAsPromised);
chai.should();
describe('Test endpoints that are not part of the openapi.yaml.', () => {
before(async () => {
try {
await app.launch();
logger.info('express server launched\n');
} catch (error) {
logger.info(error);
await app.close();
throw (error);
}
});
after(async () => {
await app.close()
.catch(error => logger.error(error));
logger.error('express server closed');
});
it('should confirm that requesting the openapi.yaml returns a successful 200 response', async () => {
const pathToCall = `${config.URL_PATH}:${config.URL_PORT}/spec/openapi.yaml`;
try {
const openapiResponse = await axios.get(pathToCall);
openapiResponse.should.have.property('status');
openapiResponse.status.should.equal(200);
} catch (e) {
logger.error(`failed to call ${pathToCall}`);
assert.fail(`Failed to call openapi.yaml - ${e.message}`);
}
});
});

View File

@ -0,0 +1,12 @@
const path = require('path');
const config = {
ROOT_DIR: path.join(__dirname, '../'),
URL_PORT: 3009,
URL_PATH: 'http://localhost',
BASE_VERSION: 'v2',
};
config.OPENAPI_YAML = path.join(config.ROOT_DIR, 'api', 'openapi.yaml');
config.FULL_PATH = `${config.URL_PATH}:${config.URL_PORT}/${config.BASE_VERSION}`;
module.exports = config;

View File

@ -0,0 +1,14 @@
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'test-service' },
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
new winston.transports.Console({ format: winston.format.simple() }),
],
});
module.exports = logger;

View File

@ -0,0 +1,147 @@
/**
* The purpose of these tests is to confirm that every path in the openapi spec, if built properly,
* returns a valid 200, with a simple text response.
* The codeGen will generate a response string including the name of the operation that was called.
* These tests confirm that the codeGen worked as expected.
* Once we start adding our own business logic, these
* tests will fail. It is recommended to keep these tests updated with the code changes.
*/
const {
describe, before, after, it,
} = require('mocha');
const assert = require('assert').strict;
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const axios = require('axios');
const yamljs = require('yamljs');
const openApiSampler = require('openapi-sampler');
const jstoxml = require('jstoxml');
const logger = require('./logger');
const config = require('./config');
const ExpressServer = require('../expressServer');
const app = new ExpressServer(config.URL_PORT, config.OPENAPI_YAML);
chai.use(chaiAsPromised);
chai.should();
const pathPrefix = `${config.URL_PATH}:${config.URL_PORT}/api/v2`;
const spec = yamljs.load(config.OPENAPI_YAML);
const parseParameters = (originalPath, schemaParameters) => {
let path = originalPath;
const headers = {};
const queryParams = [];
schemaParameters.forEach((parameter) => {
const parameterValue = parameter.example || openApiSampler.sample(parameter.schema);
switch (parameter.in) {
case 'header':
headers[parameter.name] = parameterValue;
break;
case 'path':
path = path.replace(`{${parameter.name}}`, parameterValue);
break;
case 'query':
queryParams.push(`${parameter.name}=${parameterValue}`);
break;
default:
break;
}
});
return { path, headers, queryString: queryParams.join('&') };
};
const buildRequestObject = (pathEndpoint, method, operationObject, requestsArray) => {
logger.info(`method: ${method}`);
let headers = {};
let requestBody = {};
let queryString = '';
let path = pathEndpoint;
if (operationObject.parameters !== undefined) {
logger.info('this is a request with parameters');
({ path, headers, queryString } = parseParameters(pathEndpoint, operationObject.parameters));
if (queryString.length > 0) {
path += `?${queryString}`;
}
Object.entries(headers).forEach(([headerName, headerValue]) => {
headers[headerName] = headerValue;
});
}
if (operationObject.requestBody !== undefined) {
logger.info('This is a request with a body');
const content = Object.entries(operationObject.requestBody.content);
content.forEach(([contentType, contentObject]) => {
requestBody = openApiSampler.sample(contentObject.schema, {}, spec);
let requestXML;
if (contentType === 'application/xml') {
requestXML = jstoxml.toXML(requestBody);
}
headers['Content-Type'] = contentType;
requestsArray.push({
method,
path,
body: requestXML || requestBody,
headers,
});
});
} else {
requestsArray.push({
method,
path,
headers,
});
}
};
const getApiRequestsData = (apiSchema) => {
const requestsArray = [];
Object.entries(apiSchema.paths).forEach(([pathEndpoint, pathObject]) => {
logger.info(`adding path: ${pathPrefix}${pathEndpoint} to testing array`);
Object.entries(pathObject).forEach(([operationMethod, operationObject]) => {
buildRequestObject(pathEndpoint, operationMethod, operationObject, requestsArray);
});
});
return requestsArray;
};
describe('API tests, checking that the codegen generated code that allows all paths specified in schema to work', () => {
before(async () => {
try {
await app.launch();
logger.info('express server launched\n');
} catch (error) {
logger.info(error);
await app.close();
throw (error);
}
});
after(async () => {
await app.close()
.catch(error => logger.error(error));
logger.error('express server closed');
});
const requestsArray = getApiRequestsData(spec);
requestsArray.forEach((requestObject) => {
it(`should run ${requestObject.method.toUpperCase()} request to ${requestObject.path} and return healthy 200`, async () => {
try {
const {
method, path, body, headers,
} = requestObject;
const url = `${pathPrefix}${path}`;
logger.info(`testing ${method.toUpperCase()} call to ${url}. encoding: ${headers['Content-Type']}`);
const response = await axios({
method,
url,
data: body,
headers,
});
response.should.have.property('status');
response.status.should.equal(200);
} catch (e) {
assert.fail(e.message);
}
});
});
});

View File

@ -0,0 +1,57 @@
const {
describe, before, after, it,
} = require('mocha');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const { get } = require('axios');
const logger = require('./logger');
const config = require('./config');
const ExpressServer = require('../expressServer');
const app = new ExpressServer(config.URL_PORT, config.OPENAPI_YAML);
chai.use(chaiAsPromised);
chai.should();
describe('Server tests, checking launch, terminate, and various error messages', () => {
before(async () => {
try {
await app.launch();
logger.info('express server launched\n');
} catch (error) {
logger.info(error);
await app.close();
throw (error);
}
});
after(async () => {
await app.close()
.catch(error => logger.error(error));
logger.error('express server closed');
});
it('should launch express server successfully', async () => {
const indexResponse = await get(`${config.URL_PATH}:${config.URL_PORT}/`);
indexResponse.status.should.equal(200, 'Expecting a call to root directory of server to return 200 code');
});
it('should fail with a 404 on non-existing page', async () => {
get(`${config.FULL_PATH}/someRandomPage`)
.then(response => response.status.should.equal(404, 'expecting a 404 on a non-existing page request'))
.catch((responseError) => {
responseError.response.status.should.not.equal(undefined);
responseError.response.status.should.equal(404, 'expecting to receive a 404 on requesting a non-existing page');
});
});
it('should load api-doc', async () => {
try {
const response = await get(`http://localhost:${config.URL_PORT}/api-docs`);
response.status.should.equal(200, 'Expecting 200');
} catch (e) {
console.log(e.message);
throw e;
}
});
});

View File

@ -0,0 +1,29 @@
const {
describe, it,
} = require('mocha');
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const Pet = require('../models/Pet');
const logger = require('./logger');
chai.use(chaiAsPromised);
chai.should();
describe('Model tests, checking that they are created correctly, and throw the expected message when not', () => {
it('Should create a model for Pet, according to openapi.yaml definition, and throw errors if fails',
async () => {
const photoUrls = ['petPhoto1.jpg', 'petPhoto2.jpg', 'petPhoto3.jpg'];
const name = 'petName';
const id = 0;
const category = { id: 1, name: 'categoryName' };
const tags = [{ id: 2, name: 'tagName1' }, { id: 3, name: 'tagName2' }];
const status = 'available';
const pet = new Pet(photoUrls, name, id, tags, status, category);
pet.id.should.equal(id);
pet.name.should.equal(name);
pet.category.id.should.equal(category.id);
pet.category.name.should.equal(category.name);
pet.tags.length.should.equal(tags.length);
pet.status.should.equal(status);
});
});

View File

@ -0,0 +1,67 @@
const logger = require('../logger');
const controllers = require('../controllers');
const Services = require('../services');
function handleError(err, request, response, next) {
logger.error(err);
const code = err.code || 400;
response.status(code);
response.error = err;
next(JSON.stringify({
code,
error: err,
}));
}
/**
* The purpose of this route is to collect the request variables as defined in the
* OpenAPI document and pass them to the handling controller as another Express
* middleware. All parameters are collected in the requet.swagger.values key-value object
*
* The assumption is that security handlers have already verified and allowed access
* to this path. If the business-logic of a particular path is dependant on authentication
* parameters (e.g. scope checking) - it is recommended to define the authentication header
* as one of the parameters expected in the OpenAPI/Swagger document.
*
* Requests made to paths that are not in the OpernAPI scope
* are passed on to the next middleware handler.
* @returns {Function}
*/
function openApiRouter() {
return async (request, response, next) => {
try {
/**
* This middleware runs after a previous process have applied an openapi object
* to the request.
* If none was applied This is because the path requested is not in the schema.
* If there's no openapi object, we have nothing to do, and pass on to next middleware.
*/
if (request.openapi === undefined
|| request.openapi.schema === undefined
) {
next();
return;
}
// request.swagger.paramValues = {};
// request.swagger.params.forEach((param) => {
// request.swagger.paramValues[param.name] = getValueFromRequest(request, param);
// });
const controllerName = request.openapi.schema['x-openapi-router-controller'];
const serviceName = request.openapi.schema['x-openapi-router-service'];
if (!controllers[controllerName] || controllers[controllerName] === undefined) {
handleError(`request sent to controller '${controllerName}' which has not been defined`,
request, response, next);
} else {
const apiController = new controllers[controllerName](Services[serviceName]);
const controllerOperation = request.openapi.schema.operationId;
await apiController[controllerOperation](request, response, next);
}
} catch (error) {
console.error(error);
const err = { code: 500, error: error.message };
handleError(err, request, response, next);
}
};
}
module.exports = openApiRouter;