mysql-query.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. /**
  2. * @node mysql-query
  3. * @name MySQL Query
  4. * @category database
  5. * @version 1.0.0
  6. * @description Execute SQL queries against a MySQL database with schema introspection support
  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. queryType: {
  32. type: 'string',
  33. title: 'Query Type',
  34. description: 'Type of query to execute',
  35. enum: ['select', 'insert', 'update', 'delete', 'custom'],
  36. enumLabels: ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'Custom SQL'],
  37. default: 'select'
  38. },
  39. columns: {
  40. type: 'array',
  41. title: 'Columns',
  42. description: 'Columns to select (empty for all). Enter column names manually.',
  43. items: {
  44. type: 'string'
  45. },
  46. showWhen: { field: 'queryType', value: 'select' }
  47. },
  48. whereClause: {
  49. type: 'string',
  50. title: 'WHERE Clause',
  51. description: 'Optional WHERE conditions (without WHERE keyword). Supports {{variable}} interpolation.',
  52. showWhen: { field: 'queryType', valueIn: ['select', 'update', 'delete'] }
  53. },
  54. orderBy: {
  55. type: 'string',
  56. title: 'ORDER BY',
  57. description: 'Order by clause (without ORDER BY keyword)',
  58. showWhen: { field: 'queryType', value: 'select' }
  59. },
  60. limit: {
  61. type: 'number',
  62. title: 'Limit',
  63. description: 'Maximum number of rows to return',
  64. default: 100,
  65. showWhen: { field: 'queryType', value: 'select' }
  66. },
  67. insertData: {
  68. type: 'string',
  69. title: 'Data to Insert (JSON)',
  70. description: 'JSON object with column:value pairs. Supports {{variable}} interpolation.',
  71. showWhen: { field: 'queryType', value: 'insert' }
  72. },
  73. updateData: {
  74. type: 'string',
  75. title: 'Data to Update (JSON)',
  76. description: 'JSON object with column:value pairs to set. Supports {{variable}} interpolation.',
  77. showWhen: { field: 'queryType', value: 'update' }
  78. },
  79. customQuery: {
  80. type: 'string',
  81. title: 'Custom SQL',
  82. description: 'Custom SQL query. Supports {{variable}} interpolation. Use ? for parameters.',
  83. showWhen: { field: 'queryType', value: 'custom' }
  84. },
  85. parameters: {
  86. type: 'array',
  87. title: 'Query Parameters',
  88. description: 'Parameters for the query (for ? placeholders)',
  89. items: {
  90. type: 'string'
  91. },
  92. showWhen: { field: 'queryType', value: 'custom' }
  93. },
  94. returnInsertId: {
  95. type: 'boolean',
  96. title: 'Return Insert ID',
  97. description: 'Return the auto-generated ID for INSERT queries',
  98. default: true,
  99. showWhen: { field: 'queryType', value: 'insert' }
  100. }
  101. },
  102. required: ['credentialId', 'queryType']
  103. };
  104. const inputSchema = {
  105. type: 'object',
  106. properties: {
  107. data: {
  108. type: 'any',
  109. description: 'Input data available for interpolation in queries'
  110. },
  111. parameters: {
  112. type: 'array',
  113. description: 'Override query parameters from input'
  114. }
  115. }
  116. };
  117. const outputSchema = {
  118. type: 'object',
  119. properties: {
  120. rows: { type: 'array', description: 'Query result rows (for SELECT)' },
  121. affectedRows: { type: 'number', description: 'Number of affected rows (for INSERT/UPDATE/DELETE)' },
  122. insertId: { type: 'number', description: 'Auto-generated insert ID (for INSERT)' },
  123. columns: { type: 'array', description: 'Column names from the result' },
  124. query: { type: 'string', description: 'The executed SQL query' },
  125. error: { type: 'string', description: 'Error message if query failed' }
  126. }
  127. };
  128. const outputs = [
  129. { name: 'main', displayName: 'Query Result', type: 'object', color: '#06b6d4' }
  130. ];
  131. function buildSelectQuery(config, data) {
  132. const table = config.table;
  133. if (!table) {
  134. throw new Error('Table is required for SELECT queries');
  135. }
  136. const columns = config.columns && config.columns.length > 0
  137. ? config.columns.join(', ')
  138. : '*';
  139. let query = `SELECT ${columns} FROM \`${table}\``;
  140. if (config.whereClause) {
  141. const where = smartbotic.utils.interpolate(config.whereClause, data);
  142. query += ` WHERE ${where}`;
  143. }
  144. if (config.orderBy) {
  145. query += ` ORDER BY ${config.orderBy}`;
  146. }
  147. if (config.limit && config.limit > 0) {
  148. query += ` LIMIT ${config.limit}`;
  149. }
  150. return { query, params: [] };
  151. }
  152. function buildInsertQuery(config, data) {
  153. const table = config.table;
  154. if (!table) {
  155. throw new Error('Table is required for INSERT queries');
  156. }
  157. let insertData = config.insertData;
  158. if (typeof insertData === 'string') {
  159. insertData = smartbotic.utils.interpolate(insertData, data);
  160. try {
  161. insertData = JSON.parse(insertData);
  162. } catch (e) {
  163. throw new Error('Insert data must be valid JSON: ' + e.message);
  164. }
  165. }
  166. if (!insertData || typeof insertData !== 'object') {
  167. throw new Error('Insert data is required');
  168. }
  169. const columns = Object.keys(insertData);
  170. const values = Object.values(insertData);
  171. const placeholders = columns.map(() => '?').join(', ');
  172. const query = `INSERT INTO \`${table}\` (\`${columns.join('`, `')}\`) VALUES (${placeholders})`;
  173. return { query, params: values };
  174. }
  175. function buildUpdateQuery(config, data) {
  176. const table = config.table;
  177. if (!table) {
  178. throw new Error('Table is required for UPDATE queries');
  179. }
  180. let updateData = config.updateData;
  181. if (typeof updateData === 'string') {
  182. updateData = smartbotic.utils.interpolate(updateData, data);
  183. try {
  184. updateData = JSON.parse(updateData);
  185. } catch (e) {
  186. throw new Error('Update data must be valid JSON: ' + e.message);
  187. }
  188. }
  189. if (!updateData || typeof updateData !== 'object') {
  190. throw new Error('Update data is required');
  191. }
  192. const columns = Object.keys(updateData);
  193. const values = Object.values(updateData);
  194. const setClause = columns.map(col => `\`${col}\` = ?`).join(', ');
  195. let query = `UPDATE \`${table}\` SET ${setClause}`;
  196. if (config.whereClause) {
  197. const where = smartbotic.utils.interpolate(config.whereClause, data);
  198. query += ` WHERE ${where}`;
  199. }
  200. return { query, params: values };
  201. }
  202. function buildDeleteQuery(config, data) {
  203. const table = config.table;
  204. if (!table) {
  205. throw new Error('Table is required for DELETE queries');
  206. }
  207. let query = `DELETE FROM \`${table}\``;
  208. if (config.whereClause) {
  209. const where = smartbotic.utils.interpolate(config.whereClause, data);
  210. query += ` WHERE ${where}`;
  211. } else {
  212. throw new Error('WHERE clause is required for DELETE queries to prevent accidental data loss');
  213. }
  214. return { query, params: [] };
  215. }
  216. function buildCustomQuery(config, data, input) {
  217. if (!config.customQuery) {
  218. throw new Error('Custom query is required');
  219. }
  220. const query = smartbotic.utils.interpolate(config.customQuery, data);
  221. let params = input.parameters || config.parameters || [];
  222. if (params.length > 0) {
  223. params = params.map(p => {
  224. if (typeof p === 'string') {
  225. return smartbotic.utils.interpolate(p, data);
  226. }
  227. return p;
  228. });
  229. }
  230. return { query, params };
  231. }
  232. module.exports = {
  233. configSchema,
  234. inputSchema,
  235. outputSchema,
  236. outputs,
  237. async execute(config, input, context) {
  238. const credentialId = config.credentialId;
  239. const data = input.data || input;
  240. if (!credentialId) {
  241. throw new Error('MySQL credential is required');
  242. }
  243. let queryInfo;
  244. const queryType = config.queryType || 'select';
  245. switch (queryType) {
  246. case 'select':
  247. queryInfo = buildSelectQuery(config, data);
  248. break;
  249. case 'insert':
  250. queryInfo = buildInsertQuery(config, data);
  251. break;
  252. case 'update':
  253. queryInfo = buildUpdateQuery(config, data);
  254. break;
  255. case 'delete':
  256. queryInfo = buildDeleteQuery(config, data);
  257. break;
  258. case 'custom':
  259. queryInfo = buildCustomQuery(config, data, input);
  260. break;
  261. default:
  262. throw new Error('Invalid query type: ' + queryType);
  263. }
  264. const { query, params } = queryInfo;
  265. smartbotic.log.info(`MySQL ${queryType.toUpperCase()}: ${query.substring(0, 100)}...`);
  266. const options = {
  267. credentialId,
  268. database: config.database,
  269. query,
  270. params
  271. };
  272. const result = smartbotic.mysql.query(options);
  273. if (!result.success) {
  274. smartbotic.log.error(`MySQL query failed: ${result.error}`);
  275. throw new Error(`MySQL query failed: ${result.error}`);
  276. }
  277. smartbotic.log.info(`MySQL query completed: ${result.affectedRows || 0} affected, ${(result.rows || []).length} rows returned`);
  278. return {
  279. rows: result.rows || [],
  280. affectedRows: result.affectedRows || 0,
  281. insertId: result.insertId || null,
  282. columns: result.columns || [],
  283. query: query
  284. };
  285. }
  286. };