commit
ad52b06a75
|
@ -6,6 +6,8 @@
|
|||
import { generateID } from "../../utils/helpers"
|
||||
import Icon from "../../Icon/Icon.svelte"
|
||||
import Link from "../../Link/Link.svelte"
|
||||
import Tag from "../../Tags/Tag.svelte"
|
||||
import Tags from "../../Tags/Tags.svelte"
|
||||
|
||||
const BYTES_IN_KB = 1000
|
||||
const BYTES_IN_MB = 1000000
|
||||
|
@ -18,6 +20,8 @@
|
|||
export let handleFileTooLarge = null
|
||||
export let gallery = true
|
||||
export let error = null
|
||||
export let fileTags = []
|
||||
export let maximum = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const imageExtensions = [
|
||||
|
@ -184,103 +188,118 @@
|
|||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
<div
|
||||
class="spectrum-Dropzone"
|
||||
class:is-invalid={!!error}
|
||||
class:disabled
|
||||
role="region"
|
||||
tabindex="0"
|
||||
on:dragover={handleDragOver}
|
||||
on:dragleave={handleDragLeave}
|
||||
on:dragenter={handleDragOver}
|
||||
on:drop={handleDrop}
|
||||
class:is-dragged={fileDragged}
|
||||
>
|
||||
<div class="spectrum-IllustratedMessage spectrum-IllustratedMessage--cta">
|
||||
<input
|
||||
id={fieldId}
|
||||
{disabled}
|
||||
type="file"
|
||||
multiple
|
||||
on:change={handleFile}
|
||||
/>
|
||||
<svg
|
||||
class="spectrum-IllustratedMessage-illustration"
|
||||
width="125"
|
||||
height="60"
|
||||
viewBox="0 0 199 97.7"
|
||||
>
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,
|
||||
.cls-2 {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.cls-1 {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
.cls-2 {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M110.53,85.66,100.26,95.89a1.09,1.09,0,0,1-1.52,0L88.47,85.66"
|
||||
{#if !maximum || (maximum && value?.length < maximum)}
|
||||
<div
|
||||
class="spectrum-Dropzone"
|
||||
class:is-invalid={!!error}
|
||||
class:disabled
|
||||
role="region"
|
||||
tabindex="0"
|
||||
on:dragover={handleDragOver}
|
||||
on:dragleave={handleDragLeave}
|
||||
on:dragenter={handleDragOver}
|
||||
on:drop={handleDrop}
|
||||
class:is-dragged={fileDragged}
|
||||
>
|
||||
<div class="spectrum-IllustratedMessage spectrum-IllustratedMessage--cta">
|
||||
<input
|
||||
id={fieldId}
|
||||
{disabled}
|
||||
type="file"
|
||||
multiple
|
||||
on:change={handleFile}
|
||||
/>
|
||||
<line class="cls-1" x1="99.5" y1="95.5" x2="99.5" y2="58.5" />
|
||||
<path class="cls-1" d="M105.5,73.5h19a2,2,0,0,0,2-2v-43" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M126.5,22.5h-19a2,2,0,0,1-2-2V1.5h-31a2,2,0,0,0-2,2v68a2,2,0,0,0,2,2h19"
|
||||
/>
|
||||
<line class="cls-1" x1="105.5" y1="1.5" x2="126.5" y2="22.5" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M47.93,50.49a5,5,0,1,0-4.83-5A4.93,4.93,0,0,0,47.93,50.49Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M36.6,65.93,42.05,60A2.06,2.06,0,0,1,45,60l12.68,13.2"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M3.14,73.23,22.42,53.76a1.65,1.65,0,0,1,2.38,0l19.05,19.7"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M139.5,36.5H196A1.49,1.49,0,0,1,197.5,38V72A1.49,1.49,0,0,1,196,73.5H141A1.49,1.49,0,0,1,139.5,72V32A1.49,1.49,0,0,1,141,30.5H154a2.43,2.43,0,0,1,1.67.66l6,5.66"
|
||||
/>
|
||||
<rect
|
||||
class="cls-1"
|
||||
x="1.5"
|
||||
y="34.5"
|
||||
width="58"
|
||||
height="39"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
</svg>
|
||||
<h2
|
||||
class="spectrum-Heading spectrum-Heading--sizeL spectrum-Heading--light spectrum-IllustratedMessage-heading"
|
||||
>
|
||||
Drag and drop your file
|
||||
</h2>
|
||||
{#if !disabled}
|
||||
<p
|
||||
class="spectrum-Body spectrum-Body--sizeS spectrum-IllustratedMessage-description"
|
||||
<svg
|
||||
class="spectrum-IllustratedMessage-illustration"
|
||||
width="125"
|
||||
height="60"
|
||||
viewBox="0 0 199 97.7"
|
||||
>
|
||||
<label for={fieldId} class="spectrum-Link">
|
||||
Select a file to upload
|
||||
</label>
|
||||
<br />
|
||||
from your computer
|
||||
</p>
|
||||
{/if}
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,
|
||||
.cls-2 {
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
.cls-1 {
|
||||
stroke-width: 3px;
|
||||
}
|
||||
.cls-2 {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M110.53,85.66,100.26,95.89a1.09,1.09,0,0,1-1.52,0L88.47,85.66"
|
||||
/>
|
||||
<line class="cls-1" x1="99.5" y1="95.5" x2="99.5" y2="58.5" />
|
||||
<path class="cls-1" d="M105.5,73.5h19a2,2,0,0,0,2-2v-43" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M126.5,22.5h-19a2,2,0,0,1-2-2V1.5h-31a2,2,0,0,0-2,2v68a2,2,0,0,0,2,2h19"
|
||||
/>
|
||||
<line class="cls-1" x1="105.5" y1="1.5" x2="126.5" y2="22.5" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M47.93,50.49a5,5,0,1,0-4.83-5A4.93,4.93,0,0,0,47.93,50.49Z"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M36.6,65.93,42.05,60A2.06,2.06,0,0,1,45,60l12.68,13.2"
|
||||
/>
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M3.14,73.23,22.42,53.76a1.65,1.65,0,0,1,2.38,0l19.05,19.7"
|
||||
/>
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M139.5,36.5H196A1.49,1.49,0,0,1,197.5,38V72A1.49,1.49,0,0,1,196,73.5H141A1.49,1.49,0,0,1,139.5,72V32A1.49,1.49,0,0,1,141,30.5H154a2.43,2.43,0,0,1,1.67.66l6,5.66"
|
||||
/>
|
||||
<rect
|
||||
class="cls-1"
|
||||
x="1.5"
|
||||
y="34.5"
|
||||
width="58"
|
||||
height="39"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
</svg>
|
||||
<h2
|
||||
class="spectrum-Heading spectrum-Heading--sizeL spectrum-Heading--light spectrum-IllustratedMessage-heading"
|
||||
>
|
||||
Drag and drop your file
|
||||
</h2>
|
||||
{#if !disabled}
|
||||
<p
|
||||
class="spectrum-Body spectrum-Body--sizeS spectrum-IllustratedMessage-description"
|
||||
>
|
||||
<label for={fieldId} class="spectrum-Link">
|
||||
Select a file to upload
|
||||
</label>
|
||||
<br />
|
||||
from your computer
|
||||
</p>
|
||||
{#if fileTags.length}
|
||||
<Tags>
|
||||
<div class="tags">
|
||||
{#each fileTags as tag}
|
||||
<div class="tag">
|
||||
<Tag>
|
||||
{tag}
|
||||
</Tag>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Tags>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
@ -390,4 +409,15 @@
|
|||
.disabled .spectrum-Heading--sizeL {
|
||||
color: var(--spectrum-alias-text-color-disabled);
|
||||
}
|
||||
|
||||
.tags {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tag {
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
export let processFiles = undefined
|
||||
export let handleFileTooLarge = undefined
|
||||
export let gallery = true
|
||||
export let fileTags = []
|
||||
export let maximum = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -29,6 +31,8 @@
|
|||
{processFiles}
|
||||
{handleFileTooLarge}
|
||||
{gallery}
|
||||
{fileTags}
|
||||
{maximum}
|
||||
on:change={onChange}
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -18,10 +18,22 @@
|
|||
export let disabled = false
|
||||
export let showDivider = true
|
||||
|
||||
export let showSecondaryButton = false
|
||||
export let secondaryButtonText = undefined
|
||||
export let secondaryAction = undefined
|
||||
|
||||
const { hide, cancel } = getContext(Context.Modal)
|
||||
let loading = false
|
||||
$: confirmDisabled = disabled || loading
|
||||
|
||||
async function secondary() {
|
||||
loading = true
|
||||
if (!secondaryAction || (await secondaryAction()) !== false) {
|
||||
hide()
|
||||
}
|
||||
loading = false
|
||||
}
|
||||
|
||||
async function confirm() {
|
||||
loading = true
|
||||
if (!onConfirm || (await onConfirm()) !== false) {
|
||||
|
@ -73,6 +85,15 @@
|
|||
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
|
||||
>
|
||||
<slot name="footer" />
|
||||
|
||||
{#if showSecondaryButton && secondaryButtonText && secondaryAction}
|
||||
<div class="secondary-action">
|
||||
<Button group secondary on:click={secondary}
|
||||
>{secondaryButtonText}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showCancelButton}
|
||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||
{/if}
|
||||
|
@ -136,4 +157,8 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.secondary-action {
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,6 +9,9 @@ export const Events = {
|
|||
CREATED: "Datasource Created",
|
||||
UPDATED: "Datasource Updated",
|
||||
},
|
||||
QUERIES: {
|
||||
REST: "REST Queries Imported",
|
||||
},
|
||||
TABLE: {
|
||||
CREATED: "Table Created",
|
||||
},
|
||||
|
|
|
@ -6,12 +6,18 @@
|
|||
import { IntegrationNames } from "constants"
|
||||
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
|
||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||
|
||||
export let modal
|
||||
let integrations = []
|
||||
let integration = {}
|
||||
let internalTableModal
|
||||
let externalDatasourceModal
|
||||
let importModal
|
||||
|
||||
$: showImportButton = false
|
||||
|
||||
checkShowImport()
|
||||
|
||||
const INTERNAL = "BUDIBASE"
|
||||
|
||||
|
@ -33,6 +39,15 @@
|
|||
config,
|
||||
schema: selected.datasource,
|
||||
}
|
||||
checkShowImport()
|
||||
}
|
||||
|
||||
function checkShowImport() {
|
||||
showImportButton = integration.type === "REST"
|
||||
}
|
||||
|
||||
function showImportModal() {
|
||||
importModal.show()
|
||||
}
|
||||
|
||||
function chooseNextModal() {
|
||||
|
@ -63,11 +78,24 @@
|
|||
<DatasourceConfigModal {integration} {modal} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={importModal}>
|
||||
{#if integration.type === "REST"}
|
||||
<ImportRestQueriesModal
|
||||
navigateDatasource={true}
|
||||
createDatasource={true}
|
||||
onCancel={() => modal.show()}
|
||||
/>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent
|
||||
disabled={!Object.keys(integration).length}
|
||||
title="Data"
|
||||
confirmText="Continue"
|
||||
showSecondaryButton={showImportButton}
|
||||
secondaryButtonText="Import"
|
||||
secondaryAction={() => showImportModal()}
|
||||
showCancelButton={false}
|
||||
size="M"
|
||||
onConfirm={() => {
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import {
|
||||
ModalContent,
|
||||
notifications,
|
||||
Body,
|
||||
Layout,
|
||||
Tabs,
|
||||
Tab,
|
||||
Input,
|
||||
Heading,
|
||||
TextArea,
|
||||
Dropzone,
|
||||
} from "@budibase/bbui"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { datasources, queries } from "stores/backend"
|
||||
import { writable } from "svelte/store"
|
||||
|
||||
export let navigateDatasource = false
|
||||
export let datasourceId
|
||||
export let createDatasource = false
|
||||
export let onCancel
|
||||
|
||||
const data = writable({
|
||||
url: "",
|
||||
raw: "",
|
||||
file: undefined,
|
||||
})
|
||||
|
||||
let lastTouched = "url"
|
||||
|
||||
const getData = async () => {
|
||||
let dataString
|
||||
|
||||
// parse the file into memory and send as string
|
||||
if (lastTouched === "file") {
|
||||
dataString = await $data.file.text()
|
||||
} else if (lastTouched === "url") {
|
||||
const response = await fetch($data.url)
|
||||
dataString = await response.text()
|
||||
} else if (lastTouched === "raw") {
|
||||
dataString = $data.raw
|
||||
}
|
||||
|
||||
return dataString
|
||||
}
|
||||
|
||||
async function importQueries() {
|
||||
try {
|
||||
const dataString = await getData()
|
||||
|
||||
if (!datasourceId && !createDatasource) {
|
||||
throw new Error("No datasource id")
|
||||
}
|
||||
|
||||
const body = {
|
||||
data: dataString,
|
||||
datasourceId,
|
||||
}
|
||||
|
||||
const importResult = await queries.import(body)
|
||||
if (!datasourceId) {
|
||||
datasourceId = importResult.datasourceId
|
||||
}
|
||||
|
||||
// reload
|
||||
await datasources.fetch()
|
||||
await queries.fetch()
|
||||
await datasources.select(datasourceId)
|
||||
|
||||
if (navigateDatasource) {
|
||||
$goto(`./datasource/${datasourceId}`)
|
||||
}
|
||||
|
||||
notifications.success(`Imported successfully.`)
|
||||
analytics.captureEvent(Events.QUERIES.REST.IMPORTED, {
|
||||
importType: lastTouched,
|
||||
newDatasource: createDatasource,
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
notifications.error(`Error importing: ${err}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
onConfirm={() => importQueries()}
|
||||
{onCancel}
|
||||
confirmText={"Import"}
|
||||
cancelText="Back"
|
||||
size="L"
|
||||
>
|
||||
<Layout noPadding>
|
||||
<Heading size="S">Import</Heading>
|
||||
<Body size="XS"
|
||||
>Import your rest collection using one of the options below</Body
|
||||
>
|
||||
<Tabs selected="Link">
|
||||
<Tab title="Link">
|
||||
<Input
|
||||
bind:value={$data.url}
|
||||
on:change={() => (lastTouched = "url")}
|
||||
label="Enter a URL"
|
||||
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="File">
|
||||
<Dropzone
|
||||
gallery={false}
|
||||
value={$data.file ? [$data.file] : []}
|
||||
on:change={e => {
|
||||
$data.file = e.detail?.[0]
|
||||
lastTouched = "file"
|
||||
}}
|
||||
fileTags={["OpenAPI 2.0", "Swagger 2.0", "cURL", "YAML", "JSON"]}
|
||||
maximum={1}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab title="Raw Text">
|
||||
<TextArea
|
||||
bind:value={$data.raw}
|
||||
on:change={() => (lastTouched = "raw")}
|
||||
label={"Paste raw text"}
|
||||
placeholder={'e.g. curl --location --request GET "https://example.com"'}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
</style>
|
|
@ -17,7 +17,9 @@
|
|||
import CreateExternalTableModal from "./modals/CreateExternalTableModal.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { capitalise } from "helpers"
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
|
||||
let importQueriesModal
|
||||
let relationshipModal
|
||||
let createExternalTableModal
|
||||
let selectedFromRelationship, selectedToRelationship
|
||||
|
@ -131,6 +133,15 @@
|
|||
/>
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={importQueriesModal}>
|
||||
{#if datasource.source === "REST"}
|
||||
<ImportRestQueriesModal
|
||||
createDatasource={false}
|
||||
datasourceId={datasource._id}
|
||||
/>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={createExternalTableModal}>
|
||||
<CreateExternalTableModal {datasource} />
|
||||
</Modal>
|
||||
|
@ -246,7 +257,14 @@
|
|||
<Divider />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Queries</Heading>
|
||||
<Button secondary on:click={() => $goto("./new")}>Add Query</Button>
|
||||
<div class="query-buttons">
|
||||
{#if datasource.source === "REST"}
|
||||
<Button secondary on:click={() => importQueriesModal.show()}
|
||||
>Import</Button
|
||||
>
|
||||
{/if}
|
||||
<Button secondary on:click={() => $goto("./new")}>Add Query</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="query-list">
|
||||
{#each $queries.list.filter(query => query.datasourceId === datasource._id) as query}
|
||||
|
@ -293,6 +311,11 @@
|
|||
margin: 0 0 var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.query-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.query-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -60,6 +60,15 @@ export function createQueriesStore() {
|
|||
})
|
||||
return json
|
||||
},
|
||||
import: async body => {
|
||||
const response = await api.post(`/api/queries/import`, body)
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(response.message)
|
||||
}
|
||||
|
||||
return response.json()
|
||||
},
|
||||
select: query => {
|
||||
update(state => ({ ...state, selected: query._id }))
|
||||
views.unselect()
|
||||
|
|
|
@ -69,6 +69,7 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/auth": "^1.0.8-alpha.1",
|
||||
"@budibase/client": "^1.0.8-alpha.1",
|
||||
"@budibase/string-templates": "^1.0.8-alpha.1",
|
||||
|
@ -85,12 +86,14 @@
|
|||
"bull": "^3.22.4",
|
||||
"chmodr": "1.2.0",
|
||||
"csvtojson": "2.0.10",
|
||||
"curlconverter": "^3.21.0",
|
||||
"dotenv": "8.2.0",
|
||||
"download": "8.0.0",
|
||||
"fix-path": "3.0.0",
|
||||
"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",
|
||||
|
@ -146,6 +149,7 @@
|
|||
"eslint": "^6.8.0",
|
||||
"jest": "^27.0.5",
|
||||
"nodemon": "^2.0.4",
|
||||
"openapi-types": "^9.3.1",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"prettier": "^2.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
import CouchDB from "../../../../db"
|
||||
import { queryValidation } from "../validation"
|
||||
import { generateQueryID } from "../../../../db/utils"
|
||||
import { ImportInfo, ImportSource } from "./sources/base"
|
||||
import { OpenAPI2 } from "./sources/openapi2"
|
||||
import { Query } from './../../../../definitions/common';
|
||||
import { Curl } from "./sources/curl"
|
||||
interface ImportResult {
|
||||
errorQueries: Query[]
|
||||
queries: Query[]
|
||||
}
|
||||
|
||||
export class RestImporter {
|
||||
data: string
|
||||
sources: ImportSource[]
|
||||
source!: ImportSource
|
||||
|
||||
constructor(data: string) {
|
||||
this.data = data
|
||||
this.sources = [new OpenAPI2(), new Curl()]
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
for (let source of this.sources) {
|
||||
if (await source.isSupported(this.data)) {
|
||||
this.source = source
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getInfo = async (): Promise<ImportInfo> => {
|
||||
return this.source.getInfo()
|
||||
}
|
||||
|
||||
importQueries = async (
|
||||
appId: string,
|
||||
datasourceId: string
|
||||
): Promise<ImportResult> => {
|
||||
// constuct the queries
|
||||
let queries = await this.source.getQueries(datasourceId)
|
||||
|
||||
// validate queries
|
||||
const errorQueries: Query[] = []
|
||||
const schema = queryValidation()
|
||||
queries = queries
|
||||
.filter(query => {
|
||||
const validation = schema.validate(query)
|
||||
if (validation.error) {
|
||||
errorQueries.push(query)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.map(query => {
|
||||
query._id = generateQueryID(query.datasourceId)
|
||||
return query
|
||||
})
|
||||
|
||||
// persist queries
|
||||
const db = new CouchDB(appId)
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
|
||||
import { Query, QueryParameter } from "../../../../../../definitions/common"
|
||||
|
||||
export interface ImportInfo {
|
||||
url: string
|
||||
name: string
|
||||
}
|
||||
|
||||
enum MethodToVerb {
|
||||
get = "read",
|
||||
post = "create",
|
||||
put = "update",
|
||||
patch = "patch",
|
||||
delete = "delete",
|
||||
}
|
||||
|
||||
export abstract class ImportSource {
|
||||
abstract isSupported(data: string): Promise<boolean>
|
||||
abstract getInfo(): Promise<ImportInfo>
|
||||
abstract getQueries(datasourceId: string): Promise<Query[]>
|
||||
|
||||
constructQuery = (
|
||||
datasourceId: string,
|
||||
name: string,
|
||||
method: string,
|
||||
path: string,
|
||||
queryString: string,
|
||||
headers: object = {},
|
||||
parameters: QueryParameter[] = [],
|
||||
body: object | undefined = undefined
|
||||
): Query => {
|
||||
const readable = true
|
||||
const queryVerb = this.verbFromMethod(method)
|
||||
const transformer = "return data"
|
||||
const schema = {}
|
||||
path = this.processPath(path)
|
||||
queryString = this.processQuery(queryString)
|
||||
const requestBody = JSON.stringify(body, null, 2)
|
||||
|
||||
const query: Query = {
|
||||
datasourceId,
|
||||
name,
|
||||
parameters,
|
||||
fields: {
|
||||
headers,
|
||||
queryString,
|
||||
path,
|
||||
requestBody,
|
||||
},
|
||||
transformer,
|
||||
schema,
|
||||
readable,
|
||||
queryVerb,
|
||||
}
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
verbFromMethod = (method: string) => {
|
||||
const verb = (<any>MethodToVerb)[method]
|
||||
if (!verb) {
|
||||
throw new Error(`Unsupported method: ${method}`)
|
||||
}
|
||||
return verb
|
||||
}
|
||||
|
||||
processPath = (path: string): string => {
|
||||
if (path?.startsWith("/")) {
|
||||
path = path.substring(1)
|
||||
}
|
||||
|
||||
// add extra braces around params for binding
|
||||
path = path.replace(/[{]/g, "{{")
|
||||
path = path.replace(/[}]/g, "}}")
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
processQuery = (queryString: string): string => {
|
||||
if (queryString?.startsWith("?")) {
|
||||
return queryString.substring(1)
|
||||
}
|
||||
|
||||
return queryString
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
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<OpenAPI.Document> => {
|
||||
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, {})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import { ImportSource, ImportInfo } from "./base"
|
||||
import { Query } from "../../../../../definitions/common"
|
||||
import { URL } from "url"
|
||||
const curlconverter = require("curlconverter")
|
||||
|
||||
const parseCurl = (data: string): any => {
|
||||
const curlJson = curlconverter.toJsonString(data)
|
||||
return JSON.parse(curlJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* The curl converter parses the request body into the key field of an object
|
||||
* e.g. --d '{"key":"val"}' produces an object { "{"key":"val"}" : "" }
|
||||
* This is not what we want, so we need to parse out the key from the object
|
||||
*/
|
||||
const parseBody = (curl: any) => {
|
||||
if (curl.data) {
|
||||
const keys = Object.keys(curl.data)
|
||||
if (keys.length) {
|
||||
const key = keys[0]
|
||||
try {
|
||||
return JSON.parse(key)
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const parseCookie = (curl: any) => {
|
||||
if (curl.cookies){
|
||||
return Object.entries(curl.cookies).reduce((acc, entry) => {
|
||||
const [key, value] = entry
|
||||
return acc + `${key}=${value}; `
|
||||
}, "")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Curl
|
||||
* https://curl.se/docs/manpage.html
|
||||
*/
|
||||
export class Curl extends ImportSource {
|
||||
curl: any
|
||||
|
||||
isSupported = async (data: string): Promise<boolean> => {
|
||||
try {
|
||||
const curl = parseCurl(data)
|
||||
this.curl = curl
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
getInfo = async (): Promise<ImportInfo> => {
|
||||
const url = new URL(this.curl.url)
|
||||
return {
|
||||
url: url.origin,
|
||||
name: url.hostname,
|
||||
}
|
||||
}
|
||||
|
||||
getQueries = async (datasourceId: string): Promise<Query[]> => {
|
||||
const url = new URL(this.curl.raw_url)
|
||||
const name = url.pathname
|
||||
const path = url.pathname
|
||||
const method = this.curl.method
|
||||
const queryString = url.search
|
||||
const headers = this.curl.headers
|
||||
const requestBody = parseBody(this.curl)
|
||||
|
||||
const cookieHeader = parseCookie(this.curl)
|
||||
if (cookieHeader) {
|
||||
headers["Cookie"] = cookieHeader
|
||||
}
|
||||
|
||||
const query = this.constructQuery(
|
||||
datasourceId,
|
||||
name,
|
||||
method,
|
||||
path,
|
||||
queryString,
|
||||
headers,
|
||||
[],
|
||||
requestBody
|
||||
)
|
||||
|
||||
return [query]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
import { ImportInfo } from "./base"
|
||||
import { Query, QueryParameter } from "../../../../../definitions/common"
|
||||
import { OpenAPIV2 } from "openapi-types"
|
||||
import { OpenAPISource } from "./base/openapi"
|
||||
|
||||
const parameterNotRef = (
|
||||
param: OpenAPIV2.Parameter | OpenAPIV2.ReferenceObject
|
||||
): param is OpenAPIV2.Parameter => {
|
||||
// all refs are deferenced by parser library
|
||||
return true
|
||||
}
|
||||
|
||||
const isOpenAPI2 = (document: any): document is OpenAPIV2.Document => {
|
||||
if (document.swagger === "2.0") {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
export class OpenAPI2 extends OpenAPISource {
|
||||
document!: OpenAPIV2.Document
|
||||
|
||||
isSupported = async (data: string): Promise<boolean> => {
|
||||
try {
|
||||
const document: any = await this.parseData(data)
|
||||
if (isOpenAPI2(document)) {
|
||||
this.document = document
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getInfo = async (): Promise<ImportInfo> => {
|
||||
const scheme = this.document.schemes?.includes("https") ? "https" : "http"
|
||||
const basePath = this.document.basePath || ""
|
||||
const host = this.document.host || "<host>"
|
||||
const url = `${scheme}://${host}${basePath}`
|
||||
const name = this.document.info.title || "Swagger Import"
|
||||
|
||||
return {
|
||||
url: url,
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
getQueries = async (datasourceId: string): Promise<Query[]> => {
|
||||
const queries = []
|
||||
|
||||
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
|
||||
|
||||
const methodName = key
|
||||
const name = operation.operationId || path
|
||||
let queryString = ""
|
||||
const headers: any = {}
|
||||
let requestBody = undefined
|
||||
const parameters: QueryParameter[] = []
|
||||
|
||||
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 || "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = this.constructQuery(
|
||||
datasourceId,
|
||||
name,
|
||||
methodName,
|
||||
path,
|
||||
queryString,
|
||||
headers,
|
||||
parameters,
|
||||
requestBody
|
||||
)
|
||||
queries.push(query)
|
||||
}
|
||||
}
|
||||
|
||||
return queries
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
const { Curl } = require("../../curl")
|
||||
const fs = require("fs")
|
||||
const path = require('path')
|
||||
|
||||
const getData = (file) => {
|
||||
return fs.readFileSync(path.join(__dirname, `./data/${file}.txt`), "utf8")
|
||||
}
|
||||
|
||||
describe("Curl Import", () => {
|
||||
let curl
|
||||
|
||||
beforeEach(() => {
|
||||
curl = new Curl()
|
||||
})
|
||||
|
||||
it("validates unsupported data", async () => {
|
||||
let data
|
||||
let supported
|
||||
|
||||
// JSON
|
||||
data = "{}"
|
||||
supported = await curl.isSupported(data)
|
||||
expect(supported).toBe(false)
|
||||
|
||||
// Empty
|
||||
data = ""
|
||||
supported = await curl.isSupported(data)
|
||||
expect(supported).toBe(false)
|
||||
})
|
||||
|
||||
const init = async (file) => {
|
||||
await curl.isSupported(getData(file))
|
||||
}
|
||||
|
||||
it("returns import info", async () => {
|
||||
await init("get")
|
||||
const info = await curl.getInfo()
|
||||
expect(info.url).toBe("http://example.com")
|
||||
expect(info.name).toBe("example.com")
|
||||
})
|
||||
|
||||
describe("Returns queries", () => {
|
||||
|
||||
const getQueries = async (file) => {
|
||||
await init(file)
|
||||
const queries = await curl.getQueries()
|
||||
expect(queries.length).toBe(1)
|
||||
return queries
|
||||
}
|
||||
|
||||
const testVerb = async (file, verb) => {
|
||||
const queries = await getQueries(file)
|
||||
expect(queries[0].queryVerb).toBe(verb)
|
||||
}
|
||||
|
||||
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 testPath = async (file, urlPath) => {
|
||||
const queries = await getQueries(file)
|
||||
expect(queries[0].fields.path).toBe(urlPath)
|
||||
}
|
||||
|
||||
it("populates path", async () => {
|
||||
await testPath("get", "")
|
||||
await testPath("path", "paths/abc")
|
||||
})
|
||||
|
||||
const testHeaders = async (file, headers) => {
|
||||
const queries = await getQueries(file)
|
||||
expect(queries[0].fields.headers).toStrictEqual(headers)
|
||||
}
|
||||
|
||||
it("populates headers", async () => {
|
||||
await testHeaders("get", {})
|
||||
await testHeaders("headers", { "x-bb-header-1" : "123", "x-bb-header-2" : "456"} )
|
||||
})
|
||||
|
||||
const testQuery = async (file, queryString) => {
|
||||
const queries = await getQueries(file)
|
||||
expect(queries[0].fields.queryString).toBe(queryString)
|
||||
}
|
||||
it("populates query", async () => {
|
||||
await testQuery("get", "")
|
||||
await testQuery("query", "q1=v1&q1=v2")
|
||||
})
|
||||
|
||||
const testBody = async (file, body) => {
|
||||
const queries = await getQueries(file)
|
||||
expect(queries[0].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", {})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1 @@
|
|||
curl -X DELETE 'http://example.com'
|
|
@ -0,0 +1,2 @@
|
|||
curl -X POST 'http://example.com' \
|
||||
--data-raw '{}'
|
|
@ -0,0 +1 @@
|
|||
curl 'http://example.com'
|
|
@ -0,0 +1,3 @@
|
|||
curl 'http://example.com' \
|
||||
-H 'x-bb-header-1: 123' \
|
||||
-H 'x-bb-header-2: 456'
|
|
@ -0,0 +1,2 @@
|
|||
curl -X PATCH 'http://example.com/paths/abc' \
|
||||
--data-raw '{ "key" : "val" }'
|
|
@ -0,0 +1 @@
|
|||
curl 'http://example.com/paths/abc'
|
|
@ -0,0 +1,2 @@
|
|||
curl -X POST 'http://example.com' \
|
||||
--data-raw '{ "key" : "val" }'
|
|
@ -0,0 +1,2 @@
|
|||
curl -X PUT 'http://example.com/paths/abc' \
|
||||
--data-raw '{ "key" : "val" }'
|
|
@ -0,0 +1 @@
|
|||
curl 'http://example.com/paths/abc?q1=v1&q1=v2'
|
|
@ -0,0 +1,253 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,711 @@
|
|||
swagger: "2.0"
|
||||
info:
|
||||
description: "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters."
|
||||
version: 1.0.5
|
||||
title: Swagger Petstore
|
||||
termsOfService: http://swagger.io/terms/
|
||||
contact:
|
||||
email: apiteam@swagger.io
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
host: petstore.swagger.io
|
||||
basePath: /v2
|
||||
tags:
|
||||
- name: pet
|
||||
description: Everything about your Pets
|
||||
externalDocs:
|
||||
description: Find out more
|
||||
url: http://swagger.io
|
||||
- name: store
|
||||
description: Access to Petstore orders
|
||||
- name: user
|
||||
description: Operations about user
|
||||
externalDocs:
|
||||
description: Find out more about our store
|
||||
url: http://swagger.io
|
||||
schemes:
|
||||
- https
|
||||
- http
|
||||
paths:
|
||||
/pet/{petId}/uploadImage:
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: uploads an image
|
||||
description: ""
|
||||
operationId: uploadFile
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
produces:
|
||||
- application/json
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
description: ID of pet to update
|
||||
required: true
|
||||
type: integer
|
||||
format: int64
|
||||
- name: additionalMetadata
|
||||
in: formData
|
||||
description: Additional data to pass to server
|
||||
required: false
|
||||
type: string
|
||||
- name: file
|
||||
in: formData
|
||||
description: file to upload
|
||||
required: false
|
||||
type: file
|
||||
responses:
|
||||
"200":
|
||||
description: successful operation
|
||||
schema:
|
||||
$ref: "#/definitions/ApiResponse"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
/pet:
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: Add a new pet to the store
|
||||
description: ""
|
||||
operationId: addPet
|
||||
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:
|
||||
"405":
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
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
|
||||
summary: Get user by user name
|
||||
description: ""
|
||||
operationId: getUserByName
|
||||
produces:
|
||||
- application/json
|
||||
- application/xml
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: "The name that needs to be fetched. Use user1 for testing. "
|
||||
required: true
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: successful operation
|
||||
schema:
|
||||
$ref: "#/definitions/User"
|
||||
"400":
|
||||
description: Invalid username supplied
|
||||
"404":
|
||||
description: User not found
|
||||
put:
|
||||
tags:
|
||||
- user
|
||||
summary: Updated user
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: updateUser
|
||||
consumes:
|
||||
- application/json
|
||||
produces:
|
||||
- application/json
|
||||
- application/xml
|
||||
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
|
||||
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
|
||||
"400":
|
||||
description: Invalid username/password supplied
|
||||
/user/logout:
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
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
|
||||
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"
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
securityDefinitions:
|
||||
api_key:
|
||||
type: apiKey
|
||||
name: api_key
|
||||
in: header
|
||||
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
|
||||
definitions:
|
||||
ApiResponse:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
type:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
Category:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
xml:
|
||||
name: Category
|
||||
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
|
||||
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
|
||||
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
|
||||
externalDocs:
|
||||
description: Find out more about Swagger
|
||||
url: http://swagger.io
|
|
@ -0,0 +1,239 @@
|
|||
const { OpenAPI2 } = require("../../openapi2")
|
||||
const fs = require("fs")
|
||||
const path = require('path')
|
||||
|
||||
const getData = (file, extension) => {
|
||||
return fs.readFileSync(path.join(__dirname, `./data/${file}/${file}.${extension}`), "utf8")
|
||||
}
|
||||
|
||||
describe("OpenAPI2 Import", () => {
|
||||
let openapi2
|
||||
|
||||
beforeEach(() => {
|
||||
openapi2 = new OpenAPI2()
|
||||
})
|
||||
|
||||
it("validates unsupported data", async () => {
|
||||
let data
|
||||
let supported
|
||||
|
||||
// non json / yaml
|
||||
data = "curl http://example.com"
|
||||
supported = await openapi2.isSupported(data)
|
||||
expect(supported).toBe(false)
|
||||
|
||||
// Empty
|
||||
data = ""
|
||||
supported = await openapi2.isSupported(data)
|
||||
expect(supported).toBe(false)
|
||||
})
|
||||
|
||||
const init = async (file, extension) => {
|
||||
await openapi2.isSupported(getData(file, extension))
|
||||
}
|
||||
|
||||
const runTests = async (filename, test, assertions) => {
|
||||
for (let extension of ["json", "yaml"]) {
|
||||
await test(filename, extension, assertions)
|
||||
}
|
||||
}
|
||||
|
||||
const testImportInfo = async (file, extension) => {
|
||||
await init(file, extension)
|
||||
const info = await openapi2.getInfo()
|
||||
expect(info.url).toBe("https://petstore.swagger.io/v2")
|
||||
expect(info.name).toBe("Swagger Petstore")
|
||||
}
|
||||
|
||||
it("returns import info", async () => {
|
||||
await runTests("petstore", testImportInfo)
|
||||
})
|
||||
|
||||
describe("Returns queries", () => {
|
||||
const indexQueries = (queries) => {
|
||||
return queries.reduce((acc, query) => {
|
||||
acc[query.name] = query
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const assertions = {
|
||||
"createEntity" : "create",
|
||||
"getEntities" : "read",
|
||||
"getEntity" : "read",
|
||||
"updateEntity" : "update",
|
||||
"patchEntity" : "patch",
|
||||
"deleteEntity" : "delete"
|
||||
}
|
||||
await runTests("crud", testVerb, assertions)
|
||||
})
|
||||
|
||||
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 () => {
|
||||
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, 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 () => {
|
||||
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, 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 () => {
|
||||
const assertions = {
|
||||
"createEntity" : "",
|
||||
"getEntities" : "page={{page}}&size={{size}}",
|
||||
"getEntity" : "",
|
||||
"updateEntity" : "",
|
||||
"patchEntity" : "",
|
||||
"deleteEntity" : ""
|
||||
}
|
||||
await runTests("crud", testQuery, assertions)
|
||||
})
|
||||
|
||||
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 () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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)
|
||||
})
|
||||
})
|
|
@ -1,12 +1,14 @@
|
|||
const { processString } = require("@budibase/string-templates")
|
||||
const CouchDB = require("../../db")
|
||||
const CouchDB = require("../../../db")
|
||||
const {
|
||||
generateQueryID,
|
||||
getQueryParams,
|
||||
isProdAppID,
|
||||
} = require("../../db/utils")
|
||||
const { BaseQueryVerbs } = require("../../constants")
|
||||
const { Thread, ThreadType } = require("../../threads")
|
||||
} = require("../../../db/utils")
|
||||
const { BaseQueryVerbs } = require("../../../constants")
|
||||
const { Thread, ThreadType } = require("../../../threads")
|
||||
const { save: saveDatasource } = require("../datasource")
|
||||
const { RestImporter } = require("./import")
|
||||
|
||||
const Runner = new Thread(ThreadType.QUERY, { timeoutMs: 10000 })
|
||||
|
||||
|
@ -30,9 +32,49 @@ exports.fetch = async function (ctx) {
|
|||
include_docs: true,
|
||||
})
|
||||
)
|
||||
|
||||
ctx.body = enrichQueries(body.rows.map(row => row.doc))
|
||||
}
|
||||
|
||||
exports.import = async ctx => {
|
||||
const body = ctx.request.body
|
||||
const data = body.data
|
||||
|
||||
const importer = new RestImporter(data)
|
||||
await importer.init()
|
||||
|
||||
let datasourceId
|
||||
if (!body.datasourceId) {
|
||||
// construct new datasource
|
||||
const info = await importer.getInfo()
|
||||
let datasource = {
|
||||
type: "datasource",
|
||||
source: "REST",
|
||||
config: {
|
||||
url: info.url,
|
||||
defaultHeaders: [],
|
||||
},
|
||||
name: info.name,
|
||||
}
|
||||
// save the datasource
|
||||
const datasourceCtx = { ...ctx }
|
||||
datasourceCtx.request.body.datasource = datasource
|
||||
await saveDatasource(datasourceCtx)
|
||||
datasourceId = datasourceCtx.body.datasource._id
|
||||
} else {
|
||||
// use existing datasource
|
||||
datasourceId = body.datasourceId
|
||||
}
|
||||
|
||||
const importResult = await importer.importQueries(ctx.appId, datasourceId)
|
||||
|
||||
ctx.body = {
|
||||
...importResult,
|
||||
datasourceId,
|
||||
}
|
||||
ctx.status = 200
|
||||
}
|
||||
|
||||
exports.save = async function (ctx) {
|
||||
const db = new CouchDB(ctx.appId)
|
||||
const query = ctx.request.body
|
|
@ -0,0 +1,40 @@
|
|||
const joiValidator = require("../../../middleware/joi-validator")
|
||||
const Joi = require("joi")
|
||||
|
||||
exports.queryValidation = () => {
|
||||
return Joi.object({
|
||||
_id: Joi.string(),
|
||||
_rev: Joi.string(),
|
||||
name: Joi.string().required(),
|
||||
fields: Joi.object().required(),
|
||||
datasourceId: Joi.string().required(),
|
||||
readable: Joi.boolean(),
|
||||
parameters: Joi.array().items(
|
||||
Joi.object({
|
||||
name: Joi.string(),
|
||||
default: Joi.string().allow(""),
|
||||
})
|
||||
),
|
||||
queryVerb: Joi.string().allow().required(),
|
||||
extra: Joi.object().optional(),
|
||||
schema: Joi.object({}).required().unknown(true),
|
||||
transformer: Joi.string().optional(),
|
||||
})
|
||||
}
|
||||
|
||||
exports.generateQueryValidation = () => {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(exports.queryValidation())
|
||||
}
|
||||
|
||||
exports.generateQueryPreviewValidation = () => {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
fields: Joi.object().required(),
|
||||
queryVerb: Joi.string().allow().required(),
|
||||
extra: Joi.object().optional(),
|
||||
datasourceId: Joi.string().required(),
|
||||
transformer: Joi.string().optional(),
|
||||
parameters: Joi.object({}).required().unknown(true)
|
||||
}))
|
||||
}
|
|
@ -1,53 +1,23 @@
|
|||
const Router = require("@koa/router")
|
||||
const queryController = require("../controllers/query")
|
||||
const authorized = require("../../middleware/authorized")
|
||||
const Joi = require("joi")
|
||||
const {
|
||||
PermissionLevels,
|
||||
PermissionTypes,
|
||||
BUILDER,
|
||||
} = require("@budibase/auth/permissions")
|
||||
const joiValidator = require("../../middleware/joi-validator")
|
||||
const {
|
||||
bodyResource,
|
||||
bodySubResource,
|
||||
paramResource,
|
||||
} = require("../../middleware/resourceId")
|
||||
const {
|
||||
generateQueryPreviewValidation,
|
||||
generateQueryValidation,
|
||||
} = require("../controllers/query/validation")
|
||||
|
||||
const router = Router()
|
||||
|
||||
function generateQueryValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
_id: Joi.string(),
|
||||
_rev: Joi.string(),
|
||||
name: Joi.string().required(),
|
||||
fields: Joi.object().required(),
|
||||
datasourceId: Joi.string().required(),
|
||||
readable: Joi.boolean(),
|
||||
parameters: Joi.array().items(Joi.object({
|
||||
name: Joi.string(),
|
||||
default: Joi.string().allow(""),
|
||||
})),
|
||||
queryVerb: Joi.string().allow().required(),
|
||||
extra: Joi.object().optional(),
|
||||
schema: Joi.object({}).required().unknown(true),
|
||||
transformer: Joi.string().optional(),
|
||||
}))
|
||||
}
|
||||
|
||||
function generateQueryPreviewValidation() {
|
||||
// prettier-ignore
|
||||
return joiValidator.body(Joi.object({
|
||||
fields: Joi.object().required(),
|
||||
queryVerb: Joi.string().allow().required(),
|
||||
extra: Joi.object().optional(),
|
||||
datasourceId: Joi.string().required(),
|
||||
transformer: Joi.string().optional(),
|
||||
parameters: Joi.object({}).required().unknown(true)
|
||||
}))
|
||||
}
|
||||
|
||||
router
|
||||
.get("/api/queries", authorized(BUILDER), queryController.fetch)
|
||||
.post(
|
||||
|
@ -57,6 +27,7 @@ router
|
|||
generateQueryValidation(),
|
||||
queryController.save
|
||||
)
|
||||
.post("/api/queries/import", authorized(BUILDER), queryController.import)
|
||||
.post(
|
||||
"/api/queries/preview",
|
||||
bodyResource("datasourceId"),
|
||||
|
|
|
@ -107,3 +107,25 @@ export interface Datasource extends Base {
|
|||
[key: string]: Table
|
||||
}
|
||||
}
|
||||
|
||||
export interface QueryParameter {
|
||||
name: string
|
||||
default: string
|
||||
}
|
||||
|
||||
export interface Query {
|
||||
_id?: string
|
||||
datasourceId: string
|
||||
name: string
|
||||
parameters: QueryParameter[]
|
||||
fields: {
|
||||
headers: object
|
||||
queryString: string | null
|
||||
path: string
|
||||
requestBody: string | undefined
|
||||
}
|
||||
transformer: string | null
|
||||
schema: any
|
||||
readable: boolean
|
||||
queryVerb: string
|
||||
}
|
||||
|
|
|
@ -450,7 +450,7 @@ module OracleModule {
|
|||
})
|
||||
return lastRow.rows
|
||||
} else {
|
||||
return [{ [ operation.toLowerCase() ]: true }]
|
||||
return [{ [operation.toLowerCase()]: true }]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue