diff --git a/.vscode/launch.json b/.vscode/launch.json index c3e92a3..9c231b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,7 @@ "type": "chrome", "request": "launch", "name": "Launch Chrome", - "url": "http://localhost:8080/nodes", + "url": "http://localhost:8080/api-docs-test", "webRoot": "${workspaceFolder}" }, { diff --git a/.vscode/launch.json b/.vscode/launch.json index c3e92a3..9c231b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,7 @@ "type": "chrome", "request": "launch", "name": "Launch Chrome", - "url": "http://localhost:8080/nodes", + "url": "http://localhost:8080/api-docs-test", "webRoot": "${workspaceFolder}" }, { diff --git a/src/OpenApiGenerator.ts b/src/OpenApiGenerator.ts new file mode 100644 index 0000000..573eb69 --- /dev/null +++ b/src/OpenApiGenerator.ts @@ -0,0 +1,113 @@ +import { OpenAPIV3 } from "openapi-types"; +import { Description } from "./app"; +import { Collection, Entity, Property } from "./MySchema"; + +type APISchemas = { + [key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.ArraySchemaObject | OpenAPIV3.NonArraySchemaObject; +}; + +function generateSchemas(entities: Entity[]): APISchemas { + return fromEntites(entities.map(generateSchema)); +} + +function generateSchema(entity: Entity): [string, OpenAPIV3.NonArraySchemaObject] { + const schema: OpenAPIV3.NonArraySchemaObject = { + type: "object", + required: entity.properties.filter(p => !p.isNullable).map(p => p.key).concat(["id", "ref"]), + properties: fromEntites(addIdProperties(entity.properties.map(generateProperty))), + }; + return [ + entity.name, + schema, + ]; +} + +function addIdProperties(properties: [string, OpenAPIV3.NonArraySchemaObject][]): [string, OpenAPIV3.NonArraySchemaObject][] { + return [ + ["id", { type: "number", format: "int32" }], + ...properties, + ["ref", { type: "string", format: "uri" }], + ]; +} + +function fromEntites(list: [string, T][]): { [key: string]: T; } { + const obj: { [key: string]: T; } = {}; + for (const mapped of list) { + obj[mapped[0]] = mapped[1]; + } + return obj; +} + +function generateProperty(property: Property): [string, OpenAPIV3.NonArraySchemaObject] { + const prop: OpenAPIV3.NonArraySchemaObject = { + type: property.type, + } + return [ + property.key, + prop, + ]; +} + +function generatePaths(collections: Collection[]): OpenAPIV3.PathsObject { + return fromEntites(collections.map(generateCollectionPath)); +} + +function generateCollectionPath(collection: Collection): [string, OpenAPIV3.PathItemObject] { + const route: string = "/" + collection.name.toLowerCase(); + const path: OpenAPIV3.PathItemObject = { + get: generateGetAllOperation(collection), + }; + return [route, path]; +} + +function generateGetAllOperation(collection: Collection): OpenAPIV3.OperationObject { + const operation: OpenAPIV3.OperationObject = { + responses: { + "200": { + description: `Lists all ${collection.name}`, + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "#/components/schemas/" + collection.entities.name + } + } + } + } + } + } + }; + return operation; +} + +const NotFoundResponse: OpenAPIV3.ResponseObject = { + description: "The specified resource does not exist.", + content: { + "text/plain": { + schema: { + type: "string", + example: "not found", + } + } + } +}; + +export function generateOpenAPI(description: Description): OpenAPIV3.Document { + const doc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { + title: "API for " + description.collections.map(c => c.name).join(", "), + version: "1.0.0", + }, + servers: [{ url: "http://localhost:8080" }], + components: { + schemas: generateSchemas(description.collections.map(c => c.entities)), + responses: { + NotFound: NotFoundResponse + } + }, + paths: generatePaths(description.collections), + }; + return doc; +} diff --git a/.vscode/launch.json b/.vscode/launch.json index c3e92a3..9c231b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,7 @@ "type": "chrome", "request": "launch", "name": "Launch Chrome", - "url": "http://localhost:8080/nodes", + "url": "http://localhost:8080/api-docs-test", "webRoot": "${workspaceFolder}" }, { diff --git a/src/OpenApiGenerator.ts b/src/OpenApiGenerator.ts new file mode 100644 index 0000000..573eb69 --- /dev/null +++ b/src/OpenApiGenerator.ts @@ -0,0 +1,113 @@ +import { OpenAPIV3 } from "openapi-types"; +import { Description } from "./app"; +import { Collection, Entity, Property } from "./MySchema"; + +type APISchemas = { + [key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.ArraySchemaObject | OpenAPIV3.NonArraySchemaObject; +}; + +function generateSchemas(entities: Entity[]): APISchemas { + return fromEntites(entities.map(generateSchema)); +} + +function generateSchema(entity: Entity): [string, OpenAPIV3.NonArraySchemaObject] { + const schema: OpenAPIV3.NonArraySchemaObject = { + type: "object", + required: entity.properties.filter(p => !p.isNullable).map(p => p.key).concat(["id", "ref"]), + properties: fromEntites(addIdProperties(entity.properties.map(generateProperty))), + }; + return [ + entity.name, + schema, + ]; +} + +function addIdProperties(properties: [string, OpenAPIV3.NonArraySchemaObject][]): [string, OpenAPIV3.NonArraySchemaObject][] { + return [ + ["id", { type: "number", format: "int32" }], + ...properties, + ["ref", { type: "string", format: "uri" }], + ]; +} + +function fromEntites(list: [string, T][]): { [key: string]: T; } { + const obj: { [key: string]: T; } = {}; + for (const mapped of list) { + obj[mapped[0]] = mapped[1]; + } + return obj; +} + +function generateProperty(property: Property): [string, OpenAPIV3.NonArraySchemaObject] { + const prop: OpenAPIV3.NonArraySchemaObject = { + type: property.type, + } + return [ + property.key, + prop, + ]; +} + +function generatePaths(collections: Collection[]): OpenAPIV3.PathsObject { + return fromEntites(collections.map(generateCollectionPath)); +} + +function generateCollectionPath(collection: Collection): [string, OpenAPIV3.PathItemObject] { + const route: string = "/" + collection.name.toLowerCase(); + const path: OpenAPIV3.PathItemObject = { + get: generateGetAllOperation(collection), + }; + return [route, path]; +} + +function generateGetAllOperation(collection: Collection): OpenAPIV3.OperationObject { + const operation: OpenAPIV3.OperationObject = { + responses: { + "200": { + description: `Lists all ${collection.name}`, + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "#/components/schemas/" + collection.entities.name + } + } + } + } + } + } + }; + return operation; +} + +const NotFoundResponse: OpenAPIV3.ResponseObject = { + description: "The specified resource does not exist.", + content: { + "text/plain": { + schema: { + type: "string", + example: "not found", + } + } + } +}; + +export function generateOpenAPI(description: Description): OpenAPIV3.Document { + const doc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { + title: "API for " + description.collections.map(c => c.name).join(", "), + version: "1.0.0", + }, + servers: [{ url: "http://localhost:8080" }], + components: { + schemas: generateSchemas(description.collections.map(c => c.entities)), + responses: { + NotFound: NotFoundResponse + } + }, + paths: generatePaths(description.collections), + }; + return doc; +} diff --git a/src/app.ts b/src/app.ts index dcdb3b4..1d57731 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import { generateModel } from "./ModelGenerator"; import { generateDataAccess } from "./DataAccessGenerator"; +import { generateOpenAPI } from "./OpenApiGenerator"; interface Options { typeScriptOutputFolder: string; @@ -33,20 +34,32 @@ } } +/** + * Central function, that contains all the (pure) logic. + * @param options parameters for the generators + * @param description The supplied data model description + */ function generate(options: Options, description: Description): FileWrite[] { // generate model files const modelWrites = generateModel(description.collections); - modelWrites.forEach(w => {w.location = join(options.typeScriptOutputFolder, w.location)}) + modelWrites.forEach(w => { w.location = join(options.typeScriptOutputFolder, w.location) }) // generate data access files const dataAccessWrites = generateDataAccess(description.collections); - dataAccessWrites.forEach(w => {w.location = join(options.typeScriptOutputFolder, w.location)}) + dataAccessWrites.forEach(w => { w.location = join(options.typeScriptOutputFolder, w.location) }) + + // generate openapi file + const openapiDoc = generateOpenAPI(description); + const openapiWrites: FileWrite = { location: options.openapiOutput, content: JSON.stringify(openapiDoc, null, "\t") }; // generate middleware file - // generate openapi file - // -> see https://swagger.io/docs/specification/describing-responses/ - return [...modelWrites, ...dataAccessWrites]; + + return [ + ...modelWrites, + ...dataAccessWrites, + openapiWrites, + ]; } function execute() { diff --git a/.vscode/launch.json b/.vscode/launch.json index c3e92a3..9c231b9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,7 +21,7 @@ "type": "chrome", "request": "launch", "name": "Launch Chrome", - "url": "http://localhost:8080/nodes", + "url": "http://localhost:8080/api-docs-test", "webRoot": "${workspaceFolder}" }, { diff --git a/src/OpenApiGenerator.ts b/src/OpenApiGenerator.ts new file mode 100644 index 0000000..573eb69 --- /dev/null +++ b/src/OpenApiGenerator.ts @@ -0,0 +1,113 @@ +import { OpenAPIV3 } from "openapi-types"; +import { Description } from "./app"; +import { Collection, Entity, Property } from "./MySchema"; + +type APISchemas = { + [key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.ArraySchemaObject | OpenAPIV3.NonArraySchemaObject; +}; + +function generateSchemas(entities: Entity[]): APISchemas { + return fromEntites(entities.map(generateSchema)); +} + +function generateSchema(entity: Entity): [string, OpenAPIV3.NonArraySchemaObject] { + const schema: OpenAPIV3.NonArraySchemaObject = { + type: "object", + required: entity.properties.filter(p => !p.isNullable).map(p => p.key).concat(["id", "ref"]), + properties: fromEntites(addIdProperties(entity.properties.map(generateProperty))), + }; + return [ + entity.name, + schema, + ]; +} + +function addIdProperties(properties: [string, OpenAPIV3.NonArraySchemaObject][]): [string, OpenAPIV3.NonArraySchemaObject][] { + return [ + ["id", { type: "number", format: "int32" }], + ...properties, + ["ref", { type: "string", format: "uri" }], + ]; +} + +function fromEntites(list: [string, T][]): { [key: string]: T; } { + const obj: { [key: string]: T; } = {}; + for (const mapped of list) { + obj[mapped[0]] = mapped[1]; + } + return obj; +} + +function generateProperty(property: Property): [string, OpenAPIV3.NonArraySchemaObject] { + const prop: OpenAPIV3.NonArraySchemaObject = { + type: property.type, + } + return [ + property.key, + prop, + ]; +} + +function generatePaths(collections: Collection[]): OpenAPIV3.PathsObject { + return fromEntites(collections.map(generateCollectionPath)); +} + +function generateCollectionPath(collection: Collection): [string, OpenAPIV3.PathItemObject] { + const route: string = "/" + collection.name.toLowerCase(); + const path: OpenAPIV3.PathItemObject = { + get: generateGetAllOperation(collection), + }; + return [route, path]; +} + +function generateGetAllOperation(collection: Collection): OpenAPIV3.OperationObject { + const operation: OpenAPIV3.OperationObject = { + responses: { + "200": { + description: `Lists all ${collection.name}`, + content: { + "application/json": { + schema: { + type: "array", + items: { + $ref: "#/components/schemas/" + collection.entities.name + } + } + } + } + } + } + }; + return operation; +} + +const NotFoundResponse: OpenAPIV3.ResponseObject = { + description: "The specified resource does not exist.", + content: { + "text/plain": { + schema: { + type: "string", + example: "not found", + } + } + } +}; + +export function generateOpenAPI(description: Description): OpenAPIV3.Document { + const doc: OpenAPIV3.Document = { + openapi: "3.0.0", + info: { + title: "API for " + description.collections.map(c => c.name).join(", "), + version: "1.0.0", + }, + servers: [{ url: "http://localhost:8080" }], + components: { + schemas: generateSchemas(description.collections.map(c => c.entities)), + responses: { + NotFound: NotFoundResponse + } + }, + paths: generatePaths(description.collections), + }; + return doc; +} diff --git a/src/app.ts b/src/app.ts index dcdb3b4..1d57731 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import { generateModel } from "./ModelGenerator"; import { generateDataAccess } from "./DataAccessGenerator"; +import { generateOpenAPI } from "./OpenApiGenerator"; interface Options { typeScriptOutputFolder: string; @@ -33,20 +34,32 @@ } } +/** + * Central function, that contains all the (pure) logic. + * @param options parameters for the generators + * @param description The supplied data model description + */ function generate(options: Options, description: Description): FileWrite[] { // generate model files const modelWrites = generateModel(description.collections); - modelWrites.forEach(w => {w.location = join(options.typeScriptOutputFolder, w.location)}) + modelWrites.forEach(w => { w.location = join(options.typeScriptOutputFolder, w.location) }) // generate data access files const dataAccessWrites = generateDataAccess(description.collections); - dataAccessWrites.forEach(w => {w.location = join(options.typeScriptOutputFolder, w.location)}) + dataAccessWrites.forEach(w => { w.location = join(options.typeScriptOutputFolder, w.location) }) + + // generate openapi file + const openapiDoc = generateOpenAPI(description); + const openapiWrites: FileWrite = { location: options.openapiOutput, content: JSON.stringify(openapiDoc, null, "\t") }; // generate middleware file - // generate openapi file - // -> see https://swagger.io/docs/specification/describing-responses/ - return [...modelWrites, ...dataAccessWrites]; + + return [ + ...modelWrites, + ...dataAccessWrites, + openapiWrites, + ]; } function execute() { diff --git a/src/server.ts b/src/server.ts index d70107d..3813ce8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,8 +14,10 @@ import * as swaggerUi from "swagger-ui-express"; import * as swaggerDocument from "./ExampleOutput/openapi.json"; +import * as swaggerDocumentTest from "./TestOutput/openapi.json"; app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); +app.use('/api-docs-test', swaggerUi.serve, swaggerUi.setup(swaggerDocumentTest)); app.get(`${webRoot}${INDEX_ROUTE}`, (req, res) => { res.send("Hello there :-)");