1
0
Fork 0
mirror of synced 2024-08-15 01:51:33 +12:00

Merge branch 'master' of github.com:Budibase/budibase into grid-all-datasources

This commit is contained in:
mike12345567 2023-10-17 15:46:34 +01:00
commit 70c5272ad9
21 changed files with 246 additions and 229 deletions

View file

@ -20,8 +20,8 @@ jobs:
with: with:
root-reserve-mb: 30000 root-reserve-mb: 30000
swap-size-mb: 1024 swap-size-mb: 1024
remove-android: 'true' remove-android: "true"
remove-dotnet: 'true' remove-dotnet: "true"
- name: Fail if not a tag - name: Fail if not a tag
run: | run: |
if [[ $GITHUB_REF != refs/tags/* ]]; then if [[ $GITHUB_REF != refs/tags/* ]]; then
@ -48,7 +48,7 @@ jobs:
- name: Update versions - name: Update versions
run: ./scripts/updateVersions.sh run: ./scripts/updateVersions.sh
- name: Run Yarn Build - name: Run Yarn Build
run: yarn build:docker:pre run: yarn build
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:

View file

@ -12,14 +12,14 @@ RUN chmod +x /cleanup.sh
WORKDIR /app WORKDIR /app
ADD packages/server . ADD packages/server .
COPY yarn.lock . COPY yarn.lock .
RUN yarn install --production=true --network-timeout 100000 RUN yarn install --production=true --network-timeout 1000000
RUN /cleanup.sh RUN /cleanup.sh
# build worker # build worker
WORKDIR /worker WORKDIR /worker
ADD packages/worker . ADD packages/worker .
COPY yarn.lock . COPY yarn.lock .
RUN yarn install --production=true --network-timeout 100000 RUN yarn install --production=true --network-timeout 1000000
RUN /cleanup.sh RUN /cleanup.sh
FROM budibase/couchdb FROM budibase/couchdb

View file

@ -1,5 +1,5 @@
{ {
"version": "2.11.35", "version": "2.11.36",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View file

@ -36,7 +36,7 @@
import { FieldType, FieldSubtype, SourceName } from "@budibase/types" import { FieldType, FieldSubtype, SourceName } from "@budibase/types"
import RelationshipSelector from "components/common/RelationshipSelector.svelte" import RelationshipSelector from "components/common/RelationshipSelector.svelte"
const AUTO_TYPE = "auto" const AUTO_TYPE = FIELDS.AUTO.type
const FORMULA_TYPE = FIELDS.FORMULA.type const FORMULA_TYPE = FIELDS.FORMULA.type
const LINK_TYPE = FIELDS.LINK.type const LINK_TYPE = FIELDS.LINK.type
const STRING_TYPE = FIELDS.STRING.type const STRING_TYPE = FIELDS.STRING.type
@ -60,8 +60,13 @@
{} {}
) )
function makeFieldId(type, subtype) { function makeFieldId(type, subtype, autocolumn) {
return `${type}${subtype || ""}`.toUpperCase() // don't make field IDs for auto types
if (type === AUTO_TYPE || autocolumn) {
return type.toUpperCase()
} else {
return `${type}${subtype || ""}`.toUpperCase()
}
} }
let originalName let originalName
@ -183,7 +188,8 @@
if (!savingColumn) { if (!savingColumn) {
editableColumn.fieldId = makeFieldId( editableColumn.fieldId = makeFieldId(
editableColumn.type, editableColumn.type,
editableColumn.subtype editableColumn.subtype,
editableColumn.autocolumn
) )
allowedTypes = getAllowedTypes().map(t => ({ allowedTypes = getAllowedTypes().map(t => ({
@ -419,7 +425,7 @@
FIELDS.FORMULA, FIELDS.FORMULA,
FIELDS.JSON, FIELDS.JSON,
isUsers ? FIELDS.USERS : FIELDS.USER, isUsers ? FIELDS.USERS : FIELDS.USER,
{ name: "Auto Column", type: AUTO_TYPE }, FIELDS.AUTO,
] ]
} else { } else {
let fields = [ let fields = [
@ -538,7 +544,7 @@
getOptionValue={field => field.fieldId} getOptionValue={field => field.fieldId}
getOptionIcon={field => field.icon} getOptionIcon={field => field.icon}
isOptionEnabled={option => { isOptionEnabled={option => {
if (option.type == AUTO_TYPE) { if (option.type === AUTO_TYPE) {
return availableAutoColumnKeys?.length > 0 return availableAutoColumnKeys?.length > 0
} }
return true return true

View file

@ -57,7 +57,7 @@
{#if $store.error} {#if $store.error}
<InlineAlert <InlineAlert
type="error" type="error"
header={$store.error.title} header="Error fetching {tableType}"
message={$store.error.description} message={$store.error.description}
/> />
{/if} {/if}

View file

@ -1,6 +1,6 @@
import { derived, writable, get } from "svelte/store" import { derived, writable, get } from "svelte/store"
import { keepOpen, notifications } from "@budibase/bbui" import { keepOpen, notifications } from "@budibase/bbui"
import { datasources, ImportTableError, tables } from "stores/backend" import { datasources, tables } from "stores/backend"
export const createTableSelectionStore = (integration, datasource) => { export const createTableSelectionStore = (integration, datasource) => {
const tableNamesStore = writable([]) const tableNamesStore = writable([])
@ -30,12 +30,7 @@ export const createTableSelectionStore = (integration, datasource) => {
notifications.success(`Tables fetched successfully.`) notifications.success(`Tables fetched successfully.`)
await onComplete() await onComplete()
} catch (err) { } catch (err) {
if (err instanceof ImportTableError) { errorStore.set(err)
errorStore.set(err)
} else {
notifications.error("Error fetching tables.")
}
return keepOpen return keepOpen
} }
} }

View file

@ -1,5 +1,21 @@
import { FieldType, FieldSubtype } from "@budibase/types" import { FieldType, FieldSubtype } from "@budibase/types"
export const AUTO_COLUMN_SUB_TYPES = {
AUTO_ID: "autoID",
CREATED_BY: "createdBy",
CREATED_AT: "createdAt",
UPDATED_BY: "updatedBy",
UPDATED_AT: "updatedAt",
}
export const AUTO_COLUMN_DISPLAY_NAMES = {
AUTO_ID: "Auto ID",
CREATED_BY: "Created By",
CREATED_AT: "Created At",
UPDATED_BY: "Updated By",
UPDATED_AT: "Updated At",
}
export const FIELDS = { export const FIELDS = {
STRING: { STRING: {
name: "Text", name: "Text",
@ -107,6 +123,12 @@ export const FIELDS = {
presence: false, presence: false,
}, },
}, },
AUTO: {
name: "Auto Column",
type: FieldType.AUTO,
icon: "MagicWand",
constraints: {},
},
FORMULA: { FORMULA: {
name: "Formula", name: "Formula",
type: FieldType.FORMULA, type: FieldType.FORMULA,
@ -139,22 +161,6 @@ export const FIELDS = {
}, },
} }
export const AUTO_COLUMN_SUB_TYPES = {
AUTO_ID: "autoID",
CREATED_BY: "createdBy",
CREATED_AT: "createdAt",
UPDATED_BY: "updatedBy",
UPDATED_AT: "updatedAt",
}
export const AUTO_COLUMN_DISPLAY_NAMES = {
AUTO_ID: "Auto ID",
CREATED_BY: "Created By",
CREATED_AT: "Created At",
UPDATED_BY: "Updated By",
UPDATED_AT: "Updated At",
}
export const FILE_TYPES = { export const FILE_TYPES = {
IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"], IMAGE: ["png", "tiff", "gif", "raw", "jpg", "jpeg"],
CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"], CODE: ["js", "rs", "py", "java", "rb", "hs", "yml"],

View file

@ -9,15 +9,19 @@ import { API } from "api"
import { DatasourceFeature } from "@budibase/types" import { DatasourceFeature } from "@budibase/types"
import { TableNames } from "constants" import { TableNames } from "constants"
export class ImportTableError extends Error { class TableImportError extends Error {
constructor(message) { constructor(errors) {
super(message) super()
const [title, description] = message.split(" - ") this.name = "TableImportError"
this.errors = errors
}
this.name = "TableSelectionError" get description() {
// Capitalize the first character of both the title and description let message = ""
this.title = title[0].toUpperCase() + title.substr(1) for (const key in this.errors) {
this.description = description[0].toUpperCase() + description.substr(1) message += `${key}: ${this.errors[key]}\n`
}
return message
} }
} }
@ -25,7 +29,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 +78,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, errors } = response
if (error) { if (errors && Object.keys(errors).length > 0) {
store.update(state => ({ throw new TableImportError(errors)
...state,
schemaError: error,
}))
} }
replaceDatasource(datasource._id, datasource) replaceDatasource(datasource._id, datasource)
select(datasource._id) select(datasource._id)
@ -94,20 +92,11 @@ export function createDatasourcesStore() {
} }
const updateSchema = async (datasource, tablesFilter) => { const updateSchema = async (datasource, tablesFilter) => {
try { const response = await API.buildDatasourceSchema({
const response = await API.buildDatasourceSchema({ datasourceId: datasource?._id,
datasourceId: datasource?._id, tablesFilter,
tablesFilter, })
}) updateDatasource(response)
updateDatasource(response)
} catch (e) {
// buildDatasourceSchema call returns user presentable errors with two parts divided with a " - ".
if (e.message.split(" - ").length === 2) {
throw new ImportTableError(e.message)
} else {
throw e
}
}
} }
const sourceCount = source => { const sourceCount = source => {
@ -172,12 +161,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
@ -230,7 +213,6 @@ export function createDatasourcesStore() {
create, create,
update, update,
delete: deleteDatasource, delete: deleteDatasource,
removeSchemaError,
replaceDatasource, replaceDatasource,
getTableNames, getTableNames,
} }

View file

@ -4,7 +4,7 @@ export { views } from "./views"
export { viewsV2 } from "./viewsV2" export { viewsV2 } from "./viewsV2"
export { permissions } from "./permissions" export { permissions } from "./permissions"
export { roles } from "./roles" export { roles } from "./roles"
export { datasources, ImportTableError } from "./datasources" export { datasources } from "./datasources"
export { integrations } from "./integrations" export { integrations } from "./integrations"
export { sortedIntegrations } from "./sortedIntegrations" export { sortedIntegrations } from "./sortedIntegrations"
export { queries } from "./queries" export { queries } from "./queries"

View file

@ -5,7 +5,6 @@ import {
getTableParams, getTableParams,
} from "../../db/utils" } from "../../db/utils"
import { destroy as tableDestroy } from "./table/internal" import { destroy as tableDestroy } from "./table/internal"
import { BuildSchemaErrors, InvalidColumns } from "../../constants"
import { getIntegration } from "../../integrations" import { getIntegration } from "../../integrations"
import { invalidateDynamicVariables } from "../../threads/utils" import { invalidateDynamicVariables } from "../../threads/utils"
import { context, db as dbCore, events } from "@budibase/backend-core" import { context, db as dbCore, events } from "@budibase/backend-core"
@ -14,10 +13,13 @@ import {
CreateDatasourceResponse, CreateDatasourceResponse,
Datasource, Datasource,
DatasourcePlus, DatasourcePlus,
ExternalTable,
FetchDatasourceInfoRequest, FetchDatasourceInfoRequest,
FetchDatasourceInfoResponse, FetchDatasourceInfoResponse,
IntegrationBase, IntegrationBase,
Schema,
SourceName, SourceName,
Table,
UpdateDatasourceResponse, UpdateDatasourceResponse,
UserCtx, UserCtx,
VerifyDatasourceRequest, VerifyDatasourceRequest,
@ -27,23 +29,6 @@ import sdk from "../../sdk"
import { builderSocket } from "../../websockets" import { builderSocket } from "../../websockets"
import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets" import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets"
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( async function getConnector(
datasource: Datasource datasource: Datasource
): Promise<IntegrationBase | DatasourcePlus> { ): Promise<IntegrationBase | DatasourcePlus> {
@ -71,48 +56,36 @@ 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<Schema> {
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!,
const errors = connector.schemaErrors datasource.entities! as Record<string, ExternalTable>
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 }
} }
async function buildFilteredSchema(datasource: Datasource, filter?: string[]) { async function buildFilteredSchema(
let { tables, error } = await buildSchemaHelper(datasource) datasource: Datasource,
let finalTables = tables filter?: string[]
if (filter) { ): Promise<Schema> {
finalTables = {} let schema = await buildSchemaHelper(datasource)
for (let key in tables) { if (!filter) {
if ( return schema
filter.some((filter: any) => filter.toLowerCase() === key.toLowerCase()) }
) {
finalTables[key] = tables[key] let filteredSchema: Schema = { tables: {}, errors: {} }
} for (let key in schema.tables) {
if (filter.some(filter => filter.toLowerCase() === key.toLowerCase())) {
filteredSchema.tables[key] = schema.tables[key]
} }
} }
return { tables: finalTables, error }
for (let key in schema.errors) {
if (filter.some(filter => filter.toLowerCase() === key.toLowerCase())) {
filteredSchema.errors[key] = schema.errors[key]
}
}
return filteredSchema
} }
export async function fetch(ctx: UserCtx) { export async function fetch(ctx: UserCtx) {
@ -156,7 +129,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, errors } = await buildFilteredSchema(datasource, tablesFilter)
datasource.entities = tables datasource.entities = tables
setDefaultDisplayColumns(datasource) setDefaultDisplayColumns(datasource)
@ -164,13 +137,11 @@ export async function buildSchemaFromDb(ctx: UserCtx) {
sdk.tables.populateExternalTableSchemas(datasource) sdk.tables.populateExternalTableSchemas(datasource)
) )
datasource._rev = dbResp.rev datasource._rev = dbResp.rev
const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource)
const res: any = { datasource: cleanedDatasource } ctx.body = {
if (error) { datasource: await sdk.datasources.removeSecretSingle(datasource),
res.error = error errors,
} }
ctx.body = res
} }
/** /**
@ -298,15 +269,12 @@ export async function save(
type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE, type: plus ? DocumentType.DATASOURCE_PLUS : DocumentType.DATASOURCE,
} }
let schemaError = null let errors: Record<string, string> = {}
if (fetchSchema) { if (fetchSchema) {
const { tables, error } = await buildFilteredSchema( const schema = await buildFilteredSchema(datasource, tablesFilter)
datasource, datasource.entities = schema.tables
tablesFilter
)
schemaError = error
datasource.entities = tables
setDefaultDisplayColumns(datasource) setDefaultDisplayColumns(datasource)
errors = schema.errors
} }
if (preSaveAction[datasource.source]) { if (preSaveAction[datasource.source]) {
@ -327,13 +295,10 @@ export async function save(
} }
} }
const response: CreateDatasourceResponse = { ctx.body = {
datasource: await sdk.datasources.removeSecretSingle(datasource), datasource: await sdk.datasources.removeSecretSingle(datasource),
errors,
} }
if (schemaError) {
response.error = schemaError
}
ctx.body = response
builderSocket?.emitDatasourceUpdate(ctx, datasource) builderSocket?.emitDatasourceUpdate(ctx, datasource)
} }

View file

@ -37,7 +37,7 @@ describe("/datasources", () => {
.expect(200) .expect(200)
expect(res.body.datasource.name).toEqual("Test") expect(res.body.datasource.name).toEqual("Test")
expect(res.body.errors).toBeUndefined() expect(res.body.errors).toEqual({})
expect(events.datasource.created).toBeCalledTimes(1) expect(events.datasource.created).toBeCalledTimes(1)
}) })
}) })

View file

@ -159,11 +159,6 @@ export enum InvalidColumns {
TABLE_ID = "tableId", TABLE_ID = "tableId",
} }
export enum BuildSchemaErrors {
NO_KEY = "no_key",
INVALID_COLUMN = "invalid_column",
}
export enum AutomationErrors { export enum AutomationErrors {
INCORRECT_TYPE = "INCORRECT_TYPE", INCORRECT_TYPE = "INCORRECT_TYPE",
MAX_ITERATIONS = "MAX_ITERATIONS_REACHED", MAX_ITERATIONS = "MAX_ITERATIONS_REACHED",

View file

@ -18,6 +18,7 @@ import _ from "lodash"
import { generator } from "@budibase/backend-core/tests" import { generator } from "@budibase/backend-core/tests"
import { utils } from "@budibase/backend-core" import { utils } from "@budibase/backend-core"
import { databaseTestProviders } from "../integrations/tests/utils" import { databaseTestProviders } from "../integrations/tests/utils"
import { Client } from "pg"
const config = setup.getConfig()! const config = setup.getConfig()!
@ -1055,4 +1056,46 @@ describe("postgres integrations", () => {
expect(response.body.tableNames.indexOf(primaryName)).not.toBe(-1) expect(response.body.tableNames.indexOf(primaryName)).not.toBe(-1)
}) })
}) })
describe("POST /api/datasources/:datasourceId/schema", () => {
let client: Client
beforeEach(async () => {
client = new Client(
(await databaseTestProviders.postgres.getDsConfig()).config!
)
await client.connect()
})
afterEach(async () => {
await client.query(`DROP TABLE IF EXISTS "table"`)
await client.end()
})
it("recognises when a table has no primary key", async () => {
await client.query(`CREATE TABLE "table" (id SERIAL)`)
const response = await makeRequest(
"post",
`/api/datasources/${postgresDatasource._id}/schema`
)
expect(response.body.errors).toEqual({
table: "Table must have a primary key.",
})
})
it("recognises when a table is using a reserved column name", async () => {
await client.query(`CREATE TABLE "table" (_id SERIAL PRIMARY KEY) `)
const response = await makeRequest(
"post",
`/api/datasources/${postgresDatasource._id}/schema`
)
expect(response.body.errors).toEqual({
table: "Table contains invalid columns.",
})
})
})
}) })

View file

@ -14,9 +14,14 @@ import {
SortJson, SortJson,
ExternalTable, ExternalTable,
TableRequest, TableRequest,
Schema,
} 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,
checkExternalTables,
finaliseExternalTables,
} from "./utils"
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet" import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
import fetch from "node-fetch" import fetch from "node-fetch"
import { cache, configs, context, HTTPError } from "@budibase/backend-core" import { cache, configs, context, HTTPError } from "@budibase/backend-core"
@ -138,8 +143,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
@ -281,19 +284,37 @@ class GoogleSheetsIntegration implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, ExternalTable>
) { ): Promise<Schema> {
// not fully configured yet // not fully configured yet
if (!this.config.auth) { if (!this.config.auth) {
return return { tables: {}, errors: {} }
} }
await this.connect() await this.connect()
const sheets = this.client.sheetsByIndex const sheets = this.client.sheetsByIndex
const tables: Record<string, ExternalTable> = {} const tables: Record<string, ExternalTable> = {}
let errors: Record<string, string> = {}
await utils.parallelForeach( await utils.parallelForeach(
sheets, sheets,
async sheet => { async sheet => {
// must fetch rows to determine schema // must fetch rows to determine schema
await sheet.getRows() try {
await sheet.getRows()
} catch (err) {
// We expect this to always be an Error so if it's not, rethrow it to
// make sure we don't fail quietly.
if (!(err instanceof Error)) {
throw err
}
if (err.message.startsWith("No values in the header row")) {
errors[sheet.title] = err.message
} else {
// If we get an error we don't expect, rethrow to avoid failing
// quietly.
throw err
}
return
}
const id = buildExternalTableId(datasourceId, sheet.title) const id = buildExternalTableId(datasourceId, sheet.title)
tables[sheet.title] = this.getTableSchema( tables[sheet.title] = this.getTableSchema(
@ -305,9 +326,9 @@ class GoogleSheetsIntegration implements DatasourcePlus {
}, },
10 10
) )
const final = finaliseExternalTables(tables, entities) let externalTables = finaliseExternalTables(tables, entities)
this.tables = final.tables errors = { ...errors, ...checkExternalTables(externalTables) }
this.schemaErrors = final.errors return { tables: externalTables, errors }
} }
async query(json: QueryJson) { async query(json: QueryJson) {

View file

@ -11,6 +11,7 @@ import {
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Schema,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -18,6 +19,7 @@ import {
convertSqlType, convertSqlType,
finaliseExternalTables, finaliseExternalTables,
SqlClient, SqlClient,
checkExternalTables,
} from "./utils" } from "./utils"
import Sql from "./base/sql" import Sql from "./base/sql"
import { MSSQLTablesResponse, MSSQLColumn } from "./base/types" import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"
@ -190,8 +192,6 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
private readonly config: MSSQLConfig private readonly config: MSSQLConfig
private index: number = 0 private index: number = 0
private client?: sqlServer.ConnectionPool private client?: sqlServer.ConnectionPool
public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {}
MASTER_TABLES = [ MASTER_TABLES = [
"spt_fallback_db", "spt_fallback_db",
@ -381,7 +381,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, ExternalTable>
) { ): Promise<Schema> {
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)) {
@ -445,9 +445,12 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
schema, schema,
} }
} }
const final = finaliseExternalTables(tables, entities) let externalTables = finaliseExternalTables(tables, entities)
this.tables = final.tables let errors = checkExternalTables(externalTables)
this.schemaErrors = final.errors return {
tables: externalTables,
errors,
}
} }
async queryTableNames() { async queryTableNames() {

View file

@ -10,6 +10,7 @@ import {
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Schema,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -17,6 +18,7 @@ import {
buildExternalTableId, buildExternalTableId,
convertSqlType, convertSqlType,
finaliseExternalTables, finaliseExternalTables,
checkExternalTables,
} from "./utils" } from "./utils"
import dayjs from "dayjs" import dayjs from "dayjs"
import { NUMBER_REGEX } from "../utilities" import { NUMBER_REGEX } from "../utilities"
@ -140,8 +142,6 @@ export function bindingTypeCoerce(bindings: any[]) {
class MySQLIntegration extends Sql implements DatasourcePlus { class MySQLIntegration extends Sql implements DatasourcePlus {
private config: MySQLConfig private config: MySQLConfig
private client?: mysql.Connection private client?: mysql.Connection
public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {}
constructor(config: MySQLConfig) { constructor(config: MySQLConfig) {
super(SqlClient.MY_SQL) super(SqlClient.MY_SQL)
@ -279,7 +279,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, ExternalTable>
) { ): Promise<Schema> {
const tables: { [key: string]: ExternalTable } = {} const tables: { [key: string]: ExternalTable } = {}
await this.connect() await this.connect()
@ -328,9 +328,10 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
} finally { } finally {
await this.disconnect() await this.disconnect()
} }
const final = finaliseExternalTables(tables, entities)
this.tables = final.tables let externalTables = finaliseExternalTables(tables, entities)
this.schemaErrors = final.errors let errors = checkExternalTables(tables)
return { tables: externalTables, errors }
} }
async queryTableNames() { async queryTableNames() {

View file

@ -9,9 +9,11 @@ import {
DatasourcePlus, DatasourcePlus,
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
Schema,
} from "@budibase/types" } from "@budibase/types"
import { import {
buildExternalTableId, buildExternalTableId,
checkExternalTables,
convertSqlType, convertSqlType,
finaliseExternalTables, finaliseExternalTables,
getSqlQuery, getSqlQuery,
@ -108,9 +110,6 @@ class OracleIntegration extends Sql implements DatasourcePlus {
private readonly config: OracleConfig private readonly config: OracleConfig
private index: number = 1 private index: number = 1
public tables: Record<string, ExternalTable> = {}
public schemaErrors: Record<string, string> = {}
private readonly COLUMNS_SQL = ` private readonly COLUMNS_SQL = `
SELECT SELECT
tabs.table_name, tabs.table_name,
@ -265,7 +264,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, ExternalTable>
) { ): Promise<Schema> {
const columnsResponse = await this.internalQuery<OracleColumnsResponse>({ const columnsResponse = await this.internalQuery<OracleColumnsResponse>({
sql: this.COLUMNS_SQL, sql: this.COLUMNS_SQL,
}) })
@ -326,9 +325,9 @@ class OracleIntegration extends Sql implements DatasourcePlus {
}) })
}) })
const final = finaliseExternalTables(tables, entities) let externalTables = finaliseExternalTables(tables, entities)
this.tables = final.tables let errors = checkExternalTables(externalTables)
this.schemaErrors = final.errors return { tables: externalTables, errors }
} }
async getTableNames() { async getTableNames() {

View file

@ -10,6 +10,7 @@ import {
DatasourceFeature, DatasourceFeature,
ConnectionInfo, ConnectionInfo,
SourceName, SourceName,
Schema,
} from "@budibase/types" } from "@budibase/types"
import { import {
getSqlQuery, getSqlQuery,
@ -17,6 +18,7 @@ import {
convertSqlType, convertSqlType,
finaliseExternalTables, finaliseExternalTables,
SqlClient, SqlClient,
checkExternalTables,
} from "./utils" } from "./utils"
import Sql from "./base/sql" import Sql from "./base/sql"
import { PostgresColumn } from "./base/types" import { PostgresColumn } from "./base/types"
@ -145,8 +147,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
@ -274,7 +274,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
async buildSchema( async buildSchema(
datasourceId: string, datasourceId: string,
entities: Record<string, ExternalTable> entities: Record<string, ExternalTable>
) { ): Promise<Schema> {
let tableKeys: { [key: string]: string[] } = {} let tableKeys: { [key: string]: string[] } = {}
await this.openConnection() await this.openConnection()
try { try {
@ -342,9 +342,9 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
} }
} }
const final = finaliseExternalTables(tables, entities) let finalizedTables = finaliseExternalTables(tables, entities)
this.tables = final.tables let errors = checkExternalTables(finalizedTables)
this.schemaErrors = final.errors return { tables: finalizedTables, 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 { InvalidColumns, 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,41 @@ 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, ExternalTable>,
entities: { [key: string]: any } entities: Record<string, ExternalTable>
) { ): Record<string, ExternalTable> {
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, this is for the UI to display them in alphabetical order
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 } }
export function checkExternalTables(
tables: Record<string, ExternalTable>
): Record<string, string> {
const invalidColumns = Object.values(InvalidColumns) as string[]
const errors: Record<string, string> = {}
for (let [name, table] of Object.entries(tables)) {
if (!table.primary || table.primary.length === 0) {
errors[name] = "Table must have a primary key."
}
const schemaFields = Object.keys(table.schema)
if (schemaFields.find(f => invalidColumns.includes(f))) {
errors[name] = "Table contains invalid columns."
}
}
return errors
} }
/** /**

View file

@ -2,7 +2,7 @@ import { Datasource } from "../../../documents"
export interface CreateDatasourceResponse { export interface CreateDatasourceResponse {
datasource: Datasource datasource: Datasource
error?: any errors: Record<string, string>
} }
export interface UpdateDatasourceResponse { export interface UpdateDatasourceResponse {

View file

@ -1,4 +1,4 @@
import { Table } from "../documents" import { ExternalTable, Table } from "../documents"
export const PASSWORD_REPLACEMENT = "--secret-value--" export const PASSWORD_REPLACEMENT = "--secret-value--"
@ -175,14 +175,19 @@ export interface IntegrationBase {
}): void }): void
} }
export interface DatasourcePlus extends IntegrationBase { export interface Schema {
tables: Record<string, Table> tables: Record<string, ExternalTable>
schemaErrors: Record<string, string> errors: Record<string, string>
}
export interface DatasourcePlus extends IntegrationBase {
// 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, ExternalTable>
): Promise<Schema>
getTableNames(): Promise<string[]> getTableNames(): Promise<string[]>
} }