Skip to content

Commit

Permalink
feat(apidocs): Autogenerate OpenAPI docs ZMS-100 (#552)
Browse files Browse the repository at this point in the history
* api.js added endpoint for generating openapi docs. added new info to one route in mailboxes.js and messages.js files so that the api docs generation can be done at all

* try to first generate json representation of the api docs

* add initial Joi Object parsing

* api.js make generation dynamic. messages.js add schemas from separate file. messages-schemas.js used for messages endpoint schemas

* add additions to schemas. Add new schemas to messages.js and also add response object there. Add response object parsing functionality to api.js

* add initial openapi doc yml file generation

* remove manual yaml parsing with js-yaml JSON -> YAML parsing

* fix replaceWithRefs and parseComponentsDecoupled functions, refactor, remove unnecessary comments and logs

* add support for another endpoint

* move big code from api.js to tools

* fix array type representation, fix response objects, add necessary data and changes to endpoints

* redo include logic into exclude login

* fix api generation in tools.js to accomodate new naming of objects

* fix messages.js, add structuredClone check in tools.js

* fix structured clone definition

* add one endpoint in messages.js to the api generation

* messages.js add one more endpoint to API generation

* add response to prev commit. Add new endpoint to API generation. Archive message and archive messages

* finish with post endpoints in messages.js

* added general request and response schemas. Also added req and res schemas for messages

* add multiple GET endpoints to API generation and changed them to new design. Use general schemas made earlier

* fix incorrect import of successRes

* fix mailboxes.js

* refactor general-schemas.js. Fix searchSchema in messages.js. Mailboxes.js fix response

* tools.js rename methodObj in API generation to operationObj

* tools.js api generation remove string fallbacks

* messages.js finish with GET endpoints, addition to API doc generation

* for openApi doc generation use JSON now instead of YAML
  • Loading branch information
NickOvt committed Nov 10, 2023
1 parent 4434cb5 commit ea24b93
Show file tree
Hide file tree
Showing 9 changed files with 1,339 additions and 260 deletions.
11 changes: 11 additions & 0 deletions api.js
Expand Up @@ -573,6 +573,17 @@ module.exports = done => {
);
}

server.get(
{ path: '/openapi', name: 'openapi-docs-generation' },
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');

const routes = server.router.getRoutes();

tools.generateAPiDocs(routes);
})
);

server.on('error', err => {
if (!started) {
started = true;
Expand Down
48 changes: 38 additions & 10 deletions lib/api/mailboxes.js
Expand Up @@ -7,6 +7,8 @@ const tools = require('../tools');
const roles = require('../roles');
const util = require('util');
const { sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
const { userId, mailboxId } = require('../schemas/request/general-schemas');
const { successRes } = require('../schemas/response/general-schemas');

module.exports = (db, server, mailboxHandler) => {
const getMailboxCounter = util.promisify(tools.getMailboxCounter);
Expand Down Expand Up @@ -238,19 +240,45 @@ module.exports = (db, server, mailboxHandler) => {
);

server.post(
'/users/:user/mailboxes',
{
path: '/users/:user/mailboxes',
summary: 'Create new Mailbox',
validationObjs: {
pathParams: { user: userId },
requestBody: {
path: Joi.string()
.regex(/\/{2,}|\/$/, { invert: true })
.required()
.description('Full path of the mailbox, folders are separated by slashes, ends with the mailbox name (unicode string)'),
hidden: booleanSchema.default(false).description('Is the folder hidden or not. Hidden folders can not be opened in IMAP.'),
retention: Joi.number()
.min(0)
.description('Retention policy for the created Mailbox. Milliseconds after a message added to mailbox expires. Set to 0 to disable.'),
sess: sessSchema,
ip: sessIPSchema
},
queryParams: {},
response: {
200: {
description: 'Success',
model: Joi.object({
success: successRes,
id: mailboxId
})
}
}
},
tags: ['Mailboxes']
},
tools.responseWrapper(async (req, res) => {
res.charSet('utf-8');

const schema = Joi.object().keys({
user: Joi.string().hex().lowercase().length(24).required(),
path: Joi.string()
.regex(/\/{2,}|\/$/, { invert: true })
.required(),
hidden: booleanSchema.default(false),
retention: Joi.number().min(0),
sess: sessSchema,
ip: sessIPSchema
const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs;

const schema = Joi.object({
...pathParams,
...requestBody,
...queryParams
});

const result = schema.validate(req.params, {
Expand Down
1,041 changes: 796 additions & 245 deletions lib/api/messages.js

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions lib/schemas.js
Expand Up @@ -3,13 +3,14 @@
const EJSON = require('mongodb-extended-json');
const Joi = require('joi');

const sessSchema = Joi.string().max(255).label('Session identifier');
const sessSchema = Joi.string().max(255).label('Session identifier').description('Session identifier for the logs');
const sessIPSchema = Joi.string()
.ip({
version: ['ipv4', 'ipv6'],
cidr: 'forbidden'
})
.label('Client IP');
.label('Client IP')
.description('IP address for the logs ');

/*
const tagSchema = Joi.string().max();
Expand Down Expand Up @@ -83,9 +84,11 @@ const metaDataValidator = () => (value, helpers) => {

const mongoCursorSchema = Joi.string().trim().empty('').custom(mongoCursorValidator({}), 'Cursor validation').max(1024);
const pageLimitSchema = Joi.number().default(20).min(1).max(250).label('Page size');
const pageNrSchema = Joi.number().default(1).label('Page number');
const nextPageCursorSchema = mongoCursorSchema.label('Next page cursor');
const previousPageCursorSchema = mongoCursorSchema.label('Previous page cursor');
const pageNrSchema = Joi.number().default(1).label('Page number').description('Current page number. Informational only, page numbers start from 1');
const nextPageCursorSchema = mongoCursorSchema.label('Next page cursor').description('Cursor value for next page, retrieved from nextCursor response value');
const previousPageCursorSchema = mongoCursorSchema
.label('Previous page cursor')
.description('Cursor value for previous page, retrieved from previousCursor response value');
const booleanSchema = Joi.boolean().empty('').truthy('Y', 'true', 'yes', 'on', '1', 1).falsy('N', 'false', 'no', 'off', '0', 0);
const metaDataSchema = Joi.any().custom(metaDataValidator({}), 'metadata validation');

Expand Down
13 changes: 13 additions & 0 deletions lib/schemas/request/general-schemas.js
@@ -0,0 +1,13 @@
'use strict';

const Joi = require('joi');

const userId = Joi.string().hex().lowercase().length(24).required().description('ID of the User');
const mailboxId = Joi.string().hex().lowercase().length(24).required().description('ID of the Mailbox');
const messageId = Joi.number().min(1).required().description('Message ID');

module.exports = {
userId,
mailboxId,
messageId
};
60 changes: 60 additions & 0 deletions lib/schemas/request/messages-schemas.js
@@ -0,0 +1,60 @@
'use strict';

const Joi = require('joi');
const { booleanSchema } = require('../../schemas');

const Address = Joi.object({
name: Joi.string().empty('').max(255),
address: Joi.string().email({ tlds: false }).required()
}).$_setFlag('objectName', 'Address');

const AddressOptionalName = Joi.object({
name: Joi.string().empty('').max(255),
address: Joi.string().email({ tlds: false }).required()
}).$_setFlag('objectName', 'AddressOptionalName');

const AddressOptionalNameArray = Joi.array().items(AddressOptionalName);

const Header = Joi.object({
key: Joi.string().empty('').max(255),
value: Joi.string()
.empty('')
.max(100 * 1024)
}).$_setFlag('objectName', 'Header');

const Attachment = Joi.object({
filename: Joi.string().empty('').max(255),
contentType: Joi.string().empty('').max(255),
encoding: Joi.string().empty('').default('base64'),
contentTransferEncoding: Joi.string().empty(''),
content: Joi.string().required(),
cid: Joi.string().empty('').max(255)
}).$_setFlag('objectName', 'Attachment');

const ReferenceWithAttachments = Joi.object({
mailbox: Joi.string().hex().lowercase().length(24).required(),
id: Joi.number().required(),
action: Joi.string().valid('reply', 'replyAll', 'forward').required(),
attachments: Joi.alternatives().try(
booleanSchema,
Joi.array().items(
Joi.string()
.regex(/^ATT\d+$/i)
.uppercase()
)
)
}).$_setFlag('objectName', 'ReferenceWithAttachments');

const Bimi = Joi.object({
domain: Joi.string().domain().required(),
selector: Joi.string().empty('').max(255)
}).$_setFlag('objectName', 'Bimi');

module.exports = {
Address,
AddressOptionalNameArray,
Header,
Attachment,
ReferenceWithAttachments,
Bimi
};
9 changes: 9 additions & 0 deletions lib/schemas/response/general-schemas.js
@@ -0,0 +1,9 @@
'use strict';

const { booleanSchema } = require('../../schemas');

const successRes = booleanSchema.required().description('Indicates successful response');

module.exports = {
successRes
};
18 changes: 18 additions & 0 deletions lib/schemas/response/messages-schemas.js
@@ -0,0 +1,18 @@
'use strict';
const Joi = require('joi');

const Rcpt = Joi.object({
value: Joi.string().required().description('RCPT TO address as provided by SMTP client'),
formatted: Joi.string().required().description('Normalized RCPT address')
}).$_setFlag('objectName', 'Rcpt');

const MsgEnvelope = Joi.object({
from: Joi.string().required().description('Address from MAIL FROM'),
rcpt: Joi.array().items(Rcpt).description('Array of addresses from RCPT TO (should have just one normally)')
})
.description('SMTP envelope (if available)')
.$_setFlag('objectName', 'Envelope');

module.exports = {
MsgEnvelope
};

0 comments on commit ea24b93

Please sign in to comment.