Fix javascript-apollo generator/template (#13191)

* bump apollo-datasource-rest to 3.6.1

* Fix RESTDataSource import

* fix ApiClient template

* fix parameters in callApi, add requestInit param

* change parameters to RESTDataSource convenience methods

* add .babelrc file, even in ES6

* simplify .babelrc... no need for all those fancy things, i think

* fix API test mustache template

* Update package.mustache

* add Set as a language-specific primitive

* Update JavascriptApolloClientCodegen.java

* make babel packages dev dependencies only

* Get inspiration from the main Javascript Generator 😖

* correctly get the basePath from spec

* fix basePath template reference

* Do not sanitize project description

Project descriptions are a multiline string — we just need to escape a few special characters (which we're already doing by invoking escapeText() on line 334)

* Fix module name when generating scoped package

If we set the project name to "@myorg/mypackage" (scoped package[1]), running `npm test` will fail, as it will generate faulty code.

[1] https://docs.npmjs.com/about-scopes
This commit is contained in:
João Neto 2022-08-22 12:39:50 +02:00 committed by GitHub
parent fa22ba9dd9
commit ed10360fc6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 176 additions and 53 deletions

View File

@ -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.<String, " + cp.items + ">";
return "Object.<String, " + getJSDocType(cm, cp.items) + ">";
}
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<ModelMap> 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);
}
}

View File

@ -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.<String>}
@ -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') {

View File

@ -19,9 +19,8 @@ export default class <&classname> extends ApiClient {
* @alias module:<#invokerPackage><&invokerPackage>/</invokerPackage><#apiPackage><&apiPackage>/</apiPackage><classname>
* @class
*/</emitJSDoc>
constructor() {
super();
this.baseURL = <#servers.0>basePath</servers.0><^servers.0>null</servers.0>;
constructor(baseURL = '<&basePath>') {
super(baseURL);
}
<#operations><#operation><#emitJSDoc>
@ -31,6 +30,7 @@ export default class <&classname> extends ApiClient {
* @param {<&vendorExtensions.x-jsdoc-type>} <&paramName> <&description></required></allParams><#hasOptionalParams>
* @param {Object} opts Optional parameters<#allParams><^required>
* @param {<&vendorExtensions.x-jsdoc-type>} opts.<&paramName> <&description><#defaultValue> (default to <&.>)</defaultValue></required></allParams></hasOptionalParams>
* @param requestInit Dynamic configuration. @see {@link https://github.com/apollographql/apollo-server/pull/1277}
<=| |=>* @return {Promise|#returnType|<|&vendorExtensions.x-jsdoc-type|>|/returnType|}|=< >=|
*/
</emitJSDoc> async <operationId>(<vendorExtensions.x-codegen-arg-list>) {
@ -80,7 +80,7 @@ export default class <&classname> extends ApiClient {
return this.callApi(
'<&path>', '<httpMethod>',
pathParams, queryParams, headerParams, formParams, postBody,
authNames, contentTypes, accepts, returnType
authNames, contentTypes, accepts, returnType, requestInit
);
}
</operation></operations>

View File

@ -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}}
});
});
}));

View File

@ -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"
]
}