From 47d782ad287fd50d1e12b9d61b2ad98a829bfdf3 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Fri, 2 Dec 2022 17:28:37 +0000 Subject: [PATCH] Final fix for #8882 - adding text to show the error - as well as fixing an issue with many to many updating correctly. --- .../Datasources/CreateEditRelationship.svelte | 21 +++ .../api/controllers/row/ExternalRequest.ts | 5 +- .../server/src/integrations/base/types.ts | 134 ++++++++++++++++++ .../src/integrations/microsoftSqlServer.ts | 38 +---- packages/server/src/integrations/mysql.ts | 7 +- packages/server/src/integrations/oracle.ts | 64 +++------ packages/server/src/integrations/postgres.ts | 6 +- 7 files changed, 186 insertions(+), 89 deletions(-) create mode 100644 packages/server/src/integrations/base/types.ts diff --git a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte index 230748b577..644ee9216b 100644 --- a/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte +++ b/packages/builder/src/components/backend/Datasources/CreateEditRelationship.svelte @@ -45,6 +45,23 @@ 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) { const isMany = fromRelate.relationshipType === RelationshipTypes.MANY_TO_MANY @@ -59,6 +76,10 @@ if ($touched.through && isMany && !fromRelate.through) { errObj.through = tableNotSet } + if ($touched.through && invalidThroughTable(fromRelate)) { + errObj.through = + "Table contains non-nullable columns which aren't generated" + } if ($touched.foreign && !isMany && !fromRelate.fieldName) { errObj.foreign = "Please pick the foreign key" } diff --git a/packages/server/src/api/controllers/row/ExternalRequest.ts b/packages/server/src/api/controllers/row/ExternalRequest.ts index de4ce317ef..a343553fc8 100644 --- a/packages/server/src/api/controllers/row/ExternalRequest.ts +++ b/packages/server/src/api/controllers/row/ExternalRequest.ts @@ -588,7 +588,10 @@ export class ExternalRequest { for (let [colName, { isMany, rows, tableId }] of Object.entries(related)) { const table: Table | undefined = this.getTable(tableId) // 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 } for (let row of rows) { diff --git a/packages/server/src/integrations/base/types.ts b/packages/server/src/integrations/base/types.ts new file mode 100644 index 0000000000..7144d20206 --- /dev/null +++ b/packages/server/src/integrations/base/types.ts @@ -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 } +} diff --git a/packages/server/src/integrations/microsoftSqlServer.ts b/packages/server/src/integrations/microsoftSqlServer.ts index 8c136dcc0f..437a9812a6 100644 --- a/packages/server/src/integrations/microsoftSqlServer.ts +++ b/packages/server/src/integrations/microsoftSqlServer.ts @@ -17,6 +17,7 @@ import { SqlClient, } from "./utils" import Sql from "./base/sql" +import { MSSQLTablesResponse, MSSQLColumn } from "./base/types" const sqlServer = require("mssql") const DEFAULT_SCHEMA = "dbo" @@ -31,41 +32,6 @@ interface MSSQLConfig { encrypt?: boolean } -interface TablesResponse { - TABLE_CATALOG: string - TABLE_SCHEMA: string - TABLE_NAME: string - TABLE_TYPE: string -} - -type 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 | string - NUMERIC_PRECISION_RADIX: null | string - NUMERIC_SCALE: null | string - 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 -} - const SCHEMA: Integration = { docs: "https://github.com/tediousjs/node-mssql", plus: true, @@ -238,7 +204,7 @@ class SqlServerIntegration extends Sql implements DatasourcePlus { */ async buildSchema(datasourceId: string, entities: Record) { 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)) { throw "Unable to get list of tables in database" } diff --git a/packages/server/src/integrations/mysql.ts b/packages/server/src/integrations/mysql.ts index acf8f4904a..136d84361a 100644 --- a/packages/server/src/integrations/mysql.ts +++ b/packages/server/src/integrations/mysql.ts @@ -18,6 +18,7 @@ import { import dayjs from "dayjs" const { NUMBER_REGEX } = require("../utilities") import Sql from "./base/sql" +import { MySQLColumn } from "./base/types" const mysql = require("mysql2/promise") @@ -203,11 +204,11 @@ class MySQLIntegration extends Sql implements DatasourcePlus { try { // get the tables first - const tablesResp = await this.internalQuery( + const tablesResp: Record[] = await this.internalQuery( { sql: "SHOW TABLES;" }, { connect: false } ) - const tableNames = tablesResp.map( + const tableNames: string[] = tablesResp.map( (obj: any) => obj[`Tables_in_${database}`] || obj[`Tables_in_${database.toLowerCase()}`] @@ -215,7 +216,7 @@ class MySQLIntegration extends Sql implements DatasourcePlus { for (let tableName of tableNames) { const primaryKeys = [] const schema: TableSchema = {} - const descResp = await this.internalQuery( + const descResp: MySQLColumn[] = await this.internalQuery( { sql: `DESCRIBE \`${tableName}\`;` }, { connect: false } ) diff --git a/packages/server/src/integrations/oracle.ts b/packages/server/src/integrations/oracle.ts index 13c2d11b8b..9ec9c3f858 100644 --- a/packages/server/src/integrations/oracle.ts +++ b/packages/server/src/integrations/oracle.ts @@ -24,6 +24,12 @@ import { ExecuteOptions, Result, } from "oracledb" +import { + OracleTable, + OracleColumn, + OracleColumnsResponse, + OracleConstraint, +} from "./base/types" let oracledb: any try { oracledb = require("oracledb") @@ -89,50 +95,6 @@ const SCHEMA: Integration = { 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 = { PRIMARY: "P", 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 */ - private mapColumns(result: Result): { + private mapColumns(result: Result): { [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 */ async buildSchema(datasourceId: string, entities: Record) { - const columnsResponse = await this.internalQuery({ + const columnsResponse = await this.internalQuery({ sql: this.COLUMNS_SQL, }) const oracleTables = this.mapColumns(columnsResponse) @@ -334,7 +296,9 @@ class OracleIntegration extends Sql implements DatasourcePlus { fieldSchema = { autocolumn: OracleIntegration.isAutoColumn(oracleColumn), name: columnName, - // TODO: add required constraint + constraints: { + presence: false, + }, ...this.internalConvertType(oracleColumn), } table.schema[columnName] = fieldSchema @@ -344,6 +308,12 @@ class OracleIntegration extends Sql implements DatasourcePlus { Object.values(oracleColumn.constraints).forEach(oracleConstraint => { if (oracleConstraint.type === OracleContraintTypes.PRIMARY) { table.primary!.push(columnName) + } else if ( + oracleConstraint.type === OracleContraintTypes.NOT_NULL_OR_CHECK + ) { + table.schema[columnName].constraints = { + presence: true, + } } }) }) diff --git a/packages/server/src/integrations/postgres.ts b/packages/server/src/integrations/postgres.ts index c0bda099eb..eba0d3691d 100644 --- a/packages/server/src/integrations/postgres.ts +++ b/packages/server/src/integrations/postgres.ts @@ -15,9 +15,10 @@ import { SqlClient, } from "./utils" import Sql from "./base/sql" +import { PostgresColumn } from "./base/types" +import { escapeDangerousCharacters } from "../utilities" const { Client, types } = require("pg") -const { escapeDangerousCharacters } = require("../utilities") // Return "date" and "timestamp" types as plain strings. // This lets us reference the original stored timezone. @@ -237,7 +238,8 @@ class PostgresIntegration extends Sql implements DatasourcePlus { } 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 } = {}