Fixing Open-Api Creation.

Provinding Swagger UI.
This commit is contained in:
Martin Karkowski 2020-08-25 10:21:55 +02:00
parent 8787a7cc75
commit d325c07c6c
27 changed files with 1813 additions and 81 deletions

2
api/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Logs
backend

30
lib/cli/generateFiles.ts Normal file
View File

@ -0,0 +1,30 @@
import { readFile } from 'fs/promises';
import { generateOpenAPI } from "../helpers/generateOpenAPI";
import { generateClientTemplate } from "../helpers/generateTemplate";
import { getLogger } from '../logger/getLogger';
const main = async function () {
// Read the Config in.
const config = JSON.parse(
await readFile(
'./nopeconfig.json',
{
encoding: 'utf8'
}
)
);
// Define a Logger
const logger = getLogger('info');
logger.info('Creating Dispatched API');
await generateClientTemplate(Object.assign(config.dispatcher, { logger }));
logger.info('Creating Open-API');
await generateOpenAPI(Object.assign(config.openapi, { logger }))
}
main().catch(e => console.error(e));

View File

@ -233,7 +233,10 @@ function _getType(node: Node, inputType: Type, text: string) {
};
}));
} else if (Node.isFunctionDeclaration(node) || Node.isFunctionTypeNode(node) || Node.isFunctionLikeDeclaration(node)) {
}
// The Element could be a Function.
if (Node.isFunctionDeclaration(node) || Node.isFunctionTypeNode(node) || Node.isFunctionLikeDeclaration(node)) {
baseType = "function";
// Iterate over the parameter
@ -389,6 +392,11 @@ export function getDescription(declaration: PropertyDeclaration | InterfaceDecla
// Extract the Parameters of the Function
const params: ParameterInformation[] = declaration.getParameters().map((parameter, index) => {
// Based on the Fact, whether the Parameter is a Function or not
// The inputs of the the _getType Function has to be adapted.
const isFunc = Node.isMethodDeclaration(parameter)
return Object.assign(
{
// Name of the parameter
@ -405,7 +413,7 @@ export function getDescription(declaration: PropertyDeclaration | InterfaceDecla
_getType(
parameter.getTypeNode(),
parameter.getType(),
parameter.getText()
isFunc ? parameter.getText() : parameter.getType().getText()
)
)
});

View File

@ -40,49 +40,52 @@ export async function createFile(fileName: string, content: string, options?: (B
* @param options The options to write the file. See original docu: https://nodejs.org/dist/latest-v8.x/docs/api/fs.html#fs_fs_writefile_file_data_options_callback
*/
export async function createPath(path: string) {
// Based on the OS select the Path Element.
const SPLITCHAR = type() === 'Linux' ? '/' : '\\';
// Adapt the File Pathes
path = type() === 'Linux' ? path.replace(/\\\\/g, '/') : path.replace(/\//g, '\\');
// Split the Path into different segements.
const pathParts = path.split('/');
return await mkdir(path, { recursive: true })
if (pathParts.length > 0) {
/** Check if the given Path Ends with a File Name */
if (pathParts[pathParts.length - 1].indexOf('.') !== -1) {
pathParts.pop();
}
// // Based on the OS select the Path Element.
// const SPLITCHAR = type() === 'Linux' ? '/' : '\\';
// // Adapt the File Pathes
// path = type() === 'Linux' ? path.replace(/\\\\/g, '/') : path.replace(/\//g, '\\');
// Reassemble the Segments like that:
//
// C:\\Test\\SubFolder\\AnotherFolder
// Split up:
// => fileParts = [C: , Test , SubFolder, AnotherFolder]
//
// Reassemble:
// 1) C:
// 2) C:\\Test
// 3) C:\\Test\\SubFolder
// 4) ...
//
// Everytime after an assembly Check whether the assembled
// path exists, otherwise create that folder and go on.
// // Split the Path into different segements.
// const pathParts = path.split('/');
for (const [idx, folder] of pathParts.entries()) {
// Assemble the Path.
const currentPath = pathParts.slice(0, idx + 1).join(SPLITCHAR);
// Test and create the Folder
const urlExists = await _exists(currentPath);
// Test if the provided Path exists or not.
if (currentPath && !urlExists) {
await mkdir(currentPath);
} else if (currentPath && urlExists && (await _lstat(currentPath)).isFile()) {
// if (pathParts.length > 0) {
// /** Check if the given Path Ends with a File Name */
// if (pathParts[pathParts.length - 1].indexOf('.') !== -1) {
// pathParts.pop();
// }
throw Error('Cant create File at the specified path. The given URL contains a File. See "' + currentPath + '"');
}
}
}
// // Reassemble the Segments like that:
// //
// // C:\\Test\\SubFolder\\AnotherFolder
// // Split up:
// // => fileParts = [C: , Test , SubFolder, AnotherFolder]
// //
// // Reassemble:
// // 1) C:
// // 2) C:\\Test
// // 3) C:\\Test\\SubFolder
// // 4) ...
// //
// // Everytime after an assembly Check whether the assembled
// // path exists, otherwise create that folder and go on.
return path;
// for (const [idx, folder] of pathParts.entries()) {
// // Assemble the Path.
// const currentPath = pathParts.slice(0, idx + 1).join(SPLITCHAR);
// // Test and create the Folder
// const urlExists = await _exists(currentPath);
// // Test if the provided Path exists or not.
// if (currentPath && !urlExists) {
// await mkdir(currentPath);
// } else if (currentPath && urlExists && (await _lstat(currentPath)).isFile()) {
// throw Error('Cant create File at the specified path. The given URL contains a File. See "' + currentPath + '"');
// }
// }
// }
// return path;
}

View File

@ -2,11 +2,17 @@ import { copyFile, readFile } from "fs/promises";
import * as handlebars from 'handlebars';
import { join } from 'path';
import { Project } from "ts-morph";
import { Logger } from 'winston';
import { IJsonSchema } from "../../types/IJSONSchema";
import { IExportMethodToOpenAPIParameters } from "../openapi/nopeOpenAPIDecorators";
import { DecoratorInformation, MethodInformation } from "./analyzeTypescriptFiles";
import { createFile } from "./fileHelpers";
import { createFile, createPath } from "./fileHelpers";
import { generateSchemas } from "./generateSchemas";
import { schemaGetDefinition } from "./jsonSchemaMethods";
function generateSchema(methodName: string, schema: IJsonSchema) {
return JSON.stringify(schemaGetDefinition(schema, "#/definitions/" + methodName), undefined, 4);
}
/**
* Generate the Client Templates.
@ -16,8 +22,10 @@ export async function generateOpenAPI(options: {
pathToSchemaTemplate: string,
pathToApiTemplate: string,
tempDir: string,
outputDir: string,
inputDir: string,
tsConfigFilePath: string,
logger?: Logger,
}) {
// Create the Output dir (if it doenst exists)
@ -42,10 +50,12 @@ export async function generateOpenAPI(options: {
outputSchema: string
})[] = []
await createPath(join(options.outputDir));
// Firstly copy the nopeDispatcher
await copyFile(
join(__dirname, '..', '..', '..', 'lib', 'dispatcher', 'nopeDispatcher.ts'),
join(options.tempDir, 'api', 'nopeDispatcher.ts')
join(options.outputDir, 'nopeDispatcher.ts')
);
for (const relevantClass of analysis) {
@ -66,14 +76,15 @@ export async function generateOpenAPI(options: {
customParameters: JSON.stringify(method.decoratorSettings.exportMethodToOpenAPI.parameters || [], undefined, 4),
useCustomParameters: Array.isArray(method.decoratorSettings.exportMethodToOpenAPI.parameters),
useDefaultParameters: method.params.length > 0,
inputSchema: JSON.stringify(schemaMapping[relevantClass.className], undefined, 4),
outputSchema: JSON.stringify(schemaMapping[relevantClass.className], undefined, 4)
inputSchema: generateSchema(method.name + 'Input', schemaMapping[relevantClass.className]),
outputSchema: generateSchema(method.name + 'Output', schemaMapping[relevantClass.className]),
}
const item = Object.assign(
method,
{
method: method.params.length > 0 ? 'POST' : 'GET'
method: method.params.length > 0 ? 'POST' : 'GET',
operationId: settings.className + method.name,
},
settings,
method.decoratorSettings.exportMethodToOpenAPI as IExportMethodToOpenAPIParameters
@ -81,7 +92,7 @@ export async function generateOpenAPI(options: {
methods.push(item);
const fileName = join(options.tempDir, 'api', item.baseUri, item.methodUri + '.ts');
const fileName = join(options.outputDir, item.baseUri, item.methodUri + '.ts');
// Write down the Schema:
await createFile(
@ -90,6 +101,10 @@ export async function generateOpenAPI(options: {
renderAPI(item)
);
if (options.logger) {
options.logger.info('Generated -> ' + fileName);
}
// Function to Determine new project files.
const project = new Project({
tsConfigFilePath: options.tsConfigFilePath,

View File

@ -3,9 +3,11 @@ import * as handlebars from 'handlebars';
import { join } from 'path';
import { Project } from "ts-morph";
import * as TJS from "typescript-json-schema";
import { Logger } from 'winston';
import { IJsonSchema } from "../../types/IJSONSchema";
import { analyzeFiles, defaultClassFilter, defaultPropFilter } from "./analyzeTypescriptFiles";
import { createFile, createPath } from "./fileHelpers";
import { flattenSchema } from "./jsonSchemaMethods";
/**
@ -17,6 +19,7 @@ export async function generateSchemas(options: {
tempDir: string,
inputDir: string,
tsConfigFilePath: string,
logger?: Logger,
}) {
// Create the Output dir (if it doenst exists)
@ -75,16 +78,22 @@ export async function generateSchemas(options: {
});
for (const file of files) {
// Generate the Path.
const fileName = join(options.tempDir, 'classes', file.name + '.ts');
await createFile(
// Generate the Path.
join(options.tempDir, 'classes', file.name + '.ts'),
fileName,
file.content
);
if (options.logger) {
options.logger.info('Generated -> ' + fileName);
}
}
// After all files has been written => Generate the Schemas:
const _settings: TJS.PartialArgs = {
required: true
required: true,
};
// Options for the TJS.Compiler;
@ -103,15 +112,25 @@ export async function generateSchemas(options: {
// We can either get the schema for one file and one type...
const _schema = TJS.generateSchema(_program, "*", _settings);
// Flatten the Schema.
const _flattendSchema = flattenSchema(_schema as any)
// Receive the Element
schemaMapping[file.className] = JSON.parse(JSON.stringify(_schema)) as IJsonSchema;
schemaMapping[file.className] = _flattendSchema;
// Generate the Path.
const fileName = join(options.tempDir, 'schema', file.name + '.json')
// Write down the Schema:
await createFile(
// Generate the Path.
join(options.tempDir, 'schema', file.name + '.json'),
JSON.stringify(_schema, undefined, 4)
fileName,
JSON.stringify(_flattendSchema, undefined, 4)
);
if (options.logger) {
options.logger.info('Generated -> ' + fileName);
}
}
return {

View File

@ -2,6 +2,7 @@ import { copyFile, readFile } from "fs/promises";
import * as handlebars from 'handlebars';
import { join } from 'path';
import { Project } from "ts-morph";
import { Logger } from 'winston';
import { analyzeFiles } from "./analyzeTypescriptFiles";
import { createFile, createPath } from "./fileHelpers";
@ -14,11 +15,17 @@ export async function generateClientTemplate(options: {
outputDir: string,
inputDir: string,
tsConfigFilePath: string,
// A Logger.
logger?: Logger
}) {
// Create the Output dir (if it doenst exists)
await createPath(options.outputDir);
if (options.logger) {
options.logger.info('Templates will be stored in ' + options.outputDir);
}
// Firstly copy the nopeDispatcher
await copyFile(
join(__dirname, '..', '..', '..', 'lib', 'dispatcher', 'nopeDispatcher.ts'),
@ -62,13 +69,19 @@ export async function generateClientTemplate(options: {
});
for (const file of files) {
// Define the File Name:
const fileName = join(options.outputDir, 'clients', file.name);
await createFile(
// Generate the Path.
join(options.outputDir, 'clients', file.name),
file.content
);
if (options.logger) {
options.logger.info('Generated -> ' + fileName);
}
// Function to Determine new project files.
const project = new Project({
tsConfigFilePath: options.tsConfigFilePath,

View File

@ -0,0 +1,76 @@
/**
* Function to stringify an Object. This Function will stringify Functions as well.
* @param obj The Object.
*/
export function stringifyWithFunctions(obj) {
return JSON.stringify(obj, (key, value) => {
if (typeof value === "function") {
let str: string = value.toString();
// Todo Parse Arrow-Functions Correctly!
// Details here: https://zendev.com/2018/10/01/javascript-arrow-functions-how-why-when.html
// Difference Cases For:
// 1) (a, b) => a + b;
// 2) array => array[0];
// 3) (a, b) => (a + b);
// 4) (name, description) => ({name: name, description: description})
// ....
if (!str.startsWith('function') && !str.startsWith('(')) {
const name = str.slice(0, str.indexOf('=>'));
const func = str.slice(str.indexOf('=>(') + 3, str.length - 2)
const adaptedFunc = 'function(' + name + '){ return ' + func + '; }';
str = adaptedFunc;
}
return "/Function(" + str + ")/";
}
return value;
});
}
/**
* Function to parse a JSON String, in which methods should be available.
* @param json A String containing the json Object
*/
export function parseWithFunctions(json: string) {
return JSON.parse(json, (key, value) => {
if (typeof value === "string" &&
value.startsWith("/Function(") &&
value.endsWith(")/")) {
const _value = value.substring(10, value.length - 2);
try {
return eval("(" + _value + ")");
} catch (e) {
console.log('FAILED PARSING', value, _value)
}
}
return value;
});
}
/**
* Function to stringify an Object. This Function is able to stringify Functions as well. Use the Flag withFunctions
* @param obj The Object.
* @param withFunctions Flag to Turn on / off the parsing of functions
*/
export function stringify(obj: any, withFunctions = false): string {
if (withFunctions) {
return stringifyWithFunctions(obj);
}
return JSON.stringify(obj);
}
/**
* Function to parse a JSON String. This Function is able to parse Functions as well. Use the Flag withFunctions
* @param json A String containing the json Object
* @param withFunctions Flag to Turn on / off the parsing of functions
*/
export function parse(json: string, withFunctions = false): any {
if (withFunctions) {
return parseWithFunctions(json);
}
return JSON.parse(json);
}

View File

@ -0,0 +1,55 @@
import { IJsonSchema } from "../../types/IJSONSchema";
import { flattenObject, rgetattr, rsetattr, SPLITCHAR } from "./objectMethods";
/**
* Function to Flatten a JSON-Schema.
* @param schema
*/
export function flattenSchema(schema: IJsonSchema) {
let counter = 10000;
let flattenSchema = flattenObject(schema);
const getRefKeys = (flattenSchema: Map<string, any>) => {
const relevantKeys: Array<{ schemaPath: string, searchPath: string }> = []
for (const [key, value] of flattenSchema) {
if (key.endsWith('$ref')) {
relevantKeys.push({
schemaPath: key,
searchPath: value.replace('#/', '').replace('/', SPLITCHAR)
})
}
}
return relevantKeys;
}
let refs = getRefKeys(flattenSchema);
while (refs.length > 0) {
counter--;
if (counter === 0) {
throw Error('Max amount of Recursions performed')
}
for (const ref of refs) {
const subSchema = rgetattr(schema, ref.searchPath, null, '.');
rsetattr(schema, ref.schemaPath.replace('.$ref', ''), subSchema)
}
flattenSchema = flattenObject(schema);
refs = getRefKeys(flattenSchema);
}
return schema;
}
/**
* Function to get a Schemas Definition
* @param schema the JSON-Schema
* @param reference the path of the relevant definition.
*/
export function schemaGetDefinition(schema: IJsonSchema, reference: string) {
return rgetattr(schema, reference.replace('#/', ''), null, '/');
}

View File

@ -0,0 +1,366 @@
export const SPLITCHAR = '.';
const _sentinel = new Object();
/**
* Function to recurvely get an Attribute of the Object.
*
* @export
* @param {*} _data
* @param {string} _path
* @param {*} [_default=_sentinel]
* @returns {*}
*/
export function rgetattr(_data: any, _path: string, _default: any = _sentinel, _SPLITCHAR: string = SPLITCHAR): any | null {
// Extract the Path
let _obj = _data;
if (_path.length > 0) {
/** Check if there is a Substring available perform the normal method */
if (_path.indexOf(_SPLITCHAR) !== -1) {
for (const attr of _path.split(_SPLITCHAR)) {
/** Access a Map */
if (_obj instanceof Map) {
_obj = _obj.get(attr);
} else {
/** Array or default Object */
_obj = _obj[attr];
}
if ((_obj == null) && (_default === _sentinel)) {
return null;
} else if (_obj == null) {
return _default;
}
}
} else {
/** Otherwise just return the Element */
return _obj[_path];
}
}
return _obj;
}
/**
* Function to Set recursely a Attribute of an Object
*
* @export
* @param {*} _data The Object, where the data should be stored
* @param {string} _path The Path of the Attribute. All are seprated by a '.'! For Instance 'a.b.0.a.c'
* @param {*} _value The Value which should be Stored in the Attribute.
*/
export function rsetattr(_data: any, _path: string, _value: any, _SPLITCHAR: string = SPLITCHAR): void {
let _obj = _data;
const _ptrs = _path.split(_SPLITCHAR);
_ptrs.slice(0, -1).forEach(function (attr: string, idx: number) {
// Adapt the Object by going through a loop
let _sub = _obj[attr];
if (_sub === undefined || _sub === null) {
// _obj is an Array and it doesnt contain the index
// Extract the Next Element:
const _next = _ptrs[idx + 1];
const _next_is_int = isInt(_next);
if (Array.isArray(_obj)) {
if (_next_is_int) {
_obj[attr] = new Array<any>();
} else {
_obj[attr] = {};
}
} else {
if (_next_is_int) {
_obj[attr] = [];
} else {
_obj[attr] = {};
}
}
_sub = _obj[attr];
}
_obj = _sub;
});
_obj[_ptrs[_ptrs.length - 1]] = _value;
}
/**
* Checks whether the Value is an Integer
*
* @export
* @param {*} value Value to be checked
* @returns {boolean} Result
*/
export function isInt(value: any): boolean {
return (parseInt(value) === value);
}
/**
* Checks whether the Value is a Float
*
* @export
* @param {*} value Value to be checked
* @returns {boolean} Result
*/
export function isFloat(value: any): boolean {
return (!isNaN(Number(value)));
}
/**
* Copys the Object. Creates a Deep-Copy
* of the Function
*
* @export
* @param {*} value The value which should be copied
* @returns {*} A Copy of the Value
*/
export function copy(value: any): any {
// TODO RING
// const _copy = {};
// /** Perform a Recursevly Foreach an Set an Attribute. */
// recursiveForEach(value, '', (path: string, _data: any) => {
// rsetattr(_copy, path, _data);
// });
// return _copy;
return JSON.parse(JSON.stringify(value));
}
/**
* Function Converts a Object to a Map.
*
* @export
* @param {*} _obj The Object which should be converted.
* @returns {Map<string,any>}
*/
export function objectToMap(_obj: any): Map<string, any> {
/** Define the Returntype */
const _ret = new Map<string, any>();
/** Iterate through all properties of the Object */
for (const _prop of Object.getOwnPropertyNames(_obj)) {
/** If isnt a function it could be added */
if (typeof _obj !== 'function') {
_ret.set(_prop, _obj[_prop]);
}
}
/** Return the Result */
return _ret;
}
/**
* Checks whether the Value is an Object
*
* @export
* @param {*} value Data to Test
* @returns {boolean} Flag showing whether the Presented Data is an Object
*/
export function isObject(value: any): boolean {
/** Verify whether the value contains some data. */
if (value) {
if ((typeof value === 'object') && !Array.isArray(value)) {
return Object.keys(value).length > 0;
}
}
return false;
}
/**
* Checks whether the Value is an Object
*
* @export
* @param {*} value Data to Test
* @returns {boolean} Flag showing whether the Presented Data is an Object
*/
export function isObjectOrArray(value: any): boolean {
/** Verify whether the value contains some data. */
return isObject(value) || Array.isArray(value);
}
/**
* Flattens an Object to a Map.
*
* For Instance:
*
* data = {a : { b : { c : 1, d: "hallo"}}}
*
* // Normal Call
* res = flatteObject(data,'')
* => res = {"a.b.c":1,"a.b.d":"hallo"}
*
* // With a Selected prefix 'additional.name'
* res = flatteObject(data,'additional.name')
* => res = {"additional.name.a.b.c":1,"additional.name.a.b.d":"hallo"}
*
* @export
* @param {*} data The Data that should be converted
* @param {string} [prefix=''] An additional prefix.
* @returns {Map<string, any>} The flatten Object
*/
export function flattenObject(data: any, prefix: string = '', splitchar: string = SPLITCHAR): Map<string, any> {
const _ret = new Map<string, any>();
if (isObject(data) || Array.isArray(data)) {
recursiveForEach(data, prefix, (path, _data) => {
_ret.set(path, _data);
}, splitchar, true);
}
return _ret;
}
/**
* Function which is executed on each
*
* @export
* @param {*} obj
* @param {string} [prefix='']
* @param {(path: string, data: any) => void} dataCallback
* @returns {*}
*/
export function recursiveForEach(obj: any, prefix: string = '', dataCallback: (path: string, data: any, parent?: string, level?: number) => void, _SPLITCHAR: string = SPLITCHAR, _callOnlyOnValues = true, _parent: string = '', _level = 0): any {
/** Create an Array containing all Keys. */
let keys = Array<string>();
/** Extract Keys of the Object, only if it isnt a string */
if (typeof obj !== 'string' && typeof obj !== 'function') {
keys = Object.getOwnPropertyNames(obj);
if (Array.isArray(obj)) {
keys.splice(keys.indexOf('length'), 1);
}
}
let called = false;
if (!_callOnlyOnValues) {
// Store the Element !
dataCallback(prefix, obj, _parent, _level);
called = true;
}
// If there are Keys => It is a List or a Default Object
if (keys.length > 0) {
for (const _key of keys) {
/** Var containing the Name */
const _str = (prefix === '') ? _key : prefix + _SPLITCHAR + _key;
if (obj[_key] != null) {
if (typeof obj[_key].toJSON === 'function') {
const data = obj[_key].toJSON();
/** Recursive call the Function */
recursiveForEach(data, _str, dataCallback, _SPLITCHAR, _callOnlyOnValues, prefix, _level + 1);
} else {
/** Recursive call the Function */
recursiveForEach(obj[_key], _str, dataCallback, _SPLITCHAR, _callOnlyOnValues, prefix, _level + 1);
}
}
}
} else if (!called) {
// Store the Element !
dataCallback(prefix, obj, prefix, _level);
}
}
/**
* Exports the used Types of an Object
*
* @export
* @param {*} data The Data considered
* @param {string} [prefix='']
* @returns {Map<string, string>}
*/
export function flattenObjectType(data: any, prefix: string = ''): Map<string, string> {
const _ret = new Map<string, string>();
if (isObject(data)) {
recursiveForEach(data, prefix, (path, _data) => {
_ret.set(path, typeof _data);
});
}
return _ret;
}
/**
* Deflattens an Dict Based Object.
*
* @export
* @param {Map<string, any>} _flattenObject
* @returns {*}
*/
export function deflattenObject(_flattenObject: Map<string, any>): any {
const _ret = {};
_flattenObject.forEach((_val: any, _key: string) => {
rsetattr(_ret, _key, _val);
});
return _ret;
}
/**
* Function for deeply assigning
*
* @export
* @param {*} target
* @param {*} source
* @returns
*/
export function deepAssign(target: any, source: any) {
const flattend = flattenObject(source);
for (const [path, value] of flattend.entries()) {
rsetattr(target, path, value);
}
return target;
}
export function deepClone<T>(obj: T) {
let clone: any = Object.assign({}, obj);
Object.keys(clone).forEach(
key => (clone[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
);
return (Array.isArray(obj) && obj.length
? (clone.length = obj.length) && Array.from(clone)
: Array.isArray(obj)
? Array.from(obj)
: clone) as T;
}
// export const deepEqual: (a: any, b: any) => boolean = require('deep-equal');
/**
* Function to adapt the Object and only return a specific amount of elements.
* @param obj The Object itself
* @param properties a list of properties/pathes to keep
*/
export function keepPropertiesOfObject(obj: any, properties: { [index: string]: () => any }) {
if (isObject(obj)) {
const ret: any = {};
const defaultObj = { error: true };
// Iterate over the Properties, get the content of the path, clone it an put it to the
// provided path
Object.getOwnPropertyNames(properties).map(path => {
const value = rgetattr(obj, path, defaultObj);
rsetattr(ret, path, value !== defaultObj ? (typeof value === 'object' ? deepClone(value) : value) : properties[path]());
});
// Return the Object
return ret;
}
// Wrong Datatype provided.
throw TypeError('Function can only create Objects');
}

View File

@ -0,0 +1,52 @@
/**
* Replaces all Chars in a String
* @param str base string
* @param value the value which should be replaced
* @param replacement the value which is used as replacement
*/
export function replaceAll(str: string, value: string, replacement: string): string {
return str.split(value).join(replacement);
}
/**
* Function to Pad a String.
* @param num
* @param size
* @param maxLength
*/
export function padString(num: number, size: number, maxLength = false) {
let _size = size;
if (typeof maxLength === 'boolean' && maxLength) {
_size = Math.ceil(Math.log10(size));
}
let s = num + "";
while (s.length < _size) s = "0" + s;
return s;
}
/**
* Inserts a String in the String
* @param str base string
* @param index index where the content should be inserted
* @param content the content to insert
*/
export function insert(str: string, index: number, content: string): string {
if (index > 0) {
return str.substring(0, index) + content + str.substring(index, str.length);
} else {
return content + str;
}
}
/**
* Function to Camelize a String
* @param str The String,
* @param char A, used to determine "new words"
*/
export function camelize(str: string, char = '_') {
return replaceAll(str, char, ' ').replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index == 0 ? word.toLowerCase() : word.toUpperCase();
}).replace(/\s+/g, '');
}

32
lib/logger/getLogger.ts Normal file
View File

@ -0,0 +1,32 @@
import { createLogger, format, transports } from 'winston';
export type LoggerLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly'
export function getLogger(level: LoggerLevel) {
const readableFormat = format.printf(({ level, message, timestamp }) => {
return `[${timestamp} ${level}]: ${message}`;
});
// Define a Logger
const logger = createLogger({
level,
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss-ms'
}),
format.errors({ stack: true }),
readableFormat
),
exitOnError: false,
transports: [
new (transports.Console)({
format: format.combine(
format.colorize(),
readableFormat
)
}),
]
});
return logger;
}

View File

@ -26,8 +26,7 @@ export default function (_dispatcher: nopeDispatcher) {
// Define the apiDoc for this specific Funtion
POST.apiDoc = {
{{#if methodDescription}}summary: '{{methodDescription}}',{{/if}}
{{#if operationId}}operationId: '{{operationId}}',{{/if}}{{#if operationId}}operationId: '{{operationId}}',{{/if}}
operationId: '{{operationId}}',
{{#if operationId}}operationId: '{{operationId}}'{{/if}},
parameters: [
{{#if useDefaultParameters}}
{
@ -46,12 +45,14 @@ export default function (_dispatcher: nopeDispatcher) {
{{#if hasReturnType}}
200: {
{{#if resultDescription}}description: '{{resultDescription}}',{{/if}}
{{#unless resultDescription}}description: 'Not Provided', {{/unless}}
schema: {{{outputSchema}}}
},
{{/if}}
{{#unless hasReturnType}}
200: {
{{#if resultDescription}}description: '{{resultDescription}}',{{/if}}
{{#unless resultDescription}}description: 'Not Provided', {{/unless}}
},
{{/unless}}
default: {

View File

@ -16,7 +16,7 @@ export interface {{name}}Input {
{{name}}{{#if isOptional}}?{{/if}}: {{{originalCode}}}
{{/if}}
{{#unless isBaseType}}
{{{simplifiedType}}}
{{name}}{{#if isOptional}}?{{/if}}: {{{simplifiedType}}}
{{/unless}}
{{/each}}
}

16
nopeconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"dispatcher": {
"inputDir": "./test/*.ts",
"outputDir": "./pages/backend",
"pathToTemplate": "./lib/templates/clientInterface.handlebars",
"tsConfigFilePath": "./tsconfigBackend.json"
},
"openapi": {
"inputDir": "./test/*.ts",
"outputDir": "./api/backend",
"tempDir": "./temp/",
"pathToSchemaTemplate": "./lib/templates/schema.handlebars",
"pathToApiTemplate": "./lib/templates/openApiSchema.handlebars",
"tsConfigFilePath": "./tsconfigBackend.json"
}
}

944
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,8 +40,10 @@
"rxjs": "^6.6.2",
"socket.io": "^2.3.0",
"socket.io-client": "^2.3.0",
"swagger-ui-react": "^3.32.4",
"ts-morph": "^7.3.0",
"typescript-json-schema": "^0.43.0"
"typescript-json-schema": "^0.43.0",
"winston": "^3.3.3"
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^1.0.15-4.3.1",

2
pages/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# Logs
backend

View File

@ -1,4 +1,5 @@
import 'bootstrap/dist/css/bootstrap.min.css';
import "swagger-ui-react/swagger-ui.css";
export default function App({ Component, pageProps }) {
return <Component {...pageProps} />

14
pages/docs.tsx Normal file
View File

@ -0,0 +1,14 @@
import SwaggerUI from "swagger-ui-react";
import { LocalNavbar } from './ui/navbar';
/**
* Default Docs.
*/
export default function Docs() {
return (
<>
<LocalNavbar></LocalNavbar>
<SwaggerUI url="http://localhost:3001/api/api-docs" />
</>
);
}

View File

@ -1,6 +1,15 @@
export default function Home(){
import Head from 'next/head';
import { LocalNavbar } from './ui/navbar';
export default function Home() {
return (
<h1>Welcome</h1>
<>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<LocalNavbar></LocalNavbar>
</>
)
}

24
pages/ui/navbar.tsx Normal file
View File

@ -0,0 +1,24 @@
import { Nav, Navbar, NavDropdown } from "react-bootstrap";
export function LocalNavbar() {
return (
<Navbar collapseOnSelect sticky="top" expand="lg" bg="dark" variant="dark">
<Navbar.Brand href="/">nopeBackend</Navbar.Brand>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse id="responsive-navbar-nav">
<Nav className="mr-auto">
<Nav.Link href="/docs">Docs</Nav.Link>
{/* <Nav.Link href="#features">Features</Nav.Link> */}
<NavDropdown title="Modules" id="collasible-nav-dropdown">
{/* Iterate over the deteced Modules. */}
<NavDropdown.Item href="#action/3.1">Action</NavDropdown.Item>
</NavDropdown>
</Nav>
{/* Navigation on the right side */}
{/* <Nav>
</Nav> */}
</Navbar.Collapse>
</Navbar>
)
}

View File

@ -1,3 +1,4 @@
import { getLogger } from "../lib/logger/getLogger";
import { startBackend } from "./startBackend";
startBackend({port: 3001});
startBackend({ port: 3001, logger: getLogger('debug') });

View File

@ -1,14 +1,19 @@
import { apiDoc } from '../specs/apiDoc';
import * as express from "express";
import * as bodyParser from "body-parser";
import * as cors from 'cors';
import * as express from "express";
import { initialize } from "express-openapi";
import { getBackendAccesors } from './getBackendAccessors';
import { assignIn } from 'lodash';
import { Logger } from 'winston';
import { EventLayer } from "../lib/communication/eventLayer";
import { getDispatcher } from "../lib/dispatcher/nopeDispatcher";
import { apiDoc } from '../specs/apiDoc';
import { getBackendAccesors } from './getBackendAccessors';
export function startBackend(options: {
port?: number,
basePath?: string,
}= {}) {
logger?: Logger,
} = {}) {
const app: express.Application = (express as any)();
// Define the Default Options
@ -20,24 +25,34 @@ export function startBackend(options: {
const opts = assignIn(defaults, options);
app.use(bodyParser.json());
app.use(cors());
initialize({
apiDoc: apiDoc('Test API', '0.0.1', opts.basePath),
apiDoc: apiDoc('Backend API', '1.0.0', opts.basePath),
app,
paths: './dist/api',
routesGlob: '**/*.{ts,js}',
routesIndexFileRegExp: /(?:index)?\.[tj]s$/
routesIndexFileRegExp: /(?:index)?\.[tj]s$/,
dependencies: {
_dispatcher: getDispatcher('backend', new EventLayer())
},
});
app.use(((err, req, res, next) => {
res.status(err.status).json(err);
}) as express.ErrorRequestHandler);
const server = app.listen(opts.port);
const accessor = getBackendAccesors(app);
return {
if (options.logger) {
options.logger.info('Server Running on http://localhost:' + opts.port.toString() + opts.basePath);
options.logger.info('API Documentation available on http://localhost:' + opts.port.toString() + opts.basePath + '/api-docs');
options.logger.debug('Checkout http://localhost:3000/docs')
}
return {
app,
// Accessor for the Server
accessor,

View File

@ -1,22 +1,52 @@
import { createLogger, format, transports } from 'winston';
import { generateOpenAPI } from "../lib/helpers/generateOpenAPI";
import { generateClientTemplate } from "../lib/helpers/generateTemplate";
const main = async function () {
const readableFormat = format.printf(({ level, message, label, timestamp }) => {
return `${timestamp} ${level}: ${message}`;
});
let logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
format.errors({ stack: true }),
readableFormat
),
exitOnError: false,
transports: [
new (transports.Console)({
format: format.combine(
format.colorize(),
readableFormat
)
}),
]
});
logger.info('Creating Dispatched API');
await generateClientTemplate({
inputDir: './test/*.ts',
outputDir: './dist/generated',
pathToTemplate: './lib/templates/clientInterface.handlebars',
tsConfigFilePath: './tsconfigBackend.json'
tsConfigFilePath: './tsconfigBackend.json',
logger
});
console.log('Created Files.');
logger.info('Creating Open-API');
await generateOpenAPI({
inputDir: './test/*.ts',
tempDir: './temp',
pathToSchemaTemplate: './lib/templates/schema.handlebars',
pathToApiTemplate: './lib/templates/openApiSchema.handlebars',
tsConfigFilePath: './tsconfigBackend.json'
tsConfigFilePath: './tsconfigBackend.json',
logger
})
}

View File

@ -30,6 +30,13 @@ export class CLWithInterface {
num: 0
});
@exportMethodToOpenAPI({})
@exportMethodToDispatcher({
url: 'exportedFunction'
})
async exportedFunctionShouldBeHosted(/* COMMENT */ a: IF00, b?: IF01) {
return a + b
}
/**
*
@ -49,12 +56,7 @@ export class CLWithInterface {
return await operator(a, b);
}
@exportMethodToOpenAPI({})
@exportMethodToDispatcher({
url: 'exportedFunction'
})
async exportedFunctionShouldBeHosted(/* COMMENT */ a: number, b?: number) {
}
/**

View File

@ -1,5 +1,5 @@
export type IJsonSchemaBaseTypes = 'string' | 'number' | 'integer' | 'object' | 'array' | 'boolean' | 'null';
export type IJsonSchemaTypes = IJsonSchemaBaseTypes | Array<IJsonSchemaBaseTypes>
export type IJsonSchemaTypes = IJsonSchemaBaseTypes | Array<IJsonSchemaBaseTypes> | { $ref: string }
/**