Skip to content

Commit

Permalink
Merge pull request #10621 from Budibase/feature/datasource-conns
Browse files Browse the repository at this point in the history
Verify datasource connections
  • Loading branch information
adrinr authored May 17, 2023
2 parents 7e8dcbc + afa630c commit 0cb8381
Show file tree
Hide file tree
Showing 43 changed files with 1,413 additions and 143 deletions.
14 changes: 10 additions & 4 deletions packages/backend-core/src/db/couch/DatabaseImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
isDocument,
} from "@budibase/types"
import { getCouchInfo } from "./connections"
import { directCouchCall } from "./utils"
import { directCouchUrlCall } from "./utils"
import { getPouchDB } from "./pouchDB"
import { WriteStream, ReadStream } from "fs"
import { newid } from "../../docIds/newid"
Expand Down Expand Up @@ -46,15 +46,17 @@ export class DatabaseImpl implements Database {
private readonly instanceNano?: Nano.ServerScope
private readonly pouchOpts: DatabaseOpts

private readonly couchInfo = getCouchInfo()

constructor(dbName?: string, opts?: DatabaseOpts, connection?: string) {
if (dbName == null) {
throw new Error("Database name cannot be undefined.")
}
this.name = dbName
this.pouchOpts = opts || {}
if (connection) {
const couchInfo = getCouchInfo(connection)
this.instanceNano = buildNano(couchInfo)
this.couchInfo = getCouchInfo(connection)
this.instanceNano = buildNano(this.couchInfo)
}
if (!DatabaseImpl.nano) {
DatabaseImpl.init()
Expand All @@ -67,7 +69,11 @@ export class DatabaseImpl implements Database {
}

async exists() {
let response = await directCouchCall(`/${this.name}`, "HEAD")
const response = await directCouchUrlCall({
url: `${this.couchInfo.url}/${this.name}`,
method: "HEAD",
cookie: this.couchInfo.cookie,
})
return response.status === 200
}

Expand Down
16 changes: 8 additions & 8 deletions packages/backend-core/src/db/couch/connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ export const getCouchInfo = (connection?: string) => {
const urlInfo = getUrlInfo(connection)
let username
let password
if (env.COUCH_DB_USERNAME) {
// set from env
username = env.COUCH_DB_USERNAME
} else if (urlInfo.auth.username) {
if (urlInfo.auth?.username) {
// set from url
username = urlInfo.auth.username
} else if (env.COUCH_DB_USERNAME) {
// set from env
username = env.COUCH_DB_USERNAME
} else if (!env.isTest()) {
throw new Error("CouchDB username not set")
}
if (env.COUCH_DB_PASSWORD) {
// set from env
password = env.COUCH_DB_PASSWORD
} else if (urlInfo.auth.password) {
if (urlInfo.auth?.password) {
// set from url
password = urlInfo.auth.password
} else if (env.COUCH_DB_PASSWORD) {
// set from env
password = env.COUCH_DB_PASSWORD
} else if (!env.isTest()) {
throw new Error("CouchDB password not set")
}
Expand Down
16 changes: 15 additions & 1 deletion packages/backend-core/src/db/couch/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ export async function directCouchCall(
) {
let { url, cookie } = getCouchInfo()
const couchUrl = `${url}/${path}`
return await directCouchUrlCall({ url: couchUrl, cookie, method, body })
}

export async function directCouchUrlCall({
url,
cookie,
method,
body,
}: {
url: string
cookie: string
method: string
body?: any
}) {
const params: any = {
method: method,
headers: {
Expand All @@ -19,7 +33,7 @@ export async function directCouchCall(
params.body = JSON.stringify(body)
params.headers["Content-Type"] = "application/json"
}
return await fetch(checkSlashesInUrl(encodeURI(couchUrl)), params)
return await fetch(checkSlashesInUrl(encodeURI(url)), params)
}

export async function directCouchQuery(
Expand Down
3 changes: 2 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
"mysql2": "2.3.3",
"node-fetch": "2.6.7",
"open": "8.4.0",
"pg": "8.5.1",
"pg": "8.10.0",
"posthog-node": "1.3.0",
"pouchdb": "7.3.0",
"pouchdb-adapter-memory": "7.2.2",
Expand Down Expand Up @@ -141,6 +141,7 @@
"@types/node": "14.18.20",
"@types/node-fetch": "2.6.1",
"@types/oracledb": "5.2.2",
"@types/pg": "8.6.6",
"@types/pouchdb": "6.4.0",
"@types/redis": "4.0.11",
"@types/server-destroy": "1.0.1",
Expand Down
137 changes: 88 additions & 49 deletions packages/server/src/api/controllers/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,71 @@ import {
Row,
CreateDatasourceResponse,
UpdateDatasourceResponse,
UpdateDatasourceRequest,
CreateDatasourceRequest,
VerifyDatasourceRequest,
VerifyDatasourceResponse,
IntegrationBase,
DatasourcePlus,
} from "@budibase/types"
import sdk from "../../sdk"

function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors)
.filter(entry => entry[1] === errorType)
.map(([name]) => name)
}

function updateError(error: any, newError: any, tables: string[]) {
if (!error) {
error = ""
}
if (error.length > 0) {
error += "\n"
}
error += `${newError} ${tables.join(", ")}`
return error
}

async function getConnector(
datasource: Datasource
): Promise<IntegrationBase | DatasourcePlus> {
const Connector = await getIntegration(datasource.source)
// can't enrich if it doesn't have an ID yet
if (datasource._id) {
datasource = await sdk.datasources.enrich(datasource)
}
// Connect to the DB and build the schema
return new Connector(datasource.config)
}

async function buildSchemaHelper(datasource: Datasource) {
const connector = (await getConnector(datasource)) as DatasourcePlus
await connector.buildSchema(datasource._id!, datasource.entities!)

const errors = connector.schemaErrors
let error = null
if (errors && Object.keys(errors).length > 0) {
const noKey = getErrorTables(errors, BuildSchemaErrors.NO_KEY)
const invalidCol = getErrorTables(errors, BuildSchemaErrors.INVALID_COLUMN)
if (noKey.length) {
error = updateError(
error,
"No primary key constraint found for the following:",
noKey
)
}
if (invalidCol.length) {
const invalidCols = Object.values(InvalidColumns).join(", ")
error = updateError(
error,
`Cannot use columns ${invalidCols} found in following:`,
invalidCol
)
}
}
return { tables: connector.tables, error }
}

export async function fetch(ctx: UserCtx) {
// Get internal tables
const db = context.getAppDB()
Expand Down Expand Up @@ -66,6 +126,33 @@ export async function fetch(ctx: UserCtx) {
ctx.body = [bbInternalDb, ...datasources]
}

export async function verify(
ctx: UserCtx<VerifyDatasourceRequest, VerifyDatasourceResponse>
) {
const { datasource } = ctx.request.body
let existingDatasource: undefined | Datasource
if (datasource._id) {
existingDatasource = await sdk.datasources.get(datasource._id)
}
let enrichedDatasource = datasource
if (existingDatasource) {
enrichedDatasource = sdk.datasources.mergeConfigs(
datasource,
existingDatasource
)
}
const connector = await getConnector(enrichedDatasource)
if (!connector.testConnection) {
ctx.throw(400, "Connection information verification not supported")
}
const response = await connector.testConnection()

ctx.body = {
connected: response.connected,
error: response.error,
}
}

export async function buildSchemaFromDb(ctx: UserCtx) {
const db = context.getAppDB()
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
Expand Down Expand Up @@ -311,51 +398,3 @@ export async function query(ctx: UserCtx) {
ctx.throw(400, err)
}
}

function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors)
.filter(entry => entry[1] === errorType)
.map(([name]) => name)
}

function updateError(error: any, newError: any, tables: string[]) {
if (!error) {
error = ""
}
if (error.length > 0) {
error += "\n"
}
error += `${newError} ${tables.join(", ")}`
return error
}

async function buildSchemaHelper(datasource: Datasource) {
const Connector = await getIntegration(datasource.source)
datasource = await sdk.datasources.enrich(datasource)
// Connect to the DB and build the schema
const connector = new Connector(datasource.config)
await connector.buildSchema(datasource._id, datasource.entities)

const errors = connector.schemaErrors
let error = null
if (errors && Object.keys(errors).length > 0) {
const noKey = getErrorTables(errors, BuildSchemaErrors.NO_KEY)
const invalidCol = getErrorTables(errors, BuildSchemaErrors.INVALID_COLUMN)
if (noKey.length) {
error = updateError(
error,
"No primary key constraint found for the following:",
noKey
)
}
if (invalidCol.length) {
const invalidCols = Object.values(InvalidColumns).join(", ")
error = updateError(
error,
`Cannot use columns ${invalidCols} found in following:`,
invalidCol
)
}
}
return { tables: connector.tables, error }
}
6 changes: 3 additions & 3 deletions packages/server/src/api/controllers/integration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getDefinitions } from "../../integrations"
import { getDefinition, getDefinitions } from "../../integrations"
import { BBContext } from "@budibase/types"

export async function fetch(ctx: BBContext) {
Expand All @@ -7,7 +7,7 @@ export async function fetch(ctx: BBContext) {
}

export async function find(ctx: BBContext) {
const defs = await getDefinitions()
const def = await getDefinition(ctx.params.type)
ctx.body = def
ctx.status = 200
ctx.body = defs[ctx.params.type]
}
5 changes: 5 additions & 0 deletions packages/server/src/api/routes/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ router
authorized(permissions.BUILDER),
datasourceController.fetch
)
.post(
"/api/datasources/verify",
authorized(permissions.BUILDER),
datasourceController.verify
)
.get(
"/api/datasources/:datasourceId",
authorized(
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/db/dynamoClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export function init(endpoint: string) {
docClient = new AWS.DynamoDB.DocumentClient(docClientParams)
}

if (!env.isProd()) {
if (!env.isProd() && !env.isJest()) {
env._set("AWS_ACCESS_KEY_ID", "KEY_ID")
env._set("AWS_SECRET_ACCESS_KEY", "SECRET_KEY")
init("http://localhost:8333")
Expand Down
1 change: 0 additions & 1 deletion packages/server/src/integration-test/postgres.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import _ from "lodash"
import { generator } from "@budibase/backend-core/tests"
import { utils } from "@budibase/backend-core"
import { GenericContainer } from "testcontainers"
import { generateRowIdField } from "../integrations/utils"

const config = setup.getConfig()!

Expand Down
35 changes: 31 additions & 4 deletions packages/server/src/integrations/airtable.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
Integration,
ConnectionInfo,
DatasourceFeature,
DatasourceFieldType,
QueryType,
Integration,
IntegrationBase,
QueryType,
} from "@budibase/types"

const Airtable = require("airtable")
import Airtable from "airtable"

interface AirtableConfig {
apiKey: string
Expand All @@ -18,6 +20,7 @@ const SCHEMA: Integration = {
"Airtable is a spreadsheet-database hybrid, with the features of a database but applied to a spreadsheet.",
friendlyName: "Airtable",
type: "Spreadsheet",
features: [DatasourceFeature.CONNECTION_CHECKING],
datasource: {
apiKey: {
type: DatasourceFieldType.PASSWORD,
Expand Down Expand Up @@ -81,13 +84,37 @@ const SCHEMA: Integration = {

class AirtableIntegration implements IntegrationBase {
private config: AirtableConfig
private client: any
private client

constructor(config: AirtableConfig) {
this.config = config
this.client = new Airtable(config).base(config.base)
}

async testConnection(): Promise<ConnectionInfo> {
const mockTable = Date.now().toString()
try {
await this.client.makeRequest({
path: `/${mockTable}`,
})

return { connected: true }
} catch (e: any) {
if (
e.message ===
`Could not find table ${mockTable} in application ${this.config.base}`
) {
// The request managed to check the application, so the credentials are valid
return { connected: true }
}

return {
connected: false,
error: e.message as string,
}
}
}

async create(query: { table: any; json: any }) {
const { table, json } = query

Expand Down
Loading

0 comments on commit 0cb8381

Please sign in to comment.