Giter VIP home page Giter VIP logo

express-joi-to-swagger's Introduction

Welcome to express-joi-to-swagger

Description

Solution that generates beatiful Swagger API documentation from code. πŸ’»

It lists all of endpoints registred within app with their routes, methods, relevant middlewares.

When it comes to generating πŸ“‘Swagger documentation, you have two options. Generate Swagger UI that can be served as a static file within your application, or keep documentation as data.json file within defined πŸ“location.

For more information see Config parameters bellow ⬇.

This simple tool does not require you to write any more code that necessary. Documentation is generated from source code itself without using annotations or separate doc files.

Installation

Use the package manager (npm or yarn) to install dependencies.

npm install @goodrequest/express-joi-to-swagger
or
yarn add @goodrequest/express-joi-to-swagger

Requirements

βœ– This solution is suitable for everybody who uses Express in a combination with Joi to build application's API. This version was developed and tested on versions 17.x.x of Joi. For version 14.x.x we have parallel branch v14. For proper functioning it is also necessary to use Typescipt version 3.7.5 and higher.

βœ– As mentioned before, it is not needed to use annotations in your code, however to make this tool works properly you need to obey some coding practices. You need define at least one router in your application. If you want to include request and response Joi schemas in a documentation they need to be named the same and exported.

βœ– If you are using middleware for user authorization and wish to include endpoint permissions in the documentation as well you need to name the function responsible for handling this and provide permissions array as its input parameter.

You can find simple examples of all mentioned in the demo folder of this repository. Quick usage example can also be found below ⬇.

Config parameters

Name Type Required Description
outputPath string βœ… Path to directory where output JSON file should be created.
generateUI boolean βœ… Whether Swagger UI should be generated.
permissions object ❌ Configuration parameters for parsing permissions.
permissions.parser function ❌ Custom parse function for permission middleware.
permissions.middlewareName string βœ… Name of the middleware responsible for handling API permissions.
permissions.closure string βœ… Name of the permission middleware closure.
permissions.paramName string ❌ Name of the parameter containing permissions passed to middleware.
permissionsFormatter function ❌ Custom formatting function for permissions description.
requestSchemaName string ❌ Name of the Joi schema object defining request structure.
responseSchemaName string ❌ Name of the Joi schema object defining response structure.
requestSchemaParams any[] ❌ Param for ability to pass mock params for requestSchema.
responseSchemaParams any[] ❌ Param for ability to pass mock params for responseSchema.
errorResponseSchemaName string ❌ Name of the Joi schema object defining error responses structure.
businessLogicName string βœ… Name of the function responsible for handling business logic of the request.
swaggerInitInfo ISwaggerInit ❌ Swagger initial information.
swaggerInitInfo.servers IServer[] ❌ List of API servers.
swaggerInitInfo.servers.url string ❌ API server URL.
swaggerInitInfo.info IInfo ❌ Basic API information.
swaggerInitInfo.info.description string ❌ API description.
swaggerInitInfo.info.version string ❌ API version.
swaggerInitInfo.info.title string ❌ API title.
swaggerInitInfo.info.termsOfService string ❌ Link to terms of service.
swaggerInitInfo.info.contact IContact ❌ Swagger initial information.
swaggerInitInfo.info.contact.email string βœ… Contact email.
swaggerInitInfo.info.license ILicense ❌ Swagger initial information.
swaggerInitInfo.info.license.name string βœ… License name.
swaggerInitInfo.info.license.url string βœ… License url.
tags string ❌ Configuration parameters for parsing tags.
tags.baseUrlSegmentsLength number ❌ Number of base URL segments.
tags.joinTags boolean ❌ If set to true, array of parsed tags will be joined to string by tagSeparator, otherwise array of tags is returned.
tags.tagSeparator string ❌ String used to join parsed tags.
tags.versioning boolean ❌ If you are using multiple versions of API, you can separate endpoints also by API version. In this case it is necessary to define param "baseUrlSegmentsLength".
tags.versionSeparator string ❌ String used to separate parsed tags from API version tag is versioning == true.
deprecationPathPattern string ❌ If provided, all versions of endpoints except latest will be marked as deprecated.
Pattern needs to specify api route from start segment to version segment, which have to be specified as "v*".
For example if we have api/v1/users and api/v2/users endpoints and we set deprecationPathPattern='/api/v*/', api/v1/users endpoint will be automatically marked as deprecated. For complex route schemas use pattern like deprecationPathPattern='/api/.+/v*/', api/b2b/v1/users

Usage example

// imports
import getSwagger from '@goodrequest/express-joi-to-swagger'
import path from 'path'
import app from './your-path-to-express-app'

// Config example
const config: IConfig = {
	outputPath: path.join(__dirname, 'dist'),
	generateUI: true,
	permissions: {
		middlewareName: 'permission',
		closure: 'permissionMiddleware',
		paramName: 'allowPermissions'
	},
	requestSchemaName: 'requestSchema',
	requestSchemaParams: [mockFn],
	responseSchemaName: 'responseSchema',
	errorResponseSchemaName: 'errorResponseSchemas',
	businessLogicName: 'businessLogic',
	swaggerInitInfo: {
		info: {
			description: 'Generated Store',
			title: 'Test app'
		}
	},
	tags: {}
}

// Use case example
function workflow() {
	getSwagger(app, config).then(() => {
		console.log('Apidoc was successfully generated')
	}).catch((e) => {
		console.log(`Unable to generate apidoc: ${err}`)
	})
}

// Start script
workflow()

Middlewares and router implementation.

router.get(
		'/users/:userID',
		
		// permissionMiddleware
		permissionMiddleware(['SUPERADMIN', 'TEST']),
		
		validationMiddleware(requestSchema),
		
		// businessLogic
		businessLogic
	)

//permissions middleware implementation
export const permissionMiddleware = (allowPermissions: string[]) => function permission(req: Request, res: Response, next: NextFunction) {
	...
}

Adding description for endpoints.

const userEndpointDesc = 'This is how to add swagger description for this endpoint'

export const requestSchema = Joi.object({
	params: Joi.object({
		userID: Joi.number()
	}),
	query: Joi.object({
		search: Joi.string().required()
	}),
	body: Joi.object({
		name: Joi.string().required()
	})
}).description(userEndpointDesc)

Top level request .alternatives() or .alternatives().try()..

export const requestSchema = Joi.object({
    params: Joi.object(),
    query: Joi.object(),
    body: Joi.alternatives().try(
        Joi.object().keys({
            a: Joi.string(),
            b: Joi.number()
        }),
        Joi.object().keys({
            c: Joi.boolean(),
            d: Joi.date()
        })
    )
})

..displays request example as:

{
  "warning": ".alternatives() object - select 1 option only",
  "option_0": {
    "a": "string",
    "b": 0
  },
  "option_1": {
    "c": true,
    "d": "2021-01-01T00:00:00.001Z"
  }
}

Marking endpoint as deprecated (by adding the @deprecated flag to the beginning of the description in the request schema).

export const requestSchema = Joi.object({
	params: Joi.object({
		userID: Joi.number()
	}),
	query: Joi.object({
		search: Joi.string().required()
	}),
	body: Joi.object({
		name: Joi.string().required()
	})
}).description('@deprecated Endpoint returns list of users.')

Using shared schema by calling .meta and specifying schema name in className property. Shared schemas can be used inside requestSchema body or anywhere in responseSchema or errorResponseSchema

export const userSchema = Joi.object({
	id: Joi.number(),
	name: Joi.string(),
	surname: Joi.string()
}).meta({ className: 'User' })

export const responseSchema = Joi.object({
	user: userSchema
})

Setting custom http status code for response (both responseSchema and errorResponseSchema) by setting it in description of schema.

export const responseSchema = Joi.object({
	id: Joi.number().integer().required()
}).description('201')

export const errorResponseSchemas = [
	Joi.object({
		messages: Joi.array().items(
			Joi.object({
				type: Joi.string().required(),
				message: Joi.string().required().example('Not found')
			})
		)
	}).description('404')
]

Result

Generated SwaggerUI

Generated SwaggerUI

Extra Benefits

Swagger bug reports shows inconsistency error in the schema and/or your route definition.

  1. In this case the default value is not present in valid values.
orderBy: Joi.string().lowercase()
.valid('name', 'duration', 'calories', 'views')
.empty(['', null]).default('order'),
  1. If you defined id as parameter within route but forgot to define it the schema Swagger will report error.
//route with id as parameter

router.put('/:id',

schema definition

//joi schema that does not include definition for id param

params: Joi.object()

Contribution

Any πŸ‘ contributions, πŸ› issues and 🌟 feature requests are welcome!

Feel free to check following #TODO ideas we have:

#ID Filename Description
#1 @all create tests
#2 @all update to new Open API after release 3.1.0 fix issue OAI/OpenAPI-Specification#2117
#3 @all sync with branch v14

Credits

express-joi-to-swagger's People

Contributors

daniel-zavacky avatar gr-miroslava-filcakova avatar juraj-chripko avatar l2ysho avatar lubomirigonda1 avatar matej-dugovic avatar miroslava-filcakova avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

express-joi-to-swagger's Issues

Multiple routes

It is not very clear how this package could be used to create documentation with multiple routes. Should I create a config for each route? As I see you need to specify the function names of each step (validation, permissions, business logic) and I can't see how this could be scaled.

Could you provide an example?

Thanks!

Shared schema not applied to request schema

Hello. Thank you for implementing $ref support for models in the response (see #23). Although today I noticed that same code doesn't work for request schema.

For example, I tried to implement 2 operations for POST and PUT. Both have identical schema for "body" element, so I extracted it into separate file and gave it .meta({ className: 'EventCreate' }). Unfortunately, it didn't work, and nothing was generated for the request in the data.json.

put.Event.ts

export const requestSchema = Joi.object().keys({
    params: Joi.object({
        eventId: Joi.string().uuid().required().description('ID of the event')
    }),
    body: eventCreateSchema
}).description('Update event by ID');

export const responseSchema = eventPayloadSchema;

export const errorResponseSchemas = [
    badRequestSchema, errorSchema
];

post.Event.ts:

export const requestSchema = Joi.object().keys({
    body: eventCreateSchema
});

export const responseSchema = eventPayloadSchema;

export const errorResponseSchemas = [
    badRequestSchema, errorSchema
];

Other files:
event.schema.zip

If I comment out .meta({ className: 'EventCreate' }) , then the request schema properly rendered in the Open API file.

Please check if it can be fixed.

Nested shared schema causes startup failure

I used schema with.meta({ className: ' **name**' })for 2 response schemas. It worked until I included another schema with .meta({ className: '**another_name**' }) as a property in both schemas above.

The error is: Error: Duplicate name for shared schema **another_name**.

For example:

export const interactionSchema = Joi.object().keys({
    participant_interaction_id: Joi.string().uuid().description("Unique participant interaction Id"),
    interaction_subject: Joi.string()
        .description("Subject of interaction")
        .example("Touchpoint with participant"),

    interaction_task_type: Joi.string()
        .description("Type of interaction")
        .example("Coaching/Case Management"),
    interaction_type: Joi.string()
        .description("Type of interaction")
        .example("Coaching/Case Management"),
    interaction_status: Joi.string()
        .description("Status of interaction")
        .example("coaching complete"),
}).unknown(true).meta({ className: 'InteractionPayload' })

export const timeTrackingResponseSchema = Joi.object().keys({
    coach_id: Joi.string(),
    categories: Joi.object()
        .description("The breakdown of time spent per category in minutes")
        .unknown(true)
        .pattern(Joi.string(), Joi.number().integer())
        .example({
            "coaching": 120,
            "training": 90
        }),
    total_time_minutes: Joi.number().integer()
        .positive()
        .description("The total time spent in minutes")
        .example("270"),
    breakdown: Joi.object()
        .description("The breakdown of time spent per date and category in minutes")
        .unknown(true)
        .pattern(
            Joi.string(),
            Joi.object()
                .unknown(true)
                .pattern(Joi.string(), Joi.number().integer()))
        .example({
            "2023-06-01": {
                "coaching": 60,
                "training": 30
            },
            "2023-06-02": {
                "coaching": 40,
                "training": 20
            }
        }),
    interactions: Joi.array().items(interactionSchema),  //Here is the reference number 1
    events: Joi.array().items(eventPayloadSchema)
}).unknown(true).meta({className: "TimeTrackingResponse"});

export const ParticipantList = Joi.object().keys({
    participant_id: Joi.string().required().description("Unique participant Id").example("1234-asdfasdf-1241234-1241234"),
    participant_name: Joi.string().required().description("Participant name").example("John Doe"),
    participant_phone: Joi.number().required().description("Participant phone").example(5551234567),
    status: Joi.string().required().description("Participant status").example("ACTIVE"),
    latest_complete_interaction: interactionSchema.required().description("Latest complete interaction"), //Here is the reference number 2
    task_due: Joi.string().required().description("Date for next task or communication due").example("May 16"),
}).unknown(true).meta({className: "ParticipantList"});

The error is: Error: Duplicate name for shared schema InteractionPayload.

Generator does not stop

It is happening on my pc and in the CI: https://github.com/GoodRequest/Backend-project-template/actions/runs/4730903373/jobs/8395188770

It also takes long time (~30s) to generate swagger files.

I added logs to swagger.ts script:

importing app ...
imported app
generator started
Database connection has been established successfully
generator finished
disabling rewiremock

Whole swagger.ts file:

// eslint-disable-next-line max-classes-per-file
import generator from '@goodrequest/express-joi-to-swagger'
import { AUTH_METHOD, AUTH_SCOPE } from '@goodrequest/express-joi-to-swagger/dist/utils/authSchemes'
import path from 'path'
import rewiremock from 'rewiremock'
import config from 'config'

import { name, version, author, license } from '../package.json'

const serverConfig = config.get('server')

class Model {
	static init() {}
	static belongsTo() {}
	static belongsToMany() {}
	static hasOne() {}
	static hasMany() {}
}

/* eslint-disable class-methods-use-this */
class Sequelize {
	// eslint-disable-next-line @typescript-eslint/no-empty-function
	async authenticate() {}
	beforeCreate() {}
	afterCreate() {}
	beforeBulkCreate() {}
	afterBulkCreate() {}
	beforeUpdate() {}
	afterUpdate() {}
	beforeBulkUpdate() {}
	afterBulkUpdate() {}
	beforeDestroy() {}
	afterDestroy() {}
	beforeBulkDestroy() {}
	afterBulkDestroy() {}
	beforeRestore() {}
	afterRestore() {}
	beforeBulkRestore() {}
	afterBulkRestore() {}
}
/* eslint-enable class-methods-use-this */

rewiremock('sequelize').with({
	Model,
	Sequelize,
	DataTypes: {
		ENUM: () => {},
		ARRAY: () => {},
		STRING: () => {},
		CHAR: () => {},
		GEOMETRY: () => {}
	},
	literal() {}
})

// eslint-disable-next-line @typescript-eslint/no-var-requires, import/extensions
rewiremock('@sentry/node').with(require('../tests/__mocks__/@sentry/node'))

// eslint-disable-next-line @typescript-eslint/no-var-requires, import/extensions
rewiremock('@sentry/tracing').with(require('../tests/__mocks__/@sentry/tracing'))

// eslint-disable-next-line @typescript-eslint/no-var-requires, import/extensions
rewiremock('nodemailer').with(require('../tests/__mocks__/nodemailer'))

class Redis {
	// eslint-disable-next-line class-methods-use-this
	on() {}
	// eslint-disable-next-line class-methods-use-this
	once() {}
}

rewiremock('ioredis').with(Redis)

rewiremock.enable()

console.log('importing app ...')
// eslint-disable-next-line
import app from '../src/app'
console.log('imported app')

export default (async () => {
	try {
		console.log('generator started')
		await generator(app, {
			businessLogicName: 'workflow',
			generateUI: true,
			outputPath: path.join(process.cwd(), 'swagger'),
			permissions: [{
				middlewareName: 'permission',
				closure: 'permissionMiddleware',
				paramName: 'allowPermissions'
			}],
			requestSchemaName: 'requestSchema',
			requestSchemaParams: [(v: string) => v],
			responseSchemaName: 'responseSchema',
			tags: {
				baseUrlSegmentsLength: 2,
				joinTags: true,
				tagSeparator: '-'
			},
			swaggerInitInfo: {
				servers: [
					{
						url: serverConfig.domain
					}
				],
				security: {
					methods: [
						{
							name: AUTH_METHOD.BEARER,
							config: {
								bearerFormat: 'JWT'
							}
						}
					],
					scope: AUTH_SCOPE.ENDPOINT,
					authMiddlewareName: 'authenticate'
				},
				info: {
					title: name,
					version,
					description: 'Example API',
					contact: {
						email: author
					},
					license: {
						name: license,
						url: ''
					}
				}
			}
		})
		console.log('generator finished')
	} catch (err) {
		console.log(`Unable to generate apidoc: ${err}`)
		process.exit(1)
	} finally {
		console.log('disabling rewiremock')
		rewiremock.disable()
	}
})()

Protocol schema is missing for server URL

There is a problem if no servers are provided in the config. In such case your library sets "localhost:8080" without protocol. look at this place:

url: 'localhost:8080'

In my case the server is running locally on port 3000 at http://localhost:3000. I tried to execute the API operation using Try Out button and this is what I got:

image

I think it would be better to not use "localhost:8080" and detect the current base URL automatically. If you still want to put hardcoded value, then at least put "http://localhost:8080".

Request URL is incorrectly assembled, includes URL of swagger page itself

Let's say:

  • swagger is available on projectHost.com/swagger
  • project is available on projectHost.com

Request URL assembled in Try it out -> Execute flow for endpoint /api/example should look like:

https://projectHost.com/api/example

but currently looks like this:

https://projectHost.com/swagger/projectHost.com/api/example

which prevents successfully requesting project API itself.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.