|
@@ -2,8 +2,8 @@
|
|
|
* @node mysql-query
|
|
* @node mysql-query
|
|
|
* @name MySQL Query
|
|
* @name MySQL Query
|
|
|
* @category database
|
|
* @category database
|
|
|
- * @version 1.0.0
|
|
|
|
|
- * @description Execute SQL queries against a MySQL database with schema introspection support
|
|
|
|
|
|
|
+ * @version 2.0.0
|
|
|
|
|
+ * @description Execute CRUD operations on MySQL databases with parameterized queries
|
|
|
* @icon database
|
|
* @icon database
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
@@ -29,78 +29,140 @@ const configSchema = {
|
|
|
title: 'Table',
|
|
title: 'Table',
|
|
|
description: 'Table name for query operations'
|
|
description: 'Table name for query operations'
|
|
|
},
|
|
},
|
|
|
- queryType: {
|
|
|
|
|
|
|
+ operation: {
|
|
|
type: 'string',
|
|
type: 'string',
|
|
|
- title: 'Query Type',
|
|
|
|
|
- description: 'Type of query to execute',
|
|
|
|
|
- enum: ['select', 'insert', 'update', 'delete', 'custom'],
|
|
|
|
|
- enumLabels: ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'Custom SQL'],
|
|
|
|
|
|
|
+ title: 'Operation',
|
|
|
|
|
+ description: 'Type of operation to perform',
|
|
|
|
|
+ enum: ['select', 'insert', 'update', 'delete', 'upsert', 'bulkInsert', 'customSql'],
|
|
|
|
|
+ enumLabels: ['Select', 'Insert', 'Update', 'Delete', 'Upsert', 'Bulk Insert', 'Custom SQL'],
|
|
|
default: 'select'
|
|
default: 'select'
|
|
|
},
|
|
},
|
|
|
columns: {
|
|
columns: {
|
|
|
type: 'array',
|
|
type: 'array',
|
|
|
title: 'Columns',
|
|
title: 'Columns',
|
|
|
- description: 'Columns to select (empty for all). Enter column names manually.',
|
|
|
|
|
|
|
+ description: 'Columns to select (empty for all)',
|
|
|
items: {
|
|
items: {
|
|
|
type: 'string'
|
|
type: 'string'
|
|
|
},
|
|
},
|
|
|
- showWhen: { field: 'queryType', value: 'select' }
|
|
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'select' }
|
|
|
},
|
|
},
|
|
|
- whereClause: {
|
|
|
|
|
- type: 'string',
|
|
|
|
|
- title: 'WHERE Clause',
|
|
|
|
|
- description: 'Optional WHERE conditions (without WHERE keyword). Supports {{variable}} interpolation.',
|
|
|
|
|
- showWhen: { field: 'queryType', valueIn: ['select', 'update', 'delete'] }
|
|
|
|
|
|
|
+ whereConditions: {
|
|
|
|
|
+ type: 'array',
|
|
|
|
|
+ title: 'WHERE Conditions',
|
|
|
|
|
+ description: 'Filter conditions for the query',
|
|
|
|
|
+ items: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ column: { type: 'string', title: 'Column' },
|
|
|
|
|
+ operator: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ title: 'Operator',
|
|
|
|
|
+ enum: ['=', '!=', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS NULL', 'IS NOT NULL'],
|
|
|
|
|
+ default: '='
|
|
|
|
|
+ },
|
|
|
|
|
+ value: { type: 'string', title: 'Value' }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ showWhen: { field: 'operation', valueIn: ['select', 'update', 'delete'] }
|
|
|
},
|
|
},
|
|
|
orderBy: {
|
|
orderBy: {
|
|
|
- type: 'string',
|
|
|
|
|
|
|
+ type: 'array',
|
|
|
title: 'ORDER BY',
|
|
title: 'ORDER BY',
|
|
|
- description: 'Order by clause (without ORDER BY keyword)',
|
|
|
|
|
- showWhen: { field: 'queryType', value: 'select' }
|
|
|
|
|
|
|
+ description: 'Sorting options',
|
|
|
|
|
+ items: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ column: { type: 'string', title: 'Column' },
|
|
|
|
|
+ direction: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ title: 'Direction',
|
|
|
|
|
+ enum: ['ASC', 'DESC'],
|
|
|
|
|
+ default: 'ASC'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'select' }
|
|
|
},
|
|
},
|
|
|
limit: {
|
|
limit: {
|
|
|
type: 'number',
|
|
type: 'number',
|
|
|
title: 'Limit',
|
|
title: 'Limit',
|
|
|
- description: 'Maximum number of rows to return',
|
|
|
|
|
|
|
+ description: 'Maximum number of rows to return (0 for no limit)',
|
|
|
default: 100,
|
|
default: 100,
|
|
|
- showWhen: { field: 'queryType', value: 'select' }
|
|
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'select' }
|
|
|
|
|
+ },
|
|
|
|
|
+ offset: {
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ title: 'Offset',
|
|
|
|
|
+ description: 'Number of rows to skip',
|
|
|
|
|
+ default: 0,
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'select' }
|
|
|
},
|
|
},
|
|
|
insertData: {
|
|
insertData: {
|
|
|
type: 'string',
|
|
type: 'string',
|
|
|
title: 'Data to Insert (JSON)',
|
|
title: 'Data to Insert (JSON)',
|
|
|
description: 'JSON object with column:value pairs. Supports {{variable}} interpolation.',
|
|
description: 'JSON object with column:value pairs. Supports {{variable}} interpolation.',
|
|
|
- showWhen: { field: 'queryType', value: 'insert' }
|
|
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'insert' }
|
|
|
},
|
|
},
|
|
|
updateData: {
|
|
updateData: {
|
|
|
type: 'string',
|
|
type: 'string',
|
|
|
title: 'Data to Update (JSON)',
|
|
title: 'Data to Update (JSON)',
|
|
|
description: 'JSON object with column:value pairs to set. Supports {{variable}} interpolation.',
|
|
description: 'JSON object with column:value pairs to set. Supports {{variable}} interpolation.',
|
|
|
- showWhen: { field: 'queryType', value: 'update' }
|
|
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'update' }
|
|
|
|
|
+ },
|
|
|
|
|
+ upsertData: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ title: 'Data to Upsert (JSON)',
|
|
|
|
|
+ description: 'JSON object with column:value pairs. Supports {{variable}} interpolation.',
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'upsert' }
|
|
|
|
|
+ },
|
|
|
|
|
+ conflictKey: {
|
|
|
|
|
+ type: 'array',
|
|
|
|
|
+ title: 'Conflict Key Columns',
|
|
|
|
|
+ description: 'Columns that form the unique/primary key for conflict detection',
|
|
|
|
|
+ items: {
|
|
|
|
|
+ type: 'string'
|
|
|
|
|
+ },
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'upsert' }
|
|
|
|
|
+ },
|
|
|
|
|
+ updateOnConflict: {
|
|
|
|
|
+ type: 'array',
|
|
|
|
|
+ title: 'Update Columns on Conflict',
|
|
|
|
|
+ description: 'Columns to update when a conflict is detected (empty for all non-key columns)',
|
|
|
|
|
+ items: {
|
|
|
|
|
+ type: 'string'
|
|
|
|
|
+ },
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'upsert' }
|
|
|
|
|
+ },
|
|
|
|
|
+ bulkData: {
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ title: 'Bulk Data (JSON Array)',
|
|
|
|
|
+ description: 'JSON array of objects to insert. Supports {{variable}} interpolation.',
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'bulkInsert' }
|
|
|
|
|
+ },
|
|
|
|
|
+ batchSize: {
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ title: 'Batch Size',
|
|
|
|
|
+ description: 'Number of rows per INSERT statement (0 for all at once)',
|
|
|
|
|
+ default: 100,
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'bulkInsert' }
|
|
|
},
|
|
},
|
|
|
customQuery: {
|
|
customQuery: {
|
|
|
type: 'string',
|
|
type: 'string',
|
|
|
title: 'Custom SQL',
|
|
title: 'Custom SQL',
|
|
|
- description: 'Custom SQL query. Supports {{variable}} interpolation. Use ? for parameters.',
|
|
|
|
|
- showWhen: { field: 'queryType', value: 'custom' }
|
|
|
|
|
|
|
+ description: 'Custom SQL query with ? placeholders for parameters. Supports {{variable}} interpolation.',
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'customSql' }
|
|
|
},
|
|
},
|
|
|
parameters: {
|
|
parameters: {
|
|
|
type: 'array',
|
|
type: 'array',
|
|
|
title: 'Query Parameters',
|
|
title: 'Query Parameters',
|
|
|
- description: 'Parameters for the query (for ? placeholders)',
|
|
|
|
|
|
|
+ description: 'Parameters for ? placeholders in custom SQL',
|
|
|
items: {
|
|
items: {
|
|
|
type: 'string'
|
|
type: 'string'
|
|
|
},
|
|
},
|
|
|
- showWhen: { field: 'queryType', value: 'custom' }
|
|
|
|
|
- },
|
|
|
|
|
- returnInsertId: {
|
|
|
|
|
- type: 'boolean',
|
|
|
|
|
- title: 'Return Insert ID',
|
|
|
|
|
- description: 'Return the auto-generated ID for INSERT queries',
|
|
|
|
|
- default: true,
|
|
|
|
|
- showWhen: { field: 'queryType', value: 'insert' }
|
|
|
|
|
|
|
+ showWhen: { field: 'operation', value: 'customSql' }
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- required: ['credentialId', 'queryType']
|
|
|
|
|
|
|
+ required: ['credentialId', 'operation']
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const inputSchema = {
|
|
const inputSchema = {
|
|
@@ -113,6 +175,10 @@ const inputSchema = {
|
|
|
parameters: {
|
|
parameters: {
|
|
|
type: 'array',
|
|
type: 'array',
|
|
|
description: 'Override query parameters from input'
|
|
description: 'Override query parameters from input'
|
|
|
|
|
+ },
|
|
|
|
|
+ rows: {
|
|
|
|
|
+ type: 'array',
|
|
|
|
|
+ description: 'Array of objects for bulk insert (alternative to bulkData config)'
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -120,12 +186,18 @@ const inputSchema = {
|
|
|
const outputSchema = {
|
|
const outputSchema = {
|
|
|
type: 'object',
|
|
type: 'object',
|
|
|
properties: {
|
|
properties: {
|
|
|
- rows: { type: 'array', description: 'Query result rows (for SELECT)' },
|
|
|
|
|
- affectedRows: { type: 'number', description: 'Number of affected rows (for INSERT/UPDATE/DELETE)' },
|
|
|
|
|
- insertId: { type: 'number', description: 'Auto-generated insert ID (for INSERT)' },
|
|
|
|
|
|
|
+ rows: { type: 'array', description: 'Query result rows as array of objects' },
|
|
|
|
|
+ metadata: {
|
|
|
|
|
+ type: 'object',
|
|
|
|
|
+ description: 'Operation metadata',
|
|
|
|
|
+ properties: {
|
|
|
|
|
+ affectedRows: { type: 'number', description: 'Number of affected rows' },
|
|
|
|
|
+ insertId: { type: 'number', description: 'Last auto-generated insert ID' },
|
|
|
|
|
+ rowCount: { type: 'number', description: 'Number of rows returned' }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
columns: { type: 'array', description: 'Column names from the result' },
|
|
columns: { type: 'array', description: 'Column names from the result' },
|
|
|
- query: { type: 'string', description: 'The executed SQL query' },
|
|
|
|
|
- error: { type: 'string', description: 'Error message if query failed' }
|
|
|
|
|
|
|
+ query: { type: 'string', description: 'The executed SQL query (for debugging)' }
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
@@ -133,59 +205,140 @@ const outputs = [
|
|
|
{ name: 'main', displayName: 'Query Result', type: 'object', color: '#06b6d4' }
|
|
{ name: 'main', displayName: 'Query Result', type: 'object', color: '#06b6d4' }
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
|
|
+function parseJsonData(jsonString, data, fieldName) {
|
|
|
|
|
+ if (!jsonString) {
|
|
|
|
|
+ throw new Error(fieldName + ' is required');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let parsed = jsonString;
|
|
|
|
|
+ if (typeof parsed === 'string') {
|
|
|
|
|
+ parsed = smartbotic.utils.interpolate(parsed, data);
|
|
|
|
|
+ try {
|
|
|
|
|
+ parsed = JSON.parse(parsed);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ throw new Error(fieldName + ' must be valid JSON: ' + e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return parsed;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function escapeIdentifier(name) {
|
|
|
|
|
+ return '`' + name.replace(/`/g, '``') + '`';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildWhereClause(conditions, data) {
|
|
|
|
|
+ if (!conditions || conditions.length === 0) {
|
|
|
|
|
+ return { clause: '', params: [] };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const clauses = [];
|
|
|
|
|
+ const params = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (const condition of conditions) {
|
|
|
|
|
+ if (!condition.column) continue;
|
|
|
|
|
+
|
|
|
|
|
+ const col = escapeIdentifier(condition.column);
|
|
|
|
|
+ const op = condition.operator || '=';
|
|
|
|
|
+ let value = condition.value;
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof value === 'string') {
|
|
|
|
|
+ value = smartbotic.utils.interpolate(value, data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (op === 'IS NULL') {
|
|
|
|
|
+ clauses.push(col + ' IS NULL');
|
|
|
|
|
+ } else if (op === 'IS NOT NULL') {
|
|
|
|
|
+ clauses.push(col + ' IS NOT NULL');
|
|
|
|
|
+ } else if (op === 'IN' || op === 'NOT IN') {
|
|
|
|
|
+ let values = value;
|
|
|
|
|
+ if (typeof values === 'string') {
|
|
|
|
|
+ try {
|
|
|
|
|
+ values = JSON.parse(values);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ values = values.split(',').map(v => v.trim());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(values)) {
|
|
|
|
|
+ values = [values];
|
|
|
|
|
+ }
|
|
|
|
|
+ const placeholders = values.map(() => '?').join(', ');
|
|
|
|
|
+ clauses.push(col + ' ' + op + ' (' + placeholders + ')');
|
|
|
|
|
+ params.push(...values);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ clauses.push(col + ' ' + op + ' ?');
|
|
|
|
|
+ params.push(value);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ clause: clauses.length > 0 ? ' WHERE ' + clauses.join(' AND ') : '',
|
|
|
|
|
+ params
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function buildSelectQuery(config, data) {
|
|
function buildSelectQuery(config, data) {
|
|
|
const table = config.table;
|
|
const table = config.table;
|
|
|
if (!table) {
|
|
if (!table) {
|
|
|
- throw new Error('Table is required for SELECT queries');
|
|
|
|
|
|
|
+ throw new Error('Table is required for SELECT operations');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const columns = config.columns && config.columns.length > 0
|
|
const columns = config.columns && config.columns.length > 0
|
|
|
- ? config.columns.join(', ')
|
|
|
|
|
|
|
+ ? config.columns.map(escapeIdentifier).join(', ')
|
|
|
: '*';
|
|
: '*';
|
|
|
|
|
|
|
|
- let query = `SELECT ${columns} FROM \`${table}\``;
|
|
|
|
|
|
|
+ let query = 'SELECT ' + columns + ' FROM ' + escapeIdentifier(table);
|
|
|
|
|
+ const params = [];
|
|
|
|
|
|
|
|
- if (config.whereClause) {
|
|
|
|
|
- const where = smartbotic.utils.interpolate(config.whereClause, data);
|
|
|
|
|
- query += ` WHERE ${where}`;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const where = buildWhereClause(config.whereConditions, data);
|
|
|
|
|
+ query += where.clause;
|
|
|
|
|
+ params.push(...where.params);
|
|
|
|
|
|
|
|
- if (config.orderBy) {
|
|
|
|
|
- query += ` ORDER BY ${config.orderBy}`;
|
|
|
|
|
|
|
+ if (config.orderBy && config.orderBy.length > 0) {
|
|
|
|
|
+ const orderClauses = config.orderBy
|
|
|
|
|
+ .filter(o => o.column)
|
|
|
|
|
+ .map(o => escapeIdentifier(o.column) + ' ' + (o.direction || 'ASC'));
|
|
|
|
|
+ if (orderClauses.length > 0) {
|
|
|
|
|
+ query += ' ORDER BY ' + orderClauses.join(', ');
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (config.limit && config.limit > 0) {
|
|
if (config.limit && config.limit > 0) {
|
|
|
- query += ` LIMIT ${config.limit}`;
|
|
|
|
|
|
|
+ query += ' LIMIT ?';
|
|
|
|
|
+ params.push(config.limit);
|
|
|
|
|
+
|
|
|
|
|
+ if (config.offset && config.offset > 0) {
|
|
|
|
|
+ query += ' OFFSET ?';
|
|
|
|
|
+ params.push(config.offset);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return { query, params: [] };
|
|
|
|
|
|
|
+ return { query, params };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function buildInsertQuery(config, data) {
|
|
function buildInsertQuery(config, data) {
|
|
|
const table = config.table;
|
|
const table = config.table;
|
|
|
if (!table) {
|
|
if (!table) {
|
|
|
- throw new Error('Table is required for INSERT queries');
|
|
|
|
|
|
|
+ throw new Error('Table is required for INSERT operations');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- let insertData = config.insertData;
|
|
|
|
|
- if (typeof insertData === 'string') {
|
|
|
|
|
- insertData = smartbotic.utils.interpolate(insertData, data);
|
|
|
|
|
- try {
|
|
|
|
|
- insertData = JSON.parse(insertData);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- throw new Error('Insert data must be valid JSON: ' + e.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const insertData = parseJsonData(config.insertData, data, 'Insert data');
|
|
|
|
|
|
|
|
- if (!insertData || typeof insertData !== 'object') {
|
|
|
|
|
- throw new Error('Insert data is required');
|
|
|
|
|
|
|
+ if (!insertData || typeof insertData !== 'object' || Array.isArray(insertData)) {
|
|
|
|
|
+ throw new Error('Insert data must be a JSON object');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const columns = Object.keys(insertData);
|
|
const columns = Object.keys(insertData);
|
|
|
|
|
+ if (columns.length === 0) {
|
|
|
|
|
+ throw new Error('Insert data cannot be empty');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const values = Object.values(insertData);
|
|
const values = Object.values(insertData);
|
|
|
const placeholders = columns.map(() => '?').join(', ');
|
|
const placeholders = columns.map(() => '?').join(', ');
|
|
|
|
|
|
|
|
- const query = `INSERT INTO \`${table}\` (\`${columns.join('`, `')}\`) VALUES (${placeholders})`;
|
|
|
|
|
|
|
+ const query = 'INSERT INTO ' + escapeIdentifier(table) +
|
|
|
|
|
+ ' (' + columns.map(escapeIdentifier).join(', ') + ')' +
|
|
|
|
|
+ ' VALUES (' + placeholders + ')';
|
|
|
|
|
|
|
|
return { query, params: values };
|
|
return { query, params: values };
|
|
|
}
|
|
}
|
|
@@ -193,58 +346,152 @@ function buildInsertQuery(config, data) {
|
|
|
function buildUpdateQuery(config, data) {
|
|
function buildUpdateQuery(config, data) {
|
|
|
const table = config.table;
|
|
const table = config.table;
|
|
|
if (!table) {
|
|
if (!table) {
|
|
|
- throw new Error('Table is required for UPDATE queries');
|
|
|
|
|
|
|
+ throw new Error('Table is required for UPDATE operations');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- let updateData = config.updateData;
|
|
|
|
|
- if (typeof updateData === 'string') {
|
|
|
|
|
- updateData = smartbotic.utils.interpolate(updateData, data);
|
|
|
|
|
- try {
|
|
|
|
|
- updateData = JSON.parse(updateData);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- throw new Error('Update data must be valid JSON: ' + e.message);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const updateData = parseJsonData(config.updateData, data, 'Update data');
|
|
|
|
|
|
|
|
- if (!updateData || typeof updateData !== 'object') {
|
|
|
|
|
- throw new Error('Update data is required');
|
|
|
|
|
|
|
+ if (!updateData || typeof updateData !== 'object' || Array.isArray(updateData)) {
|
|
|
|
|
+ throw new Error('Update data must be a JSON object');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const columns = Object.keys(updateData);
|
|
const columns = Object.keys(updateData);
|
|
|
|
|
+ if (columns.length === 0) {
|
|
|
|
|
+ throw new Error('Update data cannot be empty');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const values = Object.values(updateData);
|
|
const values = Object.values(updateData);
|
|
|
- const setClause = columns.map(col => `\`${col}\` = ?`).join(', ');
|
|
|
|
|
|
|
+ const setClause = columns.map(col => escapeIdentifier(col) + ' = ?').join(', ');
|
|
|
|
|
|
|
|
- let query = `UPDATE \`${table}\` SET ${setClause}`;
|
|
|
|
|
|
|
+ let query = 'UPDATE ' + escapeIdentifier(table) + ' SET ' + setClause;
|
|
|
|
|
+ const params = [...values];
|
|
|
|
|
|
|
|
- if (config.whereClause) {
|
|
|
|
|
- const where = smartbotic.utils.interpolate(config.whereClause, data);
|
|
|
|
|
- query += ` WHERE ${where}`;
|
|
|
|
|
|
|
+ const where = buildWhereClause(config.whereConditions, data);
|
|
|
|
|
+ if (!where.clause) {
|
|
|
|
|
+ throw new Error('WHERE conditions are required for UPDATE operations to prevent accidental data modification');
|
|
|
}
|
|
}
|
|
|
|
|
+ query += where.clause;
|
|
|
|
|
+ params.push(...where.params);
|
|
|
|
|
|
|
|
- return { query, params: values };
|
|
|
|
|
|
|
+ return { query, params };
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function buildDeleteQuery(config, data) {
|
|
function buildDeleteQuery(config, data) {
|
|
|
const table = config.table;
|
|
const table = config.table;
|
|
|
if (!table) {
|
|
if (!table) {
|
|
|
- throw new Error('Table is required for DELETE queries');
|
|
|
|
|
|
|
+ throw new Error('Table is required for DELETE operations');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- let query = `DELETE FROM \`${table}\``;
|
|
|
|
|
|
|
+ let query = 'DELETE FROM ' + escapeIdentifier(table);
|
|
|
|
|
+ const params = [];
|
|
|
|
|
|
|
|
- if (config.whereClause) {
|
|
|
|
|
- const where = smartbotic.utils.interpolate(config.whereClause, data);
|
|
|
|
|
- query += ` WHERE ${where}`;
|
|
|
|
|
- } else {
|
|
|
|
|
- throw new Error('WHERE clause is required for DELETE queries to prevent accidental data loss');
|
|
|
|
|
|
|
+ const where = buildWhereClause(config.whereConditions, data);
|
|
|
|
|
+ if (!where.clause) {
|
|
|
|
|
+ throw new Error('WHERE conditions are required for DELETE operations to prevent accidental data loss');
|
|
|
}
|
|
}
|
|
|
|
|
+ query += where.clause;
|
|
|
|
|
+ params.push(...where.params);
|
|
|
|
|
|
|
|
- return { query, params: [] };
|
|
|
|
|
|
|
+ return { query, params };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildUpsertQuery(config, data) {
|
|
|
|
|
+ const table = config.table;
|
|
|
|
|
+ if (!table) {
|
|
|
|
|
+ throw new Error('Table is required for UPSERT operations');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const upsertData = parseJsonData(config.upsertData, data, 'Upsert data');
|
|
|
|
|
+
|
|
|
|
|
+ if (!upsertData || typeof upsertData !== 'object' || Array.isArray(upsertData)) {
|
|
|
|
|
+ throw new Error('Upsert data must be a JSON object');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const columns = Object.keys(upsertData);
|
|
|
|
|
+ if (columns.length === 0) {
|
|
|
|
|
+ throw new Error('Upsert data cannot be empty');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const values = Object.values(upsertData);
|
|
|
|
|
+ const placeholders = columns.map(() => '?').join(', ');
|
|
|
|
|
+
|
|
|
|
|
+ const conflictKeys = config.conflictKey || [];
|
|
|
|
|
+ let updateColumns = config.updateOnConflict || [];
|
|
|
|
|
+
|
|
|
|
|
+ if (updateColumns.length === 0) {
|
|
|
|
|
+ updateColumns = columns.filter(col => !conflictKeys.includes(col));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (updateColumns.length === 0) {
|
|
|
|
|
+ updateColumns = columns;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const updateClause = updateColumns
|
|
|
|
|
+ .map(col => escapeIdentifier(col) + ' = VALUES(' + escapeIdentifier(col) + ')')
|
|
|
|
|
+ .join(', ');
|
|
|
|
|
+
|
|
|
|
|
+ const query = 'INSERT INTO ' + escapeIdentifier(table) +
|
|
|
|
|
+ ' (' + columns.map(escapeIdentifier).join(', ') + ')' +
|
|
|
|
|
+ ' VALUES (' + placeholders + ')' +
|
|
|
|
|
+ ' ON DUPLICATE KEY UPDATE ' + updateClause;
|
|
|
|
|
+
|
|
|
|
|
+ return { query, params: values };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildBulkInsertQueries(config, data, input) {
|
|
|
|
|
+ const table = config.table;
|
|
|
|
|
+ if (!table) {
|
|
|
|
|
+ throw new Error('Table is required for BULK INSERT operations');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let bulkData = input.rows;
|
|
|
|
|
+ if (!bulkData) {
|
|
|
|
|
+ bulkData = parseJsonData(config.bulkData, data, 'Bulk data');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!Array.isArray(bulkData)) {
|
|
|
|
|
+ throw new Error('Bulk data must be a JSON array of objects');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (bulkData.length === 0) {
|
|
|
|
|
+ throw new Error('Bulk data array cannot be empty');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const columns = Object.keys(bulkData[0]);
|
|
|
|
|
+ if (columns.length === 0) {
|
|
|
|
|
+ throw new Error('Bulk data objects cannot be empty');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const batchSize = config.batchSize > 0 ? config.batchSize : bulkData.length;
|
|
|
|
|
+ const queries = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < bulkData.length; i += batchSize) {
|
|
|
|
|
+ const batch = bulkData.slice(i, i + batchSize);
|
|
|
|
|
+ const allValues = [];
|
|
|
|
|
+ const rowPlaceholders = [];
|
|
|
|
|
+
|
|
|
|
|
+ for (const row of batch) {
|
|
|
|
|
+ const rowValues = columns.map(col => {
|
|
|
|
|
+ const val = row[col];
|
|
|
|
|
+ return val !== undefined ? val : null;
|
|
|
|
|
+ });
|
|
|
|
|
+ allValues.push(...rowValues);
|
|
|
|
|
+ rowPlaceholders.push('(' + columns.map(() => '?').join(', ') + ')');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const query = 'INSERT INTO ' + escapeIdentifier(table) +
|
|
|
|
|
+ ' (' + columns.map(escapeIdentifier).join(', ') + ')' +
|
|
|
|
|
+ ' VALUES ' + rowPlaceholders.join(', ');
|
|
|
|
|
+
|
|
|
|
|
+ queries.push({ query, params: allValues, rowCount: batch.length });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return queries;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function buildCustomQuery(config, data, input) {
|
|
function buildCustomQuery(config, data, input) {
|
|
|
if (!config.customQuery) {
|
|
if (!config.customQuery) {
|
|
|
- throw new Error('Custom query is required');
|
|
|
|
|
|
|
+ throw new Error('Custom SQL query is required');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const query = smartbotic.utils.interpolate(config.customQuery, data);
|
|
const query = smartbotic.utils.interpolate(config.customQuery, data);
|
|
@@ -268,7 +515,7 @@ module.exports = {
|
|
|
outputSchema,
|
|
outputSchema,
|
|
|
outputs,
|
|
outputs,
|
|
|
|
|
|
|
|
- async execute(config, input, context) {
|
|
|
|
|
|
|
+ async execute(config, input) {
|
|
|
const credentialId = config.credentialId;
|
|
const credentialId = config.credentialId;
|
|
|
const data = input.data || input;
|
|
const data = input.data || input;
|
|
|
|
|
|
|
@@ -276,10 +523,55 @@ module.exports = {
|
|
|
throw new Error('MySQL credential is required');
|
|
throw new Error('MySQL credential is required');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const operation = config.operation || 'select';
|
|
|
|
|
+ let totalAffectedRows = 0;
|
|
|
|
|
+ let lastInsertId = null;
|
|
|
|
|
+ let executedQuery = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (operation === 'bulkInsert') {
|
|
|
|
|
+ const queries = buildBulkInsertQueries(config, data, input);
|
|
|
|
|
+
|
|
|
|
|
+ smartbotic.log.info('MySQL BULK INSERT: ' + queries.length + ' batch(es)');
|
|
|
|
|
+
|
|
|
|
|
+ for (const queryInfo of queries) {
|
|
|
|
|
+ const options = {
|
|
|
|
|
+ credentialId,
|
|
|
|
|
+ database: config.database,
|
|
|
|
|
+ query: queryInfo.query,
|
|
|
|
|
+ params: queryInfo.params
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const result = smartbotic.mysql.query(options);
|
|
|
|
|
+
|
|
|
|
|
+ if (!result.success) {
|
|
|
|
|
+ smartbotic.log.error('MySQL bulk insert failed: ' + result.error);
|
|
|
|
|
+ throw new Error('MySQL bulk insert failed: ' + result.error);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ totalAffectedRows += result.affectedRows || 0;
|
|
|
|
|
+ if (result.insertId) {
|
|
|
|
|
+ lastInsertId = result.insertId;
|
|
|
|
|
+ }
|
|
|
|
|
+ executedQuery = queryInfo.query;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ smartbotic.log.info('MySQL BULK INSERT completed: ' + totalAffectedRows + ' rows inserted');
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ rows: [],
|
|
|
|
|
+ metadata: {
|
|
|
|
|
+ affectedRows: totalAffectedRows,
|
|
|
|
|
+ insertId: lastInsertId,
|
|
|
|
|
+ rowCount: 0
|
|
|
|
|
+ },
|
|
|
|
|
+ columns: [],
|
|
|
|
|
+ query: executedQuery
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
let queryInfo;
|
|
let queryInfo;
|
|
|
- const queryType = config.queryType || 'select';
|
|
|
|
|
|
|
|
|
|
- switch (queryType) {
|
|
|
|
|
|
|
+ switch (operation) {
|
|
|
case 'select':
|
|
case 'select':
|
|
|
queryInfo = buildSelectQuery(config, data);
|
|
queryInfo = buildSelectQuery(config, data);
|
|
|
break;
|
|
break;
|
|
@@ -292,16 +584,19 @@ module.exports = {
|
|
|
case 'delete':
|
|
case 'delete':
|
|
|
queryInfo = buildDeleteQuery(config, data);
|
|
queryInfo = buildDeleteQuery(config, data);
|
|
|
break;
|
|
break;
|
|
|
- case 'custom':
|
|
|
|
|
|
|
+ case 'upsert':
|
|
|
|
|
+ queryInfo = buildUpsertQuery(config, data);
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'customSql':
|
|
|
queryInfo = buildCustomQuery(config, data, input);
|
|
queryInfo = buildCustomQuery(config, data, input);
|
|
|
break;
|
|
break;
|
|
|
default:
|
|
default:
|
|
|
- throw new Error('Invalid query type: ' + queryType);
|
|
|
|
|
|
|
+ throw new Error('Invalid operation: ' + operation);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const { query, params } = queryInfo;
|
|
const { query, params } = queryInfo;
|
|
|
|
|
|
|
|
- smartbotic.log.info(`MySQL ${queryType.toUpperCase()}: ${query.substring(0, 100)}...`);
|
|
|
|
|
|
|
+ smartbotic.log.info('MySQL ' + operation.toUpperCase() + ': ' + query.substring(0, 100) + (query.length > 100 ? '...' : ''));
|
|
|
|
|
|
|
|
const options = {
|
|
const options = {
|
|
|
credentialId,
|
|
credentialId,
|
|
@@ -313,16 +608,21 @@ module.exports = {
|
|
|
const result = smartbotic.mysql.query(options);
|
|
const result = smartbotic.mysql.query(options);
|
|
|
|
|
|
|
|
if (!result.success) {
|
|
if (!result.success) {
|
|
|
- smartbotic.log.error(`MySQL query failed: ${result.error}`);
|
|
|
|
|
- throw new Error(`MySQL query failed: ${result.error}`);
|
|
|
|
|
|
|
+ smartbotic.log.error('MySQL query failed: ' + result.error);
|
|
|
|
|
+ throw new Error('MySQL query failed: ' + result.error);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- smartbotic.log.info(`MySQL query completed: ${result.affectedRows || 0} affected, ${(result.rows || []).length} rows returned`);
|
|
|
|
|
|
|
+ smartbotic.log.info('MySQL ' + operation.toUpperCase() + ' completed: ' +
|
|
|
|
|
+ (result.affectedRows || 0) + ' affected, ' +
|
|
|
|
|
+ (result.rows || []).length + ' rows returned');
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
rows: result.rows || [],
|
|
rows: result.rows || [],
|
|
|
- affectedRows: result.affectedRows || 0,
|
|
|
|
|
- insertId: result.insertId || null,
|
|
|
|
|
|
|
+ metadata: {
|
|
|
|
|
+ affectedRows: result.affectedRows || 0,
|
|
|
|
|
+ insertId: result.insertId || null,
|
|
|
|
|
+ rowCount: (result.rows || []).length
|
|
|
|
|
+ },
|
|
|
columns: result.columns || [],
|
|
columns: result.columns || [],
|
|
|
query: query
|
|
query: query
|
|
|
};
|
|
};
|