diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavascriptApolloClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavascriptApolloClientCodegen.java index d27716dffdb..39ba96500a7 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavascriptApolloClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavascriptApolloClientCodegen.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.ComposedSchema; import io.swagger.v3.oas.models.media.Schema; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; @@ -49,8 +50,11 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod public static final String MODULE_NAME = "moduleName"; public static final String PROJECT_DESCRIPTION = "projectDescription"; public static final String PROJECT_VERSION = "projectVersion"; + public static final String USE_PROMISES = "usePromises"; public static final String USE_INHERITANCE = "useInheritance"; + public static final String EMIT_MODEL_METHODS = "emitModelMethods"; public static final String EMIT_JS_DOC = "emitJSDoc"; + public static final String USE_ES6 = "useES6"; public static final String NPM_REPOSITORY = "npmRepository"; final String[][] JAVASCRIPT_SUPPORTING_FILES = { @@ -60,7 +64,8 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod new String[]{"git_push.sh.mustache", "git_push.sh"}, new String[]{"README.mustache", "README.md"}, new String[]{"mocha.opts", "mocha.opts"}, - new String[]{"travis.yml", ".travis.yml"} + new String[]{"travis.yml", ".travis.yml"}, + new String[]{"gitignore.mustache", ".gitignore"} }; final String[][] JAVASCRIPT_ES6_SUPPORTING_FILES = { @@ -71,7 +76,8 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod new String[]{"README.mustache", "README.md"}, new String[]{"mocha.opts", "mocha.opts"}, new String[]{"travis.yml", ".travis.yml"}, - new String[]{".babelrc.mustache", ".babelrc"} + new String[]{".babelrc.mustache", ".babelrc"}, + new String[]{"gitignore.mustache", ".gitignore"} }; protected String projectName; @@ -82,11 +88,14 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod protected String invokerPackage; protected String sourceFolder = "src"; + protected boolean usePromises; + protected boolean emitModelMethods; protected boolean emitJSDoc = true; protected String apiDocPath = "docs/"; protected String modelDocPath = "docs/"; protected String apiTestPath = "api/"; protected String modelTestPath = "model/"; + protected boolean useES6 = true; // default is ES6 protected String npmRepository = null; private String modelPropertyNaming = "camelCase"; @@ -95,10 +104,6 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod modifyFeatureSet(features -> features.includeDocumentationFeatures(DocumentationFeature.Readme)); - generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata) - .stability(Stability.BETA) - .build(); - outputFolder = "generated-code/js"; modelTemplateFiles.put("model.mustache", ".js"); modelTestTemplateFiles.put("model_test.mustache", ".js"); @@ -115,7 +120,7 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod hideGenerationTimestamp = Boolean.TRUE; // reference: http://www.w3schools.com/js/js_reserved.asp - setReservedWordsLowerCase( + reservedWords = new HashSet<>( Arrays.asList( "abstract", "arguments", "boolean", "break", "byte", "case", "catch", "char", "class", "const", @@ -142,10 +147,12 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod defaultIncludes = new HashSet<>(languageSpecificPrimitives); instantiationTypes.put("array", "Array"); + instantiationTypes.put("set", "Array"); instantiationTypes.put("list", "Array"); instantiationTypes.put("map", "Object"); typeMapping.clear(); typeMapping.put("array", "Array"); + typeMapping.put("set", "Array"); typeMapping.put("map", "Object"); typeMapping.put("List", "Array"); typeMapping.put("boolean", "Boolean"); @@ -153,7 +160,7 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod typeMapping.put("int", "Number"); typeMapping.put("float", "Number"); typeMapping.put("number", "Number"); - typeMapping.put("BigDecimal", "Number"); + typeMapping.put("decimal", "Number"); typeMapping.put("DateTime", "Date"); typeMapping.put("date", "Date"); typeMapping.put("long", "Number"); @@ -167,6 +174,7 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod typeMapping.put("file", "File"); typeMapping.put("UUID", "String"); typeMapping.put("URI", "String"); + typeMapping.put("AnyType", "Object"); importMapping.clear(); @@ -184,6 +192,12 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod "version of the project (Default: using info.version or \"1.0.0\")")); cliOptions.add(new CliOption(CodegenConstants.LICENSE_NAME, "name of the license the project uses (Default: using info.license.name)")); + cliOptions.add(new CliOption(USE_PROMISES, + "use Promises as return values from the client API, instead of superagent callbacks") + .defaultValue(Boolean.FALSE.toString())); + cliOptions.add(new CliOption(EMIT_MODEL_METHODS, + "generate getters and setters for model properties") + .defaultValue(Boolean.FALSE.toString())); cliOptions.add(new CliOption(EMIT_JS_DOC, "generate JSDoc comments") .defaultValue(Boolean.TRUE.toString())); @@ -241,12 +255,18 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { setInvokerPackage((String) additionalProperties.get(CodegenConstants.INVOKER_PACKAGE)); } + if (additionalProperties.containsKey(USE_PROMISES)) { + setUsePromises(convertPropertyToBooleanAndWriteBack(USE_PROMISES)); + } if (additionalProperties.containsKey(USE_INHERITANCE)) { setUseInheritance(convertPropertyToBooleanAndWriteBack(USE_INHERITANCE)); } else { supportsInheritance = true; supportsMixins = true; } + if (additionalProperties.containsKey(EMIT_MODEL_METHODS)) { + setEmitModelMethods(convertPropertyToBooleanAndWriteBack(EMIT_MODEL_METHODS)); + } if (additionalProperties.containsKey(EMIT_JS_DOC)) { setEmitJSDoc(convertPropertyToBooleanAndWriteBack(EMIT_JS_DOC)); } @@ -277,7 +297,7 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod if (StringUtils.isEmpty(info.getDescription())) { projectDescription = "JS API client generated by OpenAPI Generator"; } else { - projectDescription = sanitizeName(info.getDescription()); + projectDescription = info.getDescription(); } } @@ -293,7 +313,7 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod projectName = "openapi-js-client"; } if (StringUtils.isBlank(moduleName)) { - moduleName = camelize(underscore(projectName)); + moduleName = camelize(underscore(sanitizeName(projectName))); } if (StringUtils.isBlank(projectVersion)) { projectVersion = "1.0.0"; @@ -314,8 +334,11 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod additionalProperties.put(CodegenConstants.INVOKER_PACKAGE, invokerPackage); additionalProperties.put(CodegenConstants.MODEL_PACKAGE, modelPackage); additionalProperties.put(CodegenConstants.SOURCE_FOLDER, sourceFolder); + additionalProperties.put(USE_PROMISES, usePromises); additionalProperties.put(USE_INHERITANCE, supportsInheritance); + additionalProperties.put(EMIT_MODEL_METHODS, emitModelMethods); additionalProperties.put(EMIT_JS_DOC, emitJSDoc); + additionalProperties.put(USE_ES6, useES6); additionalProperties.put(NPM_REPOSITORY, npmRepository); // make api and model doc path available in mustache template @@ -323,6 +346,9 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod additionalProperties.put("modelDocPath", modelDocPath); String[][] supportingTemplateFiles = JAVASCRIPT_SUPPORTING_FILES; + if (useES6) { + supportingTemplateFiles = JAVASCRIPT_ES6_SUPPORTING_FILES; + } for (String[] supportingTemplateFile : supportingTemplateFiles) { supportingFiles.add(new SupportingFile(supportingTemplateFile[0], "", supportingTemplateFile[1])); @@ -417,6 +443,10 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod this.licenseName = licenseName; } + public void setUsePromises(boolean usePromises) { + this.usePromises = usePromises; + } + public void setNpmRepository(String npmRepository) { this.npmRepository = npmRepository; } @@ -426,6 +456,10 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod this.supportsMixins = useInheritance; } + public void setEmitModelMethods(boolean emitModelMethods) { + this.emitModelMethods = emitModelMethods; + } + public void setEmitJSDoc(boolean emitJSDoc) { this.emitJSDoc = emitJSDoc; } @@ -507,6 +541,11 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod return name; } + @Override + protected boolean isReservedWord(String word) { + return word != null && reservedWords.contains(word); + } + @Override public String toParamName(String name) { // should be the same as variable name @@ -867,22 +906,36 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod return null; } + private String getModelledType(String dataType) { + return "module:" + (StringUtils.isEmpty(invokerPackage) ? "" : (invokerPackage + "/")) + + (StringUtils.isEmpty(modelPackage) ? "" : (modelPackage + "/")) + dataType; + } + private String getJSDocType(CodegenModel cm, CodegenProperty cp) { if (Boolean.TRUE.equals(cp.isContainer)) { - if (cp.containerType.equals("array")) - return "Array.<" + cp.items + ">"; + if (cp.containerType.equals("array") || cp.containerType.equals("set")) + return "Array.<" + getJSDocType(cm, cp.items) + ">"; else if (cp.containerType.equals("map")) - return "Object."; + return "Object."; } String dataType = trimBrackets(cp.datatypeWithEnum); if (cp.isEnum) { dataType = cm.classname + '.' + dataType; } + if (isModelledType(cp)) + dataType = getModelledType(dataType); return dataType; } + private boolean isModelledType(CodegenProperty cp) { + // N.B. enums count as modelled types, file is not modelled (SuperAgent uses some 3rd party library). + return cp.isEnum || !languageSpecificPrimitives.contains(cp.baseType == null ? cp.dataType : cp.baseType); + } + private String getJSDocType(CodegenParameter cp) { String dataType = trimBrackets(cp.dataType); + if (isModelledType(cp)) + dataType = getModelledType(dataType); if (Boolean.TRUE.equals(cp.isArray)) { return "Array.<" + dataType + ">"; } else if (Boolean.TRUE.equals(cp.isMap)) { @@ -891,9 +944,16 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod return dataType; } + private boolean isModelledType(CodegenParameter cp) { + // N.B. enums count as modelled types, file is not modelled (SuperAgent uses some 3rd party library). + return cp.isEnum || !languageSpecificPrimitives.contains(cp.baseType == null ? cp.dataType : cp.baseType); + } + private String getJSDocType(CodegenOperation co) { String returnType = trimBrackets(co.returnType); if (returnType != null) { + if (isModelledType(co)) + returnType = getModelledType(returnType); if (Boolean.TRUE.equals(co.isArray)) { return "Array.<" + returnType + ">"; } else if (Boolean.TRUE.equals(co.isMap)) { @@ -903,12 +963,16 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod return returnType; } + private boolean isModelledType(CodegenOperation co) { + // This seems to be the only way to tell whether an operation return type is modelled. + return !Boolean.TRUE.equals(co.returnTypeIsPrimitive); + } + private boolean isPrimitiveType(String type) { final String[] primitives = {"number", "integer", "string", "boolean", "null"}; return Arrays.asList(primitives).contains(type); } - @SuppressWarnings("unchecked") @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { // Generate and store argument list string of each operation into @@ -937,6 +1001,9 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod argList.add("opts"); } + // add the 'requestInit' parameter + argList.add("requestInit"); + String joinedArgList = StringUtils.join(argList, ", "); operation.vendorExtensions.put("x-codegen-arg-list", joinedArgList); operation.vendorExtensions.put("x-codegen-has-optional-params", hasOptionalParams); @@ -1145,6 +1212,24 @@ public class JavascriptApolloClientCodegen extends DefaultCodegen implements Cod } } + @Override + protected String getCollectionFormat(CodegenParameter codegenParameter) { + // This method will return `passthrough` when the parameter data format is binary and an array. + // `passthrough` is not part of the OAS spec. However, this will act like a flag that we should + // not do any processing on the collection type (i.e. convert to tsv, csv, etc..). This is + // critical to support multi file uploads correctly. + if (codegenParameter.isArray && Objects.equals(codegenParameter.dataFormat, "binary")) { + return "passthrough"; + } + return super.getCollectionFormat(codegenParameter); + } + @Override public GeneratorLanguage generatorLanguage() { return GeneratorLanguage.JAVASCRIPT; } + + @Override + protected void addImport(ComposedSchema composed, Schema childSchema, CodegenModel model, String modelName ) { + // import everything (including child schema of a composed schema) + addImport(model, modelName); + } } diff --git a/modules/openapi-generator/src/main/resources/Javascript-Apollo/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Javascript-Apollo/ApiClient.mustache index 8914ecf5e44..44d7eb3ded0 100644 --- a/modules/openapi-generator/src/main/resources/Javascript-Apollo/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/Javascript-Apollo/ApiClient.mustache @@ -1,6 +1,6 @@ {{>licenseInfo}} -import RESTDataSource from 'apollo-datasource-rest'; +import { RESTDataSource } from 'apollo-datasource-rest'; {{#emitJSDoc}}/** * @module {{#invokerPackage}}{{.}}/{{/invokerPackage}}ApiClient @@ -14,9 +14,16 @@ import RESTDataSource from 'apollo-datasource-rest'; * @class */{{/emitJSDoc}} export default class ApiClient extends RESTDataSource { - constructor() { + constructor(baseURL = '{{{basePath}}}') { super() + {{#emitJSDoc}}/** + * The base URL against which to resolve every API call's (relative) path. + * @type {String} + * @default {{{basePath}}} + */{{/emitJSDoc}} + this.baseURL = baseURL.replace(/\/+$/, ''); + {{#emitJSDoc}}/** * The authentication methods to be included for all API calls. * @type {Array.} @@ -53,7 +60,7 @@ export default class ApiClient extends RESTDataSource { } parametrizePath(path, pathParams) { - return url.replace(/\{([\w-]+)\}/g, (fullMatch, key) => { + return path.replace(/\{([\w-]+)\}/g, (fullMatch, key) => { var value; if (pathParams.hasOwnProperty(key)) { value = this.paramToString(pathParams[key]); @@ -176,7 +183,7 @@ export default class ApiClient extends RESTDataSource { async callApi(path, httpMethod, pathParams, queryParams, headerParams, formParams, bodyParam, authNames, - returnType) { + contentTypes, accepts, returnType, requestInit) { var parameterizedPath = this.parametrizePath(path, pathParams); var fetchOptions = { @@ -203,9 +210,9 @@ export default class ApiClient extends RESTDataSource { var httpMethodFn = httpMethod.toLowerCase(); if (httpMethodFn == 'get' || httpMethodFn == 'delete') { - response = await this[httpMethodFn](parameterizedPath, fetchOptions); + response = await this[httpMethodFn](parameterizedPath, [], requestInit); } else { - response = await this[httpMethodFn](parameterizedPath, body, fetchOptions) + response = await this[httpMethodFn](parameterizedPath, body, requestInit) } var convertedResponse = ApiClient.convertToType(response, returnType); @@ -234,7 +241,7 @@ export default class ApiClient extends RESTDataSource { case 'Blob': return data; default: - if (type === Object) { + if (typeof type === "object") { // generic object, return directly return data; } else if (typeof type.constructFromObject === 'function') { diff --git a/modules/openapi-generator/src/main/resources/Javascript-Apollo/api.mustache b/modules/openapi-generator/src/main/resources/Javascript-Apollo/api.mustache index 63cb101aadb..82ab5f8c99c 100644 --- a/modules/openapi-generator/src/main/resources/Javascript-Apollo/api.mustache +++ b/modules/openapi-generator/src/main/resources/Javascript-Apollo/api.mustache @@ -19,9 +19,8 @@ export default class <&classname> extends ApiClient { * @alias module:<#invokerPackage><&invokerPackage>/<#apiPackage><&apiPackage>/ * @class */ - constructor() { - super(); - this.baseURL = <#servers.0>basePath<^servers.0>null; + constructor(baseURL = '<&basePath>') { + super(baseURL); } <#operations><#operation><#emitJSDoc> @@ -31,6 +30,7 @@ export default class <&classname> extends ApiClient { * @param {<&vendorExtensions.x-jsdoc-type>} <¶mName> <&description><#hasOptionalParams> * @param {Object} opts Optional parameters<#allParams><^required> * @param {<&vendorExtensions.x-jsdoc-type>} opts.<¶mName> <&description><#defaultValue> (default to <&.>) + * @param requestInit Dynamic configuration. @see {@link https://github.com/apollographql/apollo-server/pull/1277} <=| |=>* @return {Promise|#returnType|<|&vendorExtensions.x-jsdoc-type|>|/returnType|}|=< >=| */ async () { @@ -80,7 +80,7 @@ export default class <&classname> extends ApiClient { return this.callApi( '<&path>', '', pathParams, queryParams, headerParams, formParams, postBody, - authNames, contentTypes, accepts, returnType + authNames, contentTypes, accepts, returnType, requestInit ); } diff --git a/modules/openapi-generator/src/main/resources/Javascript-Apollo/api_test.mustache b/modules/openapi-generator/src/main/resources/Javascript-Apollo/api_test.mustache index f04f1bc3cce..ec22d00bef6 100644 --- a/modules/openapi-generator/src/main/resources/Javascript-Apollo/api_test.mustache +++ b/modules/openapi-generator/src/main/resources/Javascript-Apollo/api_test.mustache @@ -1,32 +1,38 @@ {{>licenseInfo}} -// CommonJS-like environments that support module.exports, like Node. -factory(require('expect.js'), require(process.cwd()+'/src/{{#invokerPackage}}{{.}}/{{/invokerPackage}}index')); +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. + define(['expect.js', process.cwd()+'/src/{{#invokerPackage}}{{.}}/{{/invokerPackage}}index'], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS-like environments that support module.exports, like Node. + factory(require('expect.js'), require(process.cwd()+'/src/{{#invokerPackage}}{{.}}/{{/invokerPackage}}index')); + } +}(this, function(expect, {{moduleName}}) { + 'use strict'; -'use strict'; + var instance; -var instance; + beforeEach(function() { + instance = new {{moduleName}}.{{classname}}(); + }); -beforeEach(function() { - instance = new {{moduleName}}.{{classname}}(); -}); + var getProperty = function(object, getter, property) { + // Use getter method if present; otherwise, get the property directly. + if (typeof object[getter] === 'function') + return object[getter](); + else + return object[property]; + } -var getProperty = function(object, getter, property) { - // Use getter method if present; otherwise, get the property directly. - if (typeof object[getter] === 'function') - return object[getter](); - else - return object[property]; -} + var setProperty = function(object, setter, property, value) { + // Use setter method if present; otherwise, set the property directly. + if (typeof object[setter] === 'function') + object[setter](value); + else + object[property] = value; + } -var setProperty = function(object, setter, property, value) { - // Use setter method if present; otherwise, set the property directly. - if (typeof object[setter] === 'function') - object[setter](value); - else - object[property] = value; -} - -describe('{{classname}}', function() { + describe('{{classname}}', function() { {{#operations}} {{#operation}} describe('{{operationId}}', function() { @@ -41,4 +47,6 @@ describe('{{classname}}', function() { }); {{/operation}} {{/operations}} -}); \ No newline at end of file + }); + +})); diff --git a/modules/openapi-generator/src/main/resources/Javascript-Apollo/package.mustache b/modules/openapi-generator/src/main/resources/Javascript-Apollo/package.mustache index e6d3c752295..60e3876d46c 100644 --- a/modules/openapi-generator/src/main/resources/Javascript-Apollo/package.mustache +++ b/modules/openapi-generator/src/main/resources/Javascript-Apollo/package.mustache @@ -3,8 +3,10 @@ "version": "{{{projectVersion}}}", "description": "{{{projectDescription}}}", "license": "{{licenseName}}", - "main": "src/index.js", + "main": "dist{{#invokerPackage}}/{{.}}{{/invokerPackage}}/index.js", "scripts": { + "build": "babel src -d dist", + "prepare": "npm run build", "test": "mocha --require @babel/register --recursive" }, "browser": { @@ -16,14 +18,35 @@ }, {{/npmRepository}} "dependencies": { - "apollo-datasource-rest": "^0.7.0" + "@babel/cli": "^7.0.0", + "apollo-datasource-rest": "^3.6.1", + "superagent": "^5.3.0" }, "devDependencies": { + "@babel/core": "^7.0.0", + "@babel/plugin-proposal-class-properties": "^7.0.0", + "@babel/plugin-proposal-decorators": "^7.0.0", + "@babel/plugin-proposal-do-expressions": "^7.0.0", + "@babel/plugin-proposal-export-default-from": "^7.0.0", + "@babel/plugin-proposal-export-namespace-from": "^7.0.0", + "@babel/plugin-proposal-function-bind": "^7.0.0", + "@babel/plugin-proposal-function-sent": "^7.0.0", + "@babel/plugin-proposal-json-strings": "^7.0.0", + "@babel/plugin-proposal-logical-assignment-operators": "^7.0.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.0.0", + "@babel/plugin-proposal-numeric-separator": "^7.0.0", + "@babel/plugin-proposal-optional-chaining": "^7.0.0", + "@babel/plugin-proposal-pipeline-operator": "^7.0.0", + "@babel/plugin-proposal-throw-expressions": "^7.0.0", + "@babel/plugin-syntax-dynamic-import": "^7.0.0", + "@babel/plugin-syntax-import-meta": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "@babel/register": "^7.0.0", "expect.js": "^0.3.1", - "mocha": "^5.2.0", + "mocha": "^8.0.1", "sinon": "^7.2.0" }, "files": [ - "src" + "dist" ] }