diff --git a/.travis.yml b/.travis.yml index cf9167cc..73bdfcb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ - language: node_js node_js: - "node" @@ -26,5 +25,7 @@ jobs: notifications: slack: + on_success: never + on_failure: always rooms: - secure: vOYvagXgEbjgwSJaK70QU574OcUq1MyqJYJp08wqcLA2AUfxrN3HJj24s2qd3NAMIU2Zw6jfEwQBbH3vV10cwf2u5Yp9pAgbrLOJCV+1wixMJL6GxWLesbtUuizoOCEL+f5qogFPNpy4W2FDcG0DEYwuhLSdf5qToGXP5BSn3VPuvZYRhKRq9wJpeXNTLbu+XW8hEkuUpjbr2zzm+k8pCggONGVQ5SCSCI5DVHsCnsxzumwNcKbNitLiRMQKRJiFCOQ4YQwpZ7SCEu75YxMjb+UDIEYi5CtxgxCRaoBZfYj/uI4exxavmb0/vp5CJ1dTUNmI2R3pTQUIERoFXajl479FMnxhGsKycVG7O2hKqxNCDT8tPAzxdMaF31u7AtWajdZpmOLyAIc/RjUAJfuSlzT3hcSfZUQ23VOcf8Q/yJXVT6XqYPuEO8xhVXdCJN4PtDowjzeoMqZCTSmknrPcKGfvuY86rw5aIFngYy1TysR8fhDgpxqTmsrG2HOrVzmq4oLsEsm6jGqghvLM3x03HOxX6pdo/lDoaTl1lR0ZraC1tGvGQpb1M38GxNpWFmmqb0A7Q9aHK4OBddvEMEU1nivb2t41DQou3ga+czm8vo1o0cEXOczDQmCQhOrPn0/m5ud49rzMLfxhXQbT4fphuNAAbumn2dc2eKc3ZB3w4ng= \ No newline at end of file + secure: vOYvagXgEbjgwSJaK70QU574OcUq1MyqJYJp08wqcLA2AUfxrN3HJj24s2qd3NAMIU2Zw6jfEwQBbH3vV10cwf2u5Yp9pAgbrLOJCV+1wixMJL6GxWLesbtUuizoOCEL+f5qogFPNpy4W2FDcG0DEYwuhLSdf5qToGXP5BSn3VPuvZYRhKRq9wJpeXNTLbu+XW8hEkuUpjbr2zzm+k8pCggONGVQ5SCSCI5DVHsCnsxzumwNcKbNitLiRMQKRJiFCOQ4YQwpZ7SCEu75YxMjb+UDIEYi5CtxgxCRaoBZfYj/uI4exxavmb0/vp5CJ1dTUNmI2R3pTQUIERoFXajl479FMnxhGsKycVG7O2hKqxNCDT8tPAzxdMaF31u7AtWajdZpmOLyAIc/RjUAJfuSlzT3hcSfZUQ23VOcf8Q/yJXVT6XqYPuEO8xhVXdCJN4PtDowjzeoMqZCTSmknrPcKGfvuY86rw5aIFngYy1TysR8fhDgpxqTmsrG2HOrVzmq4oLsEsm6jGqghvLM3x03HOxX6pdo/lDoaTl1lR0ZraC1tGvGQpb1M38GxNpWFmmqb0A7Q9aHK4OBddvEMEU1nivb2t41DQou3ga+czm8vo1o0cEXOczDQmCQhOrPn0/m5ud49rzMLfxhXQbT4fphuNAAbumn2dc2eKc3ZB3w4ng= diff --git a/CHANGELOG.md b/CHANGELOG.md index 0647d643..a355822b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # next release +# 1.6.3 + +Fixes: + +- Handling of nullable for $ref in OpenAPI 3.0 ([issue](https://github.com/acacode/swagger-typescript-api/issues/39)) + Plus based on this issue was fixed most other problems with using `required` and `nullable` properties + + # 1.6.2 Fixes: diff --git a/package-lock.json b/package-lock.json index dc1a862a..b2b6026b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "swagger-typescript-api", - "version": "1.6.2", + "version": "1.6.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 001918ef..c3728f2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swagger-typescript-api", - "version": "1.6.2", + "version": "1.6.3", "description": "Create typescript api module from swagger schema", "scripts": { "cli": "node index.js -d -p ./swagger-test-cli.json -n swagger-test-cli.ts", diff --git a/src/config.js b/src/config.js index cb997951..76ecb9f7 100644 --- a/src/config.js +++ b/src/config.js @@ -1,4 +1,3 @@ - const config = { /** CLI flag */ generateResponses: false, @@ -8,14 +7,17 @@ const config = { generateRouteTypes: false, /** CLI flag */ generateClient: true, - /** parsed swagger schema from getSwaggerObject() */ + /** parsed swagger schema from getSwaggerObject() */ + swaggerSchema: null, /** { "#/components/schemas/Foo": @TypeInfo, ... } */ componentsMap: {}, -} + /** flag for catching convertion from swagger 2.0 */ + convertedFromSwagger2: false, +}; /** needs to use data everywhere in project */ module.exports = { - addToConfig: configParts => Object.assign(config, configParts), + addToConfig: (configParts) => Object.assign(config, configParts), config, -} +}; diff --git a/src/index.js b/src/index.js index b76e634d..40970095 100644 --- a/src/index.js +++ b/src/index.js @@ -16,9 +16,9 @@ const { getModelType } = require("./modelTypes"); const { getSwaggerObject } = require("./swagger"); const { createComponentsMap, filterComponentsMap } = require("./components"); const { getTemplate, createFile, pathIsExist } = require("./files"); -const { addToConfig, config: defaults } = require("./config"); +const { addToConfig, config } = require("./config"); -mustache.escape = value => value; +mustache.escape = (value) => value; const prettierConfig = { printWidth: 120, @@ -33,10 +33,10 @@ module.exports = { output, url, name, - generateResponses = defaults.generateResponses, - defaultResponseAsSuccess = defaults.defaultResponseAsSuccess, - generateRouteTypes = defaults.generateRouteTypes, - generateClient = defaults.generateClient, + generateResponses = config.generateResponses, + defaultResponseAsSuccess = config.defaultResponseAsSuccess, + generateRouteTypes = config.generateRouteTypes, + generateClient = config.generateClient, }) => new Promise((resolve, reject) => { addToConfig({ @@ -46,7 +46,7 @@ module.exports = { generateResponses, }); getSwaggerObject(input, url) - .then(swaggerSchema => { + .then((swaggerSchema) => { console.log("☄️ start generating your typescript api"); addToConfig({ swaggerSchema }); @@ -62,8 +62,8 @@ module.exports = { const parsedSchemas = parseSchemas(components); const routes = parseRoutes(swaggerSchema, parsedSchemas, componentsMap, components); - const hasSecurityRoutes = routes.some(route => route.security); - const hasQueryRoutes = routes.some(route => route.hasQuery); + const hasSecurityRoutes = routes.some((route) => route.security); + const hasQueryRoutes = routes.some((route) => route.hasQuery); const apiConfig = createApiConfig({ info, servers }, hasSecurityRoutes); const configuration = { @@ -91,7 +91,7 @@ module.exports = { resolve(sourceFile); }) - .catch(e => { + .catch((e) => { reject(e); throw new Error("Swagger schema parse error!\r\n " + e); }); diff --git a/src/schema.js b/src/schema.js index d839373d..a727e2a0 100644 --- a/src/schema.js +++ b/src/schema.js @@ -19,18 +19,21 @@ const findSchemaType = (schema) => { return "primitive"; }; +const nullableExtras = (schema, value) => { + const { nullable, type } = schema || {}; + return nullable || type === "null" ? `${value} | null` : value; +}; + const getPrimitiveType = (property) => { - const { type, nullable } = property || {}; + const { type } = property || {}; const primitiveType = typeAliases[type] || type; - return primitiveType - ? (nullable && `${primitiveType} | null`) || primitiveType - : DEFAULT_PRIMITIVE_TYPE; + return primitiveType ? nullableExtras(property, primitiveType) : DEFAULT_PRIMITIVE_TYPE; }; const specificObjectTypes = { - array: ({ items }) => { + array: ({ items, ...schemaPart }) => { const { content, type } = parseSchema(items, null, inlineExtraFormatters); - return type === "primitive" ? `${content}[]` : `Array<${content}>`; + return nullableExtras(schemaPart, type === "primitive" ? `${content}[]` : `Array<${content}>`); }, }; @@ -48,14 +51,17 @@ const getType = (property) => { if (!property) return DEFAULT_PRIMITIVE_TYPE; const anotherTypeGetter = specificObjectTypes[property.type] || getPrimitiveType; - return getRefTypeName(property) || anotherTypeGetter(property); + const refType = getRefTypeName(property); + return refType ? nullableExtras(property, refType) : anotherTypeGetter(property); }; const getObjectTypeContent = (properties) => { return _.map(properties, (property, name) => { - // TODO: probably nullable should'n be use as required/no-required conditions - const isRequired = - typeof property.nullable === "undefined" ? property.required : !property.nullable; + const isRequired = config.convertedFromSwagger2 + ? typeof property.nullable === "undefined" + ? property.required + : !property.nullable + : !!property.required; return { description: property.description, isRequired, @@ -72,16 +78,21 @@ const complexSchemaParsers = { oneOf: (schema) => { // T1 | T2 const combined = _.map(schema.oneOf, complexTypeGetter); - return combined.join(" | "); + return nullableExtras(schema, combined.join(" | ")); }, allOf: (schema) => { // T1 & T2 - return _.map(schema.allOf, complexTypeGetter).join(" & "); + return nullableExtras(schema, _.map(schema.allOf, complexTypeGetter).join(" & ")); }, anyOf: (schema) => { // T1 | T2 | (T1 & T2) const combined = _.map(schema.anyOf, complexTypeGetter); - return `${combined.join(" | ")}` + (combined.length > 1 ? ` | (${combined.join(" & ")})` : ""); + const nonEmptyTypesCombined = combined.filter((type) => !jsEmptyTypes.includes(type)); + return nullableExtras( + schema, + `${combined.join(" | ")}` + + (nonEmptyTypesCombined.length > 1 ? ` | (${nonEmptyTypesCombined.join(" & ")})` : ""), + ); }, // TODO not: (schema) => { @@ -170,7 +181,8 @@ const schemaParsers = { typeIdentifier: "type", name: typeName, description: formatDescription(description), - content: contentType || getType(schema), + // TODO: probably it should be refactored. `type === 'null'` is not flexible + content: type === "null" ? type : contentType || getType(schema), }; }, }; @@ -191,12 +203,10 @@ const parseSchema = (rawSchema, typeName, formattersMap) => { let parsedSchema = null; if (typeof rawSchema === "string") { - console.log("WOW THERE IS STRING", rawSchema); return rawSchema; } if (rawSchema.$parsedSchema) { - console.log("IT IS ALREADY PARSED SCHEMA", rawSchema); schemaType = rawSchema.schemaType; parsedSchema = rawSchema; } else { diff --git a/src/swagger.js b/src/swagger.js index 69c9f496..10173d1a 100644 --- a/src/swagger.js +++ b/src/swagger.js @@ -1,7 +1,8 @@ -const _ = require('lodash'); -const yaml = require('js-yaml'); +const _ = require("lodash"); +const yaml = require("js-yaml"); const axios = require("axios"); -const converter = require('swagger2openapi'); +const converter = require("swagger2openapi"); +const { addToConfig } = require("./config"); const { pathIsExist, getFileContent } = require("./files"); const parseSwaggerFile = (file) => { @@ -10,44 +11,54 @@ const parseSwaggerFile = (file) => { try { return JSON.parse(file); } catch (e) { - return yaml.safeLoad(file) + return yaml.safeLoad(file); } -} +}; -const getSwaggerFile = (pathToSwagger, urlToSwagger) => new Promise((resolve) => { - if (pathIsExist(pathToSwagger)){ - console.log(`✨ try to get swagger by path "${pathToSwagger}"`) - resolve(getFileContent(pathToSwagger)) - } else { - console.log(`✨ try to get swagger by url "${urlToSwagger}"`) - axios.get(urlToSwagger).then(res => resolve(res.data)) - } -}) +const getSwaggerFile = (pathToSwagger, urlToSwagger) => + new Promise((resolve) => { + if (pathIsExist(pathToSwagger)) { + console.log(`✨ try to get swagger by path "${pathToSwagger}"`); + resolve(getFileContent(pathToSwagger)); + } else { + console.log(`✨ try to get swagger by url "${urlToSwagger}"`); + axios.get(urlToSwagger).then((res) => resolve(res.data)); + } + }); const getSwaggerObject = (pathToSwagger, urlToSwagger) => - new Promise(resolve => - getSwaggerFile(pathToSwagger, urlToSwagger).then(file => { - const swaggerSchema = parseSwaggerFile(file); - if (!(swaggerSchema.openapi)) { - converter.convertObj(swaggerSchema, { - warnOnly: true, - refSiblings: 'preserve', - rbname: "requestBodyName", - }, function(err, options){ - const swaggerSchema = _.get(err, 'options.openapi', _.get(options, 'openapi')) - if (!swaggerSchema && err) { - throw new Error(err) - } - resolve(swaggerSchema) - }); - } else { - resolve(swaggerSchema) - } - }).catch(e => { - throw new Error(e) - }) - ) + new Promise((resolve) => + getSwaggerFile(pathToSwagger, urlToSwagger) + .then((file) => { + const swaggerSchema = parseSwaggerFile(file); + if (!swaggerSchema.openapi) { + converter.convertObj( + swaggerSchema, + { + warnOnly: true, + refSiblings: "preserve", + rbname: "requestBodyName", + }, + function (err, options) { + const swaggerSchema = _.get(err, "options.openapi", _.get(options, "openapi")); + if (!swaggerSchema && err) { + throw new Error(err); + } + addToConfig({ + convertedFromSwagger2: true, + }); + resolve(swaggerSchema); + }, + ); + } else { + resolve(swaggerSchema); + } + }) + .catch((e) => { + throw new Error(e); + }), + ); module.exports = { getSwaggerObject, -} \ No newline at end of file +}; diff --git a/tests/generated/v3.0/nullable-refs.ts b/tests/generated/v3.0/nullable-refs.ts new file mode 100644 index 00000000..71ed05f3 --- /dev/null +++ b/tests/generated/v3.0/nullable-refs.ts @@ -0,0 +1,100 @@ +/* tslint:disable */ +/* eslint-disable */ + +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +export interface TestObject { + stringMaybeUndefined?: string; + stringMaybeNullA: string | null; + stringMaybeNullB: string | null; + stringMaybeNullAndUndefined?: string | null; + otherObjectMaybeUndefined?: OtherObject; + otherObjectMaybeNullA: OtherObject | null; + otherObjectMaybeNullB: OtherObject | null; + otherObjectMaybeNullC: OtherObject | null; + otherObjectMaybeNullD: OtherObject | null; +} + +export type OtherObject = object; + +export type RequestParams = Omit & { + secure?: boolean; +}; + +type ApiConfig = { + baseUrl?: string; + baseApiParams?: RequestParams; + securityWorker?: (securityData: SecurityDataType) => RequestParams; +}; + +export class Api { + public baseUrl = ""; + public title = "Nullable Refs Example"; + public version = "1.0.0"; + + private securityData: SecurityDataType = null as any; + private securityWorker: ApiConfig["securityWorker"] = (() => {}) as any; + + private baseApiParams: RequestParams = { + credentials: "same-origin", + headers: { + "Content-Type": "application/json", + }, + redirect: "follow", + referrerPolicy: "no-referrer", + }; + + constructor({ baseUrl, baseApiParams, securityWorker }: ApiConfig = {}) { + this.baseUrl = baseUrl || this.baseUrl; + this.baseApiParams = baseApiParams || this.baseApiParams; + this.securityWorker = securityWorker || this.securityWorker; + } + + public setSecurityData = (data: SecurityDataType) => { + this.securityData = data; + }; + + private mergeRequestOptions(params: RequestParams, securityParams?: RequestParams): RequestParams { + return { + ...this.baseApiParams, + ...params, + ...(securityParams || {}), + headers: { + ...(this.baseApiParams.headers || {}), + ...(params.headers || {}), + ...((securityParams && securityParams.headers) || {}), + }, + }; + } + + private safeParseResponse = (response: Response): Promise => + response + .json() + .then((data) => data) + .catch((e) => response.text); + + public request = ( + path: string, + method: string, + { secure, ...params }: RequestParams = {}, + body?: any, + secureByDefault?: boolean, + ): Promise => + fetch(`${this.baseUrl}${path}`, { + // @ts-ignore + ...this.mergeRequestOptions(params, (secureByDefault || secure) && this.securityWorker(this.securityData)), + method, + body: body ? JSON.stringify(body) : null, + }).then(async (response) => { + const data = await this.safeParseResponse(response); + if (!response.ok) throw data; + return data; + }); +} diff --git a/tests/schemas/v3.0/nullable-refs.yaml b/tests/schemas/v3.0/nullable-refs.yaml new file mode 100644 index 00000000..6ee34e33 --- /dev/null +++ b/tests/schemas/v3.0/nullable-refs.yaml @@ -0,0 +1,49 @@ +openapi: "3.0.1" +info: + title: Nullable Refs Example + version: 1.0.0 +components: + schemas: + TestObject: + type: object + properties: + stringMaybeUndefined: + type: string + stringMaybeNullA: + type: string + nullable: true + stringMaybeNullB: + anyOf: + - type: string + nullable: true + stringMaybeNullAndUndefined: + anyOf: + - type: string + nullable: true + otherObjectMaybeUndefined: + $ref: "#/components/schemas/OtherObject" + otherObjectMaybeNullA: + $ref: "#/components/schemas/OtherObject" + nullable: true + otherObjectMaybeNullB: + anyOf: + - $ref: "#/components/schemas/OtherObject" + nullable: true + otherObjectMaybeNullC: + anyOf: + - $ref: "#/components/schemas/OtherObject" + type: "null" + otherObjectMaybeNullD: + anyOf: + - $ref: "#/components/schemas/OtherObject" + - type: "null" + required: + - stringMaybeNullA + - stringMaybeNullB + - otherObjectMaybeNullA + - otherObjectMaybeNullB + - otherObjectMaybeNullC + - otherObjectMaybeNullD + + OtherObject: + type: object