diff --git a/package-lock.json b/package-lock.json index bec000e..c0929eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "@mockoon/commons-server", - "version": "2.15.5", + "version": "2.16.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@mockoon/commons-server", - "version": "2.15.5", + "version": "2.16.0", "license": "MIT", "dependencies": { + "@apidevtools/swagger-parser": "10.0.3", "@faker-js/faker": "5.5.3", - "@mockoon/commons": "2.8.2", + "@mockoon/commons": "2.9.0", "bson-objectid": "2.0.1", "cookie-parser": "1.4.6", "date-fns": "2.26.0", @@ -30,7 +31,6 @@ "@types/cookie-parser": "1.4.2", "@types/express": "4.17.13", "@types/faker": "5.5.9", - "@types/lodash": "4.14.177", "@types/mime-types": "2.1.1", "@types/mocha": "9.0.0", "@types/node": "16.11.9", @@ -43,6 +43,7 @@ "eslint-config-prettier": "8.3.0", "eslint-plugin-jsdoc": "37.0.3", "mocha": "9.1.3", + "openapi-types": "10.0.0", "prettier": "2.4.1", "prettier-plugin-organize-imports": "2.3.4", "ts-mocha": "8.0.0", @@ -55,6 +56,75 @@ "url": "https://mockoon.com/sponsor-us/" } }, + "../commons": { + "name": "@mockoon/commons", + "version": "2.9.0", + "extraneous": true, + "license": "MIT", + "dependencies": { + "joi": "17.4.2", + "uuid": "8.3.2" + }, + "devDependencies": { + "@types/chai": "4.2.22", + "@types/mocha": "8.2.2", + "@types/uuid": "8.3.3", + "@typescript-eslint/eslint-plugin": "5.4.0", + "@typescript-eslint/parser": "5.4.0", + "chai": "4.3.4", + "eslint": "8.3.0", + "eslint-config-prettier": "8.3.0", + "eslint-plugin-jsdoc": "37.0.3", + "mocha": "8.4.0", + "prettier": "2.4.1", + "prettier-plugin-organize-imports": "2.3.4", + "ts-mocha": "8.0.0", + "typescript": "4.5.2" + }, + "funding": { + "url": "https://mockoon.com/sponsor-us/" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", + "integrity": "sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, "node_modules/@es-joy/jsdoccomment": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.12.0.tgz", @@ -136,10 +206,15 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "node_modules/@mockoon/commons": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@mockoon/commons/-/commons-2.8.2.tgz", - "integrity": "sha512-1b3Gz4jNCwj5wyUPMmkfa//cfsiR2COPK05iElXCIzF2qj62nzWb3P0AmD6sHjV4B8u2DX9MUwrUv2CCNMLaeg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@mockoon/commons/-/commons-2.9.0.tgz", + "integrity": "sha512-j6sT+wP2HxD938R8BSqwJt7BqaXhqHtnJIllV4LnNuy/gyOwSVOkpRNOzRazJrk9vhY8vkJD/h31jZWJK4dAww==", "dependencies": { "joi": "17.4.2", "uuid": "8.3.2" @@ -184,9 +259,9 @@ } }, "node_modules/@sideway/address": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.2.tgz", - "integrity": "sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", + "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", "dependencies": { "@hapi/hoek": "^9.0.0" } @@ -275,8 +350,7 @@ "node_modules/@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, "node_modules/@types/json5": { "version": "0.0.29", @@ -285,12 +359,6 @@ "dev": true, "optional": true }, - "node_modules/@types/lodash": { - "version": "4.14.177", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", - "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==", - "dev": true - }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -603,8 +671,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -752,6 +819,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/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=" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -877,6 +949,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/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 + }, "node_modules/comment-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.2.4.tgz", @@ -2027,7 +2105,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -2108,6 +2185,16 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2395,6 +2482,11 @@ "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-10.0.0.tgz", + "integrity": "sha512-Y8xOCT2eiKGYDzMW9R4x5cmfc3vGaaI4EL2pwhDmodWw1HlK18YcZ4uJxc7Rdp7/gGzAygzH9SXr6GKYIXbRcQ==" + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -3228,6 +3320,14 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "node_modules/validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3391,9 +3491,62 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/z-schema": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", + "integrity": "sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw==", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^2.7.1" + } } }, "dependencies": { + "@apidevtools/json-schema-ref-parser": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.9.tgz", + "integrity": "sha512-GBD2Le9w2+lVFoc4vswGI/TjkNIZSVp7+9xPf+X3uidBfWnAeUWmquteSyt0+VCrhNMWj/FTABISQrD3Z/YA+w==", + "requires": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==" + }, + "@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==" + }, + "@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "requires": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + } + }, "@es-joy/jsdoccomment": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.12.0.tgz", @@ -3465,10 +3618,15 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, "@mockoon/commons": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/@mockoon/commons/-/commons-2.8.2.tgz", - "integrity": "sha512-1b3Gz4jNCwj5wyUPMmkfa//cfsiR2COPK05iElXCIzF2qj62nzWb3P0AmD6sHjV4B8u2DX9MUwrUv2CCNMLaeg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@mockoon/commons/-/commons-2.9.0.tgz", + "integrity": "sha512-j6sT+wP2HxD938R8BSqwJt7BqaXhqHtnJIllV4LnNuy/gyOwSVOkpRNOzRazJrk9vhY8vkJD/h31jZWJK4dAww==", "requires": { "joi": "17.4.2", "uuid": "8.3.2" @@ -3501,9 +3659,9 @@ } }, "@sideway/address": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.2.tgz", - "integrity": "sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.3.tgz", + "integrity": "sha512-8ncEUtmnTsMmL7z1YPB47kPUq7LpKWJNFPsRzHiIajGC5uXlWGn+AmkYPcHNl8S4tcEGx+cnORnNYaw2wvL+LQ==", "requires": { "@hapi/hoek": "^9.0.0" } @@ -3592,8 +3750,7 @@ "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" }, "@types/json5": { "version": "0.0.29", @@ -3602,12 +3759,6 @@ "dev": true, "optional": true }, - "@types/lodash": { - "version": "4.14.177", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.177.tgz", - "integrity": "sha512-0fDwydE2clKe9MNfvXHBHF9WEahRuj+msTuQqOmAApNORFvhMYZKNGGJdCzuhheVjMps/ti0Ak/iJPACMaevvw==", - "dev": true - }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -3816,8 +3967,7 @@ "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "array-flatten": { "version": "1.1.1", @@ -3940,6 +4090,11 @@ "get-intrinsic": "^1.0.2" } }, + "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=" + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4035,6 +4190,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "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 + }, "comment-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.2.4.tgz", @@ -4886,7 +5047,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "requires": { "argparse": "^2.0.1" } @@ -4949,6 +5109,16 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "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.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5161,6 +5331,11 @@ "wrappy": "1" } }, + "openapi-types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-10.0.0.tgz", + "integrity": "sha512-Y8xOCT2eiKGYDzMW9R4x5cmfc3vGaaI4EL2pwhDmodWw1HlK18YcZ4uJxc7Rdp7/gGzAygzH9SXr6GKYIXbRcQ==" + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -5752,6 +5927,11 @@ "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", "dev": true }, + "validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5872,6 +6052,17 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true + }, + "z-schema": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz", + "integrity": "sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw==", + "requires": { + "commander": "^2.7.1", + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + } } } } diff --git a/package.json b/package.json index fb43d16..5a5ad1e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@mockoon/commons-server", "description": "Mockoon's commons server library. Used in Mockoon desktop application and CLI.", - "version": "2.15.5", + "version": "2.16.0", "author": { "name": "Guillaume Monnet", "email": "hi@255kb.dev", @@ -35,8 +35,9 @@ "node": ">=12.0.0" }, "dependencies": { + "@apidevtools/swagger-parser": "10.0.3", "@faker-js/faker": "5.5.3", - "@mockoon/commons": "2.8.2", + "@mockoon/commons": "2.9.0", "bson-objectid": "2.0.1", "cookie-parser": "1.4.6", "date-fns": "2.26.0", @@ -56,7 +57,6 @@ "@types/cookie-parser": "1.4.2", "@types/express": "4.17.13", "@types/faker": "5.5.9", - "@types/lodash": "4.14.177", "@types/mime-types": "2.1.1", "@types/mocha": "9.0.0", "@types/node": "16.11.9", @@ -69,6 +69,7 @@ "eslint-config-prettier": "8.3.0", "eslint-plugin-jsdoc": "37.0.3", "mocha": "9.1.3", + "openapi-types": "10.0.0", "prettier": "2.4.1", "prettier-plugin-organize-imports": "2.3.4", "ts-mocha": "8.0.0", diff --git a/src/index.ts b/src/index.ts index f12977e..42fb6f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from './libs/faker'; +export * from './libs/openapi-converter'; export * from './libs/server'; diff --git a/src/libs/openapi-converter.ts b/src/libs/openapi-converter.ts new file mode 100644 index 0000000..efd2dea --- /dev/null +++ b/src/libs/openapi-converter.ts @@ -0,0 +1,530 @@ +import openAPI from '@apidevtools/swagger-parser'; +import { + BuildEnvironment, + BuildHeader, + BuildRoute, + BuildRouteResponse, + Environment, + GetRouteResponseContentType, + Header, + INDENT_SIZE, + Methods, + RemoveLeadingSlash, + Route, + RouteResponse +} from '@mockoon/commons'; +import { OpenAPI, OpenAPIV2, OpenAPIV3 } from 'openapi-types'; + +type SpecificationVersions = 'SWAGGER' | 'OPENAPI_V3'; + +/** + * Convert to and from Swagger/OpenAPI formats + * + * OpenAPI specifications: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.1.md + * Swagger specifications: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md + * + */ +export class OpenAPIConverter { + constructor() {} + + /** + * Import Swagger or OpenAPI format + * + * @param filePath + */ + public async convertFromOpenAPI(filePath: string, port?: number) { + try { + // .bind() due to https://github.com/APIDevTools/json-schema-ref-parser/issues/139#issuecomment-940500698 + const parsedAPI: OpenAPI.Document = await openAPI.dereference.bind( + openAPI + )(filePath, { + dereference: { circular: 'ignore' } + }); + + if (this.isSwagger(parsedAPI)) { + return this.convertFromSwagger(parsedAPI, port); + } else if (this.isOpenAPIV3(parsedAPI)) { + return this.convertFromOpenAPIV3(parsedAPI, port); + } + + return null; + } catch (error) { + throw error; + } + } + + /** + * Convert environment to OpenAPI JSON object + * + * @param environment + */ + public async convertToOpenAPIV3(environment: Environment) { + const openAPIEnvironment: OpenAPIV3.Document = { + openapi: '3.0.0', + info: { title: environment.name, version: '1.0.0' }, + servers: [ + { + url: `${ + environment.tlsOptions.enabled ? 'https' : 'http' + }://localhost:${environment.port}/${environment.endpointPrefix}` + } + ], + paths: environment.routes.reduce( + (paths, route) => { + const pathParameters = route.endpoint.match(/:[a-zA-Z0-9_]+/g); + let endpoint = '/' + route.endpoint; + + if (pathParameters && pathParameters.length > 0) { + endpoint = + '/' + route.endpoint.replace(/:([a-zA-Z0-9_]+)/g, '{$1}'); + } + + if (!paths[endpoint]) { + paths[endpoint] = {}; + } + + (paths[endpoint] as OpenAPIV3.OperationObject)[route.method] = { + description: route.documentation, + responses: route.responses.reduce( + (responses, routeResponse) => { + const responseContentType = GetRouteResponseContentType( + environment, + routeResponse + ); + + responses[routeResponse.statusCode.toString()] = { + description: routeResponse.label, + content: responseContentType + ? { [responseContentType]: {} } + : {}, + headers: [ + ...environment.headers, + ...routeResponse.headers + ].reduce<{ + [header: string]: OpenAPIV3.HeaderObject; + }>((headers, header) => { + if (header.key.toLowerCase() !== 'content-type') { + headers[header.key] = { + schema: { type: 'string' }, + example: header.value + }; + } + + return headers; + }, {}) + } as any; + + return responses; + }, + {} + ) + }; + + if (pathParameters && pathParameters.length > 0) { + ( + (paths[endpoint] as OpenAPIV3.OperationObject)[ + route.method + ] as OpenAPIV3.OperationObject + ).parameters = pathParameters.reduce( + (parameters, parameter) => { + parameters.push({ + name: parameter.slice(1, parameter.length), + in: 'path', + schema: { type: 'string' }, + required: true + }); + + return parameters; + }, + [] + ); + } + + return paths; + }, + {} + ) + }; + + try { + await openAPI.validate(openAPIEnvironment); + + return JSON.stringify(openAPIEnvironment); + } catch (error) { + throw error; + } + } + + /** + * Convert Swagger 2.0 format + * + * @param parsedAPI + */ + private convertFromSwagger( + parsedAPI: OpenAPIV2.Document, + port?: number + ): Environment { + const newEnvironment = BuildEnvironment({ + hasDefaultHeader: false, + hasDefaultRoute: false, + port + }); + + // parse the port + newEnvironment.port = + (parsedAPI.host && parseInt(parsedAPI.host.split(':')[1], 10)) || + newEnvironment.port; + + if (parsedAPI.basePath) { + newEnvironment.endpointPrefix = RemoveLeadingSlash(parsedAPI.basePath); + } + + newEnvironment.name = parsedAPI.info.title || 'Swagger import'; + + newEnvironment.routes = this.createRoutes(parsedAPI, 'SWAGGER'); + + return newEnvironment; + } + + /** + * Convert OpenAPI 3.0 format + * + * @param parsedAPI + */ + private convertFromOpenAPIV3( + parsedAPI: OpenAPIV3.Document, + port?: number + ): Environment { + const newEnvironment = BuildEnvironment({ + hasDefaultHeader: false, + hasDefaultRoute: false, + port + }); + + const server: OpenAPIV3.ServerObject[] | undefined = parsedAPI.servers; + + if (server?.[0]?.url) { + newEnvironment.endpointPrefix = RemoveLeadingSlash( + new URL(this.v3ParametersReplace(server[0].url, server[0].variables)) + .pathname + ); + } + + newEnvironment.name = parsedAPI.info.title || 'OpenAPI import'; + + newEnvironment.routes = this.createRoutes(parsedAPI, 'OPENAPI_V3'); + + return newEnvironment; + } + + /** + * Creates routes from imported swagger/OpenAPI document + * + * @param parsedAPI + * @param version + */ + private createRoutes( + parsedAPI: OpenAPIV2.Document, + version: 'SWAGGER' + ): Route[]; + private createRoutes( + parsedAPI: OpenAPIV3.Document, + version: 'OPENAPI_V3' + ): Route[]; + private createRoutes( + parsedAPI: OpenAPIV2.Document & OpenAPIV3.Document, + version: SpecificationVersions + ): Route[] { + const routes: Route[] = []; + + Object.keys(parsedAPI.paths).forEach((routePath) => { + Object.keys(parsedAPI.paths[routePath]).forEach((routeMethod) => { + const parsedRoute: OpenAPIV2.OperationObject & + OpenAPIV3.OperationObject = parsedAPI.paths[routePath][routeMethod]; + + if (routeMethod in Methods) { + const routeResponses: RouteResponse[] = []; + + Object.keys(parsedRoute.responses).forEach((responseStatus) => { + const statusCode = parseInt(responseStatus, 10); + // filter unsupported status codes (i.e. ranges containing "X", 4XX, 5XX, etc) + // consider 'default' as 200 + if ( + (statusCode >= 100 && statusCode <= 999) || + responseStatus === 'default' + ) { + const routeResponse: OpenAPIV2.ResponseObject & + OpenAPIV3.ResponseObject = parsedRoute.responses[ + responseStatus + ] as OpenAPIV2.ResponseObject & OpenAPIV3.ResponseObject; + + let contentTypeHeaders: string[] = []; + let schema: + | OpenAPIV2.SchemaObject + | OpenAPIV3.SchemaObject + | undefined; + + if (version === 'SWAGGER') { + contentTypeHeaders = + parsedRoute.produces || + parsedRoute.consumes || + parsedAPI.produces || + parsedAPI.consumes || + []; + } else if (version === 'OPENAPI_V3' && routeResponse.content) { + contentTypeHeaders = Object.keys(routeResponse.content); + } + + // extract schema + const contentTypeHeader = contentTypeHeaders.find((header) => + header.includes('application/json') + ); + + if (contentTypeHeader) { + if (version === 'SWAGGER') { + schema = routeResponse.schema; + } else if (version === 'OPENAPI_V3') { + schema = routeResponse.content?.[contentTypeHeader].schema; + } + } + + routeResponses.push({ + ...BuildRouteResponse(), + body: schema + ? this.convertJSONSchemaPrimitives( + JSON.stringify( + this.generateSchema(schema), + null, + INDENT_SIZE + ) + ) + : '', + statusCode: responseStatus === 'default' ? 200 : statusCode, + label: routeResponse.description || '', + headers: this.buildResponseHeaders( + contentTypeHeaders, + routeResponse.headers + ) + }); + } + }); + + // check if has at least one response + if (!routeResponses.length) { + routeResponses.push({ + ...BuildRouteResponse(), + headers: [BuildHeader('Content-Type', 'application/json')], + body: '' + }); + } + + const newRoute: Route = { + ...BuildRoute(false), + documentation: parsedRoute.summary || parsedRoute.description || '', + method: routeMethod as Methods, + endpoint: RemoveLeadingSlash(this.v2ParametersReplace(routePath)), + responses: routeResponses + }; + + routes.push(newRoute); + } + }); + }); + + return routes; + } + + /** + * Build route response headers from 'content' (v3) or 'produces' (v2), and 'headers' objects + * + * @param contentTypes + * @param responseHeaders + */ + private buildResponseHeaders( + contentTypes: string[], + responseHeaders: + | undefined + | OpenAPIV2.HeadersObject + | { + [key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.HeaderObject; + } + ): Header[] { + const routeContentTypeHeader = BuildHeader( + 'Content-Type', + 'application/json' + ); + + if ( + contentTypes && + contentTypes.length && + !contentTypes.includes('application/json') + ) { + routeContentTypeHeader.value = contentTypes[0]; + } + + if (responseHeaders) { + return [ + routeContentTypeHeader, + ...Object.keys(responseHeaders).map((header) => BuildHeader(header, '')) + ]; + } + + return [routeContentTypeHeader]; + } + + /** + * Replace parameters in `str` + * + * @param str + */ + private v2ParametersReplace(str: string) { + return str.replace( + /{(\w+)}/gi, + (searchValue, replaceValue) => ':' + replaceValue + ); + } + + /** + * Replace parameters in `str` with server variables + * + * @param str + * @param parameters + * @returns + */ + private v3ParametersReplace( + str: string, + parameters: + | { [variable in string]: OpenAPIV3.ServerVariableObject } + | undefined + ) { + return str.replace(/{(\w+)}/gi, (searchValue, replaceValue) => + parameters ? parameters[replaceValue].default : '' + ); + } + + /** + * Swagger specification type guard + * + * @param parsedAPI + */ + private isSwagger(parsedAPI: any): parsedAPI is OpenAPIV2.Document { + return parsedAPI.swagger !== undefined; + } + + /** + * OpenAPI v3 specification type guard + * + * @param parsedAPI + */ + private isOpenAPIV3(parsedAPI: any): parsedAPI is OpenAPIV3.Document { + return ( + parsedAPI.openapi !== undefined && parsedAPI.openapi.startsWith('3.') + ); + } + + /** + * Generate a JSON object from a schema + * + */ + private generateSchema( + schema: OpenAPIV2.SchemaObject | OpenAPIV3.SchemaObject + ) { + const typeFactories = { + integer: () => "{{faker 'random.number'}}", + number: () => "{{faker 'random.number'}}", + number_float: () => "{{faker 'random.float'}}", + number_double: () => "{{faker 'random.float'}}", + string: () => '', + string_date: () => "{{date '2019' (now) 'yyyy-MM-dd'}}", + 'string_date-time': () => "{{faker 'date.recent' 365}}", + string_email: () => "{{faker 'internet.email'}}", + string_uuid: () => "{{faker 'random.uuid'}}", + boolean: () => "{{faker 'random.boolean'}}", + array: (arraySchema) => { + const newObject = this.generateSchema(arraySchema.items); + + return arraySchema.collectionFormat === 'csv' ? newObject : [newObject]; + }, + object: (objectSchema) => { + const newObject = {}; + const { properties } = objectSchema; + + if (properties) { + Object.keys(properties).forEach((propertyName) => { + newObject[propertyName] = this.generateSchema( + properties[propertyName] + ); + }); + } + + return newObject; + } + }; + + if (schema instanceof Object) { + let type: string = + Array.isArray(schema.type) && schema.type.length >= 1 + ? schema.type[0] + : (schema.type as string); + + // use enum property if present + if (schema.enum) { + return `{{oneOf (array '${schema.enum.join("' '")}')}}`; + } + + // return example if any + if (schema.example) { + return schema.example; + } + + // return default value if any + if (schema.default) { + return schema.default; + } + + let schemaToBuild = schema; + + // check if we have an array of schemas, and take first item + ['allOf', 'oneOf', 'anyOf'].forEach((propertyName) => { + if ( + schema.hasOwnProperty(propertyName) && + schema[propertyName].length > 0 + ) { + type = schema[propertyName][0].type; + schemaToBuild = schema[propertyName][0]; + } + }); + + // sometimes we have no type but only 'properties' (=object) + if ( + !type && + schemaToBuild.properties && + schemaToBuild.properties instanceof Object + ) { + type = 'object'; + } + + const typeFactory = + typeFactories[`${type}_${schemaToBuild.format}`] || typeFactories[type]; + + if (typeFactory) { + return typeFactory(schemaToBuild); + } + + return ''; + } + } + + /** + * After generating example bodies, remove the quotes around some + * primitive helpers + * + * @param jsonSchema + */ + private convertJSONSchemaPrimitives(jsonSchema: string) { + return jsonSchema.replace( + /\"({{faker 'random\.(number|boolean|float)'}})\"/g, + '$1' + ); + } +} diff --git a/src/libs/utils.ts b/src/libs/utils.ts index bd255b2..a3bf50d 100644 --- a/src/libs/utils.ts +++ b/src/libs/utils.ts @@ -1,4 +1,4 @@ -import { Header, Transaction } from '@mockoon/commons'; +import { Header, Methods, Transaction } from '@mockoon/commons'; import { Request, Response } from 'express'; import { SafeString } from 'handlebars'; import { IncomingHttpHeaders, OutgoingHttpHeaders } from 'http'; @@ -108,7 +108,7 @@ export const CreateTransaction = ( response: Response ): Transaction => ({ request: { - method: request.method, + method: request.method.toLowerCase() as keyof typeof Methods, urlPath: new URL(request.originalUrl, 'http://localhost/').pathname, route: request.route ? request.route.path : null, params: request.params