Boolean support and linting

This commit is contained in:
Rory Powell 2021-11-18 13:35:22 +00:00
parent 345490fed3
commit b0df7fb28f
6 changed files with 176 additions and 82 deletions

View File

@ -226,7 +226,12 @@ module External {
manyRelationships: ManyRelationship[] = [] manyRelationships: ManyRelationship[] = []
for (let [key, field] of Object.entries(table.schema)) { for (let [key, field] of Object.entries(table.schema)) {
// if set already, or not set just skip it // if set already, or not set just skip it
if (row[key] == null || newRow[key] || field.autocolumn || field.type === FieldTypes.FORMULA) { if (
row[key] == null ||
newRow[key] ||
field.autocolumn ||
field.type === FieldTypes.FORMULA
) {
continue continue
} }
// if its an empty string then it means return the column to null (if possible) // if its an empty string then it means return the column to null (if possible)

View File

@ -279,7 +279,9 @@ class SqlQueryBuilder extends SqlTableQueryBuilder {
case Operation.DELETE: case Operation.DELETE:
query = buildDelete(client, json, opts) query = buildDelete(client, json, opts)
break break
case Operation.CREATE_TABLE: case Operation.UPDATE_TABLE: case Operation.DELETE_TABLE: case Operation.CREATE_TABLE:
case Operation.UPDATE_TABLE:
case Operation.DELETE_TABLE:
return this._tableQuery(json) return this._tableQuery(json)
default: default:
throw `Operation type is not supported by SQL query builder` throw `Operation type is not supported by SQL query builder`

View File

@ -6,7 +6,12 @@ import SchemaBuilder = Knex.SchemaBuilder
import CreateTableBuilder = Knex.CreateTableBuilder import CreateTableBuilder = Knex.CreateTableBuilder
const { FieldTypes, RelationshipTypes } = require("../../constants") const { FieldTypes, RelationshipTypes } = require("../../constants")
function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record<string, Table>, oldTable: null | Table = null) { function generateSchema(
schema: CreateTableBuilder,
table: Table,
tables: Record<string, Table>,
oldTable: null | Table = null
) {
let primaryKey = table && table.primary ? table.primary[0] : null let primaryKey = table && table.primary ? table.primary[0] : null
const columns = Object.values(table.schema) const columns = Object.values(table.schema)
// all columns in a junction table will be meta // all columns in a junction table will be meta
@ -19,17 +24,21 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record
schema.primary(metaCols.map(col => col.name)) schema.primary(metaCols.map(col => col.name))
} }
// check if any columns need added // check if any columns need added
const foreignKeys = Object.values(table.schema).map(col => col.foreignKey) const foreignKeys = Object.values(table.schema).map(col => col.foreignKey)
for (let [key, column] of Object.entries(table.schema)) { for (let [key, column] of Object.entries(table.schema)) {
// skip things that are already correct // skip things that are already correct
const oldColumn = oldTable ? oldTable.schema[key] : null const oldColumn = oldTable ? oldTable.schema[key] : null
if ((oldColumn && oldColumn.type === column.type) || (primaryKey === key && !isJunction)) { if (
(oldColumn && oldColumn.type === column.type) ||
(primaryKey === key && !isJunction)
) {
continue continue
} }
switch (column.type) { switch (column.type) {
case FieldTypes.STRING: case FieldTypes.OPTIONS: case FieldTypes.LONGFORM: case FieldTypes.STRING:
case FieldTypes.OPTIONS:
case FieldTypes.LONGFORM:
schema.string(key) schema.string(key)
break break
case FieldTypes.NUMBER: case FieldTypes.NUMBER:
@ -67,7 +76,9 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record
throw "Referenced table doesn't exist" throw "Referenced table doesn't exist"
} }
schema.integer(column.foreignKey).unsigned() schema.integer(column.foreignKey).unsigned()
schema.foreign(column.foreignKey).references(`${tableName}.${relatedTable.primary[0]}`) schema
.foreign(column.foreignKey)
.references(`${tableName}.${relatedTable.primary[0]}`)
} }
break break
} }
@ -76,7 +87,10 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record
// need to check if any columns have been deleted // need to check if any columns have been deleted
if (oldTable) { if (oldTable) {
const deletedColumns = Object.entries(oldTable.schema) const deletedColumns = Object.entries(oldTable.schema)
.filter(([key, schema]) => schema.type !== FieldTypes.LINK && table.schema[key] == null) .filter(
([key, schema]) =>
schema.type !== FieldTypes.LINK && table.schema[key] == null
)
.map(([key]) => key) .map(([key]) => key)
deletedColumns.forEach(key => { deletedColumns.forEach(key => {
if (oldTable.constrained && oldTable.constrained.indexOf(key) !== -1) { if (oldTable.constrained && oldTable.constrained.indexOf(key) !== -1) {
@ -92,7 +106,7 @@ function generateSchema(schema: CreateTableBuilder, table: Table, tables: Record
function buildCreateTable( function buildCreateTable(
knex: Knex, knex: Knex,
table: Table, table: Table,
tables: Record<string, Table>, tables: Record<string, Table>
): SchemaBuilder { ): SchemaBuilder {
return knex.schema.createTable(table.name, schema => { return knex.schema.createTable(table.name, schema => {
generateSchema(schema, table, tables) generateSchema(schema, table, tables)
@ -103,17 +117,14 @@ function buildUpdateTable(
knex: Knex, knex: Knex,
table: Table, table: Table,
tables: Record<string, Table>, tables: Record<string, Table>,
oldTable: Table, oldTable: Table
): SchemaBuilder { ): SchemaBuilder {
return knex.schema.alterTable(table.name, schema => { return knex.schema.alterTable(table.name, schema => {
generateSchema(schema, table, tables, oldTable) generateSchema(schema, table, tables, oldTable)
}) })
} }
function buildDeleteTable( function buildDeleteTable(knex: Knex, table: Table): SchemaBuilder {
knex: Knex,
table: Table,
): SchemaBuilder {
return knex.schema.dropTable(table.name) return knex.schema.dropTable(table.name)
} }
@ -151,7 +162,12 @@ class SqlTableQueryBuilder {
if (!json.meta || !json.meta.table) { if (!json.meta || !json.meta.table) {
throw "Must specify old table for update" throw "Must specify old table for update"
} }
query = buildUpdateTable(client, json.table, json.meta.tables, json.meta.table) query = buildUpdateTable(
client,
json.table,
json.meta.tables,
json.meta.table
)
break break
case Operation.DELETE_TABLE: case Operation.DELETE_TABLE:
query = buildDeleteTable(client, json.table) query = buildDeleteTable(client, json.table)
@ -164,4 +180,4 @@ class SqlTableQueryBuilder {
} }
export default SqlTableQueryBuilder export default SqlTableQueryBuilder
module.exports = SqlTableQueryBuilder module.exports = SqlTableQueryBuilder

View File

@ -4,7 +4,10 @@ import { Datasource } from "../../definitions/common"
module DatasourceUtils { module DatasourceUtils {
const { integrations } = require("../index") const { integrations } = require("../index")
export async function makeExternalQuery(datasource: Datasource, json: QueryJson) { export async function makeExternalQuery(
datasource: Datasource,
json: QueryJson
) {
const Integration = integrations[datasource.source] const Integration = integrations[datasource.source]
// query is the opinionated function // query is the opinionated function
if (Integration.prototype.query) { if (Integration.prototype.query) {

View File

@ -3,18 +3,28 @@ import {
DatasourceFieldTypes, DatasourceFieldTypes,
QueryTypes, QueryTypes,
SqlQuery, SqlQuery,
QueryJson QueryJson,
} from "../definitions/datasource" } from "../definitions/datasource"
import { finaliseExternalTables, getSqlQuery, buildExternalTableId, convertType } from "./utils" import {
import oracledb, { ExecuteOptions, Result, Connection, ConnectionAttributes, BindParameters } from "oracledb" finaliseExternalTables,
getSqlQuery,
buildExternalTableId,
convertType,
} from "./utils"
import oracledb, {
ExecuteOptions,
Result,
Connection,
ConnectionAttributes,
BindParameters,
} from "oracledb"
import Sql from "./base/sql" import Sql from "./base/sql"
import { Table } from "../definitions/common" import { Table } from "../definitions/common"
import { DatasourcePlus } from "./base/datasourcePlus" import { DatasourcePlus } from "./base/datasourcePlus"
import { FieldTypes } from "../constants" import { FieldTypes } from "../constants"
module OracleModule { module OracleModule {
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT
oracledb.outFormat = oracledb.OUT_FORMAT_OBJECT;
interface OracleConfig { interface OracleConfig {
host: string host: string
@ -28,7 +38,8 @@ module OracleModule {
docs: "https://github.com/oracle/node-oracledb", docs: "https://github.com/oracle/node-oracledb",
plus: true, plus: true,
friendlyName: "Oracle", friendlyName: "Oracle",
description: "Oracle Database is an object-relational database management system developed by Oracle Corporation", description:
"Oracle Database is an object-relational database management system developed by Oracle Corporation",
datasource: { datasource: {
host: { host: {
type: DatasourceFieldTypes.STRING, type: DatasourceFieldTypes.STRING,
@ -51,7 +62,7 @@ module OracleModule {
password: { password: {
type: DatasourceFieldTypes.PASSWORD, type: DatasourceFieldTypes.PASSWORD,
required: true, required: true,
} },
}, },
query: { query: {
create: { create: {
@ -69,11 +80,7 @@ module OracleModule {
}, },
} }
const UNSUPPORTED_TYPES = [ const UNSUPPORTED_TYPES = ["BLOB", "CLOB", "NCLOB"]
"BLOB",
"CLOB",
"NCLOB"
]
const TYPE_MAP = { const TYPE_MAP = {
long: FieldTypes.LONGFORM, long: FieldTypes.LONGFORM,
@ -104,7 +111,7 @@ module OracleModule {
*/ */
interface OracleConstraint { interface OracleConstraint {
name: string name: string
type: string type: string
relatedConstraintName: string | null relatedConstraintName: string | null
searchCondition: string | null searchCondition: string | null
} }
@ -117,7 +124,7 @@ module OracleModule {
type: string type: string
default: string | null default: string | null
id: number id: number
constraints: {[key: string]: OracleConstraint } constraints: { [key: string]: OracleConstraint }
} }
/** /**
@ -125,18 +132,17 @@ module OracleModule {
*/ */
interface OracleTable { interface OracleTable {
name: string name: string
columns: {[key: string]: OracleColumn } columns: { [key: string]: OracleColumn }
} }
const OracleContraintTypes = { const OracleContraintTypes = {
PRIMARY: "P", PRIMARY: "P",
NOT_NULL_OR_CHECK: "C", NOT_NULL_OR_CHECK: "C",
FOREIGN_KEY: "R", FOREIGN_KEY: "R",
UNIQUE: "U" UNIQUE: "U",
} }
class OracleIntegration extends Sql implements DatasourcePlus { class OracleIntegration extends Sql implements DatasourcePlus {
private readonly config: OracleConfig private readonly config: OracleConfig
public tables: Record<string, Table> = {} public tables: Record<string, Table> = {}
@ -176,9 +182,11 @@ module OracleModule {
} }
/** /**
* 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>): { [key: string]: OracleTable } { private mapColumns(result: Result<ColumnsResponse>): {
[key: string]: OracleTable
} {
const oracleTables: { [key: string]: OracleTable } = {} const oracleTables: { [key: string]: OracleTable } = {}
if (result.rows) { if (result.rows) {
@ -197,7 +205,7 @@ module OracleModule {
if (!table) { if (!table) {
table = { table = {
name: tableName, name: tableName,
columns: {} columns: {},
} }
oracleTables[tableName] = table oracleTables[tableName] = table
} }
@ -207,9 +215,9 @@ module OracleModule {
column = { column = {
name: columnName, name: columnName,
type: dataType, type: dataType,
default: dataDefault, default: dataDefault,
id: columnId, id: columnId,
constraints: {} constraints: {},
} }
table.columns[columnName] = column table.columns[columnName] = column
} }
@ -221,7 +229,7 @@ module OracleModule {
name: constraintName, name: constraintName,
type: constraintType, type: constraintType,
relatedConstraintName: relatedConstraintName, relatedConstraintName: relatedConstraintName,
searchCondition: searchCondition searchCondition: searchCondition,
} }
} }
column.constraints[constraintName] = constraint column.constraints[constraintName] = constraint
@ -248,13 +256,54 @@ module OracleModule {
return false return false
} }
/**
* No native boolean in oracle. Best we can do is to check if a manual 1 or 0 number constraint has been set up
* This matches the default behaviour for generating DDL used in knex.
*/
private isBooleanType(column: OracleColumn): boolean {
if (
column.type.toLowerCase() === "number" &&
Object.values(column.constraints).filter(c => {
if (
c.type === OracleContraintTypes.NOT_NULL_OR_CHECK &&
c.searchCondition
) {
const condition = c.searchCondition
.replace(/\s/g, "") // remove spaces
.replace(/[']+/g, "") // remove quotes
if (
condition.includes("in(0,1)") ||
condition.includes("in(1,0)")
) {
return true
}
}
return false
}).length > 0
) {
return true
}
return false
}
private internalConvertType(column: OracleColumn): string {
if (this.isBooleanType(column)) {
return FieldTypes.BOOLEAN
}
return convertType(column.type, TYPE_MAP)
}
/** /**
* Fetches the tables from the oracle table and assigns them to the datasource. * Fetches the tables from the oracle table and assigns them to the datasource.
* @param {*} datasourceId - datasourceId to fetch * @param {*} datasourceId - datasourceId to fetch
* @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>({ sql: this.COLUMNS_SQL }) const columnsResponse = await this.internalQuery<ColumnsResponse>({
sql: this.COLUMNS_SQL,
})
const oracleTables = this.mapColumns(columnsResponse) const oracleTables = this.mapColumns(columnsResponse)
const tables: { [key: string]: Table } = {} const tables: { [key: string]: Table } = {}
@ -274,29 +323,31 @@ module OracleModule {
// iterate each column on the table // iterate each column on the table
Object.values(oracleTable.columns) Object.values(oracleTable.columns)
// remove columns that we can't read / save // remove columns that we can't read / save
.filter(oracleColumn => this.isSupportedColumn(oracleColumn)) .filter(oracleColumn => this.isSupportedColumn(oracleColumn))
// match the order of the columns in the db // match the order of the columns in the db
.sort((c1, c2) => c1.id - c2.id) .sort((c1, c2) => c1.id - c2.id)
.forEach(oracleColumn => { .forEach(oracleColumn => {
const columnName = oracleColumn.name const columnName = oracleColumn.name
let fieldSchema = table.schema[columnName] let fieldSchema = table.schema[columnName]
if (!fieldSchema) { if (!fieldSchema) {
fieldSchema = { fieldSchema = {
autocolumn: this.isAutoColumn(oracleColumn), autocolumn: this.isAutoColumn(oracleColumn),
name: columnName, name: columnName,
type: convertType(oracleColumn.type, TYPE_MAP), type: this.internalConvertType(oracleColumn),
}
table.schema[columnName] = fieldSchema
} }
table.schema[columnName] = fieldSchema
}
// iterate each constraint on the column // iterate each constraint on the column
Object.values(oracleColumn.constraints).forEach(oracleConstraint => { Object.values(oracleColumn.constraints).forEach(
if (oracleConstraint.type === OracleContraintTypes.PRIMARY) { oracleConstraint => {
table.primary!.push(columnName) if (oracleConstraint.type === OracleContraintTypes.PRIMARY) {
} table.primary!.push(columnName)
}
}
)
}) })
})
}) })
const final = finaliseExternalTables(tables, entities) const final = finaliseExternalTables(tables, entities)
@ -305,40 +356,48 @@ module OracleModule {
} }
private async internalQuery<T>(query: SqlQuery): Promise<Result<T>> { private async internalQuery<T>(query: SqlQuery): Promise<Result<T>> {
let connection let connection
try { try {
connection = await this.getConnection() connection = await this.getConnection()
const options: ExecuteOptions = { autoCommit: true } const options: ExecuteOptions = { autoCommit: true }
const bindings: BindParameters = query.bindings || [] const bindings: BindParameters = query.bindings || []
const result: Result<T> = await connection.execute<T>(query.sql, bindings, options) const result: Result<T> = await connection.execute<T>(
query.sql,
bindings,
options
)
return result return result
} finally { } finally {
if (connection) { if (connection) {
try { try {
await connection.close(); await connection.close()
} catch (err) { } catch (err) {
console.error(err); console.error(err)
} }
} }
} }
} }
private getConnection = async (): Promise<Connection> => { private getConnection = async (): Promise<Connection> => {
//connectString : "(DESCRIPTION =(ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))(CONNECT_DATA =(SID= ORCL)))" //connectString : "(DESCRIPTION =(ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521))(CONNECT_DATA =(SID= ORCL)))"
const connectString = `${this.config.host}:${this.config.port || 1521}/${this.config.database}` const connectString = `${this.config.host}:${this.config.port || 1521}/${
this.config.database
}`
const attributes: ConnectionAttributes = { const attributes: ConnectionAttributes = {
user: this.config.user, user: this.config.user,
password: this.config.user, password: this.config.user,
connectString, connectString,
} }
return oracledb.getConnection(attributes); return oracledb.getConnection(attributes)
} }
async create(query: SqlQuery | string) { async create(query: SqlQuery | string) {
const response = await this.internalQuery(getSqlQuery(query)) const response = await this.internalQuery(getSqlQuery(query))
return response.rows && response.rows.length ? response.rows : [{ created: true }] return response.rows && response.rows.length
? response.rows
: [{ created: true }]
} }
async read(query: SqlQuery | string) { async read(query: SqlQuery | string) {
@ -348,12 +407,16 @@ module OracleModule {
async update(query: SqlQuery | string) { async update(query: SqlQuery | string) {
const response = await this.internalQuery(getSqlQuery(query)) const response = await this.internalQuery(getSqlQuery(query))
return response.rows && response.rows.length ? response.rows : [{ updated: true }] return response.rows && response.rows.length
? response.rows
: [{ updated: true }]
} }
async delete(query: SqlQuery | string) { async delete(query: SqlQuery | string) {
const response = await this.internalQuery(getSqlQuery(query)) const response = await this.internalQuery(getSqlQuery(query))
return response.rows && response.rows.length ? response.rows : [{ deleted: true }] return response.rows && response.rows.length
? response.rows
: [{ deleted: true }]
} }
async query(json: QueryJson) { async query(json: QueryJson) {
@ -367,7 +430,9 @@ module OracleModule {
return responses return responses
} else { } else {
const response = await this.internalQuery(input) const response = await this.internalQuery(input)
return response.rows && response.rows.length ? response.rows : [{ [operation]: true }] return response.rows && response.rows.length
? response.rows
: [{ [operation]: true }]
} }
} }
} }

View File

@ -124,7 +124,10 @@ function copyExistingPropsOver(
return table return table
} }
export function finaliseExternalTables(tables: { [key: string]: any }, entities: { [key: string]: any }) { export function finaliseExternalTables(
tables: { [key: string]: any },
entities: { [key: string]: any }
) {
let finalTables: { [key: string]: any } = {} let finalTables: { [key: string]: any } = {}
const errors: { [key: string]: string } = {} const errors: { [key: string]: string } = {}
for (let [name, table] of Object.entries(tables)) { for (let [name, table] of Object.entries(tables)) {
@ -138,7 +141,7 @@ export function finaliseExternalTables(tables: { [key: string]: any }, entities:
} }
// sort the tables by name // sort the tables by name
finalTables = Object.entries(finalTables) finalTables = 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 } return { tables: finalTables, errors }
} }