Merge pull request #11013 from Budibase/fix/sql-relationship-fixes
Further SQL relationship fixes
This commit is contained in:
commit
5e8cf41feb
|
@ -183,9 +183,7 @@ export async function buildSchemaFromDb(ctx: UserCtx) {
|
||||||
|
|
||||||
let { tables, error } = await buildSchemaHelper(datasource)
|
let { tables, error } = await buildSchemaHelper(datasource)
|
||||||
if (tablesFilter) {
|
if (tablesFilter) {
|
||||||
if (!datasource.entities) {
|
datasource.entities = {}
|
||||||
datasource.entities = {}
|
|
||||||
}
|
|
||||||
for (let key in tables) {
|
for (let key in tables) {
|
||||||
if (
|
if (
|
||||||
tablesFilter.some(
|
tablesFilter.some(
|
||||||
|
@ -200,7 +198,9 @@ export async function buildSchemaFromDb(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
setDefaultDisplayColumns(datasource)
|
setDefaultDisplayColumns(datasource)
|
||||||
const dbResp = await db.put(datasource)
|
const dbResp = await db.put(
|
||||||
|
sdk.tables.populateExternalTableSchemas(datasource)
|
||||||
|
)
|
||||||
datasource._rev = dbResp.rev
|
datasource._rev = dbResp.rev
|
||||||
const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource)
|
const cleanedDatasource = await sdk.datasources.removeSecretSingle(datasource)
|
||||||
|
|
||||||
|
@ -286,7 +286,9 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
|
||||||
datasource.config!.auth = auth
|
datasource.config!.auth = auth
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.put(datasource)
|
const response = await db.put(
|
||||||
|
sdk.tables.populateExternalTableSchemas(datasource)
|
||||||
|
)
|
||||||
await events.datasource.updated(datasource)
|
await events.datasource.updated(datasource)
|
||||||
datasource._rev = response.rev
|
datasource._rev = response.rev
|
||||||
|
|
||||||
|
@ -327,7 +329,9 @@ export async function save(
|
||||||
setDefaultDisplayColumns(datasource)
|
setDefaultDisplayColumns(datasource)
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbResp = await db.put(datasource)
|
const dbResp = await db.put(
|
||||||
|
sdk.tables.populateExternalTableSchemas(datasource)
|
||||||
|
)
|
||||||
await events.datasource.created(datasource)
|
await events.datasource.created(datasource)
|
||||||
datasource._rev = dbResp.rev
|
datasource._rev = dbResp.rev
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import { cloneDeep } from "lodash/fp"
|
||||||
import { processDates, processFormulas } from "../../../utilities/rowProcessor"
|
import { processDates, processFormulas } from "../../../utilities/rowProcessor"
|
||||||
import { db as dbCore } from "@budibase/backend-core"
|
import { db as dbCore } from "@budibase/backend-core"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
|
import { isEditableColumn } from "../../../sdk/app/tables/validation"
|
||||||
|
|
||||||
export interface ManyRelationship {
|
export interface ManyRelationship {
|
||||||
tableId?: string
|
tableId?: string
|
||||||
|
@ -298,8 +299,7 @@ export class ExternalRequest {
|
||||||
if (
|
if (
|
||||||
row[key] == null ||
|
row[key] == null ||
|
||||||
newRow[key] ||
|
newRow[key] ||
|
||||||
field.autocolumn ||
|
!sdk.tables.isEditableColumn(field)
|
||||||
field.type === FieldTypes.FORMULA
|
|
||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,34 @@
|
||||||
import {
|
import {
|
||||||
buildExternalTableId,
|
|
||||||
breakExternalTableId,
|
breakExternalTableId,
|
||||||
|
buildExternalTableId,
|
||||||
} from "../../../integrations/utils"
|
} from "../../../integrations/utils"
|
||||||
import {
|
import {
|
||||||
|
foreignKeyStructure,
|
||||||
generateForeignKey,
|
generateForeignKey,
|
||||||
generateJunctionTableName,
|
generateJunctionTableName,
|
||||||
foreignKeyStructure,
|
|
||||||
hasTypeChanged,
|
hasTypeChanged,
|
||||||
setStaticSchemas,
|
setStaticSchemas,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
import { FieldTypes } from "../../../constants"
|
import { FieldTypes } from "../../../constants"
|
||||||
import { makeExternalQuery } from "../../../integrations/base/query"
|
import { makeExternalQuery } from "../../../integrations/base/query"
|
||||||
import { handleRequest } from "../row/external"
|
import { handleRequest } from "../row/external"
|
||||||
import { events, context } from "@budibase/backend-core"
|
import { context, events } from "@budibase/backend-core"
|
||||||
import { parse, isRows, isSchema } from "../../../utilities/schema"
|
import { isRows, isSchema, parse } from "../../../utilities/schema"
|
||||||
import {
|
import {
|
||||||
|
AutoReason,
|
||||||
Datasource,
|
Datasource,
|
||||||
Table,
|
|
||||||
QueryJson,
|
|
||||||
Operation,
|
|
||||||
RenameColumn,
|
|
||||||
FieldSchema,
|
FieldSchema,
|
||||||
UserCtx,
|
Operation,
|
||||||
TableRequest,
|
QueryJson,
|
||||||
RelationshipTypes,
|
RelationshipTypes,
|
||||||
|
RenameColumn,
|
||||||
|
Table,
|
||||||
|
TableRequest,
|
||||||
|
UserCtx,
|
||||||
} from "@budibase/types"
|
} from "@budibase/types"
|
||||||
import sdk from "../../../sdk"
|
import sdk from "../../../sdk"
|
||||||
import { builderSocket } from "../../../websockets"
|
import { builderSocket } from "../../../websockets"
|
||||||
|
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
|
|
||||||
async function makeTableRequest(
|
async function makeTableRequest(
|
||||||
|
@ -317,7 +319,7 @@ export async function save(ctx: UserCtx) {
|
||||||
delete tableToSave._rename
|
delete tableToSave._rename
|
||||||
// store it into couch now for budibase reference
|
// store it into couch now for budibase reference
|
||||||
datasource.entities[tableToSave.name] = tableToSave
|
datasource.entities[tableToSave.name] = tableToSave
|
||||||
await db.put(datasource)
|
await db.put(sdk.tables.populateExternalTableSchemas(datasource))
|
||||||
|
|
||||||
// Since tables are stored inside datasources, we need to notify clients
|
// Since tables are stored inside datasources, we need to notify clients
|
||||||
// that the datasource definition changed
|
// that the datasource definition changed
|
||||||
|
@ -348,7 +350,7 @@ export async function destroy(ctx: UserCtx) {
|
||||||
datasource.entities = tables
|
datasource.entities = tables
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.put(datasource)
|
await db.put(sdk.tables.populateExternalTableSchemas(datasource))
|
||||||
|
|
||||||
// Since tables are stored inside datasources, we need to notify clients
|
// Since tables are stored inside datasources, we need to notify clients
|
||||||
// that the datasource definition changed
|
// that the datasource definition changed
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
} from "../../../integrations/utils"
|
} from "../../../integrations/utils"
|
||||||
import { Table, Database } from "@budibase/types"
|
import { Table, Database } from "@budibase/types"
|
||||||
import datasources from "../datasources"
|
import datasources from "../datasources"
|
||||||
|
import { populateExternalTableSchemas, isEditableColumn } from "./validation"
|
||||||
|
|
||||||
async function getAllInternalTables(db?: Database): Promise<Table[]> {
|
async function getAllInternalTables(db?: Database): Promise<Table[]> {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
|
@ -60,4 +61,6 @@ export default {
|
||||||
getAllExternalTables,
|
getAllExternalTables,
|
||||||
getExternalTable,
|
getExternalTable,
|
||||||
getTable,
|
getTable,
|
||||||
|
populateExternalTableSchemas,
|
||||||
|
isEditableColumn,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { populateExternalTableSchemas } from "../validation"
|
||||||
|
import { cloneDeep } from "lodash/fp"
|
||||||
|
import { Datasource, Table } from "@budibase/types"
|
||||||
|
import { isEqual } from "lodash"
|
||||||
|
|
||||||
|
const SCHEMA = {
|
||||||
|
entities: {
|
||||||
|
client: {
|
||||||
|
_id: "tableA",
|
||||||
|
name: "client",
|
||||||
|
primary: ["idC"],
|
||||||
|
primaryDisplay: "Name",
|
||||||
|
schema: {
|
||||||
|
idC: {
|
||||||
|
autocolumn: true,
|
||||||
|
externalType: "int unsigned",
|
||||||
|
name: "idC",
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
autocolumn: false,
|
||||||
|
externalType: "varchar(255)",
|
||||||
|
name: "Name",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
fieldName: "idC",
|
||||||
|
foreignKey: "idC",
|
||||||
|
main: true,
|
||||||
|
name: "project",
|
||||||
|
relationshipType: "many-to-one",
|
||||||
|
tableId: "tableB",
|
||||||
|
type: "link",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
project: {
|
||||||
|
_id: "tableB",
|
||||||
|
name: "project",
|
||||||
|
primary: ["idP"],
|
||||||
|
primaryDisplay: "Name",
|
||||||
|
schema: {
|
||||||
|
idC: {
|
||||||
|
externalType: "int unsigned",
|
||||||
|
name: "idC",
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
idP: {
|
||||||
|
autocolumn: true,
|
||||||
|
externalType: "int unsigned",
|
||||||
|
name: "idProject",
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
Name: {
|
||||||
|
autocolumn: false,
|
||||||
|
externalType: "varchar(255)",
|
||||||
|
name: "Name",
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
client: {
|
||||||
|
fieldName: "idC",
|
||||||
|
foreignKey: "idC",
|
||||||
|
name: "client",
|
||||||
|
relationshipType: "one-to-many",
|
||||||
|
tableId: "tableA",
|
||||||
|
type: "link",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sql: true,
|
||||||
|
type: "table",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const OTHER_CLIENT_COLS = ["idC", "Name", "project"]
|
||||||
|
const OTHER_PROJECT_COLS = ["idP", "Name", "client"]
|
||||||
|
|
||||||
|
describe("validation and update of external table schemas", () => {
|
||||||
|
function getForeignKeyColumn(datasource: Datasource) {
|
||||||
|
return datasource.entities!["project"].schema.idC
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkOtherColumns(
|
||||||
|
table: Table,
|
||||||
|
compareTable: Table,
|
||||||
|
columnsToCheck: string[]
|
||||||
|
) {
|
||||||
|
for (let columnName of columnsToCheck) {
|
||||||
|
const columnA = table.schema[columnName]
|
||||||
|
const columnB = table.schema[columnName]
|
||||||
|
expect(isEqual(columnA, columnB)).toBe(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function noOtherTableChanges(response: any) {
|
||||||
|
checkOtherColumns(
|
||||||
|
response.entities!.client!,
|
||||||
|
SCHEMA.entities.client as Table,
|
||||||
|
OTHER_CLIENT_COLS
|
||||||
|
)
|
||||||
|
checkOtherColumns(
|
||||||
|
response.entities!.project!,
|
||||||
|
SCHEMA.entities.project as Table,
|
||||||
|
OTHER_PROJECT_COLS
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should correctly set utilised foreign keys to autocolumns", () => {
|
||||||
|
const response = populateExternalTableSchemas(cloneDeep(SCHEMA) as any)
|
||||||
|
const foreignKey = getForeignKeyColumn(response)
|
||||||
|
expect(foreignKey.autocolumn).toBe(true)
|
||||||
|
expect(foreignKey.autoReason).toBe("foreign_key")
|
||||||
|
noOtherTableChanges(response)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should correctly unset foreign keys when no longer used", () => {
|
||||||
|
const setResponse = populateExternalTableSchemas(cloneDeep(SCHEMA) as any)
|
||||||
|
const beforeFk = getForeignKeyColumn(setResponse)
|
||||||
|
delete setResponse.entities!.client.schema.project
|
||||||
|
delete setResponse.entities!.project.schema.client
|
||||||
|
const response = populateExternalTableSchemas(cloneDeep(setResponse))
|
||||||
|
const afterFk = getForeignKeyColumn(response)
|
||||||
|
expect(beforeFk.autocolumn).toBe(true)
|
||||||
|
expect(beforeFk.autoReason).toBe("foreign_key")
|
||||||
|
expect(afterFk.autocolumn).toBeUndefined()
|
||||||
|
expect(afterFk.autoReason).toBeUndefined()
|
||||||
|
noOtherTableChanges(response)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,67 @@
|
||||||
|
import {
|
||||||
|
AutoReason,
|
||||||
|
Datasource,
|
||||||
|
FieldSchema,
|
||||||
|
FieldType,
|
||||||
|
RelationshipTypes,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { FieldTypes } from "../../../constants"
|
||||||
|
|
||||||
|
function checkForeignKeysAreAutoColumns(datasource: Datasource) {
|
||||||
|
if (!datasource.entities) {
|
||||||
|
return datasource
|
||||||
|
}
|
||||||
|
const tables = Object.values(datasource.entities)
|
||||||
|
// make sure all foreign key columns are marked as auto columns
|
||||||
|
const foreignKeys: { tableId: string; key: string }[] = []
|
||||||
|
for (let table of tables) {
|
||||||
|
const relationships = Object.values(table.schema).filter(
|
||||||
|
column => column.type === FieldType.LINK
|
||||||
|
)
|
||||||
|
relationships.forEach(relationship => {
|
||||||
|
if (relationship.relationshipType === RelationshipTypes.MANY_TO_MANY) {
|
||||||
|
const tableId = relationship.through!
|
||||||
|
foreignKeys.push({ key: relationship.throughTo!, tableId })
|
||||||
|
foreignKeys.push({ key: relationship.throughFrom!, tableId })
|
||||||
|
} else {
|
||||||
|
const fk = relationship.foreignKey!
|
||||||
|
const oneSide =
|
||||||
|
relationship.relationshipType === RelationshipTypes.ONE_TO_MANY
|
||||||
|
foreignKeys.push({
|
||||||
|
tableId: oneSide ? table._id! : relationship.tableId!,
|
||||||
|
key: fk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// now make sure schemas are all accurate
|
||||||
|
for (let table of tables) {
|
||||||
|
for (let column of Object.values(table.schema)) {
|
||||||
|
const shouldBeForeign = foreignKeys.find(
|
||||||
|
options => options.tableId === table._id && options.key === column.name
|
||||||
|
)
|
||||||
|
// don't change already auto-columns to it, e.g. primary keys that are foreign
|
||||||
|
if (shouldBeForeign && !column.autocolumn) {
|
||||||
|
column.autocolumn = true
|
||||||
|
column.autoReason = AutoReason.FOREIGN_KEY
|
||||||
|
} else if (column.autoReason === AutoReason.FOREIGN_KEY) {
|
||||||
|
delete column.autocolumn
|
||||||
|
delete column.autoReason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return datasource
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEditableColumn(column: FieldSchema) {
|
||||||
|
const isAutoColumn =
|
||||||
|
column.autocolumn && column.autoReason !== AutoReason.FOREIGN_KEY
|
||||||
|
const isFormula = column.type === FieldTypes.FORMULA
|
||||||
|
return !(isAutoColumn || isFormula)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function populateExternalTableSchemas(datasource: Datasource) {
|
||||||
|
return checkForeignKeysAreAutoColumns(datasource)
|
||||||
|
}
|
|
@ -9,6 +9,10 @@ export enum RelationshipTypes {
|
||||||
MANY_TO_MANY = "many-to-many",
|
MANY_TO_MANY = "many-to-many",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AutoReason {
|
||||||
|
FOREIGN_KEY = "foreign_key",
|
||||||
|
}
|
||||||
|
|
||||||
export interface FieldSchema {
|
export interface FieldSchema {
|
||||||
type: FieldType
|
type: FieldType
|
||||||
externalType?: string
|
externalType?: string
|
||||||
|
@ -21,6 +25,7 @@ export interface FieldSchema {
|
||||||
foreignKey?: string
|
foreignKey?: string
|
||||||
icon?: string
|
icon?: string
|
||||||
autocolumn?: boolean
|
autocolumn?: boolean
|
||||||
|
autoReason?: AutoReason
|
||||||
subtype?: string
|
subtype?: string
|
||||||
throughFrom?: string
|
throughFrom?: string
|
||||||
throughTo?: string
|
throughTo?: string
|
||||||
|
|
Loading…
Reference in New Issue