Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Datasource+ table fetching API #10659

Merged
merged 15 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/server/src/api/controllers/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
CreateDatasourceRequest,
VerifyDatasourceRequest,
VerifyDatasourceResponse,
FetchTablesDatasourceResponse,
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
IntegrationBase,
DatasourcePlus,
} from "@budibase/types"
Expand Down Expand Up @@ -153,6 +154,21 @@ export async function verify(
}
}

export async function fetchTables(
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
ctx: UserCtx<void, FetchTablesDatasourceResponse>
) {
const datasourceId = ctx.params.datasourceId
const datasource = await sdk.datasources.get(datasourceId, { enriched: true })
const connector = (await getConnector(datasource)) as DatasourcePlus
if (!connector.getTableNames) {
ctx.throw(400, "Table name fetching not supported by datasource")
}
const tableNames = await connector.getTableNames()
ctx.body = {
tableNames,
}
}

export async function buildSchemaFromDb(ctx: UserCtx) {
const db = context.getAppDB()
const datasource = await sdk.datasources.get(ctx.params.datasourceId)
Expand Down
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 @@ -20,6 +20,11 @@ router
authorized(permissions.BUILDER),
datasourceController.verify
)
.get(
"/api/datasources/:datasourceId/tables",
authorized(permissions.BUILDER),
datasourceController.fetchTables
)
.get(
"/api/datasources/:datasourceId",
authorized(
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/api/routes/tests/datasource.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe("/datasources", () => {
expect(contents.rows.length).toEqual(1)

// update the datasource to remove the variables
datasource.config.dynamicVariables = []
datasource.config!.dynamicVariables = []
const res = await request
.put(`/api/datasources/${datasource._id}`)
.send(datasource)
Expand Down
51 changes: 47 additions & 4 deletions packages/server/src/integration-test/postgres.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jest.setTimeout(30000)

jest.unmock("pg")

describe("row api - postgres", () => {
describe("postgres integrations", () => {
let makeRequest: MakeRequestResponse,
postgresDatasource: Datasource,
primaryPostgresTable: Table,
Expand All @@ -52,8 +52,8 @@ describe("row api - postgres", () => {
makeRequest = generateMakeRequest(apiKey, true)
})

beforeEach(async () => {
postgresDatasource = await config.createDatasource({
function pgDatasourceConfig() {
return {
datasource: {
type: "datasource",
source: SourceName.POSTGRES,
Expand All @@ -70,7 +70,11 @@ describe("row api - postgres", () => {
ca: false,
},
},
})
}
}

beforeEach(async () => {
postgresDatasource = await config.createDatasource(pgDatasourceConfig())

async function createAuxTable(prefix: string) {
return await config.createTable({
Expand Down Expand Up @@ -1024,4 +1028,43 @@ describe("row api - postgres", () => {
})
})
})

describe("POST /api/datasources/verify", () => {
it("should be able to verify the connection", async () => {
const config = pgDatasourceConfig()
const response = await makeRequest(
"post",
"/api/datasources/verify",
config
)
expect(response.status).toBe(200)
expect(response.body.connected).toBe(true)
})

it("should state an invalid datasource cannot connect", async () => {
const config = pgDatasourceConfig()
config.datasource.config.password = "wrongpassword"
const response = await makeRequest(
"post",
"/api/datasources/verify",
config
)
expect(response.status).toBe(200)
expect(response.body.connected).toBe(false)
expect(response.body.error).toBeDefined()
})
})

describe("GET /api/datasources/:datasourceId/tables", () => {
it("should fetch tables within postgres datasource", async () => {
const primaryName = primaryPostgresTable.name
const response = await makeRequest(
"get",
`/api/datasources/${postgresDatasource._id}/tables`
)
expect(response.status).toBe(200)
expect(response.body.tableNames).toBeDefined()
expect(response.body.tableNames.indexOf(primaryName)).not.toBe(-1)
})
})
})
12 changes: 10 additions & 2 deletions packages/server/src/integrations/googlesheets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,13 @@ const SCHEMA: Integration = {
relationships: false,
docs: "https://developers.google.com/sheets/api/quickstart/nodejs",
description:
"Create and collaborate on online spreadsheets in real-time and from any device. ",
"Create and collaborate on online spreadsheets in real-time and from any device.",
friendlyName: "Google Sheets",
type: "Spreadsheet",
features: [DatasourceFeature.CONNECTION_CHECKING],
features: [
DatasourceFeature.CONNECTION_CHECKING,
DatasourceFeature.FETCH_TABLE_NAMES,
],
datasource: {
spreadsheetId: {
display: "Google Sheet URL",
Expand Down Expand Up @@ -240,6 +243,11 @@ class GoogleSheetsIntegration implements DatasourcePlus {
}
}

getTableNames(): Promise<string[]> {
// TODO: implement
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
return Promise.resolve([])
}

getTableSchema(title: string, headerValues: string[], id?: string) {
// base table
const table: Table = {
Expand Down
20 changes: 18 additions & 2 deletions packages/server/src/integrations/microsoftSqlServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
} from "./utils"
import Sql from "./base/sql"
import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"

const sqlServer = require("mssql")
const DEFAULT_SCHEMA = "dbo"

Expand All @@ -41,7 +40,10 @@ const SCHEMA: Integration = {
"Microsoft SQL Server is a relational database management system developed by Microsoft. ",
friendlyName: "MS SQL Server",
type: "Relational",
features: [DatasourceFeature.CONNECTION_CHECKING],
features: [
DatasourceFeature.CONNECTION_CHECKING,
DatasourceFeature.FETCH_TABLE_NAMES,
],
datasource: {
user: {
type: DatasourceFieldType.STRING,
Expand Down Expand Up @@ -284,6 +286,20 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
this.schemaErrors = final.errors
}

async queryTableNames() {
let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL)
const schema = this.config.schema || DEFAULT_SCHEMA
return tableInfo
.filter((record: any) => record.TABLE_SCHEMA === schema)
.map((record: any) => record.TABLE_NAME)
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
}

async getTableNames() {
await this.connect()
return this.queryTableNames()
}

async read(query: SqlQuery | string) {
await this.connect()
const response = await this.internalQuery(getSqlQuery(query))
Expand Down
38 changes: 27 additions & 11 deletions packages/server/src/integrations/mysql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ const SCHEMA: Integration = {
type: "Relational",
description:
"MySQL Database Service is a fully managed database service to deploy cloud-native applications. ",
features: [DatasourceFeature.CONNECTION_CHECKING],
features: [
DatasourceFeature.CONNECTION_CHECKING,
DatasourceFeature.FETCH_TABLE_NAMES,
],
datasource: {
host: {
type: DatasourceFieldType.STRING,
Expand Down Expand Up @@ -214,20 +217,11 @@ class MySQLIntegration extends Sql implements DatasourcePlus {

async buildSchema(datasourceId: string, entities: Record<string, Table>) {
const tables: { [key: string]: Table } = {}
const database = this.config.database
await this.connect()

try {
// get the tables first
const tablesResp: Record<string, string>[] = await this.internalQuery(
{ sql: "SHOW TABLES;" },
{ connect: false }
)
const tableNames: string[] = tablesResp.map(
(obj: any) =>
obj[`Tables_in_${database}`] ||
obj[`Tables_in_${database.toLowerCase()}`]
)
const tableNames = await this.queryTableNames()
for (let tableName of tableNames) {
const primaryKeys = []
const schema: TableSchema = {}
Expand Down Expand Up @@ -274,6 +268,28 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
this.schemaErrors = final.errors
}

async queryTableNames() {
const database = this.config.database
const tablesResp: Record<string, string>[] = await this.internalQuery(
{ sql: "SHOW TABLES;" },
{ connect: false }
)
return tablesResp.map(
(obj: any) =>
obj[`Tables_in_${database}`] ||
obj[`Tables_in_${database.toLowerCase()}`]
)
}

async getTableNames() {
await this.connect()
try {
return this.queryTableNames()
} finally {
await this.disconnect()
}
}

async create(query: SqlQuery | string) {
const results = await this.internalQuery(getSqlQuery(query))
return results.length ? results : [{ created: true }]
Expand Down
16 changes: 15 additions & 1 deletion packages/server/src/integrations/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ const SCHEMA: Integration = {
type: "Relational",
description:
"Oracle Database is an object-relational database management system developed by Oracle Corporation",
features: [DatasourceFeature.CONNECTION_CHECKING],
features: [
DatasourceFeature.CONNECTION_CHECKING,
DatasourceFeature.FETCH_TABLE_NAMES,
],
datasource: {
host: {
type: DatasourceFieldType.STRING,
Expand Down Expand Up @@ -323,6 +326,17 @@ class OracleIntegration extends Sql implements DatasourcePlus {
this.schemaErrors = final.errors
}

async getTableNames() {
const columnsResponse = await this.internalQuery<OracleColumnsResponse>({
sql: this.COLUMNS_SQL,
})
if (!columnsResponse.rows) {
mike12345567 marked this conversation as resolved.
Show resolved Hide resolved
return []
} else {
return columnsResponse.rows.map(row => row.TABLE_NAME)
}
}

async testConnection() {
const response: ConnectionInfo = {
connected: false,
Expand Down
16 changes: 15 additions & 1 deletion packages/server/src/integrations/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ const SCHEMA: Integration = {
type: "Relational",
description:
"PostgreSQL, also known as Postgres, is a free and open-source relational database management system emphasizing extensibility and SQL compliance.",
features: [DatasourceFeature.CONNECTION_CHECKING],
features: [
DatasourceFeature.CONNECTION_CHECKING,
DatasourceFeature.FETCH_TABLE_NAMES,
],
datasource: {
host: {
type: DatasourceFieldType.STRING,
Expand Down Expand Up @@ -311,6 +314,17 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
}
}

async getTableNames() {
try {
await this.openConnection()
const columnsResponse: { rows: PostgresColumn[] } =
await this.client.query(this.COLUMNS_SQL)
return columnsResponse.rows.map(row => row.table_name)
} finally {
await this.closeConnection()
}
}

async create(query: SqlQuery | string) {
const response = await this.internalQuery(getSqlQuery(query))
return response.rows.length ? response.rows : [{ created: true }]
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/api/web/app/datasource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export interface VerifyDatasourceResponse {
error?: string
}

export interface FetchTablesDatasourceResponse {
tableNames: string[]
}

export interface UpdateDatasourceRequest extends Datasource {
datasource: Datasource
}
2 changes: 2 additions & 0 deletions packages/types/src/sdk/datasources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export enum FilterType {

export enum DatasourceFeature {
CONNECTION_CHECKING = "connection",
FETCH_TABLE_NAMES = "fetch_table_names",
}

export interface StepDefinition {
Expand Down Expand Up @@ -150,4 +151,5 @@ export interface DatasourcePlus extends IntegrationBase {
getBindingIdentifier(): string
getStringConcat(parts: string[]): string
buildSchema(datasourceId: string, entities: Record<string, Table>): any
getTableNames(): Promise<string[]>
}