diff --git a/src/common/libs/sqlEscaper.ts b/src/common/libs/sqlEscaper.ts deleted file mode 100644 index 956e12e2..00000000 --- a/src/common/libs/sqlEscaper.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable no-useless-escape */ -// eslint-disable-next-line no-control-regex -const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm; -const regex = new RegExp(pattern); - -function sqlEscaper (string: string) { - return string.replace(regex, char => { - const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%']; - const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%']; - return r[m.indexOf(char)] || char; - }); -} - -export { sqlEscaper }; diff --git a/src/common/libs/sqlUtils.ts b/src/common/libs/sqlUtils.ts new file mode 100644 index 00000000..d8abd2f8 --- /dev/null +++ b/src/common/libs/sqlUtils.ts @@ -0,0 +1,162 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable no-useless-escape */ +import * as moment from 'moment'; +import { lineString, point, polygon } from '@turf/helpers'; +import customizations from '../customizations'; +import { ClientCode } from '../interfaces/antares'; +import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER, TEXT_SEARCH } from 'common/fieldTypes'; +import hexToBinary, { HexChar } from './hexToBinary'; +import { getArrayDepth } from './getArrayDepth'; + +/** + * Escapes a string fo SQL use + * + * @param { String } string + * @returns { String } Escaped string + */ +export const sqlEscaper = (string: string): string => { + // eslint-disable-next-line no-control-regex + const pattern = /[\0\x08\x09\x1a\n\r"'\\\%]/gm; + const regex = new RegExp(pattern); + return string.replace(regex, char => { + const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '\"', '\\', '\\\\', '%']; + const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\\\'', '\\\"', '\\\\', '\\\\\\\\', '\%']; + return r[m.indexOf(char)] || char; + }); +}; + +export const objectToGeoJSON = (val: any) => { + if (Array.isArray(val)) { + if (getArrayDepth(val) === 1) + return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])); + else + return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], []))); + } + else + return point([val.x, val.y]); +}; + +export const escapeAndQuote = (val: string, client: ClientCode) => { + const { stringsWrapper: sw } = customizations[client]; + // eslint-disable-next-line no-control-regex + const CHARS_TO_ESCAPE = /[\0\b\t\n\r\x1a"'\\]/g; + const CHARS_ESCAPE_MAP: {[key: string]: string} = { + '\0': '\\0', + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\r': '\\r', + '\x1a': '\\Z', + '"': '\\"', + '\'': '\\\'', + '\\': '\\\\' + }; + let chunkIndex = CHARS_TO_ESCAPE.lastIndex = 0; + let escapedVal = ''; + let match; + + while ((match = CHARS_TO_ESCAPE.exec(val))) { + escapedVal += val.slice(chunkIndex, match.index) + CHARS_ESCAPE_MAP[match[0]]; + chunkIndex = CHARS_TO_ESCAPE.lastIndex; + } + + if (chunkIndex === 0) + return `${sw}${val}${sw}`; + + if (chunkIndex < val.length) + return `${sw}${escapedVal + val.slice(chunkIndex)}${sw}`; + + return `${sw}${escapedVal}${sw}`; +}; + +export const valueToSqlString = (args: { + val: any; + client: ClientCode; + field: {type: string; datePrecision: number}; + }): string => { + let parsedValue; + const { val, client, field } = args; + const { stringsWrapper: sw } = customizations[client]; + + if (val === null) + parsedValue = 'NULL'; + else if (DATE.includes(field.type)) { + parsedValue = moment(val).isValid() + ? escapeAndQuote(moment(val).format('YYYY-MM-DD'), client) + : val; + } + else if (DATETIME.includes(field.type)) { + let datePrecision = ''; + for (let i = 0; i < field.datePrecision; i++) + datePrecision += i === 0 ? '.S' : 'S'; + + parsedValue = moment(val).isValid() + ? escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`), client) + : escapeAndQuote(val, client); + } + else if ('isArray' in field) { + let localVal; + if (Array.isArray(val)) + localVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}'); + else + localVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : ''; + parsedValue = `'${localVal}'`; + } + else if (TEXT_SEARCH.includes(field.type)) + parsedValue = `'${val.replaceAll('\'', '\'\'')}'`; + else if (BIT.includes(field.type)) + parsedValue = `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`; + else if (BLOB.includes(field.type)) { + if (['mysql', 'maria'].includes(client)) + parsedValue = `X'${val.toString('hex').toUpperCase()}'`; + else if (client === 'pg') + parsedValue = `decode('${val.toString('hex').toUpperCase()}', 'hex')`; + } + else if (NUMBER.includes(field.type)) + parsedValue = val; + else if (FLOAT.includes(field.type)) + parsedValue = parseFloat(val); + else if (SPATIAL.includes(field.type)) { + let geoJson; + if (IS_MULTI_SPATIAL.includes(field.type)) { + const features = []; + for (const element of val) + features.push(objectToGeoJSON(element)); + + geoJson = { + type: 'FeatureCollection', + features + }; + } + else + geoJson = objectToGeoJSON(val); + + parsedValue = `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`; + } + else if (val === '') parsedValue = `${sw}${sw}`; + else { + parsedValue = typeof val === 'string' + ? escapeAndQuote(val, client) + : typeof val === 'object' + ? escapeAndQuote(JSON.stringify(val), client) + : val; + } + + return parsedValue; +}; + +export const jsonToSqlInsert = (args: { + json: { [key: string]: any}; + client: ClientCode; + fields: { [key: string]: {type: string; datePrecision: number}}; + table: string; + }) => { + const { client, json, fields, table } = args; + const { elementsWrapper: ew } = customizations[client]; + const fieldNames = Object.keys(json).map(key => `${ew}${key}${ew}`); + const values = Object.keys(json).map(key => ( + valueToSqlString({ val: json[key], client, field: fields[key] }) + )); + + return `INSERT INTO ${ew}${table}${ew} (${fieldNames.join(', ')}) VALUES (${values.join(', ')});`; +}; diff --git a/src/main/ipc-handlers/tables.ts b/src/main/ipc-handlers/tables.ts index 6addcb06..9048f767 100644 --- a/src/main/ipc-handlers/tables.ts +++ b/src/main/ipc-handlers/tables.ts @@ -4,7 +4,7 @@ import { InsertRowsParams } from 'common/interfaces/tableApis'; import { ipcMain } from 'electron'; import { faker } from '@faker-js/faker'; import * as moment from 'moment'; -import { sqlEscaper } from 'common/libs/sqlEscaper'; +import { sqlEscaper } from 'common/libs/sqlUtils'; import { TEXT, LONG_TEXT, ARRAY, TEXT_SEARCH, NUMBER, FLOAT, BLOB, BIT, DATE, DATETIME } from 'common/fieldTypes'; import customizations from 'common/customizations'; diff --git a/src/main/libs/exporters/sql/MysqlExporter.ts b/src/main/libs/exporters/sql/MysqlExporter.ts index 80ce3c85..19d5e396 100644 --- a/src/main/libs/exporters/sql/MysqlExporter.ts +++ b/src/main/libs/exporters/sql/MysqlExporter.ts @@ -1,12 +1,8 @@ import * as exporter from 'common/interfaces/exporter'; import * as mysql from 'mysql2/promise'; import { SqlExporter } from './SqlExporter'; -import { BLOB, BIT, DATE, DATETIME, FLOAT, SPATIAL, IS_MULTI_SPATIAL, NUMBER } from 'common/fieldTypes'; -import hexToBinary, { HexChar } from 'common/libs/hexToBinary'; -import { getArrayDepth } from 'common/libs/getArrayDepth'; -import * as moment from 'moment'; -import { lineString, point, polygon } from '@turf/helpers'; import { MySQLClient } from '../../clients/MySQLClient'; +import { valueToSqlString } from 'common/libs/sqlUtils'; export default class MysqlExporter extends SqlExporter { protected _client: MySQLClient; @@ -122,54 +118,7 @@ ${footer} const column = notGeneratedColumns[i]; const val = row[column.name]; - if (val === null) sqlInsertString += 'NULL'; - else if (DATE.includes(column.type)) { - sqlInsertString += moment(val).isValid() - ? this.escapeAndQuote(moment(val).format('YYYY-MM-DD')) - : val; - } - else if (DATETIME.includes(column.type)) { - let datePrecision = ''; - for (let i = 0; i < column.datePrecision; i++) - datePrecision += i === 0 ? '.S' : 'S'; - - sqlInsertString += moment(val).isValid() - ? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`)) - : this.escapeAndQuote(val); - } - else if (BIT.includes(column.type)) - sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`; - else if (BLOB.includes(column.type)) - sqlInsertString += `X'${val.toString('hex').toUpperCase()}'`; - else if (NUMBER.includes(column.type)) - sqlInsertString += val; - else if (FLOAT.includes(column.type)) - sqlInsertString += parseFloat(val); - else if (SPATIAL.includes(column.type)) { - let geoJson; - if (IS_MULTI_SPATIAL.includes(column.type)) { - const features = []; - for (const element of val) - features.push(this._getGeoJSON(element)); - - geoJson = { - type: 'FeatureCollection', - features - }; - } - else - geoJson = this._getGeoJSON(val); - - sqlInsertString += `ST_GeomFromGeoJSON('${JSON.stringify(geoJson)}')`; - } - else if (val === '') sqlInsertString += '\'\''; - else { - sqlInsertString += typeof val === 'string' - ? this.escapeAndQuote(val) - : typeof val === 'object' - ? this.escapeAndQuote(JSON.stringify(val)) - : val; - } + sqlInsertString += valueToSqlString({ val, client: 'mysql', field: column }); if (parseInt(i) !== notGeneratedColumns.length - 1) sqlInsertString += ', '; @@ -435,17 +384,4 @@ CREATE TABLE \`${view.Name}\`( return `'${escapedVal}'`; } - - /* eslint-disable @typescript-eslint/no-explicit-any */ - _getGeoJSON (val: any) { - if (Array.isArray(val)) { - if (getArrayDepth(val) === 1) - return lineString(val.reduce((acc, curr) => [...acc, [curr.x, curr.y]], [])); - else - return polygon(val.map(arr => arr.reduce((acc: any, curr: any) => [...acc, [curr.x, curr.y]], []))); - } - else - return point([val.x, val.y]); - } - /* eslint-enable @typescript-eslint/no-explicit-any */ } diff --git a/src/main/libs/exporters/sql/PostgreSQLExporter.ts b/src/main/libs/exporters/sql/PostgreSQLExporter.ts index ee66a784..cabf90f9 100644 --- a/src/main/libs/exporters/sql/PostgreSQLExporter.ts +++ b/src/main/libs/exporters/sql/PostgreSQLExporter.ts @@ -1,13 +1,11 @@ import * as antares from 'common/interfaces/antares'; import * as exporter from 'common/interfaces/exporter'; import { SqlExporter } from './SqlExporter'; -import { BLOB, BIT, DATE, DATETIME, FLOAT, NUMBER, TEXT_SEARCH } from 'common/fieldTypes'; -import hexToBinary, { HexChar } from 'common/libs/hexToBinary'; -import * as moment from 'moment'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import * as QueryStream from 'pg-query-stream'; import { PostgreSQLClient } from '../../clients/PostgreSQLClient'; +import { valueToSqlString } from 'common/libs/sqlUtils'; export default class PostgreSQLExporter extends SqlExporter { constructor (client: PostgreSQLClient, tables: exporter.TableParams[], options: exporter.ExportOptions) { @@ -223,47 +221,7 @@ SET row_security = off;\n\n\n`; const column = columns[i]; const val = row[column.name]; - if (val === null) sqlInsertString += 'NULL'; - else if (DATE.includes(column.type)) { - sqlInsertString += moment(val).isValid() - ? this.escapeAndQuote(moment(val).format('YYYY-MM-DD')) - : val; - } - else if (DATETIME.includes(column.type)) { - let datePrecision = ''; - for (let i = 0; i < column.datePrecision; i++) - datePrecision += i === 0 ? '.S' : 'S'; - - sqlInsertString += moment(val).isValid() - ? this.escapeAndQuote(moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`)) - : this.escapeAndQuote(val); - } - else if ('isArray' in column) { - let parsedVal; - if (Array.isArray(val)) - parsedVal = JSON.stringify(val).replaceAll('[', '{').replaceAll(']', '}'); - else - parsedVal = typeof val === 'string' ? val.replaceAll('[', '{').replaceAll(']', '}') : ''; - sqlInsertString += `'${parsedVal}'`; - } - else if (TEXT_SEARCH.includes(column.type)) - sqlInsertString += `'${val.replaceAll('\'', '\'\'')}'`; - else if (BIT.includes(column.type)) - sqlInsertString += `b'${hexToBinary(Buffer.from(val).toString('hex') as undefined as HexChar[])}'`; - else if (BLOB.includes(column.type)) - sqlInsertString += `decode('${val.toString('hex').toUpperCase()}', 'hex')`; - else if (NUMBER.includes(column.type)) - sqlInsertString += val; - else if (FLOAT.includes(column.type)) - sqlInsertString += parseFloat(val); - else if (val === '') sqlInsertString += '\'\''; - else { - sqlInsertString += typeof val === 'string' - ? this.escapeAndQuote(val) - : typeof val === 'object' - ? this.escapeAndQuote(JSON.stringify(val)) - : val; - } + sqlInsertString += valueToSqlString({ val, client: 'pg', field: column }); if (parseInt(i) !== columns.length - 1) sqlInsertString += ', '; diff --git a/src/renderer/components/ModalProcessesList.vue b/src/renderer/components/ModalProcessesList.vue index d14ef9c8..4074470f 100644 --- a/src/renderer/components/ModalProcessesList.vue +++ b/src/renderer/components/ModalProcessesList.vue @@ -136,7 +136,7 @@