diff --git a/.gitignore b/.gitignore index 625f153d..1cece908 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ npm-debug.log bin/etherpad-1.deb credentials.json out/ +.nyc_output +./package-lock.json diff --git a/src/ep.json b/src/ep.json index eeb5c640..428f5726 100644 --- a/src/ep.json +++ b/src/ep.json @@ -18,12 +18,12 @@ { "name": "admin", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/admin:expressCreateServer" } }, { "name": "adminplugins", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminplugins:expressCreateServer", - "socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } + "socketio": "ep_etherpad-lite/node/hooks/express/adminplugins:socketio" } }, { "name": "adminsettings", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings:expressCreateServer", "socketio": "ep_etherpad-lite/node/hooks/express/adminsettings:socketio" } }, - { "name": "swagger", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/swagger:expressCreateServer" } } + { "name": "openapi", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/openapi:expressCreateServer" } } ] } diff --git a/src/node/hooks/express/openapi.js b/src/node/hooks/express/openapi.js new file mode 100644 index 00000000..96f1e96a --- /dev/null +++ b/src/node/hooks/express/openapi.js @@ -0,0 +1,255 @@ +const OpenAPIBackend = require('openapi-backend').default; +const formidable = require('formidable'); +const { promisify } = require('util'); + +const apiHandler = require('../../handler/APIHandler'); +const settings = require('../../utils/Settings'); +const { API } = require('./swagger'); + +const log4js = require('log4js'); +const apiLogger = log4js.getLogger('API'); + +// https://github.com/OAI/OpenAPI-Specification/tree/master/schemas/v3.0 +const OPENAPI_VERSION = '3.0.2'; // Swagger/OAS version + +// enum for two different styles of API paths used for etherpad +const APIPathStyle = { + FLAT: 'api', // flat paths e.g. /api/createGroup + REST: 'rest', // restful paths e.g. /rest/group/create +}; +exports.APIPathStyle = APIPathStyle; + +// helper to get api root +const getApiRootForVersion = (version, style = APIPathStyle.FLAT) => `/${style}/${version}`; + +// helper to generate an OpenAPI server object when serving definitions +const generateServerForApiVersion = (apiRoot, req) => ({ + url: `${settings.ssl ? 'https' : 'http'}://${req.headers.host}${apiRoot}`, +}); + +const defaultResponses = { + 200: { + description: 'ok', + }, +}; + +// convert to a flat list of OAS Operation objects +const operations = []; +for (const resource in API) { + for (const action in API[resource]) { + const { func: operationId, description, response } = API[resource][action]; + const operation = { + operationId, + summary: description, + responses: defaultResponses, + tags: [resource], + _restPath: `/${resource}/${action}`, + }; + operations[operationId] = operation; + } +} + +const generateDefinitionForVersion = (version, style = APIPathStyle.FLAT) => { + const definition = { + openapi: OPENAPI_VERSION, + info: { + title: 'Etherpad API', + description: + 'Etherpad is a real-time collaborative editor scalable to thousands of simultaneous real time users. It provides full data export capabilities, and runs on your server, under your control.', + version, + }, + paths: {}, + components: { + responses: {}, + parameters: {}, + schemas: { + SessionInfo: { + type: 'object', + properties: { + id: { + type: 'string', + }, + authorID: { + type: 'string', + }, + groupID: { + type: 'string', + }, + validUntil: { + type: 'integer', + }, + }, + }, + UserInfo: { + type: 'object', + properties: { + id: { + type: 'string', + }, + colorId: { + type: 'string', + }, + name: { + type: 'string', + }, + timestamp: { + type: 'integer', + }, + }, + }, + Message: { + type: 'object', + properties: { + text: { + type: 'string', + }, + userId: { + type: 'string', + }, + userName: { + type: 'string', + }, + time: { + type: 'integer', + }, + }, + }, + }, + securitySchemes: { + ApiKey: { + type: 'apiKey', + in: 'query', + name: 'apikey', + }, + }, + }, + security: [{ ApiKey: [] }], + }; + + // build operations + for (const funcName in apiHandler.version[version]) { + let operation = {}; + if (operations[funcName]) { + operation = { ...operations[funcName] }; + } else { + // console.warn(`No operation found for function: ${funcName}`); + operation = { + operationId: funcName, + responses: defaultResponses, + }; + } + + // set parameters + operation.parameters = operation.parameters || []; + for (const paramName of apiHandler.version[version][funcName]) { + operation.parameters.push({ $ref: `#/components/parameters/${paramName}` }); + if (!definition.components.parameters[paramName]) { + definition.components.parameters[paramName] = { + name: paramName, + in: 'query', + schema: { + type: 'string', + }, + }; + } + } + + // set path + let path = `/${operation.operationId}`; // APIPathStyle.FLAT + if (style === APIPathStyle.REST && operation._restPath) { + path = operation._restPath; + } + delete operation._restPath; + + // add to definition + // NOTE: It may be confusing that every operation can be called with both GET and POST + definition.paths[path] = { + get: { + ...operation, + operationId: `${operation.operationId}UsingGET`, + }, + post: { + ...operation, + operationId: `${operation.operationId}UsingPOST`, + }, + }; + } + + return definition; +}; + +exports.expressCreateServer = (_, args) => { + const { app } = args; + + for (const version in apiHandler.version) { + // create two different styles of api: flat + rest + for (const style of [APIPathStyle.FLAT, APIPathStyle.REST]) { + const apiRoot = getApiRootForVersion(version, style); + + // generate openapi definition for this API version + const definition = generateDefinitionForVersion(version, style); + + // serve openapi definition file + app.get(`${apiRoot}/openapi.json`, (req, res) => { + res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] }); + }); + + // serve latest openapi definition file under /api/openapi.json + if (version === apiHandler.latestApiVersion) { + app.get(`/${style}/openapi.json`, (req, res) => { + res.json({ ...definition, servers: [generateServerForApiVersion(apiRoot, req)] }); + }); + } + + // build openapi-backend instance for this api version + const api = new OpenAPIBackend({ + apiRoot, + definition, + strict: true, + }); + + // register default handlers + api.register({ + notFound: (c, req, res) => { + res.statusCode = 404; + res.send({ code: 3, message: 'no such function', data: null }); + }, + notImplemented: (c, req, res) => { + res.statusCode = 501; + res.send({ code: 3, message: 'not implemented', data: null }); + }, + }); + + // register operation handlers (calls apiHandler.handle) + for (const funcName in apiHandler.version[version]) { + const handler = async (c, req, res) => { + // parse fields from request + const { header, params, query } = c.request; + + let formData = {}; + if (c.request.method === 'post') { + const form = new formidable.IncomingForm(); + const parseForm = promisify(form.parse).bind(form); + formData = await parseForm(req); + } + + const fields = Object.assign({}, header, params, query, formData); + + // log request + apiLogger.info(`REQUEST, v${version}:${funcName}, ${JSON.stringify(fields)}`); + + // pass to api handler + return apiHandler.handle(version, funcName, fields, req, res); + }; + + // each operation can be called with either GET or POST + api.register(`${funcName}UsingGET`, handler); + api.register(`${funcName}UsingPOST`, handler); + } + + // start and bind to express + api.init(); + app.use(apiRoot, async (req, res) => api.handleRequest(req, req, res)); + } + } +}; diff --git a/src/node/hooks/express/swagger.js b/src/node/hooks/express/swagger.js index f3f07cd0..34bee93e 100644 --- a/src/node/hooks/express/swagger.js +++ b/src/node/hooks/express/swagger.js @@ -1,62 +1,62 @@ var express = require('express'); var apiHandler = require('../../handler/APIHandler'); var apiCaller = require('./apicalls').apiCaller; -var settings = require("../../utils/Settings"); +var settings = require('../../utils/Settings'); var swaggerModels = { - 'models': { - 'SessionInfo' : { - "id": 'SessionInfo', - "properties": { - "id": { - "type": "string" + models: { + SessionInfo: { + id: 'SessionInfo', + properties: { + id: { + type: 'string', }, - "authorID": { - "type": "string" + authorID: { + type: 'string', }, - "groupID":{ - "type":"string" + groupID: { + type: 'string', }, - "validUntil":{ - "type":"long" - } - } + validUntil: { + type: 'integer', + }, + }, }, - 'UserInfo' : { - "id": 'UserInfo', - "properties": { - "id": { - "type": "string" + UserInfo: { + id: 'UserInfo', + properties: { + id: { + type: 'string', }, - "colorId": { - "type": "string" + colorId: { + type: 'string', }, - "name":{ - "type":"string" + name: { + type: 'string', }, - "timestamp":{ - "type":"long" - } - } + timestamp: { + type: 'integer', + }, + }, }, - 'Message' : { - "id": 'Message', - "properties": { - "text": { - "type": "string" + Message: { + id: 'Message', + properties: { + text: { + type: 'string', }, - "userId": { - "type": "string" + userId: { + type: 'string', }, - "userName":{ - "type":"string" + userName: { + type: 'string', }, - "time":{ - "type":"long" - } - } - } - } + time: { + type: 'integer', + }, + }, + }, + }, }; function sessionListResponseProcessor(res) { @@ -64,7 +64,7 @@ function sessionListResponseProcessor(res) { var sessions = []; for (var sessionId in res.data) { var sessionInfo = res.data[sessionId]; - sessionId["id"] = sessionId; + sessionId['id'] = sessionId; sessions.push(sessionInfo); } res.data = sessions; @@ -75,284 +75,284 @@ function sessionListResponseProcessor(res) { // We'll add some more info to the API methods var API = { - // Group - "group": { - "create" : { - "func" : "createGroup", - "description": "creates a new group", - "response": {"groupID":{"type":"string"}} + group: { + create: { + func: 'createGroup', + description: 'creates a new group', + response: { groupID: { type: 'string' } }, }, - "createIfNotExistsFor" : { - "func": "createGroupIfNotExistsFor", - "description": "this functions helps you to map your application group ids to Etherpad group ids", - "response": {"groupID":{"type":"string"}} + createIfNotExistsFor: { + func: 'createGroupIfNotExistsFor', + description: 'this functions helps you to map your application group ids to Etherpad group ids', + response: { groupID: { type: 'string' } }, }, - "delete" : { - "func": "deleteGroup", - "description": "deletes a group" + delete: { + func: 'deleteGroup', + description: 'deletes a group', }, - "listPads" : { - "func": "listPads", - "description": "returns all pads of this group", - "response": {"padIDs":{"type":"List", "items":{"type":"string"}}} + listPads: { + func: 'listPads', + description: 'returns all pads of this group', + response: { padIDs: { type: 'List', items: { type: 'string' } } }, }, - "createPad" : { - "func": "createGroupPad", - "description": "creates a new pad in this group" + createPad: { + func: 'createGroupPad', + description: 'creates a new pad in this group', }, - "listSessions": { - "func": "listSessionsOfGroup", - "responseProcessor": sessionListResponseProcessor, - "description": "", - "response": {"sessions":{"type":"List", "items":{"type":"SessionInfo"}}} + listSessions: { + func: 'listSessionsOfGroup', + responseProcessor: sessionListResponseProcessor, + description: '', + response: { sessions: { type: 'List', items: { type: 'SessionInfo' } } }, }, - "list": { - "func": "listAllGroups", - "description": "", - "response": {"groupIDs":{"type":"List", "items":{"type":"string"}}} + list: { + func: 'listAllGroups', + description: '', + response: { groupIDs: { type: 'List', items: { type: 'string' } } }, }, }, // Author - "author": { - "create" : { - "func" : "createAuthor", - "description": "creates a new author", - "response": {"authorID":{"type":"string"}} + author: { + create: { + func: 'createAuthor', + description: 'creates a new author', + response: { authorID: { type: 'string' } }, }, - "createIfNotExistsFor": { - "func": "createAuthorIfNotExistsFor", - "description": "this functions helps you to map your application author ids to Etherpad author ids", - "response": {"authorID":{"type":"string"}} + createIfNotExistsFor: { + func: 'createAuthorIfNotExistsFor', + description: 'this functions helps you to map your application author ids to Etherpad author ids', + response: { authorID: { type: 'string' } }, }, - "listPads": { - "func": "listPadsOfAuthor", - "description": "returns an array of all pads this author contributed to", - "response": {"padIDs":{"type":"List", "items":{"type":"string"}}} + listPads: { + func: 'listPadsOfAuthor', + description: 'returns an array of all pads this author contributed to', + response: { padIDs: { type: 'List', items: { type: 'string' } } }, }, - "listSessions": { - "func": "listSessionsOfAuthor", - "responseProcessor": sessionListResponseProcessor, - "description": "returns all sessions of an author", - "response": {"sessions":{"type":"List", "items":{"type":"SessionInfo"}}} + listSessions: { + func: 'listSessionsOfAuthor', + responseProcessor: sessionListResponseProcessor, + description: 'returns all sessions of an author', + response: { sessions: { type: 'List', items: { type: 'SessionInfo' } } }, }, // We need an operation that return a UserInfo so it can be picked up by the codegen :( - "getName" : { - "func": "getAuthorName", - "description": "Returns the Author Name of the author", - "responseProcessor": function(response) { + getName: { + func: 'getAuthorName', + description: 'Returns the Author Name of the author', + responseProcessor: function(response) { if (response.data) { - response["info"] = {"name": response.data.authorName}; - delete response["data"]; + response['info'] = { name: response.data.authorName }; + delete response['data']; } }, - "response": {"info":{"type":"UserInfo"}} - } - }, - "session": { - "create" : { - "func": "createSession", - "description": "creates a new session. validUntil is an unix timestamp in seconds", - "response": {"sessionID":{"type":"string"}} + response: { info: { type: 'UserInfo' } }, }, - "delete" : { - "func": "deleteSession", - "description": "deletes a session" + }, + session: { + create: { + func: 'createSession', + description: 'creates a new session. validUntil is an unix timestamp in seconds', + response: { sessionID: { type: 'string' } }, + }, + delete: { + func: 'deleteSession', + description: 'deletes a session', }, // We need an operation that returns a SessionInfo so it can be picked up by the codegen :( - "info": { - "func": "getSessionInfo", - "description": "returns informations about a session", - "responseProcessor": function(response) { + info: { + func: 'getSessionInfo', + description: 'returns informations about a session', + responseProcessor: function(response) { // move this to info if (response.data) { - response["info"] = response.data; - delete response["data"]; + response['info'] = response.data; + delete response['data']; } }, - "response": {"info":{"type":"SessionInfo"}} - } + response: { info: { type: 'SessionInfo' } }, + }, }, - "pad": { - "listAll" : { - "func": "listAllPads", - "description": "list all the pads", - "response": {"padIDs":{"type":"List", "items": {"type" : "string"}}} + pad: { + listAll: { + func: 'listAllPads', + description: 'list all the pads', + response: { padIDs: { type: 'List', items: { type: 'string' } } }, }, - "createDiffHTML" : { - "func" : "createDiffHTML", - "description": "", - "response": {} + createDiffHTML: { + func: 'createDiffHTML', + description: '', + response: {}, }, - "create" : { - "func" : "createPad", - "description": "creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad" + create: { + func: 'createPad', + description: + 'creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad', }, - "getText" : { - "func" : "getText", - "description": "returns the text of a pad", - "response": {"text":{"type":"string"}} + getText: { + func: 'getText', + description: 'returns the text of a pad', + response: { text: { type: 'string' } }, }, - "setText" : { - "func" : "setText", - "description": "sets the text of a pad" + setText: { + func: 'setText', + description: 'sets the text of a pad', }, - "getHTML": { - "func" : "getHTML", - "description": "returns the text of a pad formatted as HTML", - "response": {"html":{"type":"string"}} + getHTML: { + func: 'getHTML', + description: 'returns the text of a pad formatted as HTML', + response: { html: { type: 'string' } }, }, - "setHTML": { - "func" : "setHTML", - "description": "sets the text of a pad with HTML" + setHTML: { + func: 'setHTML', + description: 'sets the text of a pad with HTML', }, - "getRevisionsCount": { - "func" : "getRevisionsCount", - "description": "returns the number of revisions of this pad", - "response": {"revisions":{"type":"long"}} + getRevisionsCount: { + func: 'getRevisionsCount', + description: 'returns the number of revisions of this pad', + response: { revisions: { type: 'integer' } }, }, - "getLastEdited": { - "func" : "getLastEdited", - "description": "returns the timestamp of the last revision of the pad", - "response": {"lastEdited":{"type":"long"}} + getLastEdited: { + func: 'getLastEdited', + description: 'returns the timestamp of the last revision of the pad', + response: { lastEdited: { type: 'integer' } }, }, - "delete": { - "func" : "deletePad", - "description": "deletes a pad" + delete: { + func: 'deletePad', + description: 'deletes a pad', }, - "getReadOnlyID": { - "func" : "getReadOnlyID", - "description": "returns the read only link of a pad", - "response": {"readOnlyID":{"type":"string"}} + getReadOnlyID: { + func: 'getReadOnlyID', + description: 'returns the read only link of a pad', + response: { readOnlyID: { type: 'string' } }, }, - "setPublicStatus": { - "func": "setPublicStatus", - "description": "sets a boolean for the public status of a pad" + setPublicStatus: { + func: 'setPublicStatus', + description: 'sets a boolean for the public status of a pad', }, - "getPublicStatus": { - "func": "getPublicStatus", - "description": "return true of false", - "response": {"publicStatus":{"type":"boolean"}} + getPublicStatus: { + func: 'getPublicStatus', + description: 'return true of false', + response: { publicStatus: { type: 'boolean' } }, }, - "setPassword": { - "func": "setPassword", - "description": "returns ok or a error message" + setPassword: { + func: 'setPassword', + description: 'returns ok or a error message', }, - "isPasswordProtected": { - "func": "isPasswordProtected", - "description": "returns true or false", - "response": {"passwordProtection":{"type":"boolean"}} + isPasswordProtected: { + func: 'isPasswordProtected', + description: 'returns true or false', + response: { passwordProtection: { type: 'boolean' } }, }, - "authors": { - "func": "listAuthorsOfPad", - "description": "returns an array of authors who contributed to this pad", - "response": {"authorIDs":{"type":"List", "items":{"type" : "string"}}} + authors: { + func: 'listAuthorsOfPad', + description: 'returns an array of authors who contributed to this pad', + response: { authorIDs: { type: 'List', items: { type: 'string' } } }, }, - "usersCount": { - "func": "padUsersCount", - "description": "returns the number of user that are currently editing this pad", - "response": {"padUsersCount":{"type": "long"}} + usersCount: { + func: 'padUsersCount', + description: 'returns the number of user that are currently editing this pad', + response: { padUsersCount: { type: 'integer' } }, }, - "users": { - "func": "padUsers", - "description": "returns the list of users that are currently editing this pad", - "response": {"padUsers":{"type":"List", "items":{"type": "UserInfo"}}} + users: { + func: 'padUsers', + description: 'returns the list of users that are currently editing this pad', + response: { padUsers: { type: 'List', items: { type: 'UserInfo' } } }, }, - "sendClientsMessage": { - "func": "sendClientsMessage", - "description": "sends a custom message of type msg to the pad" + sendClientsMessage: { + func: 'sendClientsMessage', + description: 'sends a custom message of type msg to the pad', }, - "checkToken" : { - "func": "checkToken", - "description": "returns ok when the current api token is valid" + checkToken: { + func: 'checkToken', + description: 'returns ok when the current api token is valid', }, - "getChatHistory": { - "func": "getChatHistory", - "description": "returns the chat history", - "response": {"messages":{"type":"List", "items": {"type" : "Message"}}} + getChatHistory: { + func: 'getChatHistory', + description: 'returns the chat history', + response: { messages: { type: 'List', items: { type: 'Message' } } }, }, // We need an operation that returns a Message so it can be picked up by the codegen :( - "getChatHead": { - "func": "getChatHead", - "description": "returns the chatHead (chat-message) of the pad", - "responseProcessor": function(response) { + getChatHead: { + func: 'getChatHead', + description: 'returns the chatHead (chat-message) of the pad', + responseProcessor: function(response) { // move this to info if (response.data) { - response["chatHead"] = {"time": response.data["chatHead"]}; - delete response["data"]; + response['chatHead'] = { time: response.data['chatHead'] }; + delete response['data']; } }, - "response": {"chatHead":{"type":"Message"}} + response: { chatHead: { type: 'Message' } }, }, - "appendChatMessage": { - "func": "appendChatMessage", - "description": "appends a chat message" - } - } + appendChatMessage: { + func: 'appendChatMessage', + description: 'appends a chat message', + }, + }, }; -function capitalise(string){ +exports.API = API; + +function capitalise(string) { return string.charAt(0).toUpperCase() + string.slice(1); } for (var resource in API) { for (var func in API[resource]) { - // The base response model var responseModel = { - "properties": { - "code":{ - "type":"int" + properties: { + code: { + type: 'int', }, - "message":{ - "type":"string" - } - } + message: { + type: 'string', + }, + }, }; - var responseModelId = "Response"; + var responseModelId = 'Response'; // Add the data properties (if any) to the response - if (API[resource][func]["response"]) { + if (API[resource][func]['response']) { // This is a specific response so let's set a new id - responseModelId = capitalise(resource) + capitalise(func) + "Response"; + responseModelId = capitalise(resource) + capitalise(func) + 'Response'; - for(var prop in API[resource][func]["response"]) { - var propType = API[resource][func]["response"][prop]; - responseModel["properties"][prop] = propType; + for (var prop in API[resource][func]['response']) { + var propType = API[resource][func]['response'][prop]; + responseModel['properties'][prop] = propType; } } // Add the id - responseModel["id"] = responseModelId; + responseModel['id'] = responseModelId; // Add this to the swagger models swaggerModels['models'][responseModelId] = responseModel; // Store the response model id - API[resource][func]["responseClass"] = responseModelId; - + API[resource][func]['responseClass'] = responseModelId; } } function newSwagger() { - var swagger_module = require.resolve("swagger-node-express"); + var swagger_module = require.resolve('swagger-node-express'); if (require.cache[swagger_module]) { // delete the child modules from cache - require.cache[swagger_module].children.forEach(function(m) {delete require.cache[m.id];}); + require.cache[swagger_module].children.forEach(function(m) { + delete require.cache[m.id]; + }); // delete the module from cache delete require.cache[swagger_module]; } - return require("swagger-node-express"); + return require('swagger-node-express'); } -exports.expressCreateServer = function (hook_name, args, cb) { - +exports.expressCreateServer = function(hook_name, args, cb) { for (var version in apiHandler.version) { - var swagger = newSwagger(); - var basePath = "/rest/" + version; + var basePath = '/rest/' + version; // Let's put this under /rest for now var subpath = express(); @@ -360,52 +360,51 @@ exports.expressCreateServer = function (hook_name, args, cb) { args.app.use(basePath, subpath); //hack! - var swagger_temp = swagger + var swagger_temp = swagger; swagger = swagger.createNew(subpath); - swagger.params = swagger_temp.params - swagger.queryParam = swagger_temp.queryParam - swagger.pathParam = swagger_temp.pathParam - swagger.bodyParam = swagger_temp.bodyParam - swagger.formParam = swagger_temp.formParam - swagger.headerParam = swagger_temp.headerParam - swagger.error = swagger_temp.error + swagger.params = swagger_temp.params; + swagger.queryParam = swagger_temp.queryParam; + swagger.pathParam = swagger_temp.pathParam; + swagger.bodyParam = swagger_temp.bodyParam; + swagger.formParam = swagger_temp.formParam; + swagger.headerParam = swagger_temp.headerParam; + swagger.error = swagger_temp.error; //swagger.setAppHandler(subpath); swagger.addModels(swaggerModels); for (var resource in API) { - for (var funcName in API[resource]) { var func = API[resource][funcName]; // get the api function - var apiFunc = apiHandler.version[version][func["func"]]; + var apiFunc = apiHandler.version[version][func['func']]; // Skip this one if it does not exist in the version - if(!apiFunc) { + if (!apiFunc) { continue; } var swaggerFunc = { - 'spec': { - "description" : func["description"], - "path" : "/" + resource + "/" + funcName, - "summary" : funcName, - "nickname" : funcName, - "method": "GET", - "params" : apiFunc.map( function(param) { - return swagger.queryParam(param, param, "string"); + spec: { + description: func['description'], + path: '/' + resource + '/' + funcName, + summary: funcName, + nickname: funcName, + method: 'GET', + params: apiFunc.map(function(param) { + return swagger.queryParam(param, param, 'string'); }), - "responseClass" : func["responseClass"] + responseClass: func['responseClass'], }, - 'action': (function(func, responseProcessor) { - return function (req,res) { + action: (function(func, responseProcessor) { + return function(req, res) { req.params.version = version; req.params.func = func; // call the api function //wrap the send function so we can process the response res.__swagger_send = res.send; - res.send = function (response) { + res.send = function(response) { // ugly but we need to get this as json response = JSON.parse(response); // process the response if needed @@ -414,7 +413,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { } // Let's move everything out of "data" if (response.data) { - for(var prop in response.data) { + for (var prop in response.data) { response[prop] = response.data[prop]; delete response.data; } @@ -425,7 +424,7 @@ exports.expressCreateServer = function (hook_name, args, cb) { apiCaller(req, res, req.query); }; - })(func["func"], func["responseProcessor"]) // must use a closure here + })(func['func'], func['responseProcessor']), // must use a closure here }; swagger.addGet(swaggerFunc); @@ -433,11 +432,11 @@ exports.expressCreateServer = function (hook_name, args, cb) { } swagger.setHeaders = function setHeaders(res) { - res.header('Access-Control-Allow-Origin', "*"); + res.header('Access-Control-Allow-Origin', '*'); }; - swagger.configureSwaggerPaths("", "/api" , ""); + swagger.configureSwaggerPaths('', '/api', ''); - swagger.configure("http://" + settings.ip + ":" + settings.port + basePath, version); + swagger.configure('http://' + settings.ip + ':' + settings.port + basePath, version); } }; diff --git a/src/package-lock.json b/src/package-lock.json index 32cb321b..ae907f46 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -4,6 +4,40 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@apidevtools/json-schema-ref-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-8.0.0.tgz", + "integrity": "sha512-n4YBtwQhdpLto1BaUCyAeflizmIbaloGShsPyRtFf5qdFJxfssj+GgLavczgKJFa3Bq+3St2CKcpRJdjtB4EBw==", + "requires": { + "@jsdevtools/ono": "^7.1.0", + "call-me-maybe": "^1.0.1", + "js-yaml": "^3.13.1" + } + }, + "@apidevtools/openapi-schemas": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.0.3.tgz", + "integrity": "sha512-QoPaxGXfgqgGpK1p21FJ400z56hV681a8DOcZt3J5z0WIHgFeaIZ4+6bX5ATqmOoCpRCsH4ITEwKaOyFMz7wOA==" + }, + "@apidevtools/swagger-methods": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.1.tgz", + "integrity": "sha512-1Vlm18XYW6Yg7uHunroXeunWz5FShPFAdxBbPy8H6niB2Elz9QQsCoYHMbcc11EL1pTxaIr9HXz2An/mHXlX1Q==" + }, + "@apidevtools/swagger-parser": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-9.0.1.tgz", + "integrity": "sha512-Irqybg4dQrcHhZcxJc/UM4vO7Ksoj1Id5e+K94XUOzllqX1n47HEA50EKiXTCQbykxuJ4cYGIivjx/MRSTC5OA==", + "requires": { + "@apidevtools/json-schema-ref-parser": "^8.0.0", + "@apidevtools/openapi-schemas": "^2.0.2", + "@apidevtools/swagger-methods": "^3.0.0", + "@jsdevtools/ono": "^7.1.0", + "call-me-maybe": "^1.0.1", + "openapi-types": "^1.3.5", + "z-schema": "^4.2.2" + } + }, "@babel/code-frame": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", @@ -46,12 +80,6 @@ "ms": "^2.1.1" } }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -87,12 +115,6 @@ "source-map": "^0.5.0" }, "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -152,14 +174,6 @@ "@babel/template": "^7.8.6", "@babel/types": "^7.9.0", "lodash": "^4.17.13" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - } } }, "@babel/helper-optimise-call-expression": { @@ -304,12 +318,6 @@ "ms": "^2.1.1" } }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -327,14 +335,6 @@ "@babel/helper-validator-identifier": "^7.9.0", "lodash": "^4.17.13", "to-fast-properties": "^2.0.0" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - } } }, "@istanbuljs/load-nyc-config": { @@ -397,6 +397,11 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@jsdevtools/ono": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.1.tgz", + "integrity": "sha512-pu5fxkbLQWzRbBgfFbZfHXz0KlYojOfVdUhcNfy9lef8ZhBt0pckGr8g7zv4vPX4Out5vBNvqd/az4UaVWzZ9A==" + }, "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", @@ -576,12 +581,6 @@ "once": "^1.3.0", "path-is-absolute": "^1.0.0" } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true } } }, @@ -665,7 +664,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -750,6 +748,11 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-1.0.0.tgz", "integrity": "sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY=" }, + "bath-es5": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz", + "integrity": "sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -893,6 +896,11 @@ "write-file-atomic": "^3.0.0" } }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, "callsite": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", @@ -1056,6 +1064,12 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "optional": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -1384,13 +1398,6 @@ "agentkeepalive": "^3.4.1", "chalk": "^1.0.0", "lodash": "^4.17.10" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - } } }, "emoji-regex": { @@ -1533,8 +1540,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "etag": { "version": "1.8.1", @@ -2260,7 +2266,6 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -2306,6 +2311,16 @@ "resolved": "https://registry.npmjs.org/jsonminify/-/jsonminify-0.4.1.tgz", "integrity": "sha1-gF2vuzk5UYjO6atYLIHvlZ1+cQw=" }, + "jsonschema": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.2.4.tgz", + "integrity": "sha512-lz1nOH69GbsVHeVgEdvyavc/33oymY1AZwtePMiMj4HZPMbP5OIKK3zT9INMWjwua/V4Z4yq7wSlBbSG+g4AEw==" + }, + "jsonschema-draft4": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jsonschema-draft4/-/jsonschema-draft4-1.0.0.tgz", + "integrity": "sha1-8K8gBQVPDwrefqIRhhS2ncUS2GU=" + }, "jsprim": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", @@ -2393,9 +2408,9 @@ } }, "lodash": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", - "integrity": "sha1-pGY7U2hriV/wdOK6UE37dqjit3A=" + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" }, "lodash.assignin": { "version": "4.2.0", @@ -2439,6 +2454,16 @@ "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -2759,6 +2784,15 @@ } } }, + "mock-json-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/mock-json-schema/-/mock-json-schema-1.0.5.tgz", + "integrity": "sha512-ZqkT1hbATZrL79B/eOoe8oWesX8QaykFOaMb8hnGVeDZy2Iw1JOtObS8nC4UpFbAk1DpXRrj3zjv63hbhAPKiA==", + "requires": { + "lodash": "^4.17.11", + "openapi-types": "^1.3.2" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -6277,6 +6311,44 @@ "wrappy": "1" } }, + "openapi-backend": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/openapi-backend/-/openapi-backend-2.4.0.tgz", + "integrity": "sha512-j6gDuSg6JCu7mi/MaaGFmmOMJD5lQMuKaFUfbFh6oSVKlqxlLCa9bmtzZxWcii/IYTOGeMVZW/0LLw07WNtddg==", + "requires": { + "ajv": "^6.10.0", + "bath-es5": "^3.0.3", + "cookie": "^0.4.0", + "lodash": "^4.17.15", + "mock-json-schema": "^1.0.5", + "openapi-schema-validation": "^0.4.2", + "openapi-types": "^1.3.4", + "qs": "^6.6.0", + "swagger-parser": "^9.0.1" + }, + "dependencies": { + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + } + } + }, + "openapi-schema-validation": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/openapi-schema-validation/-/openapi-schema-validation-0.4.2.tgz", + "integrity": "sha512-K8LqLpkUf2S04p2Nphq9L+3bGFh/kJypxIG2NVGKX0ffzT4NQI9HirhiY6Iurfej9lCu7y4Ndm4tv+lm86Ck7w==", + "requires": { + "jsonschema": "1.2.4", + "jsonschema-draft4": "^1.0.0", + "swagger-schema-official": "2.0.0-bab6bed" + } + }, + "openapi-types": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-1.3.5.tgz", + "integrity": "sha512-11oi4zYorsgvg5yBarZplAqbpev5HkuVNPlZaPTknPDzAynq+lnJdXAmruGWP0s+dNYZS7bjM+xrTpJw7184Fg==" + }, "optional-js": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/optional-js/-/optional-js-2.1.1.tgz", @@ -6960,8 +7032,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sqlstring": { "version": "2.3.1", @@ -7174,8 +7245,28 @@ "integrity": "sha1-Sx/ul24RKE0nZXWRGYCyJY43nas=", "requires": { "lodash": "1.3.1" + }, + "dependencies": { + "lodash": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-1.3.1.tgz", + "integrity": "sha1-pGY7U2hriV/wdOK6UE37dqjit3A=" + } } }, + "swagger-parser": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-9.0.1.tgz", + "integrity": "sha512-oxOHUaeNetO9ChhTJm2fD+48DbGbLD09ZEOwPOWEqcW8J6zmjWxutXtSuOiXsoRgDWvORYlImbwM21Pn+EiuvQ==", + "requires": { + "@apidevtools/swagger-parser": "9.0.1" + } + }, + "swagger-schema-official": { + "version": "2.0.0-bab6bed", + "resolved": "https://registry.npmjs.org/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz", + "integrity": "sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0=" + }, "tar-stream": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", @@ -7407,6 +7498,11 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, + "validator": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-11.1.0.tgz", + "integrity": "sha512-qiQ5ktdO7CD6C/5/mYV4jku/7qnqzjrxb3C/Q5wR3vGGinHTgJZN/TdFT3ZX4vXhX2R1PXx42fB1cn5W+uJ4lg==" + }, "vargs": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/vargs/-/vargs-0.1.0.tgz", @@ -7451,12 +7547,6 @@ "requires": { "lodash": "^4.17.14" } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true } } }, @@ -7650,12 +7740,6 @@ "wrap-ansi": "^5.1.0" } }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", - "dev": true - }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -7701,6 +7785,17 @@ "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" }, + "z-schema": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-4.2.2.tgz", + "integrity": "sha512-7bGR7LohxSdlK1EOdvA/OHksvKGE4jTLSjd8dBj9YKT0S43N9pdMZ0Z7GZt9mHrBFhbNTRh3Ky6Eu2MHsPJe8g==", + "requires": { + "commander": "^2.7.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^11.0.0" + } + }, "zip-stream": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.1.3.tgz", diff --git a/src/package.json b/src/package.json index 310a5f27..c23e43be 100644 --- a/src/package.json +++ b/src/package.json @@ -51,6 +51,7 @@ "nodeify": "^1.0.1", "npm": "6.14.3", "object.values": "^1.0.4", + "openapi-backend": "^2.3.8", "request": "2.88.0", "resolve": "1.1.7", "security": "1.0.0",