diff --git a/README.md b/README.md index 75aeef76cf7..b40e0653213 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bin/nodejs-express-petstore-server.sh b/bin/nodejs-express-petstore-server.sh new file mode 100755 index 00000000000..8ebaac9de10 --- /dev/null +++ b/bin/nodejs-express-petstore-server.sh @@ -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 diff --git a/docs/generators.md b/docs/generators.md index 46a9c92365f..77f0a887afb 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -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) diff --git a/docs/generators/nodejs-express-server.md b/docs/generators/nodejs-express-server.md new file mode 100644 index 00000000000..25a84db36d6 --- /dev/null +++ b/docs/generators/nodejs-express-server.md @@ -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| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSExpressServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSExpressServerCodegen.java new file mode 100644 index 00000000000..14f305f654c --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSExpressServerCodegen.java @@ -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 postProcessOperationsWithModels(Map objs, List allModels) { + @SuppressWarnings("unchecked") + Map objectMap = (Map) objs.get("operations"); + @SuppressWarnings("unchecked") + List operations = (List) objectMap.get("operation"); + for (CodegenOperation operation : operations) { + operation.httpMethod = operation.httpMethod.toLowerCase(Locale.ROOT); + + List params = operation.allParams; + if (params != null && params.size() == 0) { + operation.allParams = null; + } + List 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> it = operation.examples.iterator(); it.hasNext(); ) { + final Map 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> getOperations(Map objs) { + List> result = new ArrayList>(); + Map apiInfo = (Map) objs.get("apiInfo"); + List> apis = (List>) apiInfo.get("apis"); + for (Map api : apis) { + result.add((Map) api.get("operations")); + } + return result; + } + + private static List> sortOperationsByPath(List ops) { + Multimap opsByPath = ArrayListMultimap.create(); + + for (CodegenOperation op : ops) { + opsByPath.put(op.path, op); + } + + List> opsByPathList = new ArrayList>(); + for (Entry> entry : opsByPath.asMap().entrySet()) { + Map opsByPathEntry = new HashMap(); + opsByPathList.add(opsByPathEntry); + opsByPathEntry.put("path", entry.getKey()); + opsByPathEntry.put("operation", entry.getValue()); + List 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 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 postProcessSupportingFileData(Map objs) { + generateYAMLSpecFile(objs); + + for (Map operations : getOperations(objs)) { + @SuppressWarnings("unchecked") + List ops = (List) operations.get("operation"); + + List> 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("\"", ""); + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index a27cc518fdc..9a8fba9fc43 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -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 diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/README.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/README.mustache new file mode 100644 index 00000000000..2bc0198b4a2 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/README.mustache @@ -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. + + + diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/app.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/app.mustache new file mode 100644 index 00000000000..34d7c9c1636 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/app.mustache @@ -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; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/config.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/config.mustache new file mode 100644 index 00000000000..80f568992bd --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/config.mustache @@ -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; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/controller.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/controller.mustache new file mode 100644 index 00000000000..61d7290a3ed --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/controller.mustache @@ -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}} diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/Controller.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/Controller.mustache new file mode 100644 index 00000000000..bdf8776c0e4 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/Controller.mustache @@ -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; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/index.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/index.mustache new file mode 100644 index 00000000000..439b7d8f025 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/index.mustache @@ -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}} +}; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/test.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/test.mustache new file mode 100644 index 00000000000..516c135b45b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/controllers/test.mustache @@ -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; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/eslintrc.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/eslintrc.mustache new file mode 100644 index 00000000000..6d8abec4c52 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/eslintrc.mustache @@ -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" + } +} diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/expressServer.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/expressServer.mustache new file mode 100644 index 00000000000..64998cf2563 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/expressServer.mustache @@ -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; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/index.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/index.mustache new file mode 100644 index 00000000000..cc9dbc7e54b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/index.mustache @@ -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)); diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/logger.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/logger.mustache new file mode 100644 index 00000000000..27134586f8e --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/logger.mustache @@ -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; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/openapi.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/openapi.mustache new file mode 100644 index 00000000000..51ebafb0187 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/openapi.mustache @@ -0,0 +1 @@ +{{{openapi-yaml}}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/package.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/package.mustache new file mode 100644 index 00000000000..e4bdc09d99b --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/package.mustache @@ -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 + } + } +} diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/service.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/service.mustache new file mode 100644 index 00000000000..b5f39b4577f --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/service.mustache @@ -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}} diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/services/Service.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/services/Service.mustache new file mode 100644 index 00000000000..11f8c9a1c3a --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/services/Service.mustache @@ -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; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/services/index.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/services/index.mustache new file mode 100644 index 00000000000..19478453fa8 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/services/index.mustache @@ -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}} +}; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/utils/openapiRouter.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/utils/openapiRouter.mustache new file mode 100644 index 00000000000..1a77fec7b61 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/utils/openapiRouter.mustache @@ -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; diff --git a/modules/openapi-generator/src/main/resources/nodejs-express-server/utils/writer.mustache b/modules/openapi-generator/src/main/resources/nodejs-express-server/utils/writer.mustache new file mode 100644 index 00000000000..d79f6e1a526 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/nodejs-express-server/utils/writer.mustache @@ -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); +} diff --git a/samples/server/petstore/nodejs-express-server/.eslintrc b/samples/server/petstore/nodejs-express-server/.eslintrc new file mode 100644 index 00000000000..6d8abec4c52 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/.eslintrc @@ -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" + } +} diff --git a/samples/server/petstore/nodejs-express-server/.eslintrc.json b/samples/server/petstore/nodejs-express-server/.eslintrc.json new file mode 100644 index 00000000000..6d8abec4c52 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/.eslintrc.json @@ -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" + } +} diff --git a/samples/server/petstore/nodejs-express-server/.openapi-generator-ignore b/samples/server/petstore/nodejs-express-server/.openapi-generator-ignore new file mode 100644 index 00000000000..7484ee590a3 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/.openapi-generator-ignore @@ -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 diff --git a/samples/server/petstore/nodejs-express-server/.openapi-generator/VERSION b/samples/server/petstore/nodejs-express-server/.openapi-generator/VERSION new file mode 100644 index 00000000000..83a328a9227 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/.openapi-generator/VERSION @@ -0,0 +1 @@ +4.1.0-SNAPSHOT \ No newline at end of file diff --git a/samples/server/petstore/nodejs-express-server/README.md b/samples/server/petstore/nodejs-express-server/README.md new file mode 100644 index 00000000000..d3d47c09fdc --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/README.md @@ -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. + + + diff --git a/samples/server/petstore/nodejs-express-server/api/openapi.yaml b/samples/server/petstore/nodejs-express-server/api/openapi.yaml new file mode 100644 index 00000000000..401e628f3bd --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/api/openapi.yaml @@ -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 diff --git a/samples/server/petstore/nodejs-express-server/api/swagger.yaml b/samples/server/petstore/nodejs-express-server/api/swagger.yaml new file mode 100644 index 00000000000..85b742e57ae --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/api/swagger.yaml @@ -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" diff --git a/samples/server/petstore/nodejs-express-server/config.js b/samples/server/petstore/nodejs-express-server/config.js new file mode 100644 index 00000000000..80f568992bd --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/config.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/controllers/Controller.js b/samples/server/petstore/nodejs-express-server/controllers/Controller.js new file mode 100644 index 00000000000..bdf8776c0e4 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/controllers/Controller.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/controllers/PetController.js b/samples/server/petstore/nodejs-express-server/controllers/PetController.js new file mode 100644 index 00000000000..488389927cb --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/controllers/PetController.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/controllers/StoreController.js b/samples/server/petstore/nodejs-express-server/controllers/StoreController.js new file mode 100644 index 00000000000..fabc3e523f8 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/controllers/StoreController.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/controllers/UserController.js b/samples/server/petstore/nodejs-express-server/controllers/UserController.js new file mode 100644 index 00000000000..4dafcfa8903 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/controllers/UserController.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/controllers/index.js b/samples/server/petstore/nodejs-express-server/controllers/index.js new file mode 100644 index 00000000000..0ad912de205 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/controllers/index.js @@ -0,0 +1,9 @@ +const PetController = require('./PetController'); +const StoreController = require('./StoreController'); +const UserController = require('./UserController'); + +module.exports = { + PetController, + StoreController, + UserController, +}; diff --git a/samples/server/petstore/nodejs-express-server/expressServer.js b/samples/server/petstore/nodejs-express-server/expressServer.js new file mode 100644 index 00000000000..64998cf2563 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/expressServer.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/index.js b/samples/server/petstore/nodejs-express-server/index.js new file mode 100644 index 00000000000..cc9dbc7e54b --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/index.js @@ -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)); diff --git a/samples/server/petstore/nodejs-express-server/logger.js b/samples/server/petstore/nodejs-express-server/logger.js new file mode 100644 index 00000000000..27134586f8e --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/logger.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/models/Category.js b/samples/server/petstore/nodejs-express-server/models/Category.js new file mode 100644 index 00000000000..b911b01bf1e --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/Category.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/models/Model.js b/samples/server/petstore/nodejs-express-server/models/Model.js new file mode 100644 index 00000000000..57527445e55 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/Model.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/models/Pet.js b/samples/server/petstore/nodejs-express-server/models/Pet.js new file mode 100644 index 00000000000..040cec2f991 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/Pet.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/models/Tag.js b/samples/server/petstore/nodejs-express-server/models/Tag.js new file mode 100644 index 00000000000..1c352a45f0f --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/Tag.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/models/UpdatePetWithFormModel.js b/samples/server/petstore/nodejs-express-server/models/UpdatePetWithFormModel.js new file mode 100644 index 00000000000..ed01cfb38c3 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/UpdatePetWithFormModel.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/models/UploadFileModel.js b/samples/server/petstore/nodejs-express-server/models/UploadFileModel.js new file mode 100644 index 00000000000..62db8911f9e --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/UploadFileModel.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/models/index.js b/samples/server/petstore/nodejs-express-server/models/index.js new file mode 100644 index 00000000000..66963054a24 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/index.js @@ -0,0 +1,9 @@ +const CategoryModel = require('./Category'); +const PetModel = require('./Pet'); +const TagModel = require('./Tag'); + +module.exports = { + CategoryModel, + PetModel, + TagModel, +}; diff --git a/samples/server/petstore/nodejs-express-server/package.json b/samples/server/petstore/nodejs-express-server/package.json new file mode 100644 index 00000000000..e4bdc09d99b --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/package.json @@ -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 + } + } +} diff --git a/samples/server/petstore/nodejs-express-server/services/PetService.js b/samples/server/petstore/nodejs-express-server/services/PetService.js new file mode 100644 index 00000000000..779b40fb99d --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/services/PetService.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/services/Service.js b/samples/server/petstore/nodejs-express-server/services/Service.js new file mode 100644 index 00000000000..11f8c9a1c3a --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/services/Service.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/services/StoreService.js b/samples/server/petstore/nodejs-express-server/services/StoreService.js new file mode 100644 index 00000000000..78b53c34ab0 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/services/StoreService.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/services/UserService.js b/samples/server/petstore/nodejs-express-server/services/UserService.js new file mode 100644 index 00000000000..49028d6e921 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/services/UserService.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/services/index.js b/samples/server/petstore/nodejs-express-server/services/index.js new file mode 100644 index 00000000000..bdf3c87fb74 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/services/index.js @@ -0,0 +1,9 @@ +const PetService = require('./PetService'); +const StoreService = require('./StoreService'); +const UserService = require('./UserService'); + +module.exports = { + PetService, + StoreService, + UserService, +}; diff --git a/samples/server/petstore/nodejs-express-server/tests/additionalEndpointsTests.js b/samples/server/petstore/nodejs-express-server/tests/additionalEndpointsTests.js new file mode 100644 index 00000000000..b795af84220 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/tests/additionalEndpointsTests.js @@ -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}`); + } + }); +}); diff --git a/samples/server/petstore/nodejs-express-server/tests/config.js b/samples/server/petstore/nodejs-express-server/tests/config.js new file mode 100644 index 00000000000..da4aa1b829f --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/tests/config.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/tests/logger.js b/samples/server/petstore/nodejs-express-server/tests/logger.js new file mode 100644 index 00000000000..b6e22aad9f4 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/tests/logger.js @@ -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; diff --git a/samples/server/petstore/nodejs-express-server/tests/routingTests.js b/samples/server/petstore/nodejs-express-server/tests/routingTests.js new file mode 100644 index 00000000000..765a8ee93d9 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/tests/routingTests.js @@ -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); + } + }); + }); +}); diff --git a/samples/server/petstore/nodejs-express-server/tests/serverTests.js b/samples/server/petstore/nodejs-express-server/tests/serverTests.js new file mode 100644 index 00000000000..035f3133430 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/tests/serverTests.js @@ -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; + } + }); +}); diff --git a/samples/server/petstore/nodejs-express-server/tests/testModels.js b/samples/server/petstore/nodejs-express-server/tests/testModels.js new file mode 100644 index 00000000000..8a3724e5070 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/tests/testModels.js @@ -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); + }); +}); diff --git a/samples/server/petstore/nodejs-express-server/utils/openapiRouter.js b/samples/server/petstore/nodejs-express-server/utils/openapiRouter.js new file mode 100644 index 00000000000..1a77fec7b61 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/utils/openapiRouter.js @@ -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;