From a5b86afa60f7a699862665d81e7830eb597a2ca5 Mon Sep 17 00:00:00 2001 From: Rory Powell Date: Thu, 2 Dec 2021 11:55:13 +0000 Subject: [PATCH] OAPI2 (swagger) complete + tests --- packages/server/package.json | 1 + .../src/api/controllers/query/import/index.ts | 32 +- .../query/import/sources/base/index.ts | 13 +- .../query/import/sources/base/openapi.ts | 16 +- .../query/import/sources/openapi2.ts | 103 +- .../query/import/sources/openapi3.ts | 40 - .../import/sources/tests/curl/curl.spec.js | 4 +- .../tests/openapi2/data/crud/crud.json | 986 +++--------------- .../tests/openapi2/data/crud/crud.yaml | 160 +++ .../sources/tests/openapi2/openapi2.spec.js | 198 +++- .../sources/tests/openapi3/openapi3.spec.js | 0 .../query/import/tests/index.spec.js | 115 ++ 12 files changed, 694 insertions(+), 974 deletions(-) delete mode 100644 packages/server/src/api/controllers/query/import/sources/openapi3.ts create mode 100644 packages/server/src/api/controllers/query/import/sources/tests/openapi2/data/crud/crud.yaml delete mode 100644 packages/server/src/api/controllers/query/import/sources/tests/openapi3/openapi3.spec.js create mode 100644 packages/server/src/api/controllers/query/import/tests/index.spec.js diff --git a/packages/server/package.json b/packages/server/package.json index 63669e5b2f..046f07de7a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -92,6 +92,7 @@ "fs-extra": "8.1.0", "jimp": "0.16.1", "joi": "17.2.1", + "js-yaml": "^4.1.0", "jsonschema": "1.4.0", "knex": "^0.95.6", "koa": "2.7.0", diff --git a/packages/server/src/api/controllers/query/import/index.ts b/packages/server/src/api/controllers/query/import/index.ts index 808959798d..7f52fbb0a8 100644 --- a/packages/server/src/api/controllers/query/import/index.ts +++ b/packages/server/src/api/controllers/query/import/index.ts @@ -3,11 +3,11 @@ import { queryValidation } from "../validation" import { generateQueryID } from "../../../../db/utils" import { Query, ImportInfo, ImportSource } from "./sources/base" import { OpenAPI2 } from "./sources/openapi2" -import { OpenAPI3 } from "./sources/openapi3" import { Curl } from "./sources/curl" interface ImportResult { errorQueries: Query[] + queries: Query[] } export class RestImporter { @@ -17,7 +17,7 @@ export class RestImporter { constructor(data: string) { this.data = data - this.sources = [new OpenAPI2(), new OpenAPI3(), new Curl()] + this.sources = [new OpenAPI2(), new Curl()] } init = async () => { @@ -37,12 +37,11 @@ export class RestImporter { appId: string, datasourceId: string, ): Promise => { - // constuct the queries let queries = await this.source.getQueries(datasourceId) // validate queries - const errorQueries = [] + const errorQueries: Query[] = [] const schema = queryValidation() queries = queries .filter(query => { @@ -57,19 +56,30 @@ export class RestImporter { query._id = generateQueryID(query.datasourceId) return query }) - + // persist queries const db = new CouchDB(appId) - for (const query of queries) { - try { - await db.put(query) - } catch (error) { - errorQueries.push(query) + const response = await db.bulkDocs(queries) + + // create index to seperate queries and errors + const queryIndex = queries.reduce((acc, query) => { + if (query._id) { + acc[query._id] = query } - } + return acc + }, ({} as { [key: string]: Query; })) + + // check for failed writes + response.forEach((query: any) => { + if (!query.ok) { + errorQueries.push(queryIndex[query.id]) + delete queryIndex[query.id] + } + }); return { errorQueries, + queries: Object.values(queryIndex) } } diff --git a/packages/server/src/api/controllers/query/import/sources/base/index.ts b/packages/server/src/api/controllers/query/import/sources/base/index.ts index 747b7b3d66..db36067985 100644 --- a/packages/server/src/api/controllers/query/import/sources/base/index.ts +++ b/packages/server/src/api/controllers/query/import/sources/base/index.ts @@ -17,7 +17,7 @@ export interface Query { headers: object queryString: string | null path: string - requestBody?: object + requestBody: string | undefined } transformer: string | null schema: any @@ -47,7 +47,7 @@ export abstract class ImportSource { queryString: string, headers: object = {}, parameters: QueryParameter[] = [], - requestBody: object | undefined = undefined, + body: object | undefined = undefined, ): Query => { const readable = true const queryVerb = this.verbFromMethod(method) @@ -55,6 +55,7 @@ export abstract class ImportSource { const schema = {} path = this.processPath(path) queryString = this.processQuery(queryString) + const requestBody = JSON.stringify(body, null, 2) const query: Query = { datasourceId, @@ -85,9 +86,13 @@ export abstract class ImportSource { processPath = (path: string): string => { if (path?.startsWith("/")) { - return path.substring(1) + path = path.substring(1) } - + + // add extra braces around params for binding + path = path.replace(/[{]/g, "{{"); + path = path.replace(/[}]/g, "}}"); + return path } diff --git a/packages/server/src/api/controllers/query/import/sources/base/openapi.ts b/packages/server/src/api/controllers/query/import/sources/base/openapi.ts index dfe3c68c08..74f0b48c42 100644 --- a/packages/server/src/api/controllers/query/import/sources/base/openapi.ts +++ b/packages/server/src/api/controllers/query/import/sources/base/openapi.ts @@ -2,11 +2,25 @@ import { ImportSource } from "." import SwaggerParser from "@apidevtools/swagger-parser"; import { OpenAPI } from "openapi-types"; +const yaml = require('js-yaml'); export abstract class OpenAPISource extends ImportSource { parseData = async (data: string): Promise => { - const json = JSON.parse(data) + let json: OpenAPI.Document; + try { + json = JSON.parse(data) + } catch (jsonErr) { + // couldn't parse json + // try to convert yaml -> json + try { + json = yaml.load(data) + } catch (yamlErr) { + // couldn't parse yaml + throw new Error("Could not parse JSON or YAML") + } + } + return SwaggerParser.validate(json, {}) } diff --git a/packages/server/src/api/controllers/query/import/sources/openapi2.ts b/packages/server/src/api/controllers/query/import/sources/openapi2.ts index 05ee41d531..b144892191 100644 --- a/packages/server/src/api/controllers/query/import/sources/openapi2.ts +++ b/packages/server/src/api/controllers/query/import/sources/openapi2.ts @@ -2,13 +2,8 @@ import { ImportInfo, QueryParameter, Query } from "./base" import { OpenAPIV2 } from "openapi-types" import { OpenAPISource } from "./base/openapi"; -const isBodyParameter = (param: OpenAPIV2.Parameter): param is OpenAPIV2.InBodyParameterObject => { - return param.in === "body" -} - -const isParameter = (param: OpenAPIV2.Parameter | OpenAPIV2.ReferenceObject): param is OpenAPIV2.Parameter => { - // we can guarantee this is not a reference object - // due to the deferencing done by the parser library +const parameterNotRef = (param: OpenAPIV2.Parameter | OpenAPIV2.ReferenceObject): param is OpenAPIV2.Parameter => { + // all refs are deferenced by parser library return true } @@ -20,6 +15,16 @@ const isOpenAPI2 = (document: any): document is OpenAPIV2.Document => { } } +const methods: string[] = Object.values(OpenAPIV2.HttpMethods) + +const isOperation = (key: string, pathItem: any): pathItem is OpenAPIV2.OperationObject => { + return methods.includes(key) +} + +const isParameter = (key: string, pathItem: any): pathItem is OpenAPIV2.Parameter => { + return !isOperation(key, pathItem) +} + /** * OpenAPI Version 2.0 - aka "Swagger" * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/2.0.md @@ -57,30 +62,70 @@ export class OpenAPI2 extends OpenAPISource { getQueries = async (datasourceId: string): Promise => { const queries = [] - let pathName: string - let path: OpenAPIV2.PathItemObject + for (let [path, pathItem] of Object.entries(this.document.paths)) { + // parameters that apply to every operation in the path + let pathParams: OpenAPIV2.Parameter[] = [] + + for (let [key, opOrParams] of Object.entries(pathItem)) { + if (isParameter(key, opOrParams)) { + const pathParameters = opOrParams as OpenAPIV2.Parameter[] + pathParams.push(...pathParameters) + continue + } + // can not be a parameter, must be an operation + const operation = opOrParams as OpenAPIV2.OperationObject - for ([pathName, path] of Object.entries(this.document.paths)) { - for (let [methodName, op] of Object.entries(path)) { - let operation = op as OpenAPIV2.OperationObject - - const name = operation.operationId || pathName - const queryString = "" - const headers = {} + const methodName = key + const name = operation.operationId || path + let queryString = "" + const headers: any = {} let requestBody = undefined const parameters: QueryParameter[] = [] - - if (operation.parameters) { - for (let param of operation.parameters) { - if (isParameter(param)) { - if (isBodyParameter(param)) { - requestBody = {} - } else { - parameters.push({ - name: param.name, - default: "", - }) - } + + if (operation.consumes) { + headers["Content-Type"] = operation.consumes[0] + } + + // combine the path parameters with the operation parameters + const operationParams = operation.parameters || [] + const allParams = [...pathParams, ...operationParams] + + for (let param of allParams) { + if (parameterNotRef(param)) { + switch (param.in) { + case "query": + let prefix = "" + if (queryString) { + prefix = "&" + } + queryString = `${queryString}${prefix}${param.name}={{${param.name}}}` + break + case "header": + headers[param.name] = `{{${param.name}}}` + break + case "path": + // do nothing: param is already in the path + break + case "formData": + // future enhancement + break + case "body": + // set the request body to the example provided + // future enhancement: generate an example from the schema + let bodyParam: OpenAPIV2.InBodyParameterObject = param as OpenAPIV2.InBodyParameterObject + if (param.schema.example) { + const schema = bodyParam.schema as OpenAPIV2.SchemaObject + requestBody = schema.example + } + break; + } + + // add the parameter if it can be bound in our config + if (['query', 'header', 'path'].includes(param.in)) { + parameters.push({ + name: param.name, + default: param.default || "", + }) } } } @@ -89,7 +134,7 @@ export class OpenAPI2 extends OpenAPISource { datasourceId, name, methodName, - pathName, + path, queryString, headers, parameters, diff --git a/packages/server/src/api/controllers/query/import/sources/openapi3.ts b/packages/server/src/api/controllers/query/import/sources/openapi3.ts deleted file mode 100644 index 655c680ed4..0000000000 --- a/packages/server/src/api/controllers/query/import/sources/openapi3.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { ImportInfo, Query } from "./base" -import { OpenAPISource } from "./base/openapi" -import { OpenAPIV3 } from "openapi-types" - -const isOpenAPI3 = (document: any): document is OpenAPIV3.Document => { - return document.openapi === "3.0.0" -} - -/** - * OpenAPI Version 3.0.0 - * https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md - */ -export class OpenAPI3 extends OpenAPISource { - document!: OpenAPIV3.Document - - isSupported = async (data: string): Promise => { - try { - const document: any = await this.parseData(data) - if (isOpenAPI3(document)) { - this.document = document - return true - } else { - return false - } - } catch (err) { - return false - } - } - - getInfo = async (): Promise => { - return { - url: "http://localhost:3000", - name: "swagger", - } - } - - getQueries = async (datasourceId: string): Promise => { - return [] - } -} diff --git a/packages/server/src/api/controllers/query/import/sources/tests/curl/curl.spec.js b/packages/server/src/api/controllers/query/import/sources/tests/curl/curl.spec.js index 0ce8465277..11869862f7 100644 --- a/packages/server/src/api/controllers/query/import/sources/tests/curl/curl.spec.js +++ b/packages/server/src/api/controllers/query/import/sources/tests/curl/curl.spec.js @@ -90,9 +90,9 @@ describe("Curl Import", () => { await testQuery("query", "q1=v1&q1=v2") }) - const testBody = async (file, queryString) => { + const testBody = async (file, body) => { const queries = await getQueries(file) - expect(queries[0].fields.requestBody).toStrictEqual(queryString) + expect(queries[0].fields.requestBody).toStrictEqual(JSON.stringify(body, null, 2)) } it("populates body", async () => { diff --git a/packages/server/src/api/controllers/query/import/sources/tests/openapi2/data/crud/crud.json b/packages/server/src/api/controllers/query/import/sources/tests/openapi2/data/crud/crud.json index ed16af32bd..abe07f136f 100644 --- a/packages/server/src/api/controllers/query/import/sources/tests/openapi2/data/crud/crud.json +++ b/packages/server/src/api/controllers/query/import/sources/tests/openapi2/data/crud/crud.json @@ -20,7 +20,7 @@ "tags": [ "entity" ], - "operationId": "create", + "operationId": "createEntity", "consumes": [ "application/json" ], @@ -29,941 +29,225 @@ ], "parameters": [ { - "in": "body", - "schema": { - "$ref": "#/definitions/CreateEntity" - } + "$ref": "#/parameters/CreateEntity" } ], "responses": { "200": { "description": "successful operation", "schema": { - "$ref": "#/definitions/ApiResponse" + "$ref": "#/definitions/Entity" } } - }, - "security": [ - { - "auth": [ - "write:entities", - "read:entites" - ] - } - ] - } - }, - "/pet": { - "put": { - "tags": [ - "pet" - ], - "summary": "Update an existing pet", - "description": "", - "operationId": "updatePet", - "consumes": [ - "application/json", - "application/xml" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Pet object that needs to be added to the store", - "required": true, - "schema": { - "$ref": "#/definitions/Pet" - } - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - }, - "405": { - "description": "Validation exception" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByStatus": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by status", - "description": "Multiple status values can be provided with comma separated strings", - "operationId": "findPetsByStatus", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "status", - "in": "query", - "description": "Status values that need to be considered for filter", - "required": true, - "type": "array", - "items": { - "type": "string", - "enum": [ - "available", - "pending", - "sold" - ], - "default": "available" - }, - "collectionFormat": "multi" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Pet" - } - } - }, - "400": { - "description": "Invalid status value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/pet/findByTags": { - "get": { - "tags": [ - "pet" - ], - "summary": "Finds Pets by tags", - "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", - "operationId": "findPetsByTags", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "tags", - "in": "query", - "description": "Tags to filter by", - "required": true, - "type": "array", - "items": { - "type": "string" - }, - "collectionFormat": "multi" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/Pet" - } - } - }, - "400": { - "description": "Invalid tag value" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ], - "deprecated": true - } - }, - "/pet/{petId}": { - "get": { - "tags": [ - "pet" - ], - "summary": "Find pet by ID", - "description": "Returns a single pet", - "operationId": "getPetById", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet to return", - "required": true, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Pet" - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - } - }, - "security": [ - { - "api_key": [] - } - ] - }, - "post": { - "tags": [ - "pet" - ], - "summary": "Updates a pet in the store with form data", - "description": "", - "operationId": "updatePetWithForm", - "consumes": [ - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "petId", - "in": "path", - "description": "ID of pet that needs to be updated", - "required": true, - "type": "integer", - "format": "int64" - }, - { - "name": "name", - "in": "formData", - "description": "Updated name of the pet", - "required": false, - "type": "string" - }, - { - "name": "status", - "in": "formData", - "description": "Updated status of the pet", - "required": false, - "type": "string" - } - ], - "responses": { - "405": { - "description": "Invalid input" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - }, - "delete": { - "tags": [ - "pet" - ], - "summary": "Deletes a pet", - "description": "", - "operationId": "deletePet", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "api_key", - "in": "header", - "required": false, - "type": "string" - }, - { - "name": "petId", - "in": "path", - "description": "Pet id to delete", - "required": true, - "type": "integer", - "format": "int64" - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Pet not found" - } - }, - "security": [ - { - "petstore_auth": [ - "write:pets", - "read:pets" - ] - } - ] - } - }, - "/store/inventory": { - "get": { - "tags": [ - "store" - ], - "summary": "Returns pet inventories by status", - "description": "Returns a map of status codes to quantities", - "operationId": "getInventory", - "produces": [ - "application/json" - ], - "parameters": [], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "type": "object", - "additionalProperties": { - "type": "integer", - "format": "int32" - } - } - } - }, - "security": [ - { - "api_key": [] - } - ] - } - }, - "/store/order": { - "post": { - "tags": [ - "store" - ], - "summary": "Place an order for a pet", - "description": "", - "operationId": "placeOrder", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "order placed for purchasing the pet", - "required": true, - "schema": { - "$ref": "#/definitions/Order" - } - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Order" - } - }, - "400": { - "description": "Invalid Order" - } - } - } - }, - "/store/order/{orderId}": { - "get": { - "tags": [ - "store" - ], - "summary": "Find purchase order by ID", - "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", - "operationId": "getOrderById", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of pet that needs to be fetched", - "required": true, - "type": "integer", - "maximum": 10, - "minimum": 1, - "format": "int64" - } - ], - "responses": { - "200": { - "description": "successful operation", - "schema": { - "$ref": "#/definitions/Order" - } - }, - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } } }, - "delete": { - "tags": [ - "store" - ], - "summary": "Delete purchase order by ID", - "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", - "operationId": "deleteOrder", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "orderId", - "in": "path", - "description": "ID of the order that needs to be deleted", - "required": true, - "type": "integer", - "minimum": 1, - "format": "int64" - } - ], - "responses": { - "400": { - "description": "Invalid ID supplied" - }, - "404": { - "description": "Order not found" - } - } - } - }, - "/user/createWithList": { - "post": { - "tags": [ - "user" - ], - "summary": "Creates list of users with given input array", - "description": "", - "operationId": "createUsersWithListInput", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "List of user object", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/User" - } - } - } - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user/{username}": { "get": { "tags": [ - "user" + "entity" ], - "summary": "Get user by user name", - "description": "", - "operationId": "getUserByName", + "operationId": "getEntities", "produces": [ - "application/json", - "application/xml" + "application/json" ], "parameters": [ { - "name": "username", - "in": "path", - "description": "The name that needs to be fetched. Use user1 for testing. ", - "required": true, - "type": "string" + "$ref": "#/parameters/Page" + }, + { + "$ref": "#/parameters/Size" } ], "responses": { "200": { "description": "successful operation", "schema": { - "$ref": "#/definitions/User" + "$ref": "#/definitions/Entities" + } + } + } + } + }, + "/entities/{entityId}": { + "parameters": [ + { + "$ref": "#/parameters/EntityId" + } + ], + "get": { + "tags": [ + "entity" + ], + "operationId": "getEntity", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Entity" } - }, - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" } } }, "put": { "tags": [ - "user" + "entity" ], - "summary": "Updated user", - "description": "This can only be done by the logged in user.", - "operationId": "updateUser", + "operationId": "updateEntity", "consumes": [ "application/json" ], "produces": [ - "application/json", - "application/xml" + "application/json" ], "parameters": [ { - "name": "username", - "in": "path", - "description": "name that need to be updated", - "required": true, - "type": "string" - }, - { - "in": "body", - "name": "body", - "description": "Updated user object", - "required": true, - "schema": { - "$ref": "#/definitions/User" - } - } - ], - "responses": { - "400": { - "description": "Invalid user supplied" - }, - "404": { - "description": "User not found" - } - } - }, - "delete": { - "tags": [ - "user" - ], - "summary": "Delete user", - "description": "This can only be done by the logged in user.", - "operationId": "deleteUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "path", - "description": "The name that needs to be deleted", - "required": true, - "type": "string" - } - ], - "responses": { - "400": { - "description": "Invalid username supplied" - }, - "404": { - "description": "User not found" - } - } - } - }, - "/user/login": { - "get": { - "tags": [ - "user" - ], - "summary": "Logs user into the system", - "description": "", - "operationId": "loginUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "name": "username", - "in": "query", - "description": "The user name for login", - "required": true, - "type": "string" - }, - { - "name": "password", - "in": "query", - "description": "The password for login in clear text", - "required": true, - "type": "string" + "$ref": "#/parameters/Entity" } ], "responses": { "200": { "description": "successful operation", - "headers": { - "X-Expires-After": { - "type": "string", - "format": "date-time", - "description": "date in UTC when token expires" - }, - "X-Rate-Limit": { - "type": "integer", - "format": "int32", - "description": "calls per hour allowed by the user" - } - }, "schema": { - "type": "string" + "$ref": "#/definitions/Entity" } - }, - "400": { - "description": "Invalid username/password supplied" } } - } - }, - "/user/logout": { - "get": { + }, + "patch": { "tags": [ - "user" + "entity" ], - "summary": "Logs out current logged in user session", - "description": "", - "operationId": "logoutUser", - "produces": [ - "application/json", - "application/xml" - ], - "parameters": [], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user/createWithArray": { - "post": { - "tags": [ - "user" - ], - "summary": "Creates list of users with given input array", - "description": "", - "operationId": "createUsersWithArrayInput", + "operationId": "patchEntity", "consumes": [ "application/json" ], "produces": [ - "application/json", - "application/xml" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "List of user object", - "required": true, - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/User" - } - } - } - ], - "responses": { - "default": { - "description": "successful operation" - } - } - } - }, - "/user": { - "post": { - "tags": [ - "user" - ], - "summary": "Create user", - "description": "This can only be done by the logged in user.", - "operationId": "createUser", - "consumes": [ "application/json" ], - "produces": [ - "application/json", - "application/xml" - ], "parameters": [ { - "in": "body", - "name": "body", - "description": "Created user object", - "required": true, - "schema": { - "$ref": "#/definitions/User" - } + "$ref": "#/parameters/Entity" } ], "responses": { - "default": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Entity" + } + } + } + }, + "delete": { + "tags": [ + "entity" + ], + "parameters": [ + { + "$ref": "#/parameters/APIKey" + } + ], + "operationId": "deleteEntity", + "produces": [ + "application/json" + ], + "responses": { + "204": { "description": "successful operation" } } } } }, - "securityDefinitions": { - "api_key": { - "type": "apiKey", - "name": "api_key", - "in": "header" + "parameters": { + "EntityId": { + "type": "integer", + "format": "int64", + "name": "entityId", + "in": "path", + "required": true }, - "petstore_auth": { - "type": "oauth2", - "authorizationUrl": "https://petstore.swagger.io/oauth/authorize", - "flow": "implicit", - "scopes": { - "read:pets": "read your pets", - "write:pets": "modify pets in your account" + "CreateEntity": { + "name": "entity", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/CreateEntity" } + }, + "Entity": { + "name": "entity", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Entity" + } + }, + "Page": { + "type": "integer", + "format": "int32", + "name": "page", + "in": "query", + "required": false + }, + "Size": { + "type": "integer", + "format": "int32", + "name": "size", + "in": "query", + "required": false + }, + "APIKey": { + "type": "string", + "name": "x-api-key", + "in": "header", + "required": false } }, "definitions": { - "ApiResponse": { + "CreateEntity": { "type": "object", "properties": { - "code": { - "type": "integer", - "format": "int32" + "name": { + "type": "string" }, "type": { "type": "string" - }, - "message": { - "type": "string" - } - } - }, - "Category": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" } }, - "xml": { - "name": "Category" + "example": { + "name": "name", + "type": "type" } }, - "Pet": { - "type": "object", - "required": [ - "name", - "photoUrls" - ], - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "category": { - "$ref": "#/definitions/Category" - }, - "name": { - "type": "string", - "example": "doggie" - }, - "photoUrls": { - "type": "array", - "xml": { - "wrapped": true - }, - "items": { - "type": "string", - "xml": { - "name": "photoUrl" + "Entity": { + "allOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" } } }, - "tags": { - "type": "array", - "xml": { - "wrapped": true - }, - "items": { - "xml": { - "name": "tag" - }, - "$ref": "#/definitions/Tag" - } - }, - "status": { - "type": "string", - "description": "pet status in the store", - "enum": [ - "available", - "pending", - "sold" - ] - } - }, - "xml": { - "name": "Pet" + { + "$ref": "#/definitions/CreateEntity" + } + ], + "example": { + "id": 1, + "name": "name", + "type": "type" } }, - "Tag": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "name": { - "type": "string" - } - }, - "xml": { - "name": "Tag" - } - }, - "Order": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "petId": { - "type": "integer", - "format": "int64" - }, - "quantity": { - "type": "integer", - "format": "int32" - }, - "shipDate": { - "type": "string", - "format": "date-time" - }, - "status": { - "type": "string", - "description": "Order Status", - "enum": [ - "placed", - "approved", - "delivered" - ] - }, - "complete": { - "type": "boolean" - } - }, - "xml": { - "name": "Order" - } - }, - "User": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "username": { - "type": "string" - }, - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "email": { - "type": "string" - }, - "password": { - "type": "string" - }, - "phone": { - "type": "string" - }, - "userStatus": { - "type": "integer", - "format": "int32", - "description": "User Status" - } - }, - "xml": { - "name": "User" + "Entities" : { + "type": "array", + "items": { + "$ref": "#/definitions/Entity" } } - }, - "externalDocs": { - "description": "Find out more about Swagger", - "url": "http://swagger.io" } } diff --git a/packages/server/src/api/controllers/query/import/sources/tests/openapi2/data/crud/crud.yaml b/packages/server/src/api/controllers/query/import/sources/tests/openapi2/data/crud/crud.yaml new file mode 100644 index 0000000000..7568c2f18f --- /dev/null +++ b/packages/server/src/api/controllers/query/import/sources/tests/openapi2/data/crud/crud.yaml @@ -0,0 +1,160 @@ +--- +swagger: "2.0" +info: + description: A basic swagger file + version: 1.0.0 + title: CRUD +host: example.com +tags: + - name: entity +schemes: + - http +paths: + "/entities": + post: + tags: + - entity + operationId: createEntity + consumes: + - application/json + produces: + - application/json + parameters: + - "$ref": "#/parameters/CreateEntity" + responses: + "200": + description: successful operation + schema: + "$ref": "#/definitions/Entity" + get: + tags: + - entity + operationId: getEntities + produces: + - application/json + parameters: + - "$ref": "#/parameters/Page" + - "$ref": "#/parameters/Size" + responses: + "200": + description: successful operation + schema: + "$ref": "#/definitions/Entities" + "/entities/{entityId}": + parameters: + - "$ref": "#/parameters/EntityId" + get: + tags: + - entity + operationId: getEntity + produces: + - application/json + responses: + "200": + description: successful operation + schema: + "$ref": "#/definitions/Entity" + put: + tags: + - entity + operationId: updateEntity + consumes: + - application/json + produces: + - application/json + parameters: + - "$ref": "#/parameters/Entity" + responses: + "200": + description: successful operation + schema: + "$ref": "#/definitions/Entity" + patch: + tags: + - entity + operationId: patchEntity + consumes: + - application/json + produces: + - application/json + parameters: + - "$ref": "#/parameters/Entity" + responses: + "200": + description: successful operation + schema: + "$ref": "#/definitions/Entity" + delete: + tags: + - entity + parameters: + - "$ref": "#/parameters/APIKey" + operationId: deleteEntity + produces: + - application/json + responses: + "204": + description: successful operation +parameters: + EntityId: + type: integer + format: int64 + name: entityId + in: path + required: true + CreateEntity: + name: entity + in: body + required: true + schema: + "$ref": "#/definitions/CreateEntity" + Entity: + name: entity + in: body + required: true + schema: + "$ref": "#/definitions/Entity" + Page: + type: integer + format: int32 + name: page + in: query + required: false + Size: + type: integer + format: int32 + name: size + in: query + required: false + APIKey: + type: string + name: x-api-key + in: header + required: false +definitions: + CreateEntity: + type: object + properties: + name: + type: string + type: + type: string + example: + name: name + type: type + Entity: + allOf: + - type: object + properties: + id: + type: integer + format: int64 + - "$ref": "#/definitions/CreateEntity" + example: + id: 1 + name: name + type: type + Entities: + type: array + items: + "$ref": "#/definitions/Entity" diff --git a/packages/server/src/api/controllers/query/import/sources/tests/openapi2/openapi2.spec.js b/packages/server/src/api/controllers/query/import/sources/tests/openapi2/openapi2.spec.js index 5b99360176..845c4f38f6 100644 --- a/packages/server/src/api/controllers/query/import/sources/tests/openapi2/openapi2.spec.js +++ b/packages/server/src/api/controllers/query/import/sources/tests/openapi2/openapi2.spec.js @@ -32,9 +32,9 @@ describe("OpenAPI2 Import", () => { await openapi2.isSupported(getData(file, extension)) } - const runTests = async (filename, test) => { + const runTests = async (filename, test, assertions) => { for (let extension of ["json", "yaml"]) { - await test(filename, extension) + await test(filename, extension, assertions) } } @@ -50,64 +50,190 @@ describe("OpenAPI2 Import", () => { }) describe("Returns queries", () => { - const getQueries = async (file) => { - await init(file) - const queries = await openapi2.getQueries() - expect(queries.length).toBe(1) - return queries + const indexQueries = (queries) => { + return queries.reduce((acc, query) => { + acc[query.name] = query + return acc + }, {}) } - const testVerb = async (file, verb) => { - const queries = await getQueries(file) - expect(queries[0].queryVerb).toBe(verb) + const getQueries = async (file, extension) => { + await init(file, extension) + const queries = await openapi2.getQueries() + expect(queries.length).toBe(6) + return indexQueries(queries) + } + + const testVerb = async (file, extension, assertions) => { + const queries = await getQueries(file, extension) + for (let [operationId, method] of Object.entries(assertions)) { + expect(queries[operationId].queryVerb).toBe(method) + } } it("populates verb", async () => { - await testVerb("get", "read") - await testVerb("post", "create") - await testVerb("put", "update") - await testVerb("delete", "delete") - await testVerb("patch", "patch") + const assertions = { + "createEntity" : "create", + "getEntities" : "read", + "getEntity" : "read", + "updateEntity" : "update", + "patchEntity" : "patch", + "deleteEntity" : "delete" + } + await runTests("crud", testVerb, assertions) }) - const testPath = async (file, urlPath) => { - const queries = await getQueries(file) - expect(queries[0].fields.path).toBe(urlPath) + const testPath = async (file, extension, assertions) => { + const queries = await getQueries(file, extension) + for (let [operationId, urlPath] of Object.entries(assertions)) { + expect(queries[operationId].fields.path).toBe(urlPath) + } } it("populates path", async () => { - await testPath("get", "") - await testPath("path", "paths/abc") + const assertions = { + "createEntity" : "entities", + "getEntities" : "entities", + "getEntity" : "entities/{{entityId}}", + "updateEntity" : "entities/{{entityId}}", + "patchEntity" : "entities/{{entityId}}", + "deleteEntity" : "entities/{{entityId}}" + } + await runTests("crud", testPath, assertions) }) - const testHeaders = async (file, headers) => { - const queries = await getQueries(file) - expect(queries[0].fields.headers).toStrictEqual(headers) + const testHeaders = async (file, extension, assertions) => { + const queries = await getQueries(file, extension) + for (let [operationId, headers] of Object.entries(assertions)) { + expect(queries[operationId].fields.headers).toStrictEqual(headers) + } + } + + const contentTypeHeader = { + "Content-Type" : "application/json", } it("populates headers", async () => { - await testHeaders("get", {}) - await testHeaders("headers", { "x-bb-header-1" : "123", "x-bb-header-2" : "456"} ) + const assertions = { + "createEntity" : { + ...contentTypeHeader + }, + "getEntities" : { + }, + "getEntity" : { + }, + "updateEntity" : { + ...contentTypeHeader + }, + "patchEntity" : { + ...contentTypeHeader + }, + "deleteEntity" : { + "x-api-key" : "{{x-api-key}}", + } + } + + await runTests("crud", testHeaders, assertions) }) - const testQuery = async (file, queryString) => { - const queries = await getQueries(file) - expect(queries[0].fields.queryString).toBe(queryString) + const testQuery = async (file, extension, assertions) => { + const queries = await getQueries(file, extension) + for (let [operationId, queryString] of Object.entries(assertions)) { + expect(queries[operationId].fields.queryString).toStrictEqual(queryString) + } } + it("populates query", async () => { - await testQuery("get", "") - await testQuery("query", "q1=v1&q1=v2") + const assertions = { + "createEntity" : "", + "getEntities" : "page={{page}}&size={{size}}", + "getEntity" : "", + "updateEntity" : "", + "patchEntity" : "", + "deleteEntity" : "" + } + await runTests("crud", testQuery, assertions) }) - const testBody = async (file, queryString) => { - const queries = await getQueries(file) - expect(queries[0].fields.requestBody).toStrictEqual(queryString) + const testParameters = async (file, extension, assertions) => { + const queries = await getQueries(file, extension) + for (let [operationId, parameters] of Object.entries(assertions)) { + expect(queries[operationId].parameters).toStrictEqual(parameters) + } } + it("populates parameters", async () => { + const assertions = { + "createEntity" : [], + "getEntities" : [ + { + "name" : "page", + "default" : "", + }, + { + "name" : "size", + "default" : "", + } + ], + "getEntity" : [ + { + "name" : "entityId", + "default" : "", + } + ], + "updateEntity" : [ + { + "name" : "entityId", + "default" : "", + } + ], + "patchEntity" : [ + { + "name" : "entityId", + "default" : "", + } + ], + "deleteEntity" : [ + { + "name" : "entityId", + "default" : "", + }, + { + "name" : "x-api-key", + "default" : "", + } + ] + } + await runTests("crud", testParameters, assertions) + }) + + const testBody = async (file, extension, assertions) => { + const queries = await getQueries(file, extension) + for (let [operationId, body] of Object.entries(assertions)) { + expect(queries[operationId].fields.requestBody).toStrictEqual(JSON.stringify(body, null, 2)) + } + } it("populates body", async () => { - await testBody("get", undefined) - await testBody("post", { "key" : "val" }) - await testBody("empty-body", {}) + const assertions = { + "createEntity" : { + "name" : "name", + "type" : "type", + }, + "getEntities" : undefined, + "getEntity" : undefined, + "updateEntity" : { + "id": 1, + "name" : "name", + "type" : "type", + }, + "patchEntity" : { + "id": 1, + "name" : "name", + "type" : "type", + }, + "deleteEntity" : undefined + } + await runTests("crud", testBody, assertions) }) }) }) \ No newline at end of file diff --git a/packages/server/src/api/controllers/query/import/sources/tests/openapi3/openapi3.spec.js b/packages/server/src/api/controllers/query/import/sources/tests/openapi3/openapi3.spec.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/packages/server/src/api/controllers/query/import/tests/index.spec.js b/packages/server/src/api/controllers/query/import/tests/index.spec.js new file mode 100644 index 0000000000..32f3b43b44 --- /dev/null +++ b/packages/server/src/api/controllers/query/import/tests/index.spec.js @@ -0,0 +1,115 @@ + +const bulkDocs = jest.fn() +const db = jest.fn(() => { + return { + bulkDocs + } +}) +jest.mock("../../../../../db", () => db) + +const { RestImporter } = require("../index") + +const fs = require("fs") +const path = require('path') + +const getData = (file) => { + return fs.readFileSync(path.join(__dirname, `../sources/tests/${file}`), "utf8") +} + +// openapi2 (swagger) +const oapi2CrudJson = getData("openapi2/data/crud/crud.json") +const oapi2CrudYaml = getData("openapi2/data/crud/crud.json") +const oapi2PetstoreJson = getData("openapi2/data/petstore/petstore.json") +const oapi2PetstoreYaml = getData("openapi2/data/petstore/petstore.json") + +// curl +const curl = getData("curl/data/post.txt") + +const datasets = { + oapi2CrudJson, + oapi2CrudYaml, + oapi2PetstoreJson, + oapi2PetstoreYaml, + curl +} + +describe("Rest Importer", () => { + let restImporter + + const init = async (data) => { + restImporter = new RestImporter(data) + await restImporter.init() + } + + const runTest = async (test, assertions) => { + for (let [key, data] of Object.entries(datasets)) { + await test(key, data, assertions) + } + } + + const testGetInfo = async (key, data, assertions) => { + await init(data) + const info = await restImporter.getInfo() + expect(info.name).toBe(assertions[key].name) + expect(info.url).toBe(assertions[key].url) + } + + it("gets info", async () => { + const assertions = { + "oapi2CrudJson" : { + name: "CRUD", + url: "http://example.com" + }, + "oapi2CrudYaml" : { + name: "CRUD", + url: "http://example.com" + }, + "oapi2PetstoreJson" : { + name: "Swagger Petstore", + url: "https://petstore.swagger.io/v2" + }, + "oapi2PetstoreYaml" :{ + name: "Swagger Petstore", + url: "https://petstore.swagger.io/v2" + }, + "curl": { + name: "example.com", + url: "http://example.com" + } + } + await runTest(testGetInfo, assertions) + }) + + const testImportQueries = async (key, data, assertions) => { + await init(data) + bulkDocs.mockReturnValue([]) + const importResult = await restImporter.importQueries("appId", "datasourceId") + expect(importResult.errorQueries.length).toBe(0) + expect(importResult.queries.length).toBe(assertions[key].count) + expect(bulkDocs).toHaveBeenCalledTimes(1) + jest.clearAllMocks() + } + + it("imports queries", async () => { + // simple sanity assertions that the whole dataset + // makes it through the importer + const assertions = { + "oapi2CrudJson" : { + count: 6, + }, + "oapi2CrudYaml" :{ + count: 6, + }, + "oapi2PetstoreJson" : { + count: 20, + }, + "oapi2PetstoreYaml" :{ + count: 20, + }, + "curl": { + count: 1 + } + } + await runTest(testImportQueries, assertions) + }) +}) \ No newline at end of file