Merge pull request #8915 from Budibase/fix/8882

Fix/8882
This commit is contained in:
Michael Drury 2022-12-05 12:56:34 +00:00 committed by GitHub
commit 5e0664e683
9 changed files with 251 additions and 67 deletions

View File

@ -45,6 +45,23 @@
const touched = writable({}) const touched = writable({})
function invalidThroughTable({ through, throughTo, throughFrom }) {
// need to know the foreign key columns to check error
if (!through || !throughTo || !throughFrom) {
return false
}
const throughTable = plusTables.find(tbl => tbl._id === through)
const otherColumns = Object.values(throughTable.schema).filter(
col => col.name !== throughFrom && col.name !== throughTo
)
for (let col of otherColumns) {
if (col.constraints?.presence && !col.autocolumn) {
return true
}
}
return false
}
function checkForErrors(fromRelate, toRelate) { function checkForErrors(fromRelate, toRelate) {
const isMany = const isMany =
fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY
@ -59,6 +76,10 @@
if ($touched.through && isMany && !fromRelate.through) { if ($touched.through && isMany && !fromRelate.through) {
errObj.through = tableNotSet errObj.through = tableNotSet
} }
if ($touched.through && invalidThroughTable(fromRelate)) {
errObj.through =
"Ensure all columns in table are nullable or auto generated"
}
if ($touched.foreign && !isMany && !fromRelate.fieldName) { if ($touched.foreign && !isMany && !fromRelate.fieldName) {
errObj.foreign = "Please pick the foreign key" errObj.foreign = "Please pick the foreign key"
} }

View File

@ -11,7 +11,7 @@ GO
CREATE TABLE products CREATE TABLE products
( (
id int IDENTITY(1,1), id int IDENTITY(1,1),
name varchar (20), name varchar (20) NOT NULL,
description varchar(30), description varchar(30),
CONSTRAINT pk_products PRIMARY KEY NONCLUSTERED (id) CONSTRAINT pk_products PRIMARY KEY NONCLUSTERED (id)
); );
@ -22,7 +22,7 @@ GO
CREATE TABLE tasks CREATE TABLE tasks
( (
taskid int IDENTITY(1,1), taskid int IDENTITY(1,1),
taskname varchar (20), taskname varchar (20) NOT NULL,
productid int, productid int,
CONSTRAINT pk_tasks PRIMARY KEY NONCLUSTERED (taskid), CONSTRAINT pk_tasks PRIMARY KEY NONCLUSTERED (taskid),
CONSTRAINT fk_products FOREIGN KEY (productid) REFERENCES products (id), CONSTRAINT fk_products FOREIGN KEY (productid) REFERENCES products (id),
@ -33,7 +33,7 @@ IF OBJECT_ID ('dbo.people', 'U') IS NOT NULL
GO GO
CREATE TABLE people CREATE TABLE people
( (
name varchar(30), name varchar(30) NOT NULL,
age varchar(20), age varchar(20),
CONSTRAINT pk_people PRIMARY KEY NONCLUSTERED (name, age) CONSTRAINT pk_people PRIMARY KEY NONCLUSTERED (name, age)
); );

View File

@ -0,0 +1,46 @@
SELECT 'CREATE DATABASE main'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'main')\gexec
CREATE TABLE IF NOT EXISTS public."Employee"
(
id integer NOT NULL,
name text COLLATE pg_catalog."default",
CONSTRAINT "Employee_pkey" PRIMARY KEY (id)
)
WITH (
OIDS = FALSE
);
INSERT INTO public."Employee" ("id", "name") VALUES (1, 'Alice');
INSERT INTO public."Employee" ("id", "name") VALUES (2, 'Bob');
CREATE TABLE IF NOT EXISTS public."Skills"
(
id integer NOT NULL,
name text COLLATE pg_catalog."default",
CONSTRAINT "Skills_pkey" PRIMARY KEY (id)
)
WITH (
OIDS = FALSE
);
INSERT INTO public."Skills" ("id", "name") VALUES (1, 'Docker');
INSERT INTO public."Skills" ("id", "name") VALUES (2, 'Microservices');
INSERT INTO public."Skills" ("id", "name") VALUES (3, 'Kubernetes');
INSERT INTO public."Skills" ("id", "name") VALUES (4, 'Spring');
CREATE TABLE IF NOT EXISTS public."jt_employee_skills_Skills_employee"
(
employee_id integer,
skills_id integer,
id integer NOT NULL,
CONSTRAINT "jt_employee_skills_Skills_employee_pkey" PRIMARY KEY (id)
)
WITH (
OIDS = FALSE
);
insert into public."jt_employee_skills_Skills_employee" ("id", "employee_id", "skills_id") VALUES (1, 1, 1);
insert into public."jt_employee_skills_Skills_employee" ("id", "employee_id", "skills_id") VALUES (2, 1, 2);
insert into public."jt_employee_skills_Skills_employee" ("id", "employee_id", "skills_id") VALUES (3, 1, 3);
insert into public."jt_employee_skills_Skills_employee" ("id", "employee_id", "skills_id") VALUES (4, 2, 2);
insert into public."jt_employee_skills_Skills_employee" ("id", "employee_id", "skills_id") VALUES (5, 2, 3);
insert into public."jt_employee_skills_Skills_employee" ("id", "employee_id", "skills_id") VALUES (6, 2, 4);

View File

@ -588,7 +588,10 @@ export class ExternalRequest {
for (let [colName, { isMany, rows, tableId }] of Object.entries(related)) { for (let [colName, { isMany, rows, tableId }] of Object.entries(related)) {
const table: Table | undefined = this.getTable(tableId) const table: Table | undefined = this.getTable(tableId)
// if its not the foreign key skip it, nothing to do // if its not the foreign key skip it, nothing to do
if (!table || (table.primary && table.primary.indexOf(colName) !== -1)) { if (
!table ||
(!isMany && table.primary && table.primary.indexOf(colName) !== -1)
) {
continue continue
} }
for (let row of rows) { for (let row of rows) {

View File

@ -0,0 +1,134 @@
export interface MSSQLTablesResponse {
TABLE_CATALOG: string
TABLE_SCHEMA: string
TABLE_NAME: string
TABLE_TYPE: string
}
export interface MSSQLColumn {
IS_COMPUTED: number
IS_IDENTITY: number
TABLE_CATALOG: string
TABLE_SCHEMA: string
TABLE_NAME: string
COLUMN_NAME: string
ORDINAL_POSITION: number
COLUMN_DEFAULT: null | any
IS_NULLABLE: "NO" | "YES"
DATA_TYPE: string
CHARACTER_MAXIMUM_LENGTH: null | number
CHARACTER_OCTET_LENGTH: null | number
NUMERIC_PRECISION: null | number
NUMERIC_PRECISION_RADIX: null | number
NUMERIC_SCALE: null | number
DATETIME_PRECISION: null | string
CHARACTER_SET_CATALOG: null | string
CHARACTER_SET_SCHEMA: null | string
CHARACTER_SET_NAME: null | string
COLLATION_CATALOG: null | string
COLLATION_SCHEMA: null | string
COLLATION_NAME: null | string
DOMAIN_CATALOG: null | string
DOMAIN_SCHEMA: null | string
DOMAIN_NAME: null | string
}
export interface PostgresColumn {
table_catalog: string
table_schema: string
table_name: string
column_name: string
ordinal_position: number
column_default: null | any
is_nullable: "NO" | "YES"
data_type: string
character_maximum_length: null | number
character_octet_length: null | number
numeric_precision: null | number
numeric_precision_radix: null | number
numeric_scale: null | number
datetime_precision: null | string
interval_type: null | string
interval_precision: null | string
character_set_catalog: null | string
character_set_schema: null | string
character_set_name: null | string
collation_catalog: null | string
collation_schema: null | string
collation_name: null | string
domain_catalog: null | string
domain_schema: null | string
domain_name: null | string
udt_catalog: string
udt_schema: string
udt_name: string
scope_catalog: null | string
scope_schema: null | string
scope_name: null | string
maximum_cardinality: null | string
dtd_identifier: string
is_self_referencing: "NO" | "YES"
is_identity: "NO" | "YES"
identity_generation: null | number
identity_start: null | number
identity_increment: null | number
identity_maximum: null | number
identity_minimum: null | number
identity_cycle: "NO" | "YES"
is_generated: "NEVER"
generation_expression: null | string
is_updatable: "NO" | "YES"
}
export interface MySQLColumn {
Field: string
Type: string
Null: "NO" | "YES"
Key: "PRI" | "MUL" | ""
Default: null | any
Extra: null | string
}
/**
* Raw query response
*/
export interface OracleColumnsResponse {
TABLE_NAME: string
COLUMN_NAME: string
DATA_TYPE: string
DATA_DEFAULT: null | string
COLUMN_ID: number
CONSTRAINT_NAME: null | string
CONSTRAINT_TYPE: null | string
R_CONSTRAINT_NAME: null | string
SEARCH_CONDITION: null | string
}
/**
* An oracle constraint
*/
export interface OracleConstraint {
name: string
type: string
relatedConstraintName: null | string
searchCondition: null | string
}
/**
* An oracle column and it's related constraints
*/
export interface OracleColumn {
name: string
type: string
default: null | string
id: number
constraints: { [key: string]: OracleConstraint }
}
/**
* An oracle table and it's related columns
*/
export interface OracleTable {
name: string
columns: { [key: string]: OracleColumn }
}

View File

@ -17,6 +17,7 @@ import {
SqlClient, SqlClient,
} from "./utils" } from "./utils"
import Sql from "./base/sql" import Sql from "./base/sql"
import { MSSQLTablesResponse, MSSQLColumn } from "./base/types"
const sqlServer = require("mssql") const sqlServer = require("mssql")
const DEFAULT_SCHEMA = "dbo" const DEFAULT_SCHEMA = "dbo"
@ -31,13 +32,6 @@ interface MSSQLConfig {
encrypt?: boolean encrypt?: boolean
} }
interface TablesResponse {
TABLE_CATALOG: string
TABLE_SCHEMA: string
TABLE_NAME: string
TABLE_TYPE: string
}
const SCHEMA: Integration = { const SCHEMA: Integration = {
docs: "https://github.com/tediousjs/node-mssql", docs: "https://github.com/tediousjs/node-mssql",
plus: true, plus: true,
@ -210,7 +204,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
*/ */
async buildSchema(datasourceId: string, entities: Record<string, Table>) { async buildSchema(datasourceId: string, entities: Record<string, Table>) {
await this.connect() await this.connect()
let tableInfo: TablesResponse[] = 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)) {
throw "Unable to get list of tables in database" throw "Unable to get list of tables in database"
} }
@ -228,15 +222,20 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
// find primary key constraints // find primary key constraints
const constraints = await this.runSQL(this.getConstraintsSQL(tableName)) const constraints = await this.runSQL(this.getConstraintsSQL(tableName))
// find the computed and identity columns (auto columns) // find the computed and identity columns (auto columns)
const columns = await this.runSQL(this.getAutoColumnsSQL(tableName)) const columns: MSSQLColumn[] = await this.runSQL(
this.getAutoColumnsSQL(tableName)
)
const primaryKeys = constraints const primaryKeys = constraints
.filter( .filter(
(constraint: any) => constraint.CONSTRAINT_TYPE === "PRIMARY KEY" (constraint: any) => constraint.CONSTRAINT_TYPE === "PRIMARY KEY"
) )
.map((constraint: any) => constraint.COLUMN_NAME) .map((constraint: any) => constraint.COLUMN_NAME)
const autoColumns = columns const autoColumns = columns
.filter((col: any) => col.IS_COMPUTED || col.IS_IDENTITY) .filter(col => col.IS_COMPUTED || col.IS_IDENTITY)
.map((col: any) => col.COLUMN_NAME) .map(col => col.COLUMN_NAME)
const requiredColumns = columns
.filter(col => col.IS_NULLABLE === "NO")
.map(col => col.COLUMN_NAME)
let schema: TableSchema = {} let schema: TableSchema = {}
for (let def of definition) { for (let def of definition) {
@ -245,8 +244,11 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
continue continue
} }
schema[name] = { schema[name] = {
autocolumn: !!autoColumns.find((col: string) => col === name), autocolumn: !!autoColumns.find(col => col === name),
name: name, name: name,
constraints: {
presence: requiredColumns.find(col => col === name),
},
...convertSqlType(def.DATA_TYPE), ...convertSqlType(def.DATA_TYPE),
externalType: def.DATA_TYPE, externalType: def.DATA_TYPE,
} }

View File

@ -18,6 +18,7 @@ import {
import dayjs from "dayjs" import dayjs from "dayjs"
const { NUMBER_REGEX } = require("../utilities") const { NUMBER_REGEX } = require("../utilities")
import Sql from "./base/sql" import Sql from "./base/sql"
import { MySQLColumn } from "./base/types"
const mysql = require("mysql2/promise") const mysql = require("mysql2/promise")
@ -203,11 +204,11 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
try { try {
// get the tables first // get the tables first
const tablesResp = await this.internalQuery( const tablesResp: Record<string, string>[] = await this.internalQuery(
{ sql: "SHOW TABLES;" }, { sql: "SHOW TABLES;" },
{ connect: false } { connect: false }
) )
const tableNames = tablesResp.map( const tableNames: string[] = tablesResp.map(
(obj: any) => (obj: any) =>
obj[`Tables_in_${database}`] || obj[`Tables_in_${database}`] ||
obj[`Tables_in_${database.toLowerCase()}`] obj[`Tables_in_${database.toLowerCase()}`]
@ -215,7 +216,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus {
for (let tableName of tableNames) { for (let tableName of tableNames) {
const primaryKeys = [] const primaryKeys = []
const schema: TableSchema = {} const schema: TableSchema = {}
const descResp = await this.internalQuery( const descResp: MySQLColumn[] = await this.internalQuery(
{ sql: `DESCRIBE \`${tableName}\`;` }, { sql: `DESCRIBE \`${tableName}\`;` },
{ connect: false } { connect: false }
) )

View File

@ -24,6 +24,12 @@ import {
ExecuteOptions, ExecuteOptions,
Result, Result,
} from "oracledb" } from "oracledb"
import {
OracleTable,
OracleColumn,
OracleColumnsResponse,
OracleConstraint,
} from "./base/types"
let oracledb: any let oracledb: any
try { try {
oracledb = require("oracledb") oracledb = require("oracledb")
@ -89,50 +95,6 @@ const SCHEMA: Integration = {
const UNSUPPORTED_TYPES = ["BLOB", "CLOB", "NCLOB"] const UNSUPPORTED_TYPES = ["BLOB", "CLOB", "NCLOB"]
/**
* Raw query response
*/
interface ColumnsResponse {
TABLE_NAME: string
COLUMN_NAME: string
DATA_TYPE: string
DATA_DEFAULT: string | null
COLUMN_ID: number
CONSTRAINT_NAME: string | null
CONSTRAINT_TYPE: string | null
R_CONSTRAINT_NAME: string | null
SEARCH_CONDITION: string | null
}
/**
* An oracle constraint
*/
interface OracleConstraint {
name: string
type: string
relatedConstraintName: string | null
searchCondition: string | null
}
/**
* An oracle column and it's related constraints
*/
interface OracleColumn {
name: string
type: string
default: string | null
id: number
constraints: { [key: string]: OracleConstraint }
}
/**
* An oracle table and it's related columns
*/
interface OracleTable {
name: string
columns: { [key: string]: OracleColumn }
}
const OracleContraintTypes = { const OracleContraintTypes = {
PRIMARY: "P", PRIMARY: "P",
NOT_NULL_OR_CHECK: "C", NOT_NULL_OR_CHECK: "C",
@ -195,7 +157,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
/** /**
* Map the flat tabular columns and constraints data into a nested object * Map the flat tabular columns and constraints data into a nested object
*/ */
private mapColumns(result: Result<ColumnsResponse>): { private mapColumns(result: Result<OracleColumnsResponse>): {
[key: string]: OracleTable [key: string]: OracleTable
} { } {
const oracleTables: { [key: string]: OracleTable } = {} const oracleTables: { [key: string]: OracleTable } = {}
@ -299,7 +261,7 @@ class OracleIntegration extends Sql implements DatasourcePlus {
* @param entities - the tables that are to be built * @param entities - the tables that are to be built
*/ */
async buildSchema(datasourceId: string, entities: Record<string, Table>) { async buildSchema(datasourceId: string, entities: Record<string, Table>) {
const columnsResponse = await this.internalQuery<ColumnsResponse>({ const columnsResponse = await this.internalQuery<OracleColumnsResponse>({
sql: this.COLUMNS_SQL, sql: this.COLUMNS_SQL,
}) })
const oracleTables = this.mapColumns(columnsResponse) const oracleTables = this.mapColumns(columnsResponse)
@ -334,6 +296,9 @@ class OracleIntegration extends Sql implements DatasourcePlus {
fieldSchema = { fieldSchema = {
autocolumn: OracleIntegration.isAutoColumn(oracleColumn), autocolumn: OracleIntegration.isAutoColumn(oracleColumn),
name: columnName, name: columnName,
constraints: {
presence: false,
},
...this.internalConvertType(oracleColumn), ...this.internalConvertType(oracleColumn),
} }
table.schema[columnName] = fieldSchema table.schema[columnName] = fieldSchema
@ -343,6 +308,12 @@ class OracleIntegration extends Sql implements DatasourcePlus {
Object.values(oracleColumn.constraints).forEach(oracleConstraint => { Object.values(oracleColumn.constraints).forEach(oracleConstraint => {
if (oracleConstraint.type === OracleContraintTypes.PRIMARY) { if (oracleConstraint.type === OracleContraintTypes.PRIMARY) {
table.primary!.push(columnName) table.primary!.push(columnName)
} else if (
oracleConstraint.type === OracleContraintTypes.NOT_NULL_OR_CHECK
) {
table.schema[columnName].constraints = {
presence: true,
}
} }
}) })
}) })

View File

@ -15,9 +15,10 @@ import {
SqlClient, SqlClient,
} from "./utils" } from "./utils"
import Sql from "./base/sql" import Sql from "./base/sql"
import { PostgresColumn } from "./base/types"
import { escapeDangerousCharacters } from "../utilities"
const { Client, types } = require("pg") const { Client, types } = require("pg")
const { escapeDangerousCharacters } = require("../utilities")
// Return "date" and "timestamp" types as plain strings. // Return "date" and "timestamp" types as plain strings.
// This lets us reference the original stored timezone. // This lets us reference the original stored timezone.
@ -237,7 +238,8 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
} }
try { try {
const columnsResponse = await this.client.query(this.COLUMNS_SQL) const columnsResponse: { rows: PostgresColumn[] } =
await this.client.query(this.COLUMNS_SQL)
const tables: { [key: string]: Table } = {} const tables: { [key: string]: Table } = {}
@ -260,6 +262,9 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
column.identity_start || column.identity_start ||
column.identity_increment column.identity_increment
) )
const constraints = {
presence: column.is_nullable === "NO",
}
const hasDefault = const hasDefault =
typeof column.column_default === "string" && typeof column.column_default === "string" &&
column.column_default.startsWith("nextval") column.column_default.startsWith("nextval")
@ -269,6 +274,7 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
tables[tableName].schema[columnName] = { tables[tableName].schema[columnName] = {
autocolumn: isAuto, autocolumn: isAuto,
name: columnName, name: columnName,
constraints,
...convertSqlType(column.data_type), ...convertSqlType(column.data_type),
externalType: column.data_type, externalType: column.data_type,
} }