1
0
Fork 0
mirror of synced 2024-06-14 08:24:48 +12:00

Update backend to be extensible for different import sources

This commit is contained in:
Rory Powell 2021-11-29 10:37:31 +00:00
parent 25fd268dd4
commit adea1d052b
11 changed files with 1195 additions and 214 deletions

View file

@ -9,6 +9,9 @@ export const Events = {
CREATED: "Datasource Created",
UPDATED: "Datasource Updated",
},
QUERIES: {
REST: "REST Queries Imported",
},
TABLE: {
CREATED: "Table Created",
},

View file

@ -6,7 +6,7 @@
import { IntegrationNames } from "constants"
import CreateTableModal from "components/backend/TableNavigator/modals/CreateTableModal.svelte"
import DatasourceConfigModal from "components/backend/DatasourceNavigator/modals/DatasourceConfigModal.svelte"
import ImportRestDatasourceModal from "./ImportRestDatasourceModal.svelte"
import ImportRestQueriesModal from "./ImportRestQueriesModal.svelte"
export let modal
let integrations = []
@ -80,7 +80,7 @@
<Modal bind:this={importModal}>
{#if integration.type === "REST"}
<ImportRestDatasourceModal {integration} {modal} />
<ImportRestQueriesModal {integration} {modal} createDatasource={true} />
{/if}
</Modal>

View file

@ -16,6 +16,8 @@
import { datasources, queries } from "stores/backend"
export let modal
export let datasourceId
export let createDatasource = false
let data = {
url: "",
@ -25,47 +27,60 @@
let lastTouched = "url"
const getPayload = async () => {
let payloadData
let type
const getData = async () => {
let dataString
// parse the file into memory and send as string
if (lastTouched === "file") {
type = "raw"
payloadData = await data.file[0].text()
} else {
type = lastTouched
payloadData = data[lastTouched]
dataString = await data.file[0].text()
} else if (lastTouched === "url") {
const response = await fetch(data.url)
dataString = await response.text()
} else if (lastTouched === "raw") {
dataString = data.raw
}
return {
type: type,
data: payloadData,
}
return dataString
}
async function importDatasource() {
async function importQueries() {
try {
const resp = await datasources.import(await getPayload())
const dataString = await getData()
if (!datasourceId && !createDatasource) {
throw new Error("No datasource id")
}
const body = {
data: dataString,
datasourceId,
}
const resp = await queries.import(body)
datasourceId = resp.datasourceId
// reload
await datasources.fetch()
await queries.fetch()
await datasources.select(resp._id)
$goto(`./datasource/${resp._id}`)
notifications.success(`Datasource imported successfully.`)
analytics.captureEvent(Events.DATASOURCE.IMPORTED, {
name: resp.name,
source: resp.source,
await datasources.select(datasourceId)
$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 datasource: ${err}`)
notifications.error(`Error importing: ${err}`)
return false
}
}
</script>
<ModalContent
onConfirm={() => importDatasource()}
onConfirm={() => importQueries()}
onCancel={() => modal.show()}
confirmText={"Import"}
cancelText="Back"

View file

@ -84,11 +84,6 @@ export function createDatasourcesStore() {
return updateDatasource(response)
},
import: async body => {
let response
response = await api.post(`/api/queries/import/swagger2`, body)
return updateDatasource(response)
},
delete: async datasource => {
const response = await api.delete(
`/api/datasources/${datasource._id}/${datasource._rev}`

View file

@ -53,6 +53,10 @@ export function createQueriesStore() {
})
return json
},
import: async body => {
const response = await api.post(`/api/queries/import`, body)
return response.json()
},
select: query => {
update(state => ({ ...state, selected: query._id }))
views.unselect()

View file

@ -77,6 +77,7 @@
"@koa/router": "8.0.0",
"@sendgrid/mail": "7.1.1",
"@sentry/node": "^6.0.0",
"@types/swagger-schema-official": "^2.0.22",
"airtable": "0.10.1",
"arangojs": "7.2.0",
"aws-sdk": "^2.767.0",

View file

@ -0,0 +1,275 @@
import CouchDB from "../../../db"
import { queryValidation } from "./validation"
import { generateQueryID } from "../../../db/utils"
import { Spec as Swagger2, Operation } from "swagger-schema-official"
// {
// "_id": "query_datasource_d62738f2d72a466997ffbf46f4952404_e7258ad382cd4c37961b81730633ff2d",
// "_rev": "1-e702a18eaa96c7cb4be1b402c34eaa59",
// "datasourceId": "datasource_d62738f2d72a466997ffbf46f4952404",
// "parameters": [
// {
// "name": "paramtest",
// "default": "defaultValue"
// }
// ],
// "fields": {
// "headers": {
// "headertest": "test"
// },
// "queryString": "query=test",
// "path": "/path/test"
// },
// "queryVerb": "read",
// "transformer": "return data.test",
// "schema": {},
// "name": "name",
// "readable": true
// }
// 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(),
// }))
interface Parameter {
name: string
default: string
}
interface Query {
_id?: string
datasourceId: string
name: string
parameters: Parameter[]
fields: {
headers: any
queryString: string
path: string
}
transformer: string | null
schema: any
readable: boolean
queryVerb: string
}
enum Strategy {
SWAGGER2,
OPENAPI3,
CURL,
}
enum MethodToVerb {
get = "read",
post = "create",
put = "update",
patch = "patch",
delete = "delete",
}
interface ImportResult {
errorQueries: Query[]
}
interface DatasourceInfo {
url: string
name: string
defaultHeaders: any[]
}
const parseImportStrategy = (data: string): Strategy => {
return Strategy.SWAGGER2
}
// SWAGGER
const parseSwagger2Info = (swagger2: Swagger2): DatasourceInfo => {
return {
url: "http://localhost:3000",
name: "swagger",
defaultHeaders: [],
}
}
const parseSwagger2Queries = (
datasourceId: string,
swagger2: Swagger2
): Query[] => {
const queries = []
for (let [pathName, path] of Object.entries(swagger2.paths)) {
for (let [methodName, op] of Object.entries(path)) {
let operation = op as Operation
const name = operation.operationId || pathName
const queryString = ""
const headers = {}
const parameters: Parameter[] = []
const query = constructQuery(
datasourceId,
name,
methodName,
pathName,
queryString,
headers,
parameters
)
queries.push(query)
}
}
return queries
}
// OPEN API
const parseOpenAPI3Info = (data: any): DatasourceInfo => {
return {
url: "http://localhost:3000",
name: "swagger",
defaultHeaders: [],
}
}
const parseOpenAPI3Queries = (datasourceId: string, data: string): Query[] => {
return []
}
// CURL
const parseCurlDatasourceInfo = (data: any): DatasourceInfo => {
return {
url: "http://localhost:3000",
name: "swagger",
defaultHeaders: [],
}
}
const parseCurlQueries = (datasourceId: string, data: string): Query[] => {
return []
}
const verbFromMethod = (method: string) => {
const verb = (<any>MethodToVerb)[method]
if (!verb) {
throw new Error(`Unsupported method: ${method}`)
}
return verb
}
const constructQuery = (
datasourceId: string,
name: string,
method: string,
path: string,
queryString: string,
headers: any = {},
parameters: Parameter[] = []
): Query => {
const readable = true
const queryVerb = verbFromMethod(method)
const transformer = "return data"
const schema = {}
const query: Query = {
datasourceId,
name,
parameters,
fields: {
headers,
queryString,
path,
},
transformer,
schema,
readable,
queryVerb,
}
return query
}
export const getDatasourceInfo = (data: string): DatasourceInfo => {
const strategy = parseImportStrategy(data)
let info: DatasourceInfo
switch (strategy) {
case Strategy.SWAGGER2:
info = parseSwagger2Info(JSON.parse(data))
break
case Strategy.OPENAPI3:
info = parseOpenAPI3Info(JSON.parse(data))
break
case Strategy.CURL:
info = parseCurlDatasourceInfo(data)
break
}
return info
}
export const importQueries = async (
appId: string,
datasourceId: string,
data: string
): Promise<ImportResult> => {
const strategy = parseImportStrategy(data)
// constuct the queries
let queries: Query[]
switch (strategy) {
case Strategy.SWAGGER2:
queries = parseSwagger2Queries(datasourceId, JSON.parse(data))
break
case Strategy.OPENAPI3:
queries = parseOpenAPI3Queries(datasourceId, JSON.parse(data))
break
case Strategy.CURL:
queries = parseCurlQueries(datasourceId, data)
break
}
// validate queries
const errorQueries = []
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)
for (const query of queries) {
try {
await db.put(query)
} catch (error) {
errorQueries.push(query)
}
}
return {
errorQueries,
}
}

View file

@ -1,13 +1,11 @@
const { processString } = require("@budibase/string-templates")
const CouchDB = require("../../db")
const { generateQueryID, getQueryParams } = require("../../db/utils")
const { BaseQueryVerbs } = require("../../constants")
const env = require("../../environment")
const { Thread, ThreadType } = require("../../threads")
const fetch = require("node-fetch")
const Joi = require("joi")
const { save: saveDatasource } = require("./datasource")
const CouchDB = require("../../../db")
const { generateQueryID, getQueryParams } = require("../../../db/utils")
const { BaseQueryVerbs } = require("../../../constants")
const env = require("../../../environment")
const { Thread, ThreadType } = require("../../../threads")
const { importQueries, getDatasourceInfo } = require("./import")
const { save: saveDatasource } = require("../datasource")
const Runner = new Thread(ThreadType.QUERY, { timeoutMs: 10000 })
@ -31,134 +29,43 @@ exports.fetch = async function (ctx) {
include_docs: true,
})
)
ctx.body = enrichQueries(body.rows.map(row => row.doc))
}
// const query = {
// datasourceId: "datasource_b9a474302a174d1295e4c273cd72bde9",
// name: "available pets (import)",
// parameters: [],
// fields: {
// headers: {},
// queryString: "status=available",
// path: "v2/pet/findByStatus",
// },
// queryVerb: "read",
// transformer: "return data",
// schema: {},
// readable: true
// }
exports.import = async ctx => {
const body = ctx.request.body
const data = body.data
function generateQueryValidation() {
// prettier-ignore
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(),
})
}
const verbs = {
get: "read",
post: "create",
put: "update",
patch: "patch",
delete: "delete",
}
const constructQuery = (datasource, swagger, path, method, config) => {
const query = {
datasourceId: datasource._id,
}
query.name = config.operationId || path
query.parameters = []
query.fields = {
headers: {},
// queryString: "status=available",
path: path,
}
query.transformer = "return data"
query.schema = {}
query.readable = true
query.queryVerb = verbs[method]
return query
}
// {
// "type": "url",
// "data": "www.url.com/swagger.json"
// }
exports.import = async function (ctx) {
const importConfig = ctx.request.body
let data
if (importConfig.type === "url") {
data = await fetch(importConfig.data).then(res => res.json())
} else if (importConfig.type === "raw") {
data = JSON.parse(importConfig.data)
} else {
throw new Error("Invalid data type")
}
const db = new CouchDB(ctx.appId)
const schema = generateQueryValidation()
// create datasource
const scheme = data.schemes.includes("https") ? "https" : "http"
const url = `${scheme}://${data.host}${data.basePath}`
const name = data.info.title
// TODO: Refactor datasource creation into shared function
const datasourceCtx = {
...ctx,
}
datasourceCtx.request.body.datasource = {
type: "datasource",
source: "REST",
config: {
url: url,
defaultHeaders: {},
},
name: name,
}
await saveDatasource(datasourceCtx)
const datasource = datasourceCtx.body.datasource
// create query
for (const [path, method] of Object.entries(data.paths)) {
for (const [methodName, config] of Object.entries(method)) {
const query = constructQuery(datasource, data, path, methodName, config)
// validate query
const { error } = schema.validate(query)
if (error) {
ctx.throw(400, `Invalid - ${error.message}`)
return
}
// persist query
query._id = generateQueryID(query.datasourceId)
await db.put(query)
let datasourceId
if (!body.datasourceId) {
// construct new datasource
const info = getDatasourceInfo(data)
let datasource = {
type: "datasource",
source: "REST",
config: {
url: info.url,
defaultHeaders: info.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
}
// return the datasource
ctx.body = { datasource }
const importResult = await importQueries(ctx.appId, datasourceId, data)
ctx.body = {
...importResult,
datasourceId,
}
ctx.status = 200
}

View file

@ -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)
}))
}

View file

@ -1,53 +1,23 @@
const Router = require("@koa/router")
const queryController = require("../controllers/query")
const authorized = require("../../middleware/authorized")
const Joi = require("joi")
const {
PermissionLevels,
PermissionTypes,
BUILDER,
} = require("@budibase/auth/permissions")
const joiValidator = require("../../middleware/joi-validator")
const {
bodyResource,
bodySubResource,
paramResource,
} = require("../../middleware/resourceId")
const {
generateQueryPreviewValidation,
generateQueryValidation,
} = require("../controllers/query/validation")
const router = Router()
function generateQueryValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
_id: Joi.string(),
_rev: Joi.string(),
name: Joi.string().required(),
fields: Joi.object().required(),
datasourceId: Joi.string().required(),
readable: Joi.boolean(),
parameters: Joi.array().items(Joi.object({
name: Joi.string(),
default: Joi.string().allow(""),
})),
queryVerb: Joi.string().allow().required(),
extra: Joi.object().optional(),
schema: Joi.object({}).required().unknown(true),
transformer: Joi.string().optional(),
}))
}
function generateQueryPreviewValidation() {
// prettier-ignore
return joiValidator.body(Joi.object({
fields: Joi.object().required(),
queryVerb: Joi.string().allow().required(),
extra: Joi.object().optional(),
datasourceId: Joi.string().required(),
transformer: Joi.string().optional(),
parameters: Joi.object({}).required().unknown(true)
}))
}
router
.get("/api/queries", authorized(BUILDER), queryController.fetch)
.post(
@ -57,11 +27,7 @@ router
generateQueryValidation(),
queryController.save
)
.post(
"/api/queries/import/swagger2",
authorized(BUILDER),
queryController.import
)
.post("/api/queries/import", authorized(BUILDER), queryController.import)
.post(
"/api/queries/preview",
bodyResource("datasourceId"),

File diff suppressed because it is too large Load diff