Merge pull request #5419 from Budibase/fix/5411
Fix for SQL IN queries, trimming the inputs to remove excess whitespace
This commit is contained in:
commit
0bf5da29d9
|
@ -136,7 +136,7 @@
|
||||||
notifications.success("Request sent successfully")
|
notifications.success("Request sent successfully")
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error running query")
|
notifications.error(`Query Error: ${error.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ CREATE TABLE Products (
|
||||||
updated time
|
updated time
|
||||||
);
|
);
|
||||||
INSERT INTO Persons (FirstName, LastName, Age, Address, City, CreatedAt) VALUES ('Mike', 'Hughes', 28.2, '123 Fake Street', 'Belfast', '2021-01-19 03:14:07');
|
INSERT INTO Persons (FirstName, LastName, Age, Address, City, CreatedAt) VALUES ('Mike', 'Hughes', 28.2, '123 Fake Street', 'Belfast', '2021-01-19 03:14:07');
|
||||||
|
INSERT INTO Persons (FirstName, LastName, Age, Address, City, CreatedAt) VALUES ('Dave', 'Johnson', 29, '124 Fake Street', 'Belfast', '2022-04-01 00:11:11');
|
||||||
INSERT INTO Tasks (PersonID, TaskName, CreatedAt) VALUES (1, 'assembling', '2020-01-01');
|
INSERT INTO Tasks (PersonID, TaskName, CreatedAt) VALUES (1, 'assembling', '2020-01-01');
|
||||||
INSERT INTO Tasks (PersonID, TaskName, CreatedAt) VALUES (2, 'processing', '2019-12-31');
|
INSERT INTO Tasks (PersonID, TaskName, CreatedAt) VALUES (2, 'processing', '2019-12-31');
|
||||||
INSERT INTO Products (name, updated) VALUES ('Meat', '11:00:22'), ('Fruit', '10:00:00');
|
INSERT INTO Products (name, updated) VALUES ('Meat', '11:00:22'), ('Fruit', '10:00:00');
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
import { findHBSBlocks, processStringSync } from "@budibase/string-templates"
|
||||||
|
import { Integration } from "../../definitions/datasource"
|
||||||
|
import { DatasourcePlus } from "../base/datasourcePlus"
|
||||||
|
|
||||||
|
const CONST_CHAR_REGEX = new RegExp("'[^']*'", "g")
|
||||||
|
|
||||||
|
export function enrichQueryFields(
|
||||||
|
fields: { [key: string]: any },
|
||||||
|
parameters = {}
|
||||||
|
) {
|
||||||
|
const enrichedQuery: { [key: string]: any } = Array.isArray(fields) ? [] : {}
|
||||||
|
|
||||||
|
// enrich the fields with dynamic parameters
|
||||||
|
for (let key of Object.keys(fields)) {
|
||||||
|
if (fields[key] == null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (typeof fields[key] === "object") {
|
||||||
|
// enrich nested fields object
|
||||||
|
enrichedQuery[key] = enrichQueryFields(fields[key], parameters)
|
||||||
|
} else if (typeof fields[key] === "string") {
|
||||||
|
// enrich string value as normal
|
||||||
|
enrichedQuery[key] = processStringSync(fields[key], parameters, {
|
||||||
|
noEscaping: true,
|
||||||
|
noHelpers: true,
|
||||||
|
escapeNewlines: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
enrichedQuery[key] = fields[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
enrichedQuery.json ||
|
||||||
|
enrichedQuery.customData ||
|
||||||
|
enrichedQuery.requestBody
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
enrichedQuery.json = JSON.parse(
|
||||||
|
enrichedQuery.json ||
|
||||||
|
enrichedQuery.customData ||
|
||||||
|
enrichedQuery.requestBody
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
// no json found, ignore
|
||||||
|
}
|
||||||
|
delete enrichedQuery.customData
|
||||||
|
}
|
||||||
|
return enrichedQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
export function interpolateSQL(
|
||||||
|
fields: { [key: string]: any },
|
||||||
|
parameters: { [key: string]: any },
|
||||||
|
integration: DatasourcePlus
|
||||||
|
) {
|
||||||
|
let sql = fields.sql
|
||||||
|
if (!sql || typeof sql !== "string") {
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
const bindings = findHBSBlocks(sql)
|
||||||
|
let variables = [],
|
||||||
|
arrays = []
|
||||||
|
for (let binding of bindings) {
|
||||||
|
// look for array/list operations in the SQL statement, which will need handled later
|
||||||
|
const listRegexMatch = sql.match(
|
||||||
|
new RegExp(`(in|IN|In|iN)( )+[(]?${binding}[)]?`)
|
||||||
|
)
|
||||||
|
// check if the variable was used as part of a string concat e.g. 'Hello {{binding}}'
|
||||||
|
// start by finding all the instances of const character strings
|
||||||
|
const charConstMatch = sql.match(CONST_CHAR_REGEX) || []
|
||||||
|
// now look within them to see if a binding is used
|
||||||
|
const charConstBindingMatch = charConstMatch.find((string: any) =>
|
||||||
|
string.match(new RegExp(`'[^']*${binding}[^']*'`))
|
||||||
|
)
|
||||||
|
if (charConstBindingMatch) {
|
||||||
|
let [part1, part2] = charConstBindingMatch.split(binding)
|
||||||
|
part1 = `'${part1.substring(1)}'`
|
||||||
|
part2 = `'${part2.substring(0, part2.length - 1)}'`
|
||||||
|
sql = sql.replace(
|
||||||
|
charConstBindingMatch,
|
||||||
|
integration.getStringConcat([
|
||||||
|
part1,
|
||||||
|
integration.getBindingIdentifier(),
|
||||||
|
part2,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// generate SQL parameterised array
|
||||||
|
else if (listRegexMatch) {
|
||||||
|
arrays.push(binding)
|
||||||
|
// determine the length of the array
|
||||||
|
const value = enrichQueryFields([binding], parameters)[0]
|
||||||
|
.split(",")
|
||||||
|
.map((val: string) => val.trim())
|
||||||
|
// build a string like ($1, $2, $3)
|
||||||
|
let replacement = `${Array.apply(null, Array(value.length))
|
||||||
|
.map(() => integration.getBindingIdentifier())
|
||||||
|
.join(",")}`
|
||||||
|
// check if parentheses are needed
|
||||||
|
if (!listRegexMatch[0].includes(`(${binding})`)) {
|
||||||
|
replacement = `(${replacement})`
|
||||||
|
}
|
||||||
|
sql = sql.replace(binding, replacement)
|
||||||
|
} else {
|
||||||
|
sql = sql.replace(binding, integration.getBindingIdentifier())
|
||||||
|
}
|
||||||
|
variables.push(binding)
|
||||||
|
}
|
||||||
|
// replicate the knex structure
|
||||||
|
fields.sql = sql
|
||||||
|
fields.bindings = enrichQueryFields(variables, parameters)
|
||||||
|
// check for arrays in the data
|
||||||
|
let updated: string[] = []
|
||||||
|
for (let i = 0; i < variables.length; i++) {
|
||||||
|
if (arrays.includes(variables[i])) {
|
||||||
|
updated = updated.concat(
|
||||||
|
fields.bindings[i].split(",").map((val: string) => val.trim())
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
updated.push(fields.bindings[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fields.bindings = updated
|
||||||
|
return fields
|
||||||
|
}
|
|
@ -36,6 +36,7 @@ class Thread {
|
||||||
maxConcurrentWorkers: this.count,
|
maxConcurrentWorkers: this.count,
|
||||||
}
|
}
|
||||||
if (opts.timeoutMs) {
|
if (opts.timeoutMs) {
|
||||||
|
this.timeoutMs = opts.timeoutMs
|
||||||
workerOpts.maxCallTime = opts.timeoutMs
|
workerOpts.maxCallTime = opts.timeoutMs
|
||||||
}
|
}
|
||||||
this.workers = workerFarm(workerOpts, typeToFile(type))
|
this.workers = workerFarm(workerOpts, typeToFile(type))
|
||||||
|
@ -43,6 +44,7 @@ class Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
run(data) {
|
run(data) {
|
||||||
|
const timeoutMs = this.timeoutMs
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let fncToCall
|
let fncToCall
|
||||||
// if in test then don't use threading
|
// if in test then don't use threading
|
||||||
|
@ -52,7 +54,11 @@ class Thread {
|
||||||
fncToCall = this.workers
|
fncToCall = this.workers
|
||||||
}
|
}
|
||||||
fncToCall(data, (err, response) => {
|
fncToCall(data, (err, response) => {
|
||||||
if (err) {
|
if (err && err.type === "TimeoutError") {
|
||||||
|
reject(
|
||||||
|
new Error(`Query response time exceeded ${timeoutMs}ms timeout.`)
|
||||||
|
)
|
||||||
|
} else if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
} else {
|
} else {
|
||||||
resolve(response)
|
resolve(response)
|
||||||
|
|
|
@ -2,14 +2,13 @@ const threadUtils = require("./utils")
|
||||||
threadUtils.threadSetup()
|
threadUtils.threadSetup()
|
||||||
const ScriptRunner = require("../utilities/scriptRunner")
|
const ScriptRunner = require("../utilities/scriptRunner")
|
||||||
const { integrations } = require("../integrations")
|
const { integrations } = require("../integrations")
|
||||||
const {
|
const { processStringSync } = require("@budibase/string-templates")
|
||||||
processStringSync,
|
|
||||||
findHBSBlocks,
|
|
||||||
} = require("@budibase/string-templates")
|
|
||||||
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
||||||
const { isSQL } = require("../integrations/utils")
|
const { isSQL } = require("../integrations/utils")
|
||||||
|
const {
|
||||||
const CONST_CHAR_REGEX = new RegExp("'[^']*'", "g")
|
enrichQueryFields,
|
||||||
|
interpolateSQL,
|
||||||
|
} = require("../integrations/queries/sql")
|
||||||
|
|
||||||
class QueryRunner {
|
class QueryRunner {
|
||||||
constructor(input, flags = { noRecursiveQuery: false }) {
|
constructor(input, flags = { noRecursiveQuery: false }) {
|
||||||
|
@ -29,76 +28,6 @@ class QueryRunner {
|
||||||
this.hasRerun = false
|
this.hasRerun = false
|
||||||
}
|
}
|
||||||
|
|
||||||
interpolateSQL(fields, parameters, integration) {
|
|
||||||
let sql = fields.sql
|
|
||||||
if (!sql) {
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
const bindings = findHBSBlocks(sql)
|
|
||||||
let variables = [],
|
|
||||||
arrays = []
|
|
||||||
for (let binding of bindings) {
|
|
||||||
// look for array/list operations in the SQL statement, which will need handled later
|
|
||||||
const listRegexMatch = sql.match(
|
|
||||||
new RegExp(`(in|IN|In|iN)( )+[(]?${binding}[)]?`)
|
|
||||||
)
|
|
||||||
// check if the variable was used as part of a string concat e.g. 'Hello {{binding}}'
|
|
||||||
// start by finding all the instances of const character strings
|
|
||||||
const charConstMatch = sql.match(CONST_CHAR_REGEX) || []
|
|
||||||
// now look within them to see if a binding is used
|
|
||||||
const charConstBindingMatch = charConstMatch.find(string =>
|
|
||||||
string.match(new RegExp(`'[^']*${binding}[^']*'`))
|
|
||||||
)
|
|
||||||
if (charConstBindingMatch) {
|
|
||||||
let [part1, part2] = charConstBindingMatch.split(binding)
|
|
||||||
part1 = `'${part1.substring(1)}'`
|
|
||||||
part2 = `'${part2.substring(0, part2.length - 1)}'`
|
|
||||||
sql = sql.replace(
|
|
||||||
charConstBindingMatch,
|
|
||||||
integration.getStringConcat([
|
|
||||||
part1,
|
|
||||||
integration.getBindingIdentifier(),
|
|
||||||
part2,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// generate SQL parameterised array
|
|
||||||
else if (listRegexMatch) {
|
|
||||||
arrays.push(binding)
|
|
||||||
// determine the length of the array
|
|
||||||
const value = this.enrichQueryFields([binding], parameters)[0].split(
|
|
||||||
","
|
|
||||||
)
|
|
||||||
// build a string like ($1, $2, $3)
|
|
||||||
let replacement = `${Array.apply(null, Array(value.length))
|
|
||||||
.map(() => integration.getBindingIdentifier())
|
|
||||||
.join(",")}`
|
|
||||||
// check if parentheses are needed
|
|
||||||
if (!listRegexMatch[0].includes(`(${binding})`)) {
|
|
||||||
replacement = `(${replacement})`
|
|
||||||
}
|
|
||||||
sql = sql.replace(binding, replacement)
|
|
||||||
} else {
|
|
||||||
sql = sql.replace(binding, integration.getBindingIdentifier())
|
|
||||||
}
|
|
||||||
variables.push(binding)
|
|
||||||
}
|
|
||||||
// replicate the knex structure
|
|
||||||
fields.sql = sql
|
|
||||||
fields.bindings = this.enrichQueryFields(variables, parameters)
|
|
||||||
// check for arrays in the data
|
|
||||||
let updated = []
|
|
||||||
for (let i = 0; i < variables.length; i++) {
|
|
||||||
if (arrays.includes(variables[i])) {
|
|
||||||
updated = updated.concat(fields.bindings[i].split(","))
|
|
||||||
} else {
|
|
||||||
updated.push(fields.bindings[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields.bindings = updated
|
|
||||||
return fields
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute() {
|
async execute() {
|
||||||
let { datasource, fields, queryVerb, transformer } = this
|
let { datasource, fields, queryVerb, transformer } = this
|
||||||
const Integration = integrations[datasource.source]
|
const Integration = integrations[datasource.source]
|
||||||
|
@ -112,9 +41,9 @@ class QueryRunner {
|
||||||
let query
|
let query
|
||||||
// handle SQL injections by interpolating the variables
|
// handle SQL injections by interpolating the variables
|
||||||
if (isSQL(datasource)) {
|
if (isSQL(datasource)) {
|
||||||
query = this.interpolateSQL(fields, parameters, integration)
|
query = interpolateSQL(fields, parameters, integration)
|
||||||
} else {
|
} else {
|
||||||
query = this.enrichQueryFields(fields, parameters)
|
query = enrichQueryFields(fields, parameters)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add pagination values for REST queries
|
// Add pagination values for REST queries
|
||||||
|
@ -259,47 +188,6 @@ class QueryRunner {
|
||||||
}
|
}
|
||||||
return parameters
|
return parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
enrichQueryFields(fields, parameters = {}) {
|
|
||||||
const enrichedQuery = Array.isArray(fields) ? [] : {}
|
|
||||||
|
|
||||||
// enrich the fields with dynamic parameters
|
|
||||||
for (let key of Object.keys(fields)) {
|
|
||||||
if (fields[key] == null) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (typeof fields[key] === "object") {
|
|
||||||
// enrich nested fields object
|
|
||||||
enrichedQuery[key] = this.enrichQueryFields(fields[key], parameters)
|
|
||||||
} else if (typeof fields[key] === "string") {
|
|
||||||
// enrich string value as normal
|
|
||||||
enrichedQuery[key] = processStringSync(fields[key], parameters, {
|
|
||||||
noEscaping: true,
|
|
||||||
noHelpers: true,
|
|
||||||
escapeNewlines: true,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
enrichedQuery[key] = fields[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
enrichedQuery.json ||
|
|
||||||
enrichedQuery.customData ||
|
|
||||||
enrichedQuery.requestBody
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
enrichedQuery.json = JSON.parse(
|
|
||||||
enrichedQuery.json ||
|
|
||||||
enrichedQuery.customData ||
|
|
||||||
enrichedQuery.requestBody
|
|
||||||
)
|
|
||||||
} catch (err) {
|
|
||||||
// no json found, ignore
|
|
||||||
}
|
|
||||||
delete enrichedQuery.customData
|
|
||||||
}
|
|
||||||
return enrichedQuery
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = (input, callback) => {
|
module.exports = (input, callback) => {
|
||||||
|
|
Loading…
Reference in New Issue