mysql-query.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. /**
  2. * @node mysql-query
  3. * @name MySQL Query
  4. * @category database
  5. * @version 2.0.0
  6. * @description Execute CRUD operations on MySQL databases with parameterized queries
  7. * @icon database
  8. */
  9. const configSchema = {
  10. type: 'object',
  11. properties: {
  12. credentialId: {
  13. type: 'string',
  14. title: 'MySQL Credential',
  15. description: 'Select the MySQL credential to use for connection',
  16. dynamicOptions: {
  17. source: 'credentials',
  18. filter: { type: 'mysql' }
  19. }
  20. },
  21. database: {
  22. type: 'string',
  23. title: 'Database',
  24. description: 'Database name (overrides credential default if set)'
  25. },
  26. table: {
  27. type: 'string',
  28. title: 'Table',
  29. description: 'Table name for query operations'
  30. },
  31. operation: {
  32. type: 'string',
  33. title: 'Operation',
  34. description: 'Type of operation to perform',
  35. enum: ['select', 'insert', 'update', 'delete', 'upsert', 'bulkInsert', 'customSql'],
  36. enumLabels: ['Select', 'Insert', 'Update', 'Delete', 'Upsert', 'Bulk Insert', 'Custom SQL'],
  37. default: 'select'
  38. },
  39. columns: {
  40. type: 'array',
  41. title: 'Columns',
  42. description: 'Columns to select (empty for all)',
  43. items: {
  44. type: 'string'
  45. },
  46. showWhen: { field: 'operation', value: 'select' }
  47. },
  48. whereConditions: {
  49. type: 'array',
  50. title: 'WHERE Conditions',
  51. description: 'Filter conditions for the query',
  52. items: {
  53. type: 'object',
  54. properties: {
  55. column: { type: 'string', title: 'Column' },
  56. operator: {
  57. type: 'string',
  58. title: 'Operator',
  59. enum: ['=', '!=', '<', '>', '<=', '>=', 'LIKE', 'NOT LIKE', 'IN', 'NOT IN', 'IS NULL', 'IS NOT NULL'],
  60. default: '='
  61. },
  62. value: { type: 'string', title: 'Value' }
  63. }
  64. },
  65. showWhen: { field: 'operation', valueIn: ['select', 'update', 'delete'] }
  66. },
  67. orderBy: {
  68. type: 'array',
  69. title: 'ORDER BY',
  70. description: 'Sorting options',
  71. items: {
  72. type: 'object',
  73. properties: {
  74. column: { type: 'string', title: 'Column' },
  75. direction: {
  76. type: 'string',
  77. title: 'Direction',
  78. enum: ['ASC', 'DESC'],
  79. default: 'ASC'
  80. }
  81. }
  82. },
  83. showWhen: { field: 'operation', value: 'select' }
  84. },
  85. limit: {
  86. type: 'number',
  87. title: 'Limit',
  88. description: 'Maximum number of rows to return (0 for no limit)',
  89. default: 100,
  90. showWhen: { field: 'operation', value: 'select' }
  91. },
  92. offset: {
  93. type: 'number',
  94. title: 'Offset',
  95. description: 'Number of rows to skip',
  96. default: 0,
  97. showWhen: { field: 'operation', value: 'select' }
  98. },
  99. insertData: {
  100. type: 'string',
  101. title: 'Data to Insert (JSON)',
  102. description: 'JSON object with column:value pairs. Supports {{variable}} interpolation.',
  103. showWhen: { field: 'operation', value: 'insert' }
  104. },
  105. updateData: {
  106. type: 'string',
  107. title: 'Data to Update (JSON)',
  108. description: 'JSON object with column:value pairs to set. Supports {{variable}} interpolation.',
  109. showWhen: { field: 'operation', value: 'update' }
  110. },
  111. upsertData: {
  112. type: 'string',
  113. title: 'Data to Upsert (JSON)',
  114. description: 'JSON object with column:value pairs. Supports {{variable}} interpolation.',
  115. showWhen: { field: 'operation', value: 'upsert' }
  116. },
  117. conflictKey: {
  118. type: 'array',
  119. title: 'Conflict Key Columns',
  120. description: 'Columns that form the unique/primary key for conflict detection',
  121. items: {
  122. type: 'string'
  123. },
  124. showWhen: { field: 'operation', value: 'upsert' }
  125. },
  126. updateOnConflict: {
  127. type: 'array',
  128. title: 'Update Columns on Conflict',
  129. description: 'Columns to update when a conflict is detected (empty for all non-key columns)',
  130. items: {
  131. type: 'string'
  132. },
  133. showWhen: { field: 'operation', value: 'upsert' }
  134. },
  135. bulkData: {
  136. type: 'string',
  137. title: 'Bulk Data (JSON Array)',
  138. description: 'JSON array of objects to insert. Supports {{variable}} interpolation.',
  139. showWhen: { field: 'operation', value: 'bulkInsert' }
  140. },
  141. batchSize: {
  142. type: 'number',
  143. title: 'Batch Size',
  144. description: 'Number of rows per INSERT statement (0 for all at once)',
  145. default: 100,
  146. showWhen: { field: 'operation', value: 'bulkInsert' }
  147. },
  148. customQuery: {
  149. type: 'string',
  150. title: 'Custom SQL',
  151. description: 'Custom SQL query with ? placeholders for parameters. Supports {{variable}} interpolation.',
  152. showWhen: { field: 'operation', value: 'customSql' }
  153. },
  154. parameters: {
  155. type: 'array',
  156. title: 'Query Parameters',
  157. description: 'Parameters for ? placeholders in custom SQL',
  158. items: {
  159. type: 'string'
  160. },
  161. showWhen: { field: 'operation', value: 'customSql' }
  162. }
  163. },
  164. required: ['credentialId', 'operation']
  165. };
  166. const inputSchema = {
  167. type: 'object',
  168. properties: {
  169. data: {
  170. type: 'any',
  171. description: 'Input data available for interpolation in queries'
  172. },
  173. parameters: {
  174. type: 'array',
  175. description: 'Override query parameters from input'
  176. },
  177. rows: {
  178. type: 'array',
  179. description: 'Array of objects for bulk insert (alternative to bulkData config)'
  180. }
  181. }
  182. };
  183. const outputSchema = {
  184. type: 'object',
  185. properties: {
  186. rows: { type: 'array', description: 'Query result rows as array of objects' },
  187. metadata: {
  188. type: 'object',
  189. description: 'Operation metadata',
  190. properties: {
  191. affectedRows: { type: 'number', description: 'Number of affected rows' },
  192. insertId: { type: 'number', description: 'Last auto-generated insert ID' },
  193. rowCount: { type: 'number', description: 'Number of rows returned' }
  194. }
  195. },
  196. columns: { type: 'array', description: 'Column names from the result' },
  197. query: { type: 'string', description: 'The executed SQL query (for debugging)' }
  198. }
  199. };
  200. const outputs = [
  201. { name: 'main', displayName: 'Query Result', type: 'object', color: '#06b6d4' }
  202. ];
  203. function parseJsonData(jsonString, data, fieldName) {
  204. if (!jsonString) {
  205. throw new Error(fieldName + ' is required');
  206. }
  207. let parsed = jsonString;
  208. if (typeof parsed === 'string') {
  209. parsed = smartbotic.utils.interpolate(parsed, data);
  210. try {
  211. parsed = JSON.parse(parsed);
  212. } catch (e) {
  213. throw new Error(fieldName + ' must be valid JSON: ' + e.message);
  214. }
  215. }
  216. return parsed;
  217. }
  218. function escapeIdentifier(name) {
  219. return '`' + name.replace(/`/g, '``') + '`';
  220. }
  221. function buildWhereClause(conditions, data) {
  222. if (!conditions || conditions.length === 0) {
  223. return { clause: '', params: [] };
  224. }
  225. const clauses = [];
  226. const params = [];
  227. for (const condition of conditions) {
  228. if (!condition.column) continue;
  229. const col = escapeIdentifier(condition.column);
  230. const op = condition.operator || '=';
  231. let value = condition.value;
  232. if (typeof value === 'string') {
  233. value = smartbotic.utils.interpolate(value, data);
  234. }
  235. if (op === 'IS NULL') {
  236. clauses.push(col + ' IS NULL');
  237. } else if (op === 'IS NOT NULL') {
  238. clauses.push(col + ' IS NOT NULL');
  239. } else if (op === 'IN' || op === 'NOT IN') {
  240. let values = value;
  241. if (typeof values === 'string') {
  242. try {
  243. values = JSON.parse(values);
  244. } catch (e) {
  245. values = values.split(',').map(v => v.trim());
  246. }
  247. }
  248. if (!Array.isArray(values)) {
  249. values = [values];
  250. }
  251. const placeholders = values.map(() => '?').join(', ');
  252. clauses.push(col + ' ' + op + ' (' + placeholders + ')');
  253. params.push(...values);
  254. } else {
  255. clauses.push(col + ' ' + op + ' ?');
  256. params.push(value);
  257. }
  258. }
  259. return {
  260. clause: clauses.length > 0 ? ' WHERE ' + clauses.join(' AND ') : '',
  261. params
  262. };
  263. }
  264. function buildSelectQuery(config, data) {
  265. const table = config.table;
  266. if (!table) {
  267. throw new Error('Table is required for SELECT operations');
  268. }
  269. const columns = config.columns && config.columns.length > 0
  270. ? config.columns.map(escapeIdentifier).join(', ')
  271. : '*';
  272. let query = 'SELECT ' + columns + ' FROM ' + escapeIdentifier(table);
  273. const params = [];
  274. const where = buildWhereClause(config.whereConditions, data);
  275. query += where.clause;
  276. params.push(...where.params);
  277. if (config.orderBy && config.orderBy.length > 0) {
  278. const orderClauses = config.orderBy
  279. .filter(o => o.column)
  280. .map(o => escapeIdentifier(o.column) + ' ' + (o.direction || 'ASC'));
  281. if (orderClauses.length > 0) {
  282. query += ' ORDER BY ' + orderClauses.join(', ');
  283. }
  284. }
  285. if (config.limit && config.limit > 0) {
  286. query += ' LIMIT ?';
  287. params.push(config.limit);
  288. if (config.offset && config.offset > 0) {
  289. query += ' OFFSET ?';
  290. params.push(config.offset);
  291. }
  292. }
  293. return { query, params };
  294. }
  295. function buildInsertQuery(config, data) {
  296. const table = config.table;
  297. if (!table) {
  298. throw new Error('Table is required for INSERT operations');
  299. }
  300. const insertData = parseJsonData(config.insertData, data, 'Insert data');
  301. if (!insertData || typeof insertData !== 'object' || Array.isArray(insertData)) {
  302. throw new Error('Insert data must be a JSON object');
  303. }
  304. const columns = Object.keys(insertData);
  305. if (columns.length === 0) {
  306. throw new Error('Insert data cannot be empty');
  307. }
  308. const values = Object.values(insertData);
  309. const placeholders = columns.map(() => '?').join(', ');
  310. const query = 'INSERT INTO ' + escapeIdentifier(table) +
  311. ' (' + columns.map(escapeIdentifier).join(', ') + ')' +
  312. ' VALUES (' + placeholders + ')';
  313. return { query, params: values };
  314. }
  315. function buildUpdateQuery(config, data) {
  316. const table = config.table;
  317. if (!table) {
  318. throw new Error('Table is required for UPDATE operations');
  319. }
  320. const updateData = parseJsonData(config.updateData, data, 'Update data');
  321. if (!updateData || typeof updateData !== 'object' || Array.isArray(updateData)) {
  322. throw new Error('Update data must be a JSON object');
  323. }
  324. const columns = Object.keys(updateData);
  325. if (columns.length === 0) {
  326. throw new Error('Update data cannot be empty');
  327. }
  328. const values = Object.values(updateData);
  329. const setClause = columns.map(col => escapeIdentifier(col) + ' = ?').join(', ');
  330. let query = 'UPDATE ' + escapeIdentifier(table) + ' SET ' + setClause;
  331. const params = [...values];
  332. const where = buildWhereClause(config.whereConditions, data);
  333. if (!where.clause) {
  334. throw new Error('WHERE conditions are required for UPDATE operations to prevent accidental data modification');
  335. }
  336. query += where.clause;
  337. params.push(...where.params);
  338. return { query, params };
  339. }
  340. function buildDeleteQuery(config, data) {
  341. const table = config.table;
  342. if (!table) {
  343. throw new Error('Table is required for DELETE operations');
  344. }
  345. let query = 'DELETE FROM ' + escapeIdentifier(table);
  346. const params = [];
  347. const where = buildWhereClause(config.whereConditions, data);
  348. if (!where.clause) {
  349. throw new Error('WHERE conditions are required for DELETE operations to prevent accidental data loss');
  350. }
  351. query += where.clause;
  352. params.push(...where.params);
  353. return { query, params };
  354. }
  355. function buildUpsertQuery(config, data) {
  356. const table = config.table;
  357. if (!table) {
  358. throw new Error('Table is required for UPSERT operations');
  359. }
  360. const upsertData = parseJsonData(config.upsertData, data, 'Upsert data');
  361. if (!upsertData || typeof upsertData !== 'object' || Array.isArray(upsertData)) {
  362. throw new Error('Upsert data must be a JSON object');
  363. }
  364. const columns = Object.keys(upsertData);
  365. if (columns.length === 0) {
  366. throw new Error('Upsert data cannot be empty');
  367. }
  368. const values = Object.values(upsertData);
  369. const placeholders = columns.map(() => '?').join(', ');
  370. const conflictKeys = config.conflictKey || [];
  371. let updateColumns = config.updateOnConflict || [];
  372. if (updateColumns.length === 0) {
  373. updateColumns = columns.filter(col => !conflictKeys.includes(col));
  374. }
  375. if (updateColumns.length === 0) {
  376. updateColumns = columns;
  377. }
  378. const updateClause = updateColumns
  379. .map(col => escapeIdentifier(col) + ' = VALUES(' + escapeIdentifier(col) + ')')
  380. .join(', ');
  381. const query = 'INSERT INTO ' + escapeIdentifier(table) +
  382. ' (' + columns.map(escapeIdentifier).join(', ') + ')' +
  383. ' VALUES (' + placeholders + ')' +
  384. ' ON DUPLICATE KEY UPDATE ' + updateClause;
  385. return { query, params: values };
  386. }
  387. function buildBulkInsertQueries(config, data, input) {
  388. const table = config.table;
  389. if (!table) {
  390. throw new Error('Table is required for BULK INSERT operations');
  391. }
  392. let bulkData = input.rows;
  393. if (!bulkData) {
  394. bulkData = parseJsonData(config.bulkData, data, 'Bulk data');
  395. }
  396. if (!Array.isArray(bulkData)) {
  397. throw new Error('Bulk data must be a JSON array of objects');
  398. }
  399. if (bulkData.length === 0) {
  400. throw new Error('Bulk data array cannot be empty');
  401. }
  402. const columns = Object.keys(bulkData[0]);
  403. if (columns.length === 0) {
  404. throw new Error('Bulk data objects cannot be empty');
  405. }
  406. const batchSize = config.batchSize > 0 ? config.batchSize : bulkData.length;
  407. const queries = [];
  408. for (let i = 0; i < bulkData.length; i += batchSize) {
  409. const batch = bulkData.slice(i, i + batchSize);
  410. const allValues = [];
  411. const rowPlaceholders = [];
  412. for (const row of batch) {
  413. const rowValues = columns.map(col => {
  414. const val = row[col];
  415. return val !== undefined ? val : null;
  416. });
  417. allValues.push(...rowValues);
  418. rowPlaceholders.push('(' + columns.map(() => '?').join(', ') + ')');
  419. }
  420. const query = 'INSERT INTO ' + escapeIdentifier(table) +
  421. ' (' + columns.map(escapeIdentifier).join(', ') + ')' +
  422. ' VALUES ' + rowPlaceholders.join(', ');
  423. queries.push({ query, params: allValues, rowCount: batch.length });
  424. }
  425. return queries;
  426. }
  427. function buildCustomQuery(config, data, input) {
  428. if (!config.customQuery) {
  429. throw new Error('Custom SQL query is required');
  430. }
  431. const query = smartbotic.utils.interpolate(config.customQuery, data);
  432. let params = input.parameters || config.parameters || [];
  433. if (params.length > 0) {
  434. params = params.map(p => {
  435. if (typeof p === 'string') {
  436. return smartbotic.utils.interpolate(p, data);
  437. }
  438. return p;
  439. });
  440. }
  441. return { query, params };
  442. }
  443. module.exports = {
  444. configSchema,
  445. inputSchema,
  446. outputSchema,
  447. outputs,
  448. async execute(config, input) {
  449. const credentialId = config.credentialId;
  450. const data = input.data || input;
  451. if (!credentialId) {
  452. throw new Error('MySQL credential is required');
  453. }
  454. const operation = config.operation || 'select';
  455. let totalAffectedRows = 0;
  456. let lastInsertId = null;
  457. let executedQuery = '';
  458. if (operation === 'bulkInsert') {
  459. const queries = buildBulkInsertQueries(config, data, input);
  460. smartbotic.log.info('MySQL BULK INSERT: ' + queries.length + ' batch(es)');
  461. for (const queryInfo of queries) {
  462. const options = {
  463. credentialId,
  464. database: config.database,
  465. query: queryInfo.query,
  466. params: queryInfo.params
  467. };
  468. const result = smartbotic.mysql.query(options);
  469. if (!result.success) {
  470. smartbotic.log.error('MySQL bulk insert failed: ' + result.error);
  471. throw new Error('MySQL bulk insert failed: ' + result.error);
  472. }
  473. totalAffectedRows += result.affectedRows || 0;
  474. if (result.insertId) {
  475. lastInsertId = result.insertId;
  476. }
  477. executedQuery = queryInfo.query;
  478. }
  479. smartbotic.log.info('MySQL BULK INSERT completed: ' + totalAffectedRows + ' rows inserted');
  480. return {
  481. rows: [],
  482. metadata: {
  483. affectedRows: totalAffectedRows,
  484. insertId: lastInsertId,
  485. rowCount: 0
  486. },
  487. columns: [],
  488. query: executedQuery
  489. };
  490. }
  491. let queryInfo;
  492. switch (operation) {
  493. case 'select':
  494. queryInfo = buildSelectQuery(config, data);
  495. break;
  496. case 'insert':
  497. queryInfo = buildInsertQuery(config, data);
  498. break;
  499. case 'update':
  500. queryInfo = buildUpdateQuery(config, data);
  501. break;
  502. case 'delete':
  503. queryInfo = buildDeleteQuery(config, data);
  504. break;
  505. case 'upsert':
  506. queryInfo = buildUpsertQuery(config, data);
  507. break;
  508. case 'customSql':
  509. queryInfo = buildCustomQuery(config, data, input);
  510. break;
  511. default:
  512. throw new Error('Invalid operation: ' + operation);
  513. }
  514. const { query, params } = queryInfo;
  515. smartbotic.log.info('MySQL ' + operation.toUpperCase() + ': ' + query.substring(0, 100) + (query.length > 100 ? '...' : ''));
  516. const options = {
  517. credentialId,
  518. database: config.database,
  519. query,
  520. params
  521. };
  522. const result = smartbotic.mysql.query(options);
  523. if (!result.success) {
  524. smartbotic.log.error('MySQL query failed: ' + result.error);
  525. throw new Error('MySQL query failed: ' + result.error);
  526. }
  527. smartbotic.log.info('MySQL ' + operation.toUpperCase() + ' completed: ' +
  528. (result.affectedRows || 0) + ' affected, ' +
  529. (result.rows || []).length + ' rows returned');
  530. return {
  531. rows: result.rows || [],
  532. metadata: {
  533. affectedRows: result.affectedRows || 0,
  534. insertId: result.insertId || null,
  535. rowCount: (result.rows || []).length
  536. },
  537. columns: result.columns || [],
  538. query: query
  539. };
  540. }
  541. };