Refactor DatasourcePlus's buildSchema to return a value, rather than rely on member variables.

This commit is contained in:
Sam Rose 2023-10-12 16:27:18 +01:00
parent 6e1962e6ea
commit 85b3af2971
9 changed files with 56 additions and 138 deletions

View File

@ -25,7 +25,6 @@ export function createDatasourcesStore() {
const store = writable({ const store = writable({
list: [], list: [],
selectedDatasourceId: null, selectedDatasourceId: null,
schemaError: null,
}) })
const derivedStore = derived([store, tables], ([$store, $tables]) => { const derivedStore = derived([store, tables], ([$store, $tables]) => {
@ -75,18 +74,13 @@ export function createDatasourcesStore() {
store.update(state => ({ store.update(state => ({
...state, ...state,
selectedDatasourceId: id, selectedDatasourceId: id,
// Remove any possible schema error
schemaError: null,
})) }))
} }
const updateDatasource = response => { const updateDatasource = response => {
const { datasource, error } = response const { datasource, error } = response
if (error) { if (error) {
store.update(state => ({ store.update(state => ({ ...state }))
...state,
schemaError: error,
}))
} }
replaceDatasource(datasource._id, datasource) replaceDatasource(datasource._id, datasource)
select(datasource._id) select(datasource._id)
@ -171,12 +165,6 @@ export function createDatasourcesStore() {
replaceDatasource(datasource._id, null) replaceDatasource(datasource._id, null)
} }
const removeSchemaError = () => {
store.update(state => {
return { ...state, schemaError: null }
})
}
const replaceDatasource = (datasourceId, datasource) => { const replaceDatasource = (datasourceId, datasource) => {
if (!datasourceId) { if (!datasourceId) {
return return
@ -229,7 +217,6 @@ export function createDatasourcesStore() {
create, create,
update, update,
delete: deleteDatasource, delete: deleteDatasource,
removeSchemaError,
replaceDatasource, replaceDatasource,
getTableNames, getTableNames,
} }

View File

@ -18,6 +18,7 @@ import {
FetchDatasourceInfoResponse, FetchDatasourceInfoResponse,
IntegrationBase, IntegrationBase,
SourceName, SourceName,
Table,
UpdateDatasourceResponse, UpdateDatasourceResponse,
UserCtx, UserCtx,
VerifyDatasourceRequest, VerifyDatasourceRequest,
@ -71,46 +72,18 @@ async function getAndMergeDatasource(datasource: Datasource) {
return await sdk.datasources.enrich(enrichedDatasource) return await sdk.datasources.enrich(enrichedDatasource)
} }
async function buildSchemaHelper(datasource: Datasource) { async function buildSchemaHelper(
datasource: Datasource
): Promise<Record<string, Table>> {
const connector = (await getConnector(datasource)) as DatasourcePlus const connector = (await getConnector(datasource)) as DatasourcePlus
await connector.buildSchema(datasource._id!, datasource.entities!) return 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)
if (noKey.length) {
error = updateError(
error,
"No primary key constraint found for the following:",
noKey
)
}
const invalidCol = getErrorTables(errors, BuildSchemaErrors.INVALID_COLUMN)
if (invalidCol.length) {
const invalidCols = Object.values(InvalidColumns).join(", ")
error = updateError(
error,
`Cannot use columns ${invalidCols} found in following:`,
invalidCol
)
}
const noHeader = getErrorTables(errors, BuildSchemaErrors.NO_HEADER_ROW)
if (noHeader.length) {
error = updateError(
error,
`No header row found in the following:`,
noHeader
)
}
}
return { tables: connector.tables, error }
} }
async function buildFilteredSchema(datasource: Datasource, filter?: string[]) { async function buildFilteredSchema(
let { tables, error } = await buildSchemaHelper(datasource) datasource: Datasource,
filter?: string[]
): Promise<{ tables: Record<string, Table> }> {
let tables = await buildSchemaHelper(datasource)
let finalTables = tables let finalTables = tables
if (filter) { if (filter) {
finalTables = {} finalTables = {}
@ -122,7 +95,7 @@ async function buildFilteredSchema(datasource: Datasource, filter?: string[]) {
} }
} }
} }
return { tables: finalTables, error } return { tables: finalTables }
} }
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx) {
@ -166,7 +139,7 @@ export async function buildSchemaFromDb(ctx: UserCtx) {
const tablesFilter = ctx.request.body.tablesFilter const tablesFilter = ctx.request.body.tablesFilter
const datasource = await sdk.datasources.get(ctx.params.datasourceId) const datasource = await sdk.datasources.get(ctx.params.datasourceId)
const { tables, error } = await buildFilteredSchema(datasource, tablesFilter) const { tables } = await buildFilteredSchema(datasource, tablesFilter)
datasource.entities = tables datasource.entities = tables
setDefaultDisplayColumns(datasource) setDefaultDisplayColumns(datasource)
@ -177,9 +150,6 @@ export async function buildSchemaFromDb(ctx: UserCtx) {
const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource) const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource)
const res: any = { datasource: cleanedDatasource } const res: any = { datasource: cleanedDatasource }
if (error) {
res.error = error
}
ctx.body = res ctx.body = res
} }
@ -308,13 +278,8 @@ export async function save(
type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE, type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE,
} }
let schemaError = null
if (fetchSchema) { if (fetchSchema) {
const { tables, error } = await buildFilteredSchema( const { tables } = await buildFilteredSchema(datasource, tablesFilter)
datasource,
tablesFilter
)
schemaError = error
datasource.entities = tables datasource.entities = tables
setDefaultDisplayColumns(datasource) setDefaultDisplayColumns(datasource)
} }
@ -340,9 +305,6 @@ export async function save(
const response: CreateDatasourceResponse = { const response: CreateDatasourceResponse = {
datasource: await sdk.datasources.removeSecretSingle(datasource), datasource: await sdk.datasources.removeSecretSingle(datasource),
} }
if (schemaError) {
response.error = schemaError
}
ctx.body = response ctx.body = response
builderSocket?.emitDatasourceUpdate(ctx, datasource) builderSocket?.emitDatasourceUpdate(ctx, datasource)
} }

View File

@ -14,6 +14,7 @@ import {
SortJson, SortJson,
ExternalTable, ExternalTable,
TableRequest, TableRequest,
Table,
} from "@budibase/types" } from "@budibase/types"
import { OAuth2Client } from "google-auth-library" import { OAuth2Client } from "google-auth-library"
import { buildExternalTableId, finaliseExternalTables } from "./utils" import { buildExternalTableId, finaliseExternalTables } from "./utils"
@ -138,8 +139,6 @@ const SCHEMA: Integration = {
class GoogleSheetsIntegration implements DatasourcePlus { class GoogleSheetsIntegration implements DatasourcePlus {
private readonly config: GoogleSheetsConfig private readonly config: GoogleSheetsConfig
private client: GoogleSpreadsheet private client: GoogleSpreadsheet
public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {}
constructor(config: GoogleSheetsConfig) { constructor(config: GoogleSheetsConfig) {
this.config = config this.config = config
@ -280,11 +279,11 @@ class GoogleSheetsIntegration implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
) { ): Promise<Record<string, Table>> {
// not fully configured yet // not fully configured yet
if (!this.config.auth) { if (!this.config.auth) {
return return {}
} }
await this.connect() await this.connect()
const sheets = this.client.sheetsByIndex const sheets = this.client.sheetsByIndex
@ -300,7 +299,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
// We expect this to always be an Error so if it's not, rethrow it to // We expect this to always be an Error so if it's not, rethrow it to
// make sure we don't fail quietly. // make sure we don't fail quietly.
if (!(err instanceof Error)) { if (!(err instanceof Error)) {
throw err; throw err
} }
if (err.message.startsWith("No values in the header row")) { if (err.message.startsWith("No values in the header row")) {
@ -308,7 +307,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
} else { } else {
// If we get an error we don't have a BuildSchemaErrors enum variant // If we get an error we don't have a BuildSchemaErrors enum variant
// for, rethrow to avoid failing quietly. // for, rethrow to avoid failing quietly.
throw err; throw err
} }
return return
} }
@ -323,9 +322,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
}, },
10 10
) )
const final = finaliseExternalTables(tables, entities) return finaliseExternalTables(tables, entities)
this.tables = final.tables
this.schemaErrors = { ...final.errors, ...errors }
} }
async query(json: QueryJson) { async query(json: QueryJson) {

View File

@ -11,6 +11,7 @@ import {
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Table,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -380,8 +381,8 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
*/ */
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
) { ): Promise<Record<string, Table>> {
await this.connect() await this.connect()
let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL) let tableInfo: MSSQLTablesResponse[] = await this.runSQL(this.TABLES_SQL)
if (tableInfo == null || !Array.isArray(tableInfo)) { if (tableInfo == null || !Array.isArray(tableInfo)) {
@ -394,7 +395,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
.map((record: any) => record.TABLE_NAME) .map((record: any) => record.TABLE_NAME)
.filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1) .filter((name: string) => this.MASTER_TABLES.indexOf(name) === -1)
const tables: Record<string, ExternalTable> = {} const tables: Record<string, Table> = {}
for (let tableName of tableNames) { for (let tableName of tableNames) {
// get the column definition (type) // get the column definition (type)
const definition = await this.runSQL( const definition = await this.runSQL(
@ -445,9 +446,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
schema, schema,
} }
} }
const final = finaliseExternalTables(tables, entities) return finaliseExternalTables(tables, entities)
this.tables = final.tables
this.schemaErrors = final.errors
} }
async queryTableNames() { async queryTableNames() {

View File

@ -10,6 +10,7 @@ import {
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Table,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -278,9 +279,9 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
) { ): Promise<Record<string, Table>> {
const tables: { [key: string]: ExternalTable } = {} const tables: { [key: string]: Table } = {}
await this.connect() await this.connect()
try { try {
@ -328,9 +329,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
} finally { } finally {
await this.disconnect() await this.disconnect()
} }
const final = finaliseExternalTables(tables, entities) return finaliseExternalTables(tables, entities)
this.tables = final.tables
this.schemaErrors = final.errors
} }
async queryTableNames() { async queryTableNames() {

View File

@ -9,6 +9,7 @@ import {
DatasourcePlus, DatasourcePlus,
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
Table,
} from "@budibase/types" } from "@budibase/types"
import { import {
buildExternalTableId, buildExternalTableId,
@ -264,8 +265,8 @@ class OracleIntegration extends Sql implements DatasourcePlus {
*/ */
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
) { ): Promise<Record<string, Table>> {
const columnsResponse = await this.internalQuery<OracleColumnsResponse>({ const columnsResponse = await this.internalQuery<OracleColumnsResponse>({
sql: this.COLUMNS_SQL, sql: this.COLUMNS_SQL,
}) })
@ -326,9 +327,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
}) })
}) })
const final = finaliseExternalTables(tables, entities) return finaliseExternalTables(tables, entities)
this.tables = final.tables
this.schemaErrors = final.errors
} }
async getTableNames() { async getTableNames() {

View File

@ -10,6 +10,7 @@ import {
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Table,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -145,8 +146,6 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
private readonly config: PostgresConfig private readonly config: PostgresConfig
private index: number = 1 private index: number = 1
private open: boolean private open: boolean
public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {}
COLUMNS_SQL!: string COLUMNS_SQL!: string
@ -273,8 +272,8 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
*/ */
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, Table>
) { ): Promise<Record<string, Table>> {
let tableKeys: { [key: string]: string[] } = {} let tableKeys: { [key: string]: string[] } = {}
await this.openConnection() await this.openConnection()
try { try {
@ -342,9 +341,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
} }
} }
const final = finaliseExternalTables(tables, entities) return finaliseExternalTables(tables, entities)
this.tables = final.tables
this.schemaErrors = final.errors
} catch (err) { } catch (err) {
// @ts-ignore // @ts-ignore
throw new Error(err) throw new Error(err)

View File

@ -4,13 +4,10 @@ import {
SearchFilters, SearchFilters,
Datasource, Datasource,
FieldType, FieldType,
ExternalTable,
} from "@budibase/types" } from "@budibase/types"
import { DocumentType, SEPARATOR } from "../db/utils" import { DocumentType, SEPARATOR } from "../db/utils"
import { import { NoEmptyFilterStrings } from "../constants"
BuildSchemaErrors,
InvalidColumns,
NoEmptyFilterStrings,
} from "../constants"
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}` const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
@ -266,9 +263,9 @@ export function shouldCopySpecialColumn(
function copyExistingPropsOver( function copyExistingPropsOver(
tableName: string, tableName: string,
table: Table, table: Table,
entities: { [key: string]: any }, entities: Record<string, Table>,
tableIds: [string] tableIds: string[]
) { ): Table {
if (entities && entities[tableName]) { if (entities && entities[tableName]) {
if (entities[tableName]?.primaryDisplay) { if (entities[tableName]?.primaryDisplay) {
table.primaryDisplay = entities[tableName].primaryDisplay table.primaryDisplay = entities[tableName].primaryDisplay
@ -295,42 +292,23 @@ function copyExistingPropsOver(
/** /**
* Look through the final table definitions to see if anything needs to be * Look through the final table definitions to see if anything needs to be
* copied over from the old and if any errors have occurred mark them so * copied over from the old.
* that the user can be made aware.
* @param tables The list of tables that have been retrieved from the external database. * @param tables The list of tables that have been retrieved from the external database.
* @param entities The old list of tables, if there was any to look for definitions in. * @param entities The old list of tables, if there was any to look for definitions in.
*/ */
export function finaliseExternalTables( export function finaliseExternalTables(
tables: { [key: string]: any }, tables: Record<string, Table>,
entities: { [key: string]: any } entities: Record<string, Table>
) { ): Record<string, Table> {
const invalidColumns = Object.values(InvalidColumns) let finalTables: Record<string, Table> = {}
let finalTables: { [key: string]: any } = {} const tableIds = Object.values(tables).map(table => table._id!)
const errors: { [key: string]: string } = {}
// @ts-ignore
const tableIds: [string] = Object.values(tables).map(table => table._id)
for (let [name, table] of Object.entries(tables)) { for (let [name, table] of Object.entries(tables)) {
const schemaFields = Object.keys(table.schema)
// make sure every table has a key
if (table.primary == null || table.primary.length === 0) {
errors[name] = BuildSchemaErrors.NO_KEY
continue
} else if (
schemaFields.find(field =>
invalidColumns.includes(field as InvalidColumns)
)
) {
errors[name] = BuildSchemaErrors.INVALID_COLUMN
continue
}
// make sure all previous props have been added back
finalTables[name] = copyExistingPropsOver(name, table, entities, tableIds) finalTables[name] = copyExistingPropsOver(name, table, entities, tableIds)
} }
// sort the tables by name // sort the tables by name (TODO(samwho): why?)
finalTables = Object.entries(finalTables) return Object.entries(finalTables)
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.reduce((r, [k, v]) => ({ ...r, [k]: v }), {}) .reduce((r, [k, v]) => ({ ...r, [k]: v }), {})
return { tables: finalTables, errors }
} }
/** /**

View File

@ -175,13 +175,13 @@ export interface IntegrationBase {
} }
export interface DatasourcePlus extends IntegrationBase { export interface DatasourcePlus extends IntegrationBase {
tables: Record<string, Table>
schemaErrors: Record<string, string>
// if the datasource supports the use of bindings directly (to protect against SQL injection) // if the datasource supports the use of bindings directly (to protect against SQL injection)
// this returns the format of the identifier // this returns the format of the identifier
getBindingIdentifier(): string getBindingIdentifier(): string
getStringConcat(parts: string[]): string getStringConcat(parts: string[]): string
buildSchema(datasourceId: string, entities: Record<string, Table>): any buildSchema(
datasourceId: string,
entities: Record<string, Table>
): Promise<Record<string, Table>>
getTableNames(): Promise<string[]> getTableNames(): Promise<string[]>
} }