Merge branch 'develop' into feature/rest-redesign
This commit is contained in:
commit
8df37dae85
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -63,9 +63,9 @@
|
|||
const getFilteredOptions = (options, term, getLabel) => {
|
||||
if (autocomplete && term) {
|
||||
const lowerCaseTerm = term.toLowerCase()
|
||||
return options.filter(option =>
|
||||
getLabel(option)?.toLowerCase().includes(lowerCaseTerm)
|
||||
)
|
||||
return options.filter(option => {
|
||||
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.7",
|
||||
"@budibase/client": "^1.0.7",
|
||||
"@budibase/bbui": "^1.0.8",
|
||||
"@budibase/client": "^1.0.8",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^1.0.7",
|
||||
"@budibase/string-templates": "^1.0.8",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -9,6 +9,9 @@ export const Events = {
|
|||
CREATED: "Datasource Created",
|
||||
UPDATED: "Datasource Updated",
|
||||
},
|
||||
QUERIES: {
|
||||
REST: "REST Queries Imported",
|
||||
},
|
||||
TABLE: {
|
||||
CREATED: "Table Created",
|
||||
},
|
||||
|
|
|
@ -96,13 +96,16 @@
|
|||
allSteps[idx].schema?.outputs?.properties ?? {}
|
||||
)
|
||||
bindings = bindings.concat(
|
||||
outputs.map(([name, value]) => ({
|
||||
label: name,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
||||
path: idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}`,
|
||||
}))
|
||||
outputs.map(([name, value]) => {
|
||||
const runtime = idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}`
|
||||
return {
|
||||
label: runtime,
|
||||
type: value.type,
|
||||
description: value.description,
|
||||
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
||||
path: runtime,
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
return bindings
|
||||
|
@ -261,7 +264,6 @@
|
|||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
allowJS={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -8,12 +8,18 @@
|
|||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||
import { createRestDatasource } from "builderStore/datasource"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||
|
||||
export let modal
|
||||
let integrations = []
|
||||
let integration = {}
|
||||
let internalTableModal
|
||||
let externalDatasourceModal
|
||||
let importModal
|
||||
|
||||
$: showImportButton = false
|
||||
|
||||
checkShowImport()
|
||||
|
||||
onMount(() => {
|
||||
fetchIntegrations()
|
||||
|
@ -33,6 +39,15 @@
|
|||
config,
|
||||
schema: selected.datasource,
|
||||
}
|
||||
checkShowImport()
|
||||
}
|
||||
|
||||
function checkShowImport() {
|
||||
showImportButton = integration.type === "REST"
|
||||
}
|
||||
|
||||
function showImportModal() {
|
||||
importModal.show()
|
||||
}
|
||||
|
||||
async function chooseNextModal() {
|
||||
|
@ -67,11 +82,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>
|
|
@ -11,6 +11,14 @@
|
|||
await queries.delete(query)
|
||||
notifications.success("Query deleted")
|
||||
}
|
||||
|
||||
async function duplicateQuery() {
|
||||
try {
|
||||
await queries.duplicate(query)
|
||||
} catch (e) {
|
||||
notifications.error(e.message)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionMenu>
|
||||
|
@ -18,6 +26,7 @@
|
|||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||
<MenuItem icon="Duplicate" on:click={duplicateQuery}>Duplicate</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
<ConfirmDialog
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { goto, url } from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import {
|
||||
|
@ -13,24 +12,13 @@
|
|||
} from "@budibase/bbui"
|
||||
import TableDataImport from "../TableDataImport.svelte"
|
||||
import analytics, { Events } from "analytics"
|
||||
import screenTemplates from "builderStore/store/screenTemplates"
|
||||
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
|
||||
import { NEW_ROW_TEMPLATE } from "builderStore/store/screenTemplates/newRowScreen"
|
||||
import { ROW_DETAIL_TEMPLATE } from "builderStore/store/screenTemplates/rowDetailScreen"
|
||||
import { ROW_LIST_TEMPLATE } from "builderStore/store/screenTemplates/rowListScreen"
|
||||
|
||||
const defaultScreens = [
|
||||
NEW_ROW_TEMPLATE,
|
||||
ROW_DETAIL_TEMPLATE,
|
||||
ROW_LIST_TEMPLATE,
|
||||
]
|
||||
|
||||
$: tableNames = $tables.list.map(table => table.name)
|
||||
|
||||
export let name
|
||||
let dataImport
|
||||
let error = ""
|
||||
let createAutoscreens = true
|
||||
let autoColumns = getAutoColumnInformation()
|
||||
|
||||
function addAutoColumns(tableName, schema) {
|
||||
|
@ -69,27 +57,6 @@
|
|||
notifications.success(`Table ${name} created successfully.`)
|
||||
analytics.captureEvent(Events.TABLE.CREATED, { name })
|
||||
|
||||
// Create auto screens
|
||||
if (createAutoscreens) {
|
||||
const screens = screenTemplates($store, [table])
|
||||
.filter(template => defaultScreens.includes(template.id))
|
||||
.map(template => template.create())
|
||||
for (let screen of screens) {
|
||||
// Record the table that created this screen so we can link it later
|
||||
screen.autoTableId = table._id
|
||||
await store.actions.screens.create(screen)
|
||||
}
|
||||
|
||||
// Create autolink to newly created list screen
|
||||
const listScreen = screens.find(screen =>
|
||||
screen.props._instanceName.endsWith("List")
|
||||
)
|
||||
await store.actions.components.links.save(
|
||||
listScreen.routing.route,
|
||||
table.name
|
||||
)
|
||||
}
|
||||
|
||||
// Navigate to new table
|
||||
const currentUrl = $url()
|
||||
const path = currentUrl.endsWith("data")
|
||||
|
@ -128,10 +95,6 @@
|
|||
</div>
|
||||
<Divider />
|
||||
</div>
|
||||
<Toggle
|
||||
text="Generate screens in Design section"
|
||||
bind:value={createAutoscreens}
|
||||
/>
|
||||
<div>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
disabled={!selectedScreens.length}
|
||||
size="L"
|
||||
>
|
||||
<Body size="XS"
|
||||
<Body size="S"
|
||||
>Please select the screens you would like to add to your application.
|
||||
Autogenerated screens come with CRUD functionality.</Body
|
||||
>
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
queries as queriesStore,
|
||||
} from "stores/backend"
|
||||
import { datasources, integrations } from "stores/backend"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
|
@ -31,6 +30,7 @@
|
|||
const arrayTypes = ["attachment", "array"]
|
||||
let anchorRight, dropdownRight
|
||||
let drawer
|
||||
let tmpQueryParams
|
||||
|
||||
$: text = value?.label ?? "Choose an option"
|
||||
$: tables = $tablesStore.list.map(m => ({
|
||||
|
@ -105,12 +105,12 @@
|
|||
}
|
||||
})
|
||||
|
||||
function handleSelected(selected) {
|
||||
const handleSelected = selected => {
|
||||
dispatch("change", selected)
|
||||
dropdownRight.hide()
|
||||
}
|
||||
|
||||
function fetchQueryDefinition(query) {
|
||||
const fetchQueryDefinition = query => {
|
||||
const source = $datasources.list.find(
|
||||
ds => ds._id === query.datasourceId
|
||||
).source
|
||||
|
@ -124,6 +124,19 @@
|
|||
const getQueryDatasource = query => {
|
||||
return $datasources.list.find(ds => ds._id === query?.datasourceId)
|
||||
}
|
||||
|
||||
const openQueryParamsDrawer = () => {
|
||||
tmpQueryParams = value.queryParams
|
||||
drawer.show()
|
||||
}
|
||||
|
||||
const saveQueryParams = () => {
|
||||
handleSelected({
|
||||
...value,
|
||||
queryParams: tmpQueryParams,
|
||||
})
|
||||
drawer.hide()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" bind:this={anchorRight}>
|
||||
|
@ -134,24 +147,14 @@
|
|||
on:click={dropdownRight.show}
|
||||
/>
|
||||
{#if value?.type === "query"}
|
||||
<i class="ri-settings-5-line" on:click={drawer.show} />
|
||||
<i class="ri-settings-5-line" on:click={openQueryParamsDrawer} />
|
||||
<Drawer title={"Query Parameters"} bind:this={drawer}>
|
||||
<Button
|
||||
slot="buttons"
|
||||
cta
|
||||
on:click={() => {
|
||||
notifications.success("Query parameters saved.")
|
||||
handleSelected(value)
|
||||
drawer.hide()
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button slot="buttons" cta on:click={saveQueryParams}>Save</Button>
|
||||
<DrawerContent slot="body">
|
||||
<Layout noPadding>
|
||||
{#if getQueryParams(value).length > 0}
|
||||
<ParameterBuilder
|
||||
bind:customParams={value.queryParams}
|
||||
bind:customParams={tmpQueryParams}
|
||||
parameters={getQueryParams(value)}
|
||||
{bindings}
|
||||
/>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
export let value = []
|
||||
let drawer
|
||||
let links = cloneDeep(value)
|
||||
let links = cloneDeep(value || [])
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const save = () => {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Duplicates a name with respect to a collection of existing names
|
||||
* e.g.
|
||||
* name all names result
|
||||
* ------ ----------- --------
|
||||
* ("foo") ["foo"] "foo (1)"
|
||||
* ("foo") ["foo", "foo (1)"] "foo (2)"
|
||||
* ("foo (1)") ["foo", "foo (1)"] "foo (2)"
|
||||
* ("foo") ["foo", "foo (2)"] "foo (1)"
|
||||
*
|
||||
* Repl
|
||||
*/
|
||||
export const duplicateName = (name, allNames) => {
|
||||
const baseName = name.split(" (")[0]
|
||||
const isDuplicate = new RegExp(`${baseName}\\s\\((\\d+)\\)$`)
|
||||
|
||||
// get the sequence from matched names
|
||||
const sequence = []
|
||||
allNames.filter(n => {
|
||||
if (n === baseName) {
|
||||
return true
|
||||
}
|
||||
const match = n.match(isDuplicate)
|
||||
if (match) {
|
||||
sequence.push(parseInt(match[1]))
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
sequence.sort((a, b) => a - b)
|
||||
|
||||
// get the next number in the sequence
|
||||
let number
|
||||
if (sequence.length === 0) {
|
||||
number = 1
|
||||
} else {
|
||||
// get the next number in the sequence
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
if (sequence[i] !== i + 1) {
|
||||
number = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!number) {
|
||||
number = sequence.length + 1
|
||||
}
|
||||
}
|
||||
|
||||
return `${baseName} (${number})`
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
const { duplicateName } = require("../duplicate")
|
||||
|
||||
describe("duplicate", () => {
|
||||
|
||||
describe("duplicates a name ", () => {
|
||||
it("with a single existing", async () => {
|
||||
const names = ["foo"]
|
||||
const name = "foo"
|
||||
|
||||
const duplicate = duplicateName(name, names)
|
||||
|
||||
expect(duplicate).toBe("foo (1)")
|
||||
})
|
||||
|
||||
it("with multiple existing", async () => {
|
||||
const names = ["foo", "foo (1)", "foo (2)"]
|
||||
const name = "foo"
|
||||
|
||||
const duplicate = duplicateName(name, names)
|
||||
|
||||
expect(duplicate).toBe("foo (3)")
|
||||
})
|
||||
|
||||
it("with mixed multiple existing", async () => {
|
||||
const names = ["foo", "foo (1)", "foo (2)", "bar", "bar (1)", "bar (2)"]
|
||||
const name = "foo"
|
||||
|
||||
const duplicate = duplicateName(name, names)
|
||||
|
||||
expect(duplicate).toBe("foo (3)")
|
||||
})
|
||||
|
||||
it("with incomplete sequence", async () => {
|
||||
const names = ["foo", "foo (2)", "foo (3)"]
|
||||
const name = "foo"
|
||||
|
||||
const duplicate = duplicateName(name, names)
|
||||
|
||||
expect(duplicate).toBe("foo (1)")
|
||||
})
|
||||
})
|
||||
})
|
|
@ -8,6 +8,7 @@
|
|||
Layout,
|
||||
notifications,
|
||||
Table,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||
|
@ -19,6 +20,9 @@
|
|||
import { isEqual } from "lodash"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||
let importQueriesModal
|
||||
|
||||
let baseDatasource, changed
|
||||
const querySchema = {
|
||||
name: {},
|
||||
|
@ -67,6 +71,15 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={importQueriesModal}>
|
||||
{#if datasource.source === "REST"}
|
||||
<ImportRestQueriesModal
|
||||
createDatasource={false}
|
||||
datasourceId={datasource._id}
|
||||
/>
|
||||
{/if}
|
||||
</Modal>
|
||||
|
||||
{#if datasource && integration}
|
||||
<section>
|
||||
<Layout>
|
||||
|
@ -97,9 +110,16 @@
|
|||
<Divider size="S" />
|
||||
<div class="query-header">
|
||||
<Heading size="S">Queries</Heading>
|
||||
<Button cta icon="Add" on:click={() => $goto("./new")}
|
||||
>Add query
|
||||
</Button>
|
||||
<div class="query-buttons">
|
||||
{#if datasource?.source === IntegrationTypes.REST}
|
||||
<Button secondary on:click={() => importQueriesModal.show()}
|
||||
>Import</Button
|
||||
>
|
||||
{/if}
|
||||
<Button cta icon="Add" on:click={() => $goto("./new")}
|
||||
>Add query
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Body size="S">
|
||||
To build an app using a datasource, you must first query the data. A
|
||||
|
@ -151,6 +171,11 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.query-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.query-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -179,7 +179,7 @@
|
|||
<Layout gap="S" justifyItems="center">
|
||||
<img class="img-size" alt="logo" src={Logo} />
|
||||
<div class="new-screen-text">
|
||||
<Detail size="M">Let's add some life to this screen</Detail>
|
||||
<Detail size="M">LET’S BRING THIS APP TO LIFE</Detail>
|
||||
</div>
|
||||
<Button on:click={() => showModal()} size="M" cta>
|
||||
<div class="new-screen-button">
|
||||
|
@ -290,12 +290,9 @@
|
|||
}
|
||||
|
||||
.centered {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 10%;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: calc(100% - 350px);
|
||||
height: calc(100% - 100px);
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { datasources, integrations, tables, views } from "./"
|
||||
import api from "builderStore/api"
|
||||
import { duplicateName } from "../../helpers/duplicate"
|
||||
|
||||
const sortQueries = queryList => {
|
||||
queryList.sort((q1, q2) => {
|
||||
return q1.name.localeCompare(q2.name)
|
||||
})
|
||||
}
|
||||
|
||||
export function createQueriesStore() {
|
||||
const { subscribe, set, update } = writable({ list: [], selected: null })
|
||||
const store = writable({ list: [], selected: null })
|
||||
const { subscribe, set, update } = store
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
update,
|
||||
const actions = {
|
||||
init: async () => {
|
||||
const response = await api.get(`/api/queries`)
|
||||
const json = await response.json()
|
||||
|
@ -17,6 +22,7 @@ export function createQueriesStore() {
|
|||
fetch: async () => {
|
||||
const response = await api.get(`/api/queries`)
|
||||
const json = await response.json()
|
||||
sortQueries(json)
|
||||
update(state => ({ ...state, list: json }))
|
||||
return json
|
||||
},
|
||||
|
@ -49,10 +55,20 @@ export function createQueriesStore() {
|
|||
} else {
|
||||
queries.push(json)
|
||||
}
|
||||
sortQueries(queries)
|
||||
return { list: queries, selected: json._id }
|
||||
})
|
||||
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()
|
||||
|
@ -105,6 +121,27 @@ export function createQueriesStore() {
|
|||
})
|
||||
return response
|
||||
},
|
||||
duplicate: async query => {
|
||||
let list = get(store).list
|
||||
const newQuery = { ...query }
|
||||
const datasourceId = query.datasourceId
|
||||
|
||||
delete newQuery._id
|
||||
delete newQuery._rev
|
||||
newQuery.name = duplicateName(
|
||||
query.name,
|
||||
list.map(q => q.name)
|
||||
)
|
||||
|
||||
actions.save(datasourceId, newQuery)
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
set,
|
||||
update,
|
||||
...actions,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ jest.mock('builderStore/api');
|
|||
import { SOME_QUERY, SAVE_QUERY_RESPONSE } from './fixtures/queries'
|
||||
|
||||
import { createQueriesStore } from "../queries"
|
||||
import { datasources } from '../datasources'
|
||||
|
||||
describe("Queries Store", () => {
|
||||
let store = createQueriesStore()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -393,13 +393,13 @@
|
|||
{
|
||||
"label": "Column",
|
||||
"value": "column",
|
||||
"barIcon": "ViewRow",
|
||||
"barIcon": "ViewColumn",
|
||||
"barTitle": "Column layout"
|
||||
},
|
||||
{
|
||||
"label": "Row",
|
||||
"value": "row",
|
||||
"barIcon": "ViewColumn",
|
||||
"barIcon": "ViewRow",
|
||||
"barTitle": "Row layout"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.7",
|
||||
"@budibase/bbui": "^1.0.8",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^1.0.7",
|
||||
"@budibase/string-templates": "^1.0.8",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
|
|
@ -21,11 +21,25 @@
|
|||
$: target = openInNewTab ? "_blank" : "_self"
|
||||
$: placeholder = $builderStore.inBuilder && !text
|
||||
$: componentText = getComponentText(text, $builderStore, $component)
|
||||
$: sanitizedUrl = getSanitizedUrl(url, externalLink, openInNewTab)
|
||||
|
||||
// Add color styles to main styles object, otherwise the styleable helper
|
||||
// overrides the color when it's passed as inline style.
|
||||
$: styles = enrichStyles($component.styles, color)
|
||||
|
||||
const getSanitizedUrl = (url, externalLink, newTab) => {
|
||||
if (!url) {
|
||||
return externalLink || newTab ? "#/" : "/"
|
||||
}
|
||||
if (externalLink) {
|
||||
return url
|
||||
}
|
||||
if (openInNewTab) {
|
||||
return `#${url}`
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
const getComponentText = (text, builderState, componentState) => {
|
||||
if (!builderState.inBuilder || componentState.editing) {
|
||||
return text || ""
|
||||
|
@ -65,10 +79,10 @@
|
|||
{componentText}
|
||||
</div>
|
||||
{:else if $builderStore.inBuilder || componentText}
|
||||
{#if externalLink}
|
||||
{#if externalLink || openInNewTab}
|
||||
<a
|
||||
{target}
|
||||
href={url || "/"}
|
||||
href={sanitizedUrl}
|
||||
use:styleable={styles}
|
||||
class:placeholder
|
||||
class:bold
|
||||
|
@ -79,18 +93,20 @@
|
|||
{componentText}
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
use:linkable
|
||||
href={url || "/"}
|
||||
use:styleable={styles}
|
||||
class:placeholder
|
||||
class:bold
|
||||
class:italic
|
||||
class:underline
|
||||
class="align--{align || 'left'} size--{size || 'M'}"
|
||||
>
|
||||
{componentText}
|
||||
</a>
|
||||
{#key sanitizedUrl}
|
||||
<a
|
||||
use:linkable
|
||||
href={sanitizedUrl}
|
||||
use:styleable={styles}
|
||||
class:placeholder
|
||||
class:bold
|
||||
class:italic
|
||||
class:underline
|
||||
class="align--{align || 'left'} size--{size || 'M'}"
|
||||
>
|
||||
{componentText}
|
||||
</a>
|
||||
{/key}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -69,9 +69,10 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^1.0.7",
|
||||
"@budibase/client": "^1.0.7",
|
||||
"@budibase/string-templates": "^1.0.7",
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/auth": "^1.0.8",
|
||||
"@budibase/client": "^1.0.8",
|
||||
"@budibase/string-templates": "^1.0.8",
|
||||
"@bull-board/api": "^3.7.0",
|
||||
"@bull-board/koa": "^3.7.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
@ -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,54 +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(),
|
||||
flags: Joi.object().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(
|
||||
|
@ -58,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
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.7",
|
||||
"version": "1.0.8",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -29,8 +29,8 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^1.0.7",
|
||||
"@budibase/string-templates": "^1.0.7",
|
||||
"@budibase/auth": "^1.0.8",
|
||||
"@budibase/string-templates": "^1.0.8",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@sentry/node": "^6.0.0",
|
||||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
|
|
Loading…
Reference in New Issue