| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- /**
- * @node mysql-query
- * @name MySQL Query
- * @category database
- * @version 2.0.0
- * @description Execute CRUD operations on MySQL databases with parameterized queries
- * @icon database
- */
- const configSchema = {
- type: 'object',
- properties: {
- credentialId: {
- type: 'string',
- title: 'MySQL Credential',
- description: 'Select the MySQL credential to use for connection',
- dynamicOptions: {
- source: 'credentials',
- filter: { type: 'mysql' }
- }
- },
- database: {
- type: 'string',
- title: 'Database',
- description: 'Database name (overrides credential default if set)'
- },
- table: {
- type: 'string',
- title: 'Table',
- description: 'Table name for query operations'
- },
- operation: {
- type: 'string',
- 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'
- },
- columns: {
- type: 'array',
- title: 'Columns',
- description: 'Columns to select (empty for all)',
- items: {
- type: 'string'
- },
- showWhen: { field: 'operation', value: 'select' }
- },
- 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: {
- type: 'array',
- title: 'ORDER BY',
- 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: {
- type: 'number',
- title: 'Limit',
- description: 'Maximum number of rows to return (0 for no limit)',
- default: 100,
- showWhen: { field: 'operation', value: 'select' }
- },
- offset: {
- type: 'number',
- title: 'Offset',
- description: 'Number of rows to skip',
- default: 0,
- showWhen: { field: 'operation', value: 'select' }
- },
- insertData: {
- type: 'string',
- title: 'Data to Insert (JSON)',
- description: 'JSON object with column:value pairs. Supports {{variable}} interpolation.',
- showWhen: { field: 'operation', value: 'insert' }
- },
- updateData: {
- type: 'string',
- title: 'Data to Update (JSON)',
- description: 'JSON object with column:value pairs to set. Supports {{variable}} interpolation.',
- 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: {
- type: 'string',
- title: 'Custom SQL',
- description: 'Custom SQL query with ? placeholders for parameters. Supports {{variable}} interpolation.',
- showWhen: { field: 'operation', value: 'customSql' }
- },
- parameters: {
- type: 'array',
- title: 'Query Parameters',
- description: 'Parameters for ? placeholders in custom SQL',
- items: {
- type: 'string'
- },
- showWhen: { field: 'operation', value: 'customSql' }
- }
- },
- required: ['credentialId', 'operation']
- };
- const inputSchema = {
- type: 'object',
- properties: {
- data: {
- type: 'any',
- description: 'Input data available for interpolation in queries'
- },
- parameters: {
- type: 'array',
- description: 'Override query parameters from input'
- },
- rows: {
- type: 'array',
- description: 'Array of objects for bulk insert (alternative to bulkData config)'
- }
- }
- };
- const outputSchema = {
- type: 'object',
- properties: {
- 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' },
- query: { type: 'string', description: 'The executed SQL query (for debugging)' }
- }
- };
- const outputs = [
- { 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) {
- const table = config.table;
- if (!table) {
- throw new Error('Table is required for SELECT operations');
- }
- const columns = config.columns && config.columns.length > 0
- ? config.columns.map(escapeIdentifier).join(', ')
- : '*';
- let query = 'SELECT ' + columns + ' FROM ' + escapeIdentifier(table);
- const params = [];
- const where = buildWhereClause(config.whereConditions, data);
- query += where.clause;
- params.push(...where.params);
- 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) {
- query += ' LIMIT ?';
- params.push(config.limit);
- if (config.offset && config.offset > 0) {
- query += ' OFFSET ?';
- params.push(config.offset);
- }
- }
- return { query, params };
- }
- function buildInsertQuery(config, data) {
- const table = config.table;
- if (!table) {
- throw new Error('Table is required for INSERT operations');
- }
- const insertData = parseJsonData(config.insertData, data, 'Insert data');
- if (!insertData || typeof insertData !== 'object' || Array.isArray(insertData)) {
- throw new Error('Insert data must be a JSON object');
- }
- const columns = Object.keys(insertData);
- if (columns.length === 0) {
- throw new Error('Insert data cannot be empty');
- }
- const values = Object.values(insertData);
- const placeholders = columns.map(() => '?').join(', ');
- const query = 'INSERT INTO ' + escapeIdentifier(table) +
- ' (' + columns.map(escapeIdentifier).join(', ') + ')' +
- ' VALUES (' + placeholders + ')';
- return { query, params: values };
- }
- function buildUpdateQuery(config, data) {
- const table = config.table;
- if (!table) {
- throw new Error('Table is required for UPDATE operations');
- }
- const updateData = parseJsonData(config.updateData, data, 'Update data');
- if (!updateData || typeof updateData !== 'object' || Array.isArray(updateData)) {
- throw new Error('Update data must be a JSON object');
- }
- const columns = Object.keys(updateData);
- if (columns.length === 0) {
- throw new Error('Update data cannot be empty');
- }
- const values = Object.values(updateData);
- const setClause = columns.map(col => escapeIdentifier(col) + ' = ?').join(', ');
- let query = 'UPDATE ' + escapeIdentifier(table) + ' SET ' + setClause;
- const params = [...values];
- 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 };
- }
- function buildDeleteQuery(config, data) {
- const table = config.table;
- if (!table) {
- throw new Error('Table is required for DELETE operations');
- }
- let query = 'DELETE FROM ' + escapeIdentifier(table);
- const params = [];
- 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 };
- }
- 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) {
- if (!config.customQuery) {
- throw new Error('Custom SQL query is required');
- }
- const query = smartbotic.utils.interpolate(config.customQuery, data);
- let params = input.parameters || config.parameters || [];
- if (params.length > 0) {
- params = params.map(p => {
- if (typeof p === 'string') {
- return smartbotic.utils.interpolate(p, data);
- }
- return p;
- });
- }
- return { query, params };
- }
- module.exports = {
- configSchema,
- inputSchema,
- outputSchema,
- outputs,
- async execute(config, input) {
- const credentialId = config.credentialId;
- const data = input.data || input;
- if (!credentialId) {
- 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;
- switch (operation) {
- case 'select':
- queryInfo = buildSelectQuery(config, data);
- break;
- case 'insert':
- queryInfo = buildInsertQuery(config, data);
- break;
- case 'update':
- queryInfo = buildUpdateQuery(config, data);
- break;
- case 'delete':
- queryInfo = buildDeleteQuery(config, data);
- break;
- case 'upsert':
- queryInfo = buildUpsertQuery(config, data);
- break;
- case 'customSql':
- queryInfo = buildCustomQuery(config, data, input);
- break;
- default:
- throw new Error('Invalid operation: ' + operation);
- }
- const { query, params } = queryInfo;
- smartbotic.log.info('MySQL ' + operation.toUpperCase() + ': ' + query.substring(0, 100) + (query.length > 100 ? '...' : ''));
- const options = {
- credentialId,
- database: config.database,
- query,
- params
- };
- const result = smartbotic.mysql.query(options);
- if (!result.success) {
- smartbotic.log.error('MySQL query failed: ' + result.error);
- throw new Error('MySQL query failed: ' + result.error);
- }
- smartbotic.log.info('MySQL ' + operation.toUpperCase() + ' completed: ' +
- (result.affectedRows || 0) + ' affected, ' +
- (result.rows || []).length + ' rows returned');
- return {
- rows: result.rows || [],
- metadata: {
- affectedRows: result.affectedRows || 0,
- insertId: result.insertId || null,
- rowCount: (result.rows || []).length
- },
- columns: result.columns || [],
- query: query
- };
- }
- };
|