Merge branch 'master' into feature/app-list-actions

This commit is contained in:
deanhannigan 2024-03-06 09:04:51 +00:00 committed by GitHub
commit 63f9995a21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 223 additions and 35 deletions

View File

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

View File

@ -1,12 +1,27 @@
import { import {
QueryJson, Datasource,
SearchFilters,
Table,
Row,
DatasourcePlusQueryResponse, DatasourcePlusQueryResponse,
Operation,
QueryJson,
Row,
SearchFilters,
} from "@budibase/types" } from "@budibase/types"
import { getDatasourceAndQuery } from "../../../sdk/app/rows/utils" import { getSQLClient } from "../../../sdk/app/rows/utils"
import { cloneDeep } from "lodash" import { cloneDeep } from "lodash"
import sdk from "../../../sdk"
import { makeExternalQuery } from "../../../integrations/base/query"
import { SqlClient } from "../../../integrations/utils"
const WRITE_OPERATIONS: Operation[] = [
Operation.CREATE,
Operation.UPDATE,
Operation.DELETE,
]
const DISABLED_WRITE_CLIENTS: SqlClient[] = [
SqlClient.MY_SQL,
SqlClient.MS_SQL,
SqlClient.ORACLE,
]
class CharSequence { class CharSequence {
static alphabet = "abcdefghijklmnopqrstuvwxyz" static alphabet = "abcdefghijklmnopqrstuvwxyz"
@ -43,6 +58,25 @@ export default class AliasTables {
this.charSeq = new CharSequence() this.charSeq = new CharSequence()
} }
isAliasingEnabled(json: QueryJson, datasource: Datasource) {
const fieldLength = json.resource?.fields?.length
if (!fieldLength || fieldLength <= 0) {
return false
}
try {
const sqlClient = getSQLClient(datasource)
const isWrite = WRITE_OPERATIONS.includes(json.endpoint.operation)
const isDisabledClient = DISABLED_WRITE_CLIENTS.includes(sqlClient)
if (isWrite && isDisabledClient) {
return false
}
} catch (err) {
// if we can't get an SQL client, we can't alias
return false
}
return true
}
getAlias(tableName: string) { getAlias(tableName: string) {
if (this.aliases[tableName]) { if (this.aliases[tableName]) {
return this.aliases[tableName] return this.aliases[tableName]
@ -111,8 +145,10 @@ export default class AliasTables {
} }
async queryWithAliasing(json: QueryJson): DatasourcePlusQueryResponse { async queryWithAliasing(json: QueryJson): DatasourcePlusQueryResponse {
const fieldLength = json.resource?.fields?.length const datasourceId = json.endpoint.datasourceId
const aliasingEnabled = fieldLength && fieldLength > 0 const datasource = await sdk.datasources.get(datasourceId)
const aliasingEnabled = this.isAliasingEnabled(json, datasource)
if (aliasingEnabled) { if (aliasingEnabled) {
json = cloneDeep(json) json = cloneDeep(json)
// run through the query json to update anywhere a table may be used // run through the query json to update anywhere a table may be used
@ -158,7 +194,7 @@ export default class AliasTables {
} }
json.tableAliases = invertedTableAliases json.tableAliases = invertedTableAliases
} }
const response = await getDatasourceAndQuery(json) const response = await makeExternalQuery(datasource, json)
if (Array.isArray(response) && aliasingEnabled) { if (Array.isArray(response) && aliasingEnabled) {
return this.reverse(response) return this.reverse(response)
} else { } else {

View File

@ -435,10 +435,12 @@ class InternalBuilder {
aliases?: QueryJson["tableAliases"] aliases?: QueryJson["tableAliases"]
): Knex.QueryBuilder { ): Knex.QueryBuilder {
const tableName = endpoint.entityId const tableName = endpoint.entityId
const tableAliased = aliases?.[tableName] const tableAlias = aliases?.[tableName]
? `${tableName} as ${aliases?.[tableName]}` let table: string | Record<string, string> = tableName
: tableName if (tableAlias) {
let query = knex(tableAliased) table = { [tableAlias]: tableName }
}
let query = knex(table)
if (endpoint.schema) { if (endpoint.schema) {
query = query.withSchema(endpoint.schema) query = query.withSchema(endpoint.schema)
} }

View File

@ -14,7 +14,12 @@ import firebase from "./firebase"
import redis from "./redis" import redis from "./redis"
import snowflake from "./snowflake" import snowflake from "./snowflake"
import oracle from "./oracle" import oracle from "./oracle"
import { SourceName, Integration, PluginType } from "@budibase/types" import {
SourceName,
Integration,
PluginType,
IntegrationBase,
} from "@budibase/types"
import { getDatasourcePlugin } from "../utilities/fileSystem" import { getDatasourcePlugin } from "../utilities/fileSystem"
import env from "../environment" import env from "../environment"
import cloneDeep from "lodash/cloneDeep" import cloneDeep from "lodash/cloneDeep"
@ -40,7 +45,10 @@ const DEFINITIONS: Record<SourceName, Integration | undefined> = {
[SourceName.BUDIBASE]: undefined, [SourceName.BUDIBASE]: undefined,
} }
const INTEGRATIONS: Record<SourceName, any> = { type IntegrationBaseConstructor = new (...args: any[]) => IntegrationBase
const INTEGRATIONS: Record<SourceName, IntegrationBaseConstructor | undefined> =
{
[SourceName.POSTGRES]: postgres.integration, [SourceName.POSTGRES]: postgres.integration,
[SourceName.DYNAMODB]: dynamodb.integration, [SourceName.DYNAMODB]: dynamodb.integration,
[SourceName.MONGODB]: mongodb.integration, [SourceName.MONGODB]: mongodb.integration,
@ -58,7 +66,7 @@ const INTEGRATIONS: Record<SourceName, any> = {
[SourceName.SNOWFLAKE]: snowflake.integration, [SourceName.SNOWFLAKE]: snowflake.integration,
[SourceName.ORACLE]: undefined, [SourceName.ORACLE]: undefined,
[SourceName.BUDIBASE]: undefined, [SourceName.BUDIBASE]: undefined,
} }
// optionally add oracle integration if the oracle binary can be installed // optionally add oracle integration if the oracle binary can be installed
if ( if (

View File

@ -1,4 +1,4 @@
import { QueryJson } from "@budibase/types" import { Datasource, Operation, QueryJson, SourceName } from "@budibase/types"
import { join } from "path" import { join } from "path"
import Sql from "../base/sql" import Sql from "../base/sql"
import { SqlClient } from "../utils" import { SqlClient } from "../utils"
@ -198,6 +198,114 @@ describe("Captures of real examples", () => {
}) })
}) })
describe("check aliasing is disabled/enabled", () => {
const tables = ["tableA", "tableB"]
function getDatasource(source: SourceName): Datasource {
return {
source,
type: "datasource",
isSQL: true,
}
}
function getQuery(op: Operation, fields: string[] = ["a"]): QueryJson {
return {
endpoint: { datasourceId: "", entityId: "", operation: op },
resource: {
fields,
},
}
}
it("should check for Postgres aliased status", () => {
const aliasing = new AliasTables(tables)
const datasource = getDatasource(SourceName.POSTGRES)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.CREATE), datasource)
).toEqual(true)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.READ), datasource)
).toEqual(true)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.UPDATE), datasource)
).toEqual(true)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.DELETE), datasource)
).toEqual(true)
})
it("should check for MS-SQL aliased status", () => {
const aliasing = new AliasTables(tables)
const datasource = getDatasource(SourceName.SQL_SERVER)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.CREATE), datasource)
).toEqual(false)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.READ), datasource)
).toEqual(true)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.UPDATE), datasource)
).toEqual(false)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.DELETE), datasource)
).toEqual(false)
})
it("should check for MySQL aliased status", () => {
const aliasing = new AliasTables(tables)
const datasource = getDatasource(SourceName.MYSQL)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.CREATE), datasource)
).toEqual(false)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.READ), datasource)
).toEqual(true)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.UPDATE), datasource)
).toEqual(false)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.DELETE), datasource)
).toEqual(false)
})
it("should check for Oracle aliased status", () => {
const aliasing = new AliasTables(tables)
const datasource = getDatasource(SourceName.ORACLE)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.CREATE), datasource)
).toEqual(false)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.READ), datasource)
).toEqual(true)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.UPDATE), datasource)
).toEqual(false)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.DELETE), datasource)
).toEqual(false)
})
it("should disable aliasing for non-SQL datasources", () => {
const aliasing = new AliasTables(tables)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.READ), {
source: SourceName.GOOGLE_SHEETS,
type: "datasource",
isSQL: false,
})
)
})
it("should disable when no fields", () => {
const aliasing = new AliasTables(tables)
const datasource = getDatasource(SourceName.POSTGRES)
expect(
aliasing.isAliasingEnabled(getQuery(Operation.READ, []), datasource)
).toEqual(false)
})
})
describe("check some edge cases", () => { describe("check some edge cases", () => {
const tableNames = ["hello", "world"] const tableNames = ["hello", "world"]

View File

@ -1,17 +1,51 @@
import cloneDeep from "lodash/cloneDeep" import cloneDeep from "lodash/cloneDeep"
import validateJs from "validate.js" import validateJs from "validate.js"
import { import {
Datasource,
DatasourcePlusQueryResponse,
FieldType, FieldType,
QueryJson, QueryJson,
Row, Row,
SourceName,
Table, Table,
TableSchema, TableSchema,
DatasourcePlusQueryResponse,
} from "@budibase/types" } from "@budibase/types"
import { makeExternalQuery } from "../../../integrations/base/query" import { makeExternalQuery } from "../../../integrations/base/query"
import { Format } from "../../../api/controllers/view/exporters" import { Format } from "../../../api/controllers/view/exporters"
import sdk from "../.." import sdk from "../.."
import { isRelationshipColumn } from "../../../db/utils" import { isRelationshipColumn } from "../../../db/utils"
import { SqlClient } from "../../../integrations/utils"
const SQL_CLIENT_SOURCE_MAP: Record<SourceName, SqlClient | undefined> = {
[SourceName.POSTGRES]: SqlClient.POSTGRES,
[SourceName.MYSQL]: SqlClient.MY_SQL,
[SourceName.SQL_SERVER]: SqlClient.MS_SQL,
[SourceName.ORACLE]: SqlClient.ORACLE,
[SourceName.DYNAMODB]: undefined,
[SourceName.MONGODB]: undefined,
[SourceName.ELASTICSEARCH]: undefined,
[SourceName.COUCHDB]: undefined,
[SourceName.S3]: undefined,
[SourceName.AIRTABLE]: undefined,
[SourceName.ARANGODB]: undefined,
[SourceName.REST]: undefined,
[SourceName.FIRESTORE]: undefined,
[SourceName.GOOGLE_SHEETS]: undefined,
[SourceName.REDIS]: undefined,
[SourceName.SNOWFLAKE]: undefined,
[SourceName.BUDIBASE]: undefined,
}
export function getSQLClient(datasource: Datasource): SqlClient {
if (!datasource.isSQL) {
throw new Error("Cannot get SQL Client for non-SQL datasource")
}
const lookup = SQL_CLIENT_SOURCE_MAP[datasource.source]
if (lookup) {
return lookup
}
throw new Error("Unable to determine client for SQL datasource")
}
export async function getDatasourceAndQuery( export async function getDatasourceAndQuery(
json: QueryJson json: QueryJson