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",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/auth",
|
"name": "@budibase/auth",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"description": "Authentication middlewares for budibase builder and apps",
|
"description": "Authentication middlewares for budibase builder and apps",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/bbui",
|
"name": "@budibase/bbui",
|
||||||
"description": "A UI solution used in the different Budibase projects.",
|
"description": "A UI solution used in the different Budibase projects.",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"svelte": "src/index.js",
|
"svelte": "src/index.js",
|
||||||
"module": "dist/bbui.es.js",
|
"module": "dist/bbui.es.js",
|
||||||
|
|
|
@ -6,6 +6,8 @@
|
||||||
import { generateID } from "../../utils/helpers"
|
import { generateID } from "../../utils/helpers"
|
||||||
import Icon from "../../Icon/Icon.svelte"
|
import Icon from "../../Icon/Icon.svelte"
|
||||||
import Link from "../../Link/Link.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_KB = 1000
|
||||||
const BYTES_IN_MB = 1000000
|
const BYTES_IN_MB = 1000000
|
||||||
|
@ -18,6 +20,8 @@
|
||||||
export let handleFileTooLarge = null
|
export let handleFileTooLarge = null
|
||||||
export let gallery = true
|
export let gallery = true
|
||||||
export let error = null
|
export let error = null
|
||||||
|
export let fileTags = []
|
||||||
|
export let maximum = null
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const imageExtensions = [
|
const imageExtensions = [
|
||||||
|
@ -184,6 +188,7 @@
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if !maximum || (maximum && value?.length < maximum)}
|
||||||
<div
|
<div
|
||||||
class="spectrum-Dropzone"
|
class="spectrum-Dropzone"
|
||||||
class:is-invalid={!!error}
|
class:is-invalid={!!error}
|
||||||
|
@ -278,9 +283,23 @@
|
||||||
<br />
|
<br />
|
||||||
from your computer
|
from your computer
|
||||||
</p>
|
</p>
|
||||||
|
{#if fileTags.length}
|
||||||
|
<Tags>
|
||||||
|
<div class="tags">
|
||||||
|
{#each fileTags as tag}
|
||||||
|
<div class="tag">
|
||||||
|
<Tag>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -390,4 +409,15 @@
|
||||||
.disabled .spectrum-Heading--sizeL {
|
.disabled .spectrum-Heading--sizeL {
|
||||||
color: var(--spectrum-alias-text-color-disabled);
|
color: var(--spectrum-alias-text-color-disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -63,9 +63,9 @@
|
||||||
const getFilteredOptions = (options, term, getLabel) => {
|
const getFilteredOptions = (options, term, getLabel) => {
|
||||||
if (autocomplete && term) {
|
if (autocomplete && term) {
|
||||||
const lowerCaseTerm = term.toLowerCase()
|
const lowerCaseTerm = term.toLowerCase()
|
||||||
return options.filter(option =>
|
return options.filter(option => {
|
||||||
getLabel(option)?.toLowerCase().includes(lowerCaseTerm)
|
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
||||||
)
|
})
|
||||||
}
|
}
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
export let processFiles = undefined
|
export let processFiles = undefined
|
||||||
export let handleFileTooLarge = undefined
|
export let handleFileTooLarge = undefined
|
||||||
export let gallery = true
|
export let gallery = true
|
||||||
|
export let fileTags = []
|
||||||
|
export let maximum = undefined
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const onChange = e => {
|
const onChange = e => {
|
||||||
|
@ -29,6 +31,8 @@
|
||||||
{processFiles}
|
{processFiles}
|
||||||
{handleFileTooLarge}
|
{handleFileTooLarge}
|
||||||
{gallery}
|
{gallery}
|
||||||
|
{fileTags}
|
||||||
|
{maximum}
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
|
@ -18,10 +18,22 @@
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let showDivider = true
|
export let showDivider = true
|
||||||
|
|
||||||
|
export let showSecondaryButton = false
|
||||||
|
export let secondaryButtonText = undefined
|
||||||
|
export let secondaryAction = undefined
|
||||||
|
|
||||||
const { hide, cancel } = getContext(Context.Modal)
|
const { hide, cancel } = getContext(Context.Modal)
|
||||||
let loading = false
|
let loading = false
|
||||||
$: confirmDisabled = disabled || loading
|
$: confirmDisabled = disabled || loading
|
||||||
|
|
||||||
|
async function secondary() {
|
||||||
|
loading = true
|
||||||
|
if (!secondaryAction || (await secondaryAction()) !== false) {
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
|
||||||
async function confirm() {
|
async function confirm() {
|
||||||
loading = true
|
loading = true
|
||||||
if (!onConfirm || (await onConfirm()) !== false) {
|
if (!onConfirm || (await onConfirm()) !== false) {
|
||||||
|
@ -73,6 +85,15 @@
|
||||||
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
|
class="spectrum-ButtonGroup spectrum-Dialog-buttonGroup spectrum-Dialog-buttonGroup--noFooter"
|
||||||
>
|
>
|
||||||
<slot name="footer" />
|
<slot name="footer" />
|
||||||
|
|
||||||
|
{#if showSecondaryButton && secondaryButtonText && secondaryAction}
|
||||||
|
<div class="secondary-action">
|
||||||
|
<Button group secondary on:click={secondary}
|
||||||
|
>{secondaryButtonText}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showCancelButton}
|
{#if showCancelButton}
|
||||||
<Button group secondary on:click={close}>{cancelText}</Button>
|
<Button group secondary on:click={close}>{cancelText}</Button>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -136,4 +157,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secondary-action {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/builder",
|
"name": "@budibase/builder",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -65,10 +65,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.7",
|
"@budibase/bbui": "^1.0.8",
|
||||||
"@budibase/client": "^1.0.7",
|
"@budibase/client": "^1.0.8",
|
||||||
"@budibase/colorpicker": "1.1.2",
|
"@budibase/colorpicker": "1.1.2",
|
||||||
"@budibase/string-templates": "^1.0.7",
|
"@budibase/string-templates": "^1.0.8",
|
||||||
"@sentry/browser": "5.19.1",
|
"@sentry/browser": "5.19.1",
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
|
|
|
@ -9,6 +9,9 @@ export const Events = {
|
||||||
CREATED: "Datasource Created",
|
CREATED: "Datasource Created",
|
||||||
UPDATED: "Datasource Updated",
|
UPDATED: "Datasource Updated",
|
||||||
},
|
},
|
||||||
|
QUERIES: {
|
||||||
|
REST: "REST Queries Imported",
|
||||||
|
},
|
||||||
TABLE: {
|
TABLE: {
|
||||||
CREATED: "Table Created",
|
CREATED: "Table Created",
|
||||||
},
|
},
|
||||||
|
|
|
@ -96,13 +96,16 @@
|
||||||
allSteps[idx].schema?.outputs?.properties ?? {}
|
allSteps[idx].schema?.outputs?.properties ?? {}
|
||||||
)
|
)
|
||||||
bindings = bindings.concat(
|
bindings = bindings.concat(
|
||||||
outputs.map(([name, value]) => ({
|
outputs.map(([name, value]) => {
|
||||||
label: name,
|
const runtime = idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}`
|
||||||
|
return {
|
||||||
|
label: runtime,
|
||||||
type: value.type,
|
type: value.type,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
category: idx === 0 ? "Trigger outputs" : `Step ${idx} outputs`,
|
||||||
path: idx === 0 ? `trigger.${name}` : `steps.${idx}.${name}`,
|
path: runtime,
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return bindings
|
return bindings
|
||||||
|
@ -261,7 +264,6 @@
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
{bindings}
|
{bindings}
|
||||||
allowJS={false}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -8,12 +8,18 @@
|
||||||
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
|
||||||
import { createRestDatasource } from "builderStore/datasource"
|
import { createRestDatasource } from "builderStore/datasource"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
|
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
|
||||||
|
|
||||||
export let modal
|
export let modal
|
||||||
let integrations = []
|
let integrations = []
|
||||||
let integration = {}
|
let integration = {}
|
||||||
let internalTableModal
|
let internalTableModal
|
||||||
let externalDatasourceModal
|
let externalDatasourceModal
|
||||||
|
let importModal
|
||||||
|
|
||||||
|
$: showImportButton = false
|
||||||
|
|
||||||
|
checkShowImport()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchIntegrations()
|
fetchIntegrations()
|
||||||
|
@ -33,6 +39,15 @@
|
||||||
config,
|
config,
|
||||||
schema: selected.datasource,
|
schema: selected.datasource,
|
||||||
}
|
}
|
||||||
|
checkShowImport()
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkShowImport() {
|
||||||
|
showImportButton = integration.type === "REST"
|
||||||
|
}
|
||||||
|
|
||||||
|
function showImportModal() {
|
||||||
|
importModal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function chooseNextModal() {
|
async function chooseNextModal() {
|
||||||
|
@ -67,11 +82,24 @@
|
||||||
<DatasourceConfigModal {integration} {modal} />
|
<DatasourceConfigModal {integration} {modal} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal bind:this={importModal}>
|
||||||
|
{#if integration.type === "REST"}
|
||||||
|
<ImportRestQueriesModal
|
||||||
|
navigateDatasource={true}
|
||||||
|
createDatasource={true}
|
||||||
|
onCancel={() => modal.show()}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
disabled={!Object.keys(integration).length}
|
disabled={!Object.keys(integration).length}
|
||||||
title="Data"
|
title="Data"
|
||||||
confirmText="Continue"
|
confirmText="Continue"
|
||||||
|
showSecondaryButton={showImportButton}
|
||||||
|
secondaryButtonText="Import"
|
||||||
|
secondaryAction={() => showImportModal()}
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
size="M"
|
size="M"
|
||||||
onConfirm={() => {
|
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)
|
await queries.delete(query)
|
||||||
notifications.success("Query deleted")
|
notifications.success("Query deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function duplicateQuery() {
|
||||||
|
try {
|
||||||
|
await queries.duplicate(query)
|
||||||
|
} catch (e) {
|
||||||
|
notifications.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionMenu>
|
<ActionMenu>
|
||||||
|
@ -18,6 +26,7 @@
|
||||||
<Icon size="S" hoverable name="MoreSmallList" />
|
<Icon size="S" hoverable name="MoreSmallList" />
|
||||||
</div>
|
</div>
|
||||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||||
|
<MenuItem icon="Duplicate" on:click={duplicateQuery}>Duplicate</MenuItem>
|
||||||
</ActionMenu>
|
</ActionMenu>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { goto, url } from "@roxi/routify"
|
import { goto, url } from "@roxi/routify"
|
||||||
import { store } from "builderStore"
|
|
||||||
import { tables } from "stores/backend"
|
import { tables } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
import { notifications } from "@budibase/bbui"
|
||||||
import {
|
import {
|
||||||
|
@ -13,24 +12,13 @@
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import TableDataImport from "../TableDataImport.svelte"
|
import TableDataImport from "../TableDataImport.svelte"
|
||||||
import analytics, { Events } from "analytics"
|
import analytics, { Events } from "analytics"
|
||||||
import screenTemplates from "builderStore/store/screenTemplates"
|
|
||||||
import { buildAutoColumn, getAutoColumnInformation } from "builderStore/utils"
|
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)
|
$: tableNames = $tables.list.map(table => table.name)
|
||||||
|
|
||||||
export let name
|
export let name
|
||||||
let dataImport
|
let dataImport
|
||||||
let error = ""
|
let error = ""
|
||||||
let createAutoscreens = true
|
|
||||||
let autoColumns = getAutoColumnInformation()
|
let autoColumns = getAutoColumnInformation()
|
||||||
|
|
||||||
function addAutoColumns(tableName, schema) {
|
function addAutoColumns(tableName, schema) {
|
||||||
|
@ -69,27 +57,6 @@
|
||||||
notifications.success(`Table ${name} created successfully.`)
|
notifications.success(`Table ${name} created successfully.`)
|
||||||
analytics.captureEvent(Events.TABLE.CREATED, { name })
|
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
|
// Navigate to new table
|
||||||
const currentUrl = $url()
|
const currentUrl = $url()
|
||||||
const path = currentUrl.endsWith("data")
|
const path = currentUrl.endsWith("data")
|
||||||
|
@ -128,10 +95,6 @@
|
||||||
</div>
|
</div>
|
||||||
<Divider />
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
<Toggle
|
|
||||||
text="Generate screens in Design section"
|
|
||||||
bind:value={createAutoscreens}
|
|
||||||
/>
|
|
||||||
<div>
|
<div>
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
<Label grey extraSmall>Create Table from CSV (Optional)</Label>
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
disabled={!selectedScreens.length}
|
disabled={!selectedScreens.length}
|
||||||
size="L"
|
size="L"
|
||||||
>
|
>
|
||||||
<Body size="XS"
|
<Body size="S"
|
||||||
>Please select the screens you would like to add to your application.
|
>Please select the screens you would like to add to your application.
|
||||||
Autogenerated screens come with CRUD functionality.</Body
|
Autogenerated screens come with CRUD functionality.</Body
|
||||||
>
|
>
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
queries as queriesStore,
|
queries as queriesStore,
|
||||||
} from "stores/backend"
|
} from "stores/backend"
|
||||||
import { datasources, integrations } from "stores/backend"
|
import { datasources, integrations } from "stores/backend"
|
||||||
import { notifications } from "@budibase/bbui"
|
|
||||||
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
import ParameterBuilder from "components/integration/QueryParameterBuilder.svelte"
|
||||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||||
|
@ -31,6 +30,7 @@
|
||||||
const arrayTypes = ["attachment", "array"]
|
const arrayTypes = ["attachment", "array"]
|
||||||
let anchorRight, dropdownRight
|
let anchorRight, dropdownRight
|
||||||
let drawer
|
let drawer
|
||||||
|
let tmpQueryParams
|
||||||
|
|
||||||
$: text = value?.label ?? "Choose an option"
|
$: text = value?.label ?? "Choose an option"
|
||||||
$: tables = $tablesStore.list.map(m => ({
|
$: tables = $tablesStore.list.map(m => ({
|
||||||
|
@ -105,12 +105,12 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleSelected(selected) {
|
const handleSelected = selected => {
|
||||||
dispatch("change", selected)
|
dispatch("change", selected)
|
||||||
dropdownRight.hide()
|
dropdownRight.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchQueryDefinition(query) {
|
const fetchQueryDefinition = query => {
|
||||||
const source = $datasources.list.find(
|
const source = $datasources.list.find(
|
||||||
ds => ds._id === query.datasourceId
|
ds => ds._id === query.datasourceId
|
||||||
).source
|
).source
|
||||||
|
@ -124,6 +124,19 @@
|
||||||
const getQueryDatasource = query => {
|
const getQueryDatasource = query => {
|
||||||
return $datasources.list.find(ds => ds._id === query?.datasourceId)
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="container" bind:this={anchorRight}>
|
<div class="container" bind:this={anchorRight}>
|
||||||
|
@ -134,24 +147,14 @@
|
||||||
on:click={dropdownRight.show}
|
on:click={dropdownRight.show}
|
||||||
/>
|
/>
|
||||||
{#if value?.type === "query"}
|
{#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}>
|
<Drawer title={"Query Parameters"} bind:this={drawer}>
|
||||||
<Button
|
<Button slot="buttons" cta on:click={saveQueryParams}>Save</Button>
|
||||||
slot="buttons"
|
|
||||||
cta
|
|
||||||
on:click={() => {
|
|
||||||
notifications.success("Query parameters saved.")
|
|
||||||
handleSelected(value)
|
|
||||||
drawer.hide()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
<DrawerContent slot="body">
|
<DrawerContent slot="body">
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
{#if getQueryParams(value).length > 0}
|
{#if getQueryParams(value).length > 0}
|
||||||
<ParameterBuilder
|
<ParameterBuilder
|
||||||
bind:customParams={value.queryParams}
|
bind:customParams={tmpQueryParams}
|
||||||
parameters={getQueryParams(value)}
|
parameters={getQueryParams(value)}
|
||||||
{bindings}
|
{bindings}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
export let value = []
|
export let value = []
|
||||||
let drawer
|
let drawer
|
||||||
let links = cloneDeep(value)
|
let links = cloneDeep(value || [])
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const save = () => {
|
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,
|
Layout,
|
||||||
notifications,
|
notifications,
|
||||||
Table,
|
Table,
|
||||||
|
Modal,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { datasources, integrations, queries, tables } from "stores/backend"
|
import { datasources, integrations, queries, tables } from "stores/backend"
|
||||||
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
|
||||||
|
@ -19,6 +20,9 @@
|
||||||
import { isEqual } from "lodash"
|
import { isEqual } from "lodash"
|
||||||
import { cloneDeep } from "lodash/fp"
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
|
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
|
||||||
|
let importQueriesModal
|
||||||
|
|
||||||
let baseDatasource, changed
|
let baseDatasource, changed
|
||||||
const querySchema = {
|
const querySchema = {
|
||||||
name: {},
|
name: {},
|
||||||
|
@ -67,6 +71,15 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={importQueriesModal}>
|
||||||
|
{#if datasource.source === "REST"}
|
||||||
|
<ImportRestQueriesModal
|
||||||
|
createDatasource={false}
|
||||||
|
datasourceId={datasource._id}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{#if datasource && integration}
|
{#if datasource && integration}
|
||||||
<section>
|
<section>
|
||||||
<Layout>
|
<Layout>
|
||||||
|
@ -97,10 +110,17 @@
|
||||||
<Divider size="S" />
|
<Divider size="S" />
|
||||||
<div class="query-header">
|
<div class="query-header">
|
||||||
<Heading size="S">Queries</Heading>
|
<Heading size="S">Queries</Heading>
|
||||||
|
<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")}
|
<Button cta icon="Add" on:click={() => $goto("./new")}
|
||||||
>Add query
|
>Add query
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Body size="S">
|
<Body size="S">
|
||||||
To build an app using a datasource, you must first query the data. A
|
To build an app using a datasource, you must first query the data. A
|
||||||
query is a request for data or information from a datasource, for
|
query is a request for data or information from a datasource, for
|
||||||
|
@ -151,6 +171,11 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.query-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
.query-list {
|
.query-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -179,7 +179,7 @@
|
||||||
<Layout gap="S" justifyItems="center">
|
<Layout gap="S" justifyItems="center">
|
||||||
<img class="img-size" alt="logo" src={Logo} />
|
<img class="img-size" alt="logo" src={Logo} />
|
||||||
<div class="new-screen-text">
|
<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>
|
</div>
|
||||||
<Button on:click={() => showModal()} size="M" cta>
|
<Button on:click={() => showModal()} size="M" cta>
|
||||||
<div class="new-screen-button">
|
<div class="new-screen-button">
|
||||||
|
@ -290,12 +290,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered {
|
.centered {
|
||||||
top: 0;
|
width: calc(100% - 350px);
|
||||||
bottom: 0;
|
height: calc(100% - 100px);
|
||||||
left: 10%;
|
position: absolute;
|
||||||
right: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
import { writable, get } from "svelte/store"
|
import { writable, get } from "svelte/store"
|
||||||
import { datasources, integrations, tables, views } from "./"
|
import { datasources, integrations, tables, views } from "./"
|
||||||
import api from "builderStore/api"
|
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() {
|
export function createQueriesStore() {
|
||||||
const { subscribe, set, update } = writable({ list: [], selected: null })
|
const store = writable({ list: [], selected: null })
|
||||||
|
const { subscribe, set, update } = store
|
||||||
|
|
||||||
return {
|
const actions = {
|
||||||
subscribe,
|
|
||||||
set,
|
|
||||||
update,
|
|
||||||
init: async () => {
|
init: async () => {
|
||||||
const response = await api.get(`/api/queries`)
|
const response = await api.get(`/api/queries`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
@ -17,6 +22,7 @@ export function createQueriesStore() {
|
||||||
fetch: async () => {
|
fetch: async () => {
|
||||||
const response = await api.get(`/api/queries`)
|
const response = await api.get(`/api/queries`)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
|
sortQueries(json)
|
||||||
update(state => ({ ...state, list: json }))
|
update(state => ({ ...state, list: json }))
|
||||||
return json
|
return json
|
||||||
},
|
},
|
||||||
|
@ -49,10 +55,20 @@ export function createQueriesStore() {
|
||||||
} else {
|
} else {
|
||||||
queries.push(json)
|
queries.push(json)
|
||||||
}
|
}
|
||||||
|
sortQueries(queries)
|
||||||
return { list: queries, selected: json._id }
|
return { list: queries, selected: json._id }
|
||||||
})
|
})
|
||||||
return json
|
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 => {
|
select: query => {
|
||||||
update(state => ({ ...state, selected: query._id }))
|
update(state => ({ ...state, selected: query._id }))
|
||||||
views.unselect()
|
views.unselect()
|
||||||
|
@ -105,6 +121,27 @@ export function createQueriesStore() {
|
||||||
})
|
})
|
||||||
return response
|
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 { SOME_QUERY, SAVE_QUERY_RESPONSE } from './fixtures/queries'
|
||||||
|
|
||||||
import { createQueriesStore } from "../queries"
|
import { createQueriesStore } from "../queries"
|
||||||
import { datasources } from '../datasources'
|
|
||||||
|
|
||||||
describe("Queries Store", () => {
|
describe("Queries Store", () => {
|
||||||
let store = createQueriesStore()
|
let store = createQueriesStore()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/cli",
|
"name": "@budibase/cli",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -393,13 +393,13 @@
|
||||||
{
|
{
|
||||||
"label": "Column",
|
"label": "Column",
|
||||||
"value": "column",
|
"value": "column",
|
||||||
"barIcon": "ViewRow",
|
"barIcon": "ViewColumn",
|
||||||
"barTitle": "Column layout"
|
"barTitle": "Column layout"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Row",
|
"label": "Row",
|
||||||
"value": "row",
|
"value": "row",
|
||||||
"barIcon": "ViewColumn",
|
"barIcon": "ViewRow",
|
||||||
"barTitle": "Row layout"
|
"barTitle": "Row layout"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/client",
|
"name": "@budibase/client",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"module": "dist/budibase-client.js",
|
"module": "dist/budibase-client.js",
|
||||||
"main": "dist/budibase-client.js",
|
"main": "dist/budibase-client.js",
|
||||||
|
@ -19,9 +19,9 @@
|
||||||
"dev:builder": "rollup -cw"
|
"dev:builder": "rollup -cw"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/bbui": "^1.0.7",
|
"@budibase/bbui": "^1.0.8",
|
||||||
"@budibase/standard-components": "^0.9.139",
|
"@budibase/standard-components": "^0.9.139",
|
||||||
"@budibase/string-templates": "^1.0.7",
|
"@budibase/string-templates": "^1.0.8",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
"shortid": "^2.2.15",
|
"shortid": "^2.2.15",
|
||||||
"svelte-spa-router": "^3.0.5"
|
"svelte-spa-router": "^3.0.5"
|
||||||
|
|
|
@ -21,11 +21,25 @@
|
||||||
$: target = openInNewTab ? "_blank" : "_self"
|
$: target = openInNewTab ? "_blank" : "_self"
|
||||||
$: placeholder = $builderStore.inBuilder && !text
|
$: placeholder = $builderStore.inBuilder && !text
|
||||||
$: componentText = getComponentText(text, $builderStore, $component)
|
$: componentText = getComponentText(text, $builderStore, $component)
|
||||||
|
$: sanitizedUrl = getSanitizedUrl(url, externalLink, openInNewTab)
|
||||||
|
|
||||||
// Add color styles to main styles object, otherwise the styleable helper
|
// Add color styles to main styles object, otherwise the styleable helper
|
||||||
// overrides the color when it's passed as inline style.
|
// overrides the color when it's passed as inline style.
|
||||||
$: styles = enrichStyles($component.styles, color)
|
$: 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) => {
|
const getComponentText = (text, builderState, componentState) => {
|
||||||
if (!builderState.inBuilder || componentState.editing) {
|
if (!builderState.inBuilder || componentState.editing) {
|
||||||
return text || ""
|
return text || ""
|
||||||
|
@ -65,10 +79,10 @@
|
||||||
{componentText}
|
{componentText}
|
||||||
</div>
|
</div>
|
||||||
{:else if $builderStore.inBuilder || componentText}
|
{:else if $builderStore.inBuilder || componentText}
|
||||||
{#if externalLink}
|
{#if externalLink || openInNewTab}
|
||||||
<a
|
<a
|
||||||
{target}
|
{target}
|
||||||
href={url || "/"}
|
href={sanitizedUrl}
|
||||||
use:styleable={styles}
|
use:styleable={styles}
|
||||||
class:placeholder
|
class:placeholder
|
||||||
class:bold
|
class:bold
|
||||||
|
@ -79,9 +93,10 @@
|
||||||
{componentText}
|
{componentText}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
|
{#key sanitizedUrl}
|
||||||
<a
|
<a
|
||||||
use:linkable
|
use:linkable
|
||||||
href={url || "/"}
|
href={sanitizedUrl}
|
||||||
use:styleable={styles}
|
use:styleable={styles}
|
||||||
class:placeholder
|
class:placeholder
|
||||||
class:bold
|
class:bold
|
||||||
|
@ -91,6 +106,7 @@
|
||||||
>
|
>
|
||||||
{componentText}
|
{componentText}
|
||||||
</a>
|
</a>
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/server",
|
"name": "@budibase/server",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"description": "Budibase Web Server",
|
"description": "Budibase Web Server",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -69,9 +69,10 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^1.0.7",
|
"@apidevtools/swagger-parser": "^10.0.3",
|
||||||
"@budibase/client": "^1.0.7",
|
"@budibase/auth": "^1.0.8",
|
||||||
"@budibase/string-templates": "^1.0.7",
|
"@budibase/client": "^1.0.8",
|
||||||
|
"@budibase/string-templates": "^1.0.8",
|
||||||
"@bull-board/api": "^3.7.0",
|
"@bull-board/api": "^3.7.0",
|
||||||
"@bull-board/koa": "^3.7.0",
|
"@bull-board/koa": "^3.7.0",
|
||||||
"@elastic/elasticsearch": "7.10.0",
|
"@elastic/elasticsearch": "7.10.0",
|
||||||
|
@ -85,12 +86,14 @@
|
||||||
"bull": "^3.22.4",
|
"bull": "^3.22.4",
|
||||||
"chmodr": "1.2.0",
|
"chmodr": "1.2.0",
|
||||||
"csvtojson": "2.0.10",
|
"csvtojson": "2.0.10",
|
||||||
|
"curlconverter": "^3.21.0",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
"download": "8.0.0",
|
"download": "8.0.0",
|
||||||
"fix-path": "3.0.0",
|
"fix-path": "3.0.0",
|
||||||
"fs-extra": "8.1.0",
|
"fs-extra": "8.1.0",
|
||||||
"jimp": "0.16.1",
|
"jimp": "0.16.1",
|
||||||
"joi": "17.2.1",
|
"joi": "17.2.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"jsonschema": "1.4.0",
|
"jsonschema": "1.4.0",
|
||||||
"knex": "^0.95.6",
|
"knex": "^0.95.6",
|
||||||
"koa": "2.7.0",
|
"koa": "2.7.0",
|
||||||
|
@ -146,6 +149,7 @@
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"jest": "^27.0.5",
|
"jest": "^27.0.5",
|
||||||
"nodemon": "^2.0.4",
|
"nodemon": "^2.0.4",
|
||||||
|
"openapi-types": "^9.3.1",
|
||||||
"path-to-regexp": "^6.2.0",
|
"path-to-regexp": "^6.2.0",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"rimraf": "^3.0.2",
|
"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 { processString } = require("@budibase/string-templates")
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../../db")
|
||||||
const {
|
const {
|
||||||
generateQueryID,
|
generateQueryID,
|
||||||
getQueryParams,
|
getQueryParams,
|
||||||
isProdAppID,
|
isProdAppID,
|
||||||
} = require("../../db/utils")
|
} = require("../../../db/utils")
|
||||||
const { BaseQueryVerbs } = require("../../constants")
|
const { BaseQueryVerbs } = require("../../../constants")
|
||||||
const { Thread, ThreadType } = require("../../threads")
|
const { Thread, ThreadType } = require("../../../threads")
|
||||||
|
const { save: saveDatasource } = require("../datasource")
|
||||||
|
const { RestImporter } = require("./import")
|
||||||
|
|
||||||
const Runner = new Thread(ThreadType.QUERY, { timeoutMs: 10000 })
|
const Runner = new Thread(ThreadType.QUERY, { timeoutMs: 10000 })
|
||||||
|
|
||||||
|
@ -30,9 +32,49 @@ exports.fetch = async function (ctx) {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx.body = enrichQueries(body.rows.map(row => row.doc))
|
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) {
|
exports.save = async function (ctx) {
|
||||||
const db = new CouchDB(ctx.appId)
|
const db = new CouchDB(ctx.appId)
|
||||||
const query = ctx.request.body
|
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 Router = require("@koa/router")
|
||||||
const queryController = require("../controllers/query")
|
const queryController = require("../controllers/query")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const Joi = require("joi")
|
|
||||||
const {
|
const {
|
||||||
PermissionLevels,
|
PermissionLevels,
|
||||||
PermissionTypes,
|
PermissionTypes,
|
||||||
BUILDER,
|
BUILDER,
|
||||||
} = require("@budibase/auth/permissions")
|
} = require("@budibase/auth/permissions")
|
||||||
const joiValidator = require("../../middleware/joi-validator")
|
|
||||||
const {
|
const {
|
||||||
bodyResource,
|
bodyResource,
|
||||||
bodySubResource,
|
bodySubResource,
|
||||||
paramResource,
|
paramResource,
|
||||||
} = require("../../middleware/resourceId")
|
} = require("../../middleware/resourceId")
|
||||||
|
const {
|
||||||
|
generateQueryPreviewValidation,
|
||||||
|
generateQueryValidation,
|
||||||
|
} = require("../controllers/query/validation")
|
||||||
|
|
||||||
const router = Router()
|
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
|
router
|
||||||
.get("/api/queries", authorized(BUILDER), queryController.fetch)
|
.get("/api/queries", authorized(BUILDER), queryController.fetch)
|
||||||
.post(
|
.post(
|
||||||
|
@ -58,6 +27,7 @@ router
|
||||||
generateQueryValidation(),
|
generateQueryValidation(),
|
||||||
queryController.save
|
queryController.save
|
||||||
)
|
)
|
||||||
|
.post("/api/queries/import", authorized(BUILDER), queryController.import)
|
||||||
.post(
|
.post(
|
||||||
"/api/queries/preview",
|
"/api/queries/preview",
|
||||||
bodyResource("datasourceId"),
|
bodyResource("datasourceId"),
|
||||||
|
|
|
@ -107,3 +107,25 @@ export interface Datasource extends Base {
|
||||||
[key: string]: Table
|
[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
|
return lastRow.rows
|
||||||
} else {
|
} 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",
|
"name": "@budibase/string-templates",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"description": "Handlebars wrapper for Budibase templating.",
|
"description": "Handlebars wrapper for Budibase templating.",
|
||||||
"main": "src/index.cjs",
|
"main": "src/index.cjs",
|
||||||
"module": "dist/bundle.mjs",
|
"module": "dist/bundle.mjs",
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@budibase/worker",
|
"name": "@budibase/worker",
|
||||||
"email": "hi@budibase.com",
|
"email": "hi@budibase.com",
|
||||||
"version": "1.0.7",
|
"version": "1.0.8",
|
||||||
"description": "Budibase background service",
|
"description": "Budibase background service",
|
||||||
"main": "src/index.js",
|
"main": "src/index.js",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -29,8 +29,8 @@
|
||||||
"author": "Budibase",
|
"author": "Budibase",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@budibase/auth": "^1.0.7",
|
"@budibase/auth": "^1.0.8",
|
||||||
"@budibase/string-templates": "^1.0.7",
|
"@budibase/string-templates": "^1.0.8",
|
||||||
"@koa/router": "^8.0.0",
|
"@koa/router": "^8.0.0",
|
||||||
"@sentry/node": "^6.0.0",
|
"@sentry/node": "^6.0.0",
|
||||||
"@techpass/passport-openidconnect": "^0.3.0",
|
"@techpass/passport-openidconnect": "^0.3.0",
|
||||||
|
|
Loading…
Reference in New Issue