0.9.1 • Published 4 years ago

ts-swagger-joi-converter v0.9.1

Weekly downloads
2
License
MIT
Repository
github
Last release
4 years ago

Typescript Swagger Joi Converter

What is it?

This project provides a generic structure for defining data, and allows generation of TypeScript interfaces, Joi schemas, and Swagger schemas from that data, allowing changes to more easily propagate through a complex system.

How is it used?

The main function you'll want to use is compileObjects, which takes in an object that contains all the schemas to compile. This function can create appropriate directories and files for the ouput, or it can return the output as strings, depending on how you want to use it. The format is meant to be all-encompassing, supporting as many features as possible. Features that are not available in a given framework will be ignored by that framework (for instance encoding is a Joi property, and won't show up in TypeScript output).

Usage examples

Example 1: The Basics

const { FieldTypes, Constants, compileObjects } = require('ts-swagger-joi-converter');

const RequestObjectOne = {
	type: Constants.Types.Model,
	fields: {
		strField: {
			type: FieldTypes.String,
			required: false,
			max: 100,
			encoding: 'utf8'
		},
		createdAt: {
			type: FieldTypes.Date,
			required: true
		},
		extraData: {
			type: FieldTypes.Obj,
			required: false
		}
	}
};

const RequestObjectTwo = {
	type: Constants.Types.Model,
	fields: {
		num1: {
			type: FieldTypes.Number,
			required: false
		}
	}
};

const output = compileObjects({
	RequestObjectOne,
	RequestObjectTwo
});

console.log('TypeScript:');
console.log(output.typeScript);
console.log('Swagger:');
console.log(output.swagger);
console.log('Joi:');
console.log(output.joi);

At this point, output will contain three fields:

  • typeScript - this contains the typescript output
  • swagger - this contains swagger output
  • joi - this contains the joi output

TypeScript data will look like this:

export interface RequestObjectOne {
    strField?: string;
    createdAt: Date;
    extraData?: object;
}

export interface RequestObjectTwo {
    num1?: number;
}

Swagger data will look like this:

    RequestObjectOne:
      properties:
        strField:
          type: string
        createdAt:
          type: string
          format: date-time
          required: true
        extraData:
          type: object


    RequestObjectTwo:
      properties:
        num1:
          type: number

Joi data will look like this:

export const RequestObjectOne = Joi.object({
    strField: Joi.string().max(100,'utf8').label('strField').optional(),
    createdAt: Joi.date().label('createdAt').required(),
    extraData: Joi.object().label('extraData').optional()
});

export const RequestObjectTwo = Joi.object({
    num1: Joi.number().label('num1').optional()
});

Note each of these outputs is meant to be a snippet for the given objects. They are missing important data like require statements or swagger headers. If you want the system to generate full files with this information, then you will have to switch the output mode (see example below)

Example 2: Enums

Enums are handled very differently in TypeScript vs other frameworks, and there are also two ways to create them.

Note that you can pass both an array and an object into enum values. In the case of an object, the "key" will be the name for TypeScript, and will be ignored for Joi and Swagger, and the "value" will be used as the enum value for all three.

const { FieldTypes, Constants, compileObjects, Types } = require('../src');

const EnumObject = {
	type: Constants.Types.Enum,
	values: ['val1', 'val2']
};

const EnumObject2 = {
	type: Constants.Types.Enum,
	values: {
		'Value1': '1',
		'Value2': '2'
	}
};

const ModelObject = {
	type: Constants.Types.Model,
	fields: {
		field1: {
			type: FieldTypes.Enum,
			typeName: 'EnumObject',
			required: true
		},
		field2: {
			type: FieldTypes.Enum,
			values: ['Foo', 'Bar'],
			required: true
		},
		field3: {
			type: FieldTypes.Enum,
			typeName: 'EnumObject2',
			required: true
		},
	}
}

const output = compileObjects({
	EnumObject,
	EnumObject2,
	ModelObject
}, {
	outputFormat: Constants.OutputTypes.File,
	outputDirectory: __dirname
});

After running these objects through the compiler, you will have the following:

TypeScript:

export enum EnumObject {
    val1 = 'val1',
    val2 = 'val2'
}

export enum EnumObject2 {
    Value1 = '1',
    Value2 = '2'
}

export interface ModelObject {
    field1: EnumObject;
    field2: ModelObject_field2;
    field3: EnumObject2;
}

export enum ModelObject_field2 {
    foo = 'foo',
    bar = 'bar'
}

Swagger

    ModelObject:
      properties:
        field1:
          type: string
          enum: [val1, val2]
          required: true
        field2:
          type: string
          enum: [foo, bar]
          required: true
        field3:
          type: string
          enum: [1, 2]
          required: true

Joi

export const ModelObject = Joi.object({
    field1: Joi.string().only(['val1', 'val2']).label('field1').required(),
    field2: Joi.string().only(['foo', 'bar']).label('field2').required(),
    field3: Joi.string().only(['1', '2']).label('field3').required()
});

Notice that the compiler can use both a pre-created enum for TypeScript as well as create one at compile-time if desired.

Example 3: Standard Objects

There are a few different options for objects. Currently only TypeScript supports most of them-swagger and Joi will just show a simple object type.

const { FieldTypes, Constants, compileObjects } = require('ts-swagger-joi-converter');

const ObjectOne = {
	type: Constants.Types.Model,
	fields: {
		item_id: {
			type: FieldTypes.Obj,
			required: true,
			keys: {
				type: FieldTypes.String
			},
			values: {
				type: FieldTypes.String,
			}
		}
	}
};

const ObjectTwo = {
	type: Constants.Types.Model,
	fields: {
		item: {
			type: FieldTypes.Obj,
			required: true,
            values: {
                typeName: 'ObjectOne'
            }
		},
		thing: {
            type: FieldTypes.Obj,
            typeName: 'ObjectOne',
            required: true
		},
		props: {
			type: FieldTypes.Obj
		}
	}
};

const output = compileObjects({
	ObjectOne,
	ObjectTwo
}, {
	outputFormat: Constants.OutputTypes.File,
	outputDirectory: __dirname
});

TypeScript

export interface ObjectOne {
    [item_id: string]: string;
}

export interface ObjectTwo {
    item: ObjectOne;
    thing: ObjectOne;
    props?: object;
}

Swagger

    ObjectOne:
      properties:
        item_id:
          type: object

          required: true

    ObjectTwo:
      properties:
        item:
          type: object

          required: true
        thing:
          type: object
          $ref: '#/components/schemas/ObjectOne'
          required: true
        props:
          type: object

Joi

export const ObjectOne = Joi.object({
    item_id: Joi.object().pattern(/.*/,[Joi.string()]).label('item_id').required()
});

export const ObjectTwo = Joi.object({
    item: Joi.object().label('item').required(),
    thing: Joi.object().pattern(/.*/,[Joi.string()]).required().label('thing').required(),
    props: Joi.object().label('props').optional()
});

Example 4: Arrays

Arrays are supported as both fields on their own, or as values of objects. This example also demonstrates dynamic keys. Arrays can take a special key, "allowSingle". This affects Joi compilation only, and will add the .single() method to the Joi array, which allows a single element to be passed.

Arrays support the following extra parameters: |----|----| | Param | Description | | allowSingle | Joi only, adds .single() to the array definition. This is generally useful when using repeated parameters in a query string, for allowing single values | | allowEmpty | Joi only, makes the inner object to the array optional, which allows an empty array to be passed |

Note that extra parameters are ignored on all but the topmost array when nesting arrays.

const { compileObjects, FieldTypes, Constants } = require('../src/index');

const ObjectOne = {
	type: Constants.Types.Model,
	fields: {
		field1_1: {
			type: FieldTypes.String,
			required: true
		},
	}
};

const ArrayOne = {
	type: Constants.Types.Model,
	fields: {
		field1: {
			type: FieldTypes.Array,
			required: false,
			subType: {
				type: FieldTypes.String
			},
			allowSingle: true
		},
		field2: {
			type: FieldTypes.Obj,
			keys: {
				type: FieldTypes.String
			},
			values: {
				type: FieldTypes.Array,
				data: {
					subType: {
						type: FieldTypes.String
					}
				}
			}
		},
		field3: {
			type: FieldTypes.Array,
			required: true,
			subType: {
				type: FieldTypes.Obj,
				typeName: 'ObjectOne'
			},
			allowEmpty: true
		},
	}
};

const NestedArray = {
	type: Constants.Types.Model,
	fields: {
		nestedArray: {
			type: FieldTypes.Array,
			subType: {
				type: FieldTypes.Array,
				subType: {
					type: FieldTypes.Array,
					subType: {
						type: FieldTypes.Obj,
						typeName: 'ObjectOne'
					}
				}
			},
			allowEmpty: true
		}
	}
}

compileObjects({ ObjectOne, ArrayOne, NestedArray }, {
	outputFormat: Constants.OutputTypes.File,
	outputDirectory: __dirname
});

TypeScript

export interface ObjectOne {
    field1_1: string;
}

export interface ArrayOne {
    field1?: string[];
    [field2: string]: string[];
    field3?: ObjectOne[];
}

export interface NestedArray {
    nestedArray?: ObjectOne[][][];
}

Swagger

    ObjectOne:
      properties:
        field1_1:
          type: string
          required: true

    ArrayOne:
      properties:
        field1:
          type: array
          items:
            type: string
        field2:
          type: object

        field3:
          type: array
          items:
            $ref: '#/components/schemas/ObjectOne'
          required: true

    NestedArray:
      properties:
        nestedArray:
          type: array
          items:
            type: array
            items:
              type: array
              items:
                $ref: '#/components/schemas/ObjectOne'
            required: true
          required: true

Joi

export const ObjectOne = Joi.object({
    field1_1: Joi.string().label('field1_1').required()
});

export const ArrayOne = Joi.object({
    field1: Joi.array().single().items(Joi.string().required()).label('field1').optional(),
    field2: Joi.object().pattern(/.*/,[Joi.string()]).label('field2').optional(),
    field3: Joi.array().items(Joi.object({
        field1_1: Joi.string().label('field1_1').required()
    }).optional()).label('field3').optional()
});

export const NestedArray = Joi.object({
    nestedArray: Joi.array().items(
        Joi.array().items(
            Joi.array().items(Joi.object({
                field1_1: Joi.string().label('field1_1').required()
            }).optional())
        )
    ).label('nestedArray').optional()
});

Example 5: Nested Objects

An object is nested, similar to arrays, by using the typeName field. The referenced object must have already been compiled.

const { compileObjects, FieldTypes, Constants } = require('ts-swagger-joi-converter');

const FirstChild = {
	type: Constants.Types.Model,
	fields: {
		field1_1: {
			type: FieldTypes.String,
			required: true
		},
	}
};

const SecondChild = {
	type: Constants.Types.Model,
	fields: {
		field_2_1: {
            type: FieldTypes.Obj,
            keys: {
                type: FieldTypes.Number
            },
            values: {
                type: FieldTypes.Array,
                data: {
                    subType: {
                        type: FieldTypes.Obj,
                        typeName: 'FirstChild'
                    }
                }
            }
		},
		field_2_2: {
			type: FieldTypes.Boolean,
			required: false
		},
		
	}
}

const ParentObject = {
	type: Constants.Types.Model,
	fields: {
		child: {
			type: FieldTypes.Obj,
			typeName: 'SecondChild',
			required: true
		},
		field_parent_1: {
            type: FieldTypes.Obj,
            keys: {
                type: FieldTypes.Number
            },
            values: {
            	type: FieldTypes.Obj,
				data: {
                	typeName: 'FirstChild'
				}
            }
		},
	}
};

const output = compileObjects({
	FirstChild,
	SecondChild,
	ParentObject
});

console.log('TypeScript:');
console.log(output.typeScript);
console.log('Swagger:');
console.log(output.swagger);
console.log('Joi:');
console.log(output.joi);

TypeScript

export interface FirstChild {
    field1_1: string;
}

export interface SecondChild {
    [field_2_1: number]: FirstChild[];
    field_2_2?: boolean;
}

export interface ParentObject {
    child: SecondChild;
    [field_parent_1: number]: FirstChild;
}

Swagger

    FirstChild:
      properties:
        field1_1:
          type: string
          required: true

    SecondChild:
      properties:
        field_2_1:
          type: object

        field_2_2:
          type: boolean

    ParentObject:
      properties:
        child:
          type: object
          $ref: '#/components/schemas/SecondChild'
          required: true
        field_parent_1:
          type: object

Joi

export const FirstChild = Joi.object({
    field1_1: Joi.string().label('field1_1').required()
});

export const SecondChild = Joi.object({
    field_2_1: Joi.object().pattern(/.*/,[Joi.object({
        field1_1: Joi.string().label('field1_1').required()
    })]).label('field_2_1').optional(),
    field_2_2: Joi.boolean().label('field_2_2').optional()
});

export const ParentObject = Joi.object({
    child: Joi.object({
        field_2_1: Joi.object().pattern(/.*/,[Joi.object({
            field1_1: Joi.string().label('field1_1').required()
        })]).label('field_2_1').optional(),
        field_2_2: Joi.boolean().label('field_2_2').optional()
    }).label('child').required(),
    field_parent_1: Joi.object().pattern(/.*/,[Joi.object({
        field1_1: Joi.string().label('field1_1').required()
    })]).label('field_parent_1').optional()
});

Example 6: Skipping Compilation

Sometimes we may not want certain objects to be compiled in certain situations. For example, take the models for example #5. The FirstChild is never used in Joi, so it probably should not be output in the Joi file.

In order to handle situations like this, we can use the skipJoi, skipTypeScript, and skipSwagger fields on the models. See example below

const { compileObjects, FieldTypes, Constants } = require('ts-swagger-joi-converter');

const Object1 = {
	type: Constants.Types.Model,
	fields: {
		field1: {
			type: FieldTypes.String
		}
	},
	skipJoi: true
}

const Object2 = {
	type: Constants.Types.Model,
	fields: {
		field1: {
			type: FieldTypes.String
		}
	},
	skipTypeScript: true
}

const Object3 = {
	type: Constants.Types.Model,
	fields: {
		field1: {
			type: FieldTypes.String
		}
	},
	skipSwagger: true
}

const output = compileObjects({
	Object1,
	Object2,
	Object3
});

console.log('TypeScript:');
console.log(output.typeScript);
console.log('Swagger:');
console.log(output.swagger);
console.log('Joi:');
console.log(output.joi);

TypeScript

export interface Object1 {
    field1?: string;
}

export interface Object3 {
    field1?: string;
}

Swagger

    Object1:
      properties:
        field1:
          type: string

    Object2:
      properties:
        field1:
          type: string

Joi

export const Object2 = Joi.object({
    field1: Joi.string().label('field1').optional()
});

export const Object3 = Joi.object({
    field1: Joi.string().label('field1').optional()
});

Example 7: JOI Tags

Joi tags are a subset of tags in general. There are three, one for Body, one for Params, and one for Query. When used, they generate a special Joi structure that is designed to be loaded into celebrate.

const { compileObjects, FieldTypes, Constants } = require('ts-swagger-joi-converter');

const Object1 = {
	type: Constants.Types.Model,
	fields: {
		field1: {
			type: FieldTypes.Boolean,
			tag: Constants.JoiTags.Body
		},
		field2: {
			type: FieldTypes.String,
			tag: Constants.JoiTags.Query
		},
		field3: {
			type: FieldTypes.String,
			tag: Constants.JoiTags.Params
		},
		field4: {
			type: FieldTypes.Boolean,
			tag: Constants.JoiTags.Body
		}
	}
};

const Object2 = {
	type: Constants.Types.Model,
	fields: {
		field1: {
			type: FieldTypes.Obj,
			tag: Constants.JoiTags.Body
		}
	}
};

const output = compileObjects({
	Object1,
	Object2
});

console.log('TypeScript:');
console.log(output.typeScript);
console.log('Swagger:');
console.log(output.swagger);
console.log('Joi:');
console.log(output.joi);

The Joi output is as follows:

export const Object1 = {
    body: Joi.object({
        field1: Joi.boolean().label('field1').optional(),
        field4: Joi.boolean().label('field4').optional()
    }),
    query: Joi.object({
        field2: Joi.string().label('field2').optional()
    }),
    params: Joi.object({
        field3: Joi.string().label('field3').optional()
    })
};
export const Object2 = {
    body: Joi.object({
        field1: Joi.object().label('field1').optional()
    })
};

This does not affect Swagger or Typescript generation.

Example 8: Extending Definitions

This module supports extending definitions in a similar way to how TypeScript works (though with different outputs). To use, add the "extends" key to a model definition. The value must be a string that corresponds to another model. Due to current limitations in the system, the extended model must be compiled in the same compile step as the extending model.

Note that this feature did actually exist prior to 0.8.0 but only was supported by the TypeScript compiler. Also note that Joi does not support extending definitions, so for Joi output, the extended fields are merged into the normal fields.

const { compileObjects, FieldTypes, Constants } = require('../src/index');

const Object1 = {
	type: Constants.Types.Model,
	fields: {
		field1_1: {
			type: FieldTypes.Number,
			required: true
		},
		field1_2: {
			type: FieldTypes.Obj,
            keys: {
                type: FieldTypes.Number
            },
            values: {
                type: FieldTypes.Array,
                data: {
                    subType: {
                        type: FieldTypes.Obj
                    }
                }
            }
		}
	}
};

const Object2 = {
	type: Constants.Types.Model,
	extends: 'Object1',
	fields: {
		field2_1: {
			type: FieldTypes.Boolean
		}
	}
};

const Object3 = {
	type: Constants.Types.Model,
	extends: 'Object2',
	fields: {
		field1_2: {
			type: FieldTypes.Date
		}
	}
};

const Object4 = {
	type: Constants.Types.Model,
	fields: {
		object2_field: {
			type: FieldTypes.Obj,
			typeName: 'Object3'
		}
	}
};

compileObjects({ Object1, Object2, Object3, Object4 }, {
	outputFormat: Constants.OutputTypes.File,
	outputDirectory: __dirname
});

TypeScript

export interface Object1 {
    field1_1: number;
    [field1_2: number]: object[];
}

export interface Object2 extends Object1 {
    field2_1?: boolean;
}

export interface Object3 extends Object2 {
    field1_2?: Date;
}

export interface Object4 {
    object2_field?: Object3;
}

Swagger

    Object1:
      properties:
        field1_1:
          type: number
          required: true
        field1_2:
          type: object


    Object2:
      properties:
        allOf:
          - $ref: '#/components/schemas/Object1'
          - type: object
            properties:
              field2_1:
                type: boolean

    Object3:
      properties:
        allOf:
          - $ref: '#/components/schemas/Object2'
          - type: object
            properties:
              field1_2:
                type: string
                format: date-time

    Object4:
      properties:
        object2_field:
          type: object
          $ref: '#/components/schemas/Object3'

Joi

export const Object1 = Joi.object({
    field1_1: Joi.number().label('field1_1').required(),
    field1_2: Joi.object().pattern(/.*/,[Joi.object()]).label('field1_2').optional()
});

export const Object2 = Joi.object({
    field2_1: Joi.boolean().label('field2_1').optional(),
    field1_1: Joi.number().label('field1_1').required(),
    field1_2: Joi.object().pattern(/.*/,[Joi.object()]).label('field1_2').optional()
});

export const Object3 = Joi.object({
    field1_2: Joi.date().label('field1_2').optional(),
    field2_1: Joi.boolean().label('field2_1').optional(),
    field1_1: Joi.number().label('field1_1').required()
});

export const Object4 = Joi.object({
    object2_field: Joi.object({
        field1_2: Joi.date().label('field1_2').optional(),
        field2_1: Joi.boolean().label('field2_1').optional(),
        field1_1: Joi.number().label('field1_1').required()
    }).label('object2_field').optional()
});

Compile Options

When compiling your objects, The first parameter must be an object containing all the objects to compile. The second parameter is an options object. Only compile output options are supported right now.

Compile Output Options

ParameterDescription
outputFormatDetermines the format the output takes
outputDirectoryOnly used when outputFormat is File. Determines the directory to use as a base for creating new files
filePrefixOnly used when outputFormat is File. Sets the prefix of created files.
removeExistingDirectoriesOnly used when outputFormat is File. See description below for use

Output Format

The following values can be used for the outputFormat:

  • Constants.OutputTypes.Json - The compiler will output an object containing plaintext compiled TypeScript, Swagger, and Joi. This is the default.
  • Constants.OutputTypes.File - The compiler will attempt to create directories for each type it is compiling, and will create a file inside those directories with the compiled code.

File Prefix

By default, generated files are named after their type. For example <baseDir>/joi/joi.ts would be a normal output for a compiled Joi file. The filePrefix option allows you to override that filename. This can be useful if you are trying to generate multiple files into the same base directory.

So if compileObjects was called with the following:

compileObjects({ ... }
	outputFormat: Constants.OutputTypes.File,
	outputDirectory: __dirname,
	filePrefix: 'myFiles'
});

The output would be:

  • /joi/myFiles.ts
  • /swagger/myFiles.yml
  • /typeScript/myFiles.ts

Remove Existing Directories

This option, if set to true (default) will remove the existing swagger, typeScript, and joi directories with each call to compileObjects. If turned off, it will preserve these directories.

Note, however, that if you don't use prefixes and set this to false, the "joi.ts", "swagger.yml" and "typeScript.ts" will still be overwritten by subsequent calls.

ToDo

  • tags and compiling with tags
  • Swagger params vs body
  • Allow dynamic names via callbacks
0.9.1

4 years ago

0.9.0

4 years ago

0.8.2

4 years ago

0.8.1

4 years ago

0.8.0

4 years ago

0.7.2

5 years ago

0.7.1

5 years ago

0.7.0

5 years ago

0.6.5

5 years ago

0.6.4

5 years ago

0.6.3

5 years ago

0.6.2

5 years ago

0.6.1

5 years ago

0.6.0

5 years ago

0.5.3

5 years ago

0.5.2

5 years ago

0.4.2

5 years ago

0.4.1

5 years ago

0.4.0

5 years ago

0.3.0

5 years ago

0.2.0

5 years ago

0.1.0

5 years ago