Fixing some issues discovered with many to many relationships in SQL, as well as problems uncovered by #3531.
This commit is contained in:
parent
bf44c15b74
commit
f6ecd423ce
|
@ -197,9 +197,9 @@ module External {
|
||||||
return row
|
return row
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMany(field: FieldSchema) {
|
function isOneSide(field: FieldSchema) {
|
||||||
return (
|
return (
|
||||||
field.relationshipType && field.relationshipType.split("-")[0] === "many"
|
field.relationshipType && field.relationshipType.split("-")[0] === "one"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,25 +274,37 @@ module External {
|
||||||
const linkTable = this.tables[linkTableName]
|
const linkTable = this.tables[linkTableName]
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const linkTablePrimary = linkTable.primary[0]
|
const linkTablePrimary = linkTable.primary[0]
|
||||||
if (!isMany(field)) {
|
// one to many
|
||||||
|
if (isOneSide(field)) {
|
||||||
newRow[field.foreignKey || linkTablePrimary] = breakRowIdField(
|
newRow[field.foreignKey || linkTablePrimary] = breakRowIdField(
|
||||||
row[key][0]
|
row[key][0]
|
||||||
)[0]
|
)[0]
|
||||||
} else {
|
}
|
||||||
|
// many to many
|
||||||
|
else if (field.through) {
|
||||||
// we're not inserting a doc, will be a bunch of update calls
|
// we're not inserting a doc, will be a bunch of update calls
|
||||||
const isUpdate = !field.through
|
const otherKey: string = field.throughFrom || linkTablePrimary
|
||||||
const thisKey: string = isUpdate
|
const thisKey: string = field.throughTo || tablePrimary
|
||||||
? "id"
|
|
||||||
: field.throughTo || linkTablePrimary
|
|
||||||
// @ts-ignore
|
|
||||||
const otherKey: string = isUpdate
|
|
||||||
? field.fieldName
|
|
||||||
: field.throughFrom || tablePrimary
|
|
||||||
row[key].map((relationship: any) => {
|
row[key].map((relationship: any) => {
|
||||||
// we don't really support composite keys for relationships, this is why [0] is used
|
|
||||||
manyRelationships.push({
|
manyRelationships.push({
|
||||||
tableId: field.through || field.tableId,
|
tableId: field.through || field.tableId,
|
||||||
isUpdate,
|
isUpdate: false,
|
||||||
|
key: otherKey,
|
||||||
|
[otherKey]: breakRowIdField(relationship)[0],
|
||||||
|
// leave the ID for enrichment later
|
||||||
|
[thisKey]: `{{ literal ${tablePrimary} }}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// many to one
|
||||||
|
else {
|
||||||
|
const thisKey: string = "id"
|
||||||
|
// @ts-ignore
|
||||||
|
const otherKey: string = field.fieldName
|
||||||
|
row[key].map((relationship: any) => {
|
||||||
|
manyRelationships.push({
|
||||||
|
tableId: field.tableId,
|
||||||
|
isUpdate: true,
|
||||||
key: otherKey,
|
key: otherKey,
|
||||||
[thisKey]: breakRowIdField(relationship)[0],
|
[thisKey]: breakRowIdField(relationship)[0],
|
||||||
// leave the ID for enrichment later
|
// leave the ID for enrichment later
|
||||||
|
@ -425,8 +437,8 @@ module External {
|
||||||
)
|
)
|
||||||
definition.through = throughTableName
|
definition.through = throughTableName
|
||||||
// don't support composite keys for relationships
|
// don't support composite keys for relationships
|
||||||
definition.from = field.throughFrom || table.primary[0]
|
definition.from = field.throughTo || table.primary[0]
|
||||||
definition.to = field.throughTo || linkTable.primary[0]
|
definition.to = field.throughFrom || linkTable.primary[0]
|
||||||
definition.fromPrimary = table.primary[0]
|
definition.fromPrimary = table.primary[0]
|
||||||
definition.toPrimary = linkTable.primary[0]
|
definition.toPrimary = linkTable.primary[0]
|
||||||
}
|
}
|
||||||
|
@ -448,24 +460,36 @@ module External {
|
||||||
// make a new request to get the row with all its relationships
|
// make a new request to get the row with all its relationships
|
||||||
// we need this to work out if any relationships need removed
|
// we need this to work out if any relationships need removed
|
||||||
for (let field of Object.values(table.schema)) {
|
for (let field of Object.values(table.schema)) {
|
||||||
if (field.type !== FieldTypes.LINK || !field.fieldName) {
|
if (
|
||||||
|
field.type !== FieldTypes.LINK ||
|
||||||
|
!field.fieldName ||
|
||||||
|
isOneSide(field)
|
||||||
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const isMany = field.relationshipType === RelationshipTypes.MANY_TO_MANY
|
const isMany = field.relationshipType === RelationshipTypes.MANY_TO_MANY
|
||||||
const tableId = isMany ? field.through : field.tableId
|
const tableId = isMany ? field.through : field.tableId
|
||||||
const manyKey = field.throughFrom || primaryKey
|
const { tableName: relatedTableName } = breakExternalTableId(tableId)
|
||||||
|
// @ts-ignore
|
||||||
|
const linkPrimaryKey = this.tables[relatedTableName].primary[0]
|
||||||
|
const manyKey = field.throughTo || primaryKey
|
||||||
|
const lookupField = isMany ? primaryKey : field.foreignKey
|
||||||
const fieldName = isMany ? manyKey : field.fieldName
|
const fieldName = isMany ? manyKey : field.fieldName
|
||||||
|
if (!lookupField || !row[lookupField]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const response = await getDatasourceAndQuery(this.appId, {
|
const response = await getDatasourceAndQuery(this.appId, {
|
||||||
endpoint: getEndpoint(tableId, DataSourceOperation.READ),
|
endpoint: getEndpoint(tableId, DataSourceOperation.READ),
|
||||||
filters: {
|
filters: {
|
||||||
equal: {
|
equal: {
|
||||||
[fieldName]: row[primaryKey],
|
[fieldName]: row[lookupField],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// this is the response from knex if no rows found
|
// this is the response from knex if no rows found
|
||||||
const rows = !response[0].read ? response : []
|
const rows = !response[0].read ? response : []
|
||||||
related[fieldName] = { rows, isMany, tableId }
|
const storeTo = isMany ? field.throughFrom || linkPrimaryKey : manyKey
|
||||||
|
related[storeTo] = { rows, isMany, tableId }
|
||||||
}
|
}
|
||||||
return related
|
return related
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Knex, knex } from "knex"
|
import { Knex, knex } from "knex"
|
||||||
import {
|
import {
|
||||||
Operation, PaginationJson,
|
Operation,
|
||||||
QueryJson,
|
QueryJson,
|
||||||
QueryOptions,
|
QueryOptions,
|
||||||
RelationshipsJson,
|
RelationshipsJson,
|
||||||
SearchFilters,
|
SearchFilters,
|
||||||
SortDirection, SortJson,
|
SortDirection,
|
||||||
} from "../../definitions/datasource"
|
} from "../../definitions/datasource"
|
||||||
import { isIsoDateString, SqlClients } from "../utils"
|
import { isIsoDateString, SqlClients } from "../utils"
|
||||||
import SqlTableQueryBuilder from "./sqlTable"
|
import SqlTableQueryBuilder from "./sqlTable"
|
||||||
|
@ -21,6 +21,9 @@ function parse(input: any) {
|
||||||
if (Array.isArray(input)) {
|
if (Array.isArray(input)) {
|
||||||
return JSON.stringify(input)
|
return JSON.stringify(input)
|
||||||
}
|
}
|
||||||
|
if (input == undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
if (typeof input !== "string") {
|
if (typeof input !== "string") {
|
||||||
return input
|
return input
|
||||||
}
|
}
|
||||||
|
@ -43,7 +46,10 @@ function parseBody(body: any) {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFilters(filters: SearchFilters): SearchFilters {
|
function parseFilters(filters: SearchFilters | undefined): SearchFilters {
|
||||||
|
if (!filters) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
for (let [key, value] of Object.entries(filters)) {
|
for (let [key, value] of Object.entries(filters)) {
|
||||||
let parsed
|
let parsed
|
||||||
if (typeof value === "object") {
|
if (typeof value === "object") {
|
||||||
|
@ -152,21 +158,19 @@ class InternalBuilder {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
addSorting(
|
addSorting(query: KnexQuery, json: QueryJson): KnexQuery {
|
||||||
query: KnexQuery,
|
let { sort, paginate } = json
|
||||||
sort: SortJson | undefined,
|
|
||||||
paginate: PaginationJson | undefined
|
|
||||||
): KnexQuery {
|
|
||||||
if (!sort) {
|
if (!sort) {
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
const table = json.meta?.table
|
||||||
for (let [key, value] of Object.entries(sort)) {
|
for (let [key, value] of Object.entries(sort)) {
|
||||||
const direction = value === SortDirection.ASCENDING ? "asc" : "desc"
|
const direction = value === SortDirection.ASCENDING ? "asc" : "desc"
|
||||||
query = query.orderBy(key, direction)
|
query = query.orderBy(`${table?.name}.${key}`, direction)
|
||||||
}
|
}
|
||||||
if (this.client === SqlClients.MS_SQL && !sort && paginate?.limit) {
|
if (this.client === SqlClients.MS_SQL && !sort && paginate?.limit) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
query = query.orderBy(json.meta?.table?.primary[0])
|
query = query.orderBy(`${table?.name}.${table?.primary[0]}`)
|
||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
@ -270,15 +274,15 @@ class InternalBuilder {
|
||||||
}
|
}
|
||||||
query = this.addFilters(tableName, query, filters)
|
query = this.addFilters(tableName, query, filters)
|
||||||
// add sorting to pre-query
|
// add sorting to pre-query
|
||||||
query = this.addSorting(query, sort, paginate)
|
query = this.addSorting(query, json)
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
let preQuery : KnexQuery = knex({
|
let preQuery: KnexQuery = knex({
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
[tableName]: query,
|
[tableName]: query,
|
||||||
}).select(selectStatement)
|
}).select(selectStatement)
|
||||||
// have to add after as well (this breaks MS-SQL)
|
// have to add after as well (this breaks MS-SQL)
|
||||||
if (this.client !== SqlClients.MS_SQL) {
|
if (this.client !== SqlClients.MS_SQL) {
|
||||||
preQuery = this.addSorting(preQuery, sort, paginate)
|
preQuery = this.addSorting(preQuery, json)
|
||||||
}
|
}
|
||||||
// handle joins
|
// handle joins
|
||||||
return this.addRelationships(
|
return this.addRelationships(
|
||||||
|
|
|
@ -40,9 +40,9 @@ const INTEGRATIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionally add oracle integration if the oracle binary can be installed
|
// optionally add oracle integration if the oracle binary can be installed
|
||||||
if (!(process.arch === 'arm64' && process.platform === 'darwin')) {
|
if (!(process.arch === "arm64" && process.platform === "darwin")) {
|
||||||
const oracle = require("./oracle")
|
const oracle = require("./oracle")
|
||||||
DEFINITIONS[SourceNames.ORACLE] = oracle.schema
|
DEFINITIONS[SourceNames.ORACLE] = oracle.schema
|
||||||
INTEGRATIONS[SourceNames.ORACLE] = oracle.integration
|
INTEGRATIONS[SourceNames.ORACLE] = oracle.integration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -134,7 +134,9 @@ module MySQLModule {
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
const tableNames = tablesResp.map(
|
const tableNames = tablesResp.map(
|
||||||
(obj: any) => obj[`Tables_in_${database}`] || obj[`Tables_in_${database.toLowerCase()}`]
|
(obj: any) =>
|
||||||
|
obj[`Tables_in_${database}`] ||
|
||||||
|
obj[`Tables_in_${database.toLowerCase()}`]
|
||||||
)
|
)
|
||||||
for (let tableName of tableNames) {
|
for (let tableName of tableNames) {
|
||||||
const primaryKeys = []
|
const primaryKeys = []
|
||||||
|
|
|
@ -352,14 +352,23 @@ module OracleModule {
|
||||||
* Knex default returning behaviour does not work with oracle
|
* Knex default returning behaviour does not work with oracle
|
||||||
* Manually add the behaviour for the return column
|
* Manually add the behaviour for the return column
|
||||||
*/
|
*/
|
||||||
private addReturning(query: SqlQuery, bindings: BindParameters, returnColumn: string) {
|
private addReturning(
|
||||||
|
query: SqlQuery,
|
||||||
|
bindings: BindParameters,
|
||||||
|
returnColumn: string
|
||||||
|
) {
|
||||||
if (bindings instanceof Array) {
|
if (bindings instanceof Array) {
|
||||||
bindings.push({ dir: oracledb.BIND_OUT })
|
bindings.push({ dir: oracledb.BIND_OUT })
|
||||||
query.sql = query.sql + ` returning \"${returnColumn}\" into :${bindings.length}`
|
query.sql =
|
||||||
|
query.sql + ` returning \"${returnColumn}\" into :${bindings.length}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async internalQuery<T>(query: SqlQuery, returnColum?: string, operation?: string): Promise<Result<T>> {
|
private async internalQuery<T>(
|
||||||
|
query: SqlQuery,
|
||||||
|
returnColum?: string,
|
||||||
|
operation?: string
|
||||||
|
): Promise<Result<T>> {
|
||||||
let connection
|
let connection
|
||||||
try {
|
try {
|
||||||
connection = await this.getConnection()
|
connection = await this.getConnection()
|
||||||
|
@ -367,7 +376,10 @@ module OracleModule {
|
||||||
const options: ExecuteOptions = { autoCommit: true }
|
const options: ExecuteOptions = { autoCommit: true }
|
||||||
const bindings: BindParameters = query.bindings || []
|
const bindings: BindParameters = query.bindings || []
|
||||||
|
|
||||||
if (returnColum && (operation === Operation.CREATE || operation === Operation.UPDATE)) {
|
if (
|
||||||
|
returnColum &&
|
||||||
|
(operation === Operation.CREATE || operation === Operation.UPDATE)
|
||||||
|
) {
|
||||||
this.addReturning(query, bindings, returnColum)
|
this.addReturning(query, bindings, returnColum)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,14 +426,14 @@ module OracleModule {
|
||||||
return response.rows ? response.rows : []
|
return response.rows ? response.rows : []
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(query: SqlQuery | string): Promise<any[]> {
|
async update(query: SqlQuery | string): Promise<any[]> {
|
||||||
const response = await this.internalQuery(getSqlQuery(query))
|
const response = await this.internalQuery(getSqlQuery(query))
|
||||||
return response.rows && response.rows.length
|
return response.rows && response.rows.length
|
||||||
? response.rows
|
? response.rows
|
||||||
: [{ updated: true }]
|
: [{ updated: true }]
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(query: SqlQuery | string): Promise<any[]> {
|
async delete(query: SqlQuery | string): Promise<any[]> {
|
||||||
const response = await this.internalQuery(getSqlQuery(query))
|
const response = await this.internalQuery(getSqlQuery(query))
|
||||||
return response.rows && response.rows.length
|
return response.rows && response.rows.length
|
||||||
? response.rows
|
? response.rows
|
||||||
|
@ -431,8 +443,9 @@ module OracleModule {
|
||||||
async query(json: QueryJson) {
|
async query(json: QueryJson) {
|
||||||
const primaryKeys = json.meta!.table!.primary
|
const primaryKeys = json.meta!.table!.primary
|
||||||
const primaryKey = primaryKeys ? primaryKeys[0] : undefined
|
const primaryKey = primaryKeys ? primaryKeys[0] : undefined
|
||||||
const queryFn = (query: any, operation: string) => this.internalQuery(query, primaryKey, operation)
|
const queryFn = (query: any, operation: string) =>
|
||||||
const processFn = (response: any) => response.rows ? response.rows : []
|
this.internalQuery(query, primaryKey, operation)
|
||||||
|
const processFn = (response: any) => (response.rows ? response.rows : [])
|
||||||
const output = await this.queryWithReturning(json, queryFn, processFn)
|
const output = await this.queryWithReturning(json, queryFn, processFn)
|
||||||
return output
|
return output
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,11 @@ import { SqlQuery } from "../definitions/datasource"
|
||||||
import { Datasource, Table } from "../definitions/common"
|
import { Datasource, Table } from "../definitions/common"
|
||||||
import { SourceNames } from "../definitions/datasource"
|
import { SourceNames } from "../definitions/datasource"
|
||||||
const { DocumentTypes, SEPARATOR } = require("../db/utils")
|
const { DocumentTypes, SEPARATOR } = require("../db/utils")
|
||||||
const { FieldTypes, BuildSchemaErrors, InvalidColumns } = require("../constants")
|
const {
|
||||||
|
FieldTypes,
|
||||||
|
BuildSchemaErrors,
|
||||||
|
InvalidColumns,
|
||||||
|
} = require("../constants")
|
||||||
|
|
||||||
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
|
||||||
const ROW_ID_REGEX = /^\[.*]$/g
|
const ROW_ID_REGEX = /^\[.*]$/g
|
||||||
|
@ -42,7 +46,7 @@ export enum SqlClients {
|
||||||
MS_SQL = "mssql",
|
MS_SQL = "mssql",
|
||||||
POSTGRES = "pg",
|
POSTGRES = "pg",
|
||||||
MY_SQL = "mysql",
|
MY_SQL = "mysql",
|
||||||
ORACLE = "oracledb"
|
ORACLE = "oracledb",
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isExternalTable(tableId: string) {
|
export function isExternalTable(tableId: string) {
|
||||||
|
|
Loading…
Reference in New Issue