Преглед на файлове

feat: US-002 - MySQL Node - CRUD Operations

- Add operation dropdown with Select, Insert, Update, Delete, Upsert,
  Bulk Insert, and Custom SQL options
- Implement structured WHERE conditions with column/operator/value UI
- Add Upsert operation using INSERT ON DUPLICATE KEY UPDATE
- Add Bulk Insert operation with configurable batch size
- Implement parameterized queries for all operations to prevent SQL injection
- Add output metadata with affectedRows, insertId, and rowCount
- Add ORDER BY array with column/direction configuration
- Add LIMIT and OFFSET support for pagination
- Update version to 2.0.0
fszontagh преди 6 месеца
родител
ревизия
299655ca56
променени са 9 файла, в които са добавени 1136 реда и са изтрити 100 реда
  1. 12 0
      .ralph-tui/config.toml
  2. 11 0
      .ralph-tui/progress.md
  3. 7 0
      .ralph-tui/ralph.lock
  4. 14 0
      .ralph-tui/session-meta.json
  5. 108 0
      .ralph-tui/session.json
  6. 400 100
      nodes/mysql/mysql-query.js
  7. 6 0
      package-lock.json
  8. 237 0
      tasks/prd-new-workflow-automation-nodes.md
  9. 341 0
      tasks/prd.json

+ 12 - 0
.ralph-tui/config.toml

@@ -0,0 +1,12 @@
+# Ralph TUI Configuration
+# Generated by setup wizard
+# See: ralph-tui config help
+
+configVersion = "2.1"
+tracker = "json"
+agent = "claude"
+maxIterations = 10
+autoCommit = true
+
+[trackerOptions]
+[agentOptions]

+ 11 - 0
.ralph-tui/progress.md

@@ -0,0 +1,11 @@
+# Ralph Progress Log
+
+This file tracks progress across iterations. Agents update this file
+after each iteration and it's included in prompts for context.
+
+## Codebase Patterns (Study These First)
+
+*Add reusable patterns discovered during development here.*
+
+---
+

+ 7 - 0
.ralph-tui/ralph.lock

@@ -0,0 +1,7 @@
+{
+  "pid": 3460651,
+  "sessionId": "072a1531-57de-448f-8478-68491ab3b8af",
+  "acquiredAt": "2026-01-28T14:25:22.715Z",
+  "cwd": "/data/smartbotic",
+  "hostname": "mulan"
+}

+ 14 - 0
.ralph-tui/session-meta.json

@@ -0,0 +1,14 @@
+{
+  "id": "ecd9252a-67c5-45c7-b431-6ef24fdff189",
+  "status": "running",
+  "startedAt": "2026-01-28T14:25:22.717Z",
+  "updatedAt": "2026-01-28T14:25:22.717Z",
+  "agentPlugin": "claude",
+  "trackerPlugin": "json",
+  "prdPath": "./tasks/prd.json",
+  "currentIteration": 0,
+  "maxIterations": 10,
+  "totalTasks": 0,
+  "tasksCompleted": 0,
+  "cwd": "/data/smartbotic"
+}

+ 108 - 0
.ralph-tui/session.json

@@ -0,0 +1,108 @@
+{
+  "version": 1,
+  "sessionId": "ecd9252a-67c5-45c7-b431-6ef24fdff189",
+  "status": "running",
+  "startedAt": "2026-01-28T14:25:25.505Z",
+  "updatedAt": "2026-01-28T14:56:29.300Z",
+  "currentIteration": 0,
+  "maxIterations": 10,
+  "tasksCompleted": 0,
+  "isPaused": false,
+  "agentPlugin": "claude",
+  "trackerState": {
+    "plugin": "json",
+    "prdPath": "./tasks/prd.json",
+    "totalTasks": 14,
+    "tasks": [
+      {
+        "id": "US-001",
+        "title": "MySQL Node - Connection & Schema Introspection",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-002",
+        "title": "MySQL Node - CRUD Operations",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-003",
+        "title": "PostgreSQL Node - Connection & Schema Introspection",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-004",
+        "title": "PostgreSQL Node - CRUD Operations",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-005",
+        "title": "RSS Reader Node - Feed Parsing & Filtering",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-006",
+        "title": "RSS Reader Node - Stateful New Item Detection",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-007",
+        "title": "Crypto Node - Hashing & HMAC",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-008",
+        "title": "Crypto Node - Random Generation",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-009",
+        "title": "Crypto Node - Encrypt & Decrypt",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-010",
+        "title": "Image Node - Information & Metadata",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-011",
+        "title": "Image Node - Manipulation (Resize, Crop, Rotate)",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-012",
+        "title": "Image Node - Filters & Watermark",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-013",
+        "title": "Code Node - JavaScript Execution",
+        "status": "open",
+        "completedInSession": false
+      },
+      {
+        "id": "US-014",
+        "title": "Node Registration & Editor Integration",
+        "status": "open",
+        "completedInSession": false
+      }
+    ]
+  },
+  "iterations": [],
+  "skippedTaskIds": [],
+  "cwd": "/data/smartbotic",
+  "activeTaskIds": [],
+  "subagentPanelVisible": true
+}

+ 400 - 100
nodes/mysql/mysql-query.js

@@ -2,8 +2,8 @@
  * @node mysql-query
  * @name MySQL Query
  * @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
  */
 
@@ -29,78 +29,140 @@ const configSchema = {
             title: 'Table',
             description: 'Table name for query operations'
         },
-        queryType: {
+        operation: {
             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'
         },
         columns: {
             type: 'array',
             title: 'Columns',
-            description: 'Columns to select (empty for all). Enter column names manually.',
+            description: 'Columns to select (empty for all)',
             items: {
                 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: {
-            type: 'string',
+            type: 'array',
             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: {
             type: 'number',
             title: 'Limit',
-            description: 'Maximum number of rows to return',
+            description: 'Maximum number of rows to return (0 for no limit)',
             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: {
             type: 'string',
             title: 'Data to Insert (JSON)',
             description: 'JSON object with column:value pairs. Supports {{variable}} interpolation.',
-            showWhen: { field: 'queryType', value: 'insert' }
+            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: '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: {
             type: 'string',
             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: {
             type: 'array',
             title: 'Query Parameters',
-            description: 'Parameters for the query (for ? placeholders)',
+            description: 'Parameters for ? placeholders in custom SQL',
             items: {
                 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 = {
@@ -113,6 +175,10 @@ const inputSchema = {
         parameters: {
             type: 'array',
             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 = {
     type: 'object',
     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' },
-        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' }
 ];
 
+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 queries');
+        throw new Error('Table is required for SELECT operations');
     }
 
     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) {
-        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) {
     const table = config.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);
+    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 \`${table}\` (\`${columns.join('`, `')}\`) VALUES (${placeholders})`;
+    const query = 'INSERT INTO ' + escapeIdentifier(table) +
+        ' (' + columns.map(escapeIdentifier).join(', ') + ')' +
+        ' VALUES (' + placeholders + ')';
 
     return { query, params: values };
 }
@@ -193,58 +346,152 @@ function buildInsertQuery(config, data) {
 function buildUpdateQuery(config, data) {
     const table = config.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);
+    if (columns.length === 0) {
+        throw new Error('Update data cannot be empty');
+    }
+
     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) {
     const table = config.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) {
     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);
@@ -268,7 +515,7 @@ module.exports = {
     outputSchema,
     outputs,
 
-    async execute(config, input, context) {
+    async execute(config, input) {
         const credentialId = config.credentialId;
         const data = input.data || input;
 
@@ -276,10 +523,55 @@ module.exports = {
             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;
-        const queryType = config.queryType || 'select';
 
-        switch (queryType) {
+        switch (operation) {
             case 'select':
                 queryInfo = buildSelectQuery(config, data);
                 break;
@@ -292,16 +584,19 @@ module.exports = {
             case 'delete':
                 queryInfo = buildDeleteQuery(config, data);
                 break;
-            case 'custom':
+            case 'upsert':
+                queryInfo = buildUpsertQuery(config, data);
+                break;
+            case 'customSql':
                 queryInfo = buildCustomQuery(config, data, input);
                 break;
             default:
-                throw new Error('Invalid query type: ' + queryType);
+                throw new Error('Invalid operation: ' + operation);
         }
 
         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 = {
             credentialId,
@@ -313,16 +608,21 @@ module.exports = {
         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.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 {
             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 || [],
             query: query
         };

+ 6 - 0
package-lock.json

@@ -0,0 +1,6 @@
+{
+  "name": "smartbotic",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {}
+}

+ 237 - 0
tasks/prd-new-workflow-automation-nodes.md

@@ -0,0 +1,237 @@
+# PRD: New Workflow Automation Nodes
+
+## Overview
+Add six new automation nodes to the SmartBotic workflow engine: MySQL, PostgreSQL, RSS Reader, Crypto, Image Manipulation, and Code (JavaScript). These nodes expand the platform's integration and data processing capabilities, following the existing node architecture (JavaScript modules executed in QuickJS on the Runner service). Database nodes leverage the existing credential management system and provide live schema introspection. The Image node uses the existing binary storage logic for passing image data between nodes.
+
+## Goals
+- Provide MySQL and PostgreSQL connectivity with full CRUD, bulk insert, upsert, and custom SQL support
+- Enable RSS feed reading with filtering and optional stateful new-item detection using database-backed collections
+- Offer cryptographic operations including hashing, HMAC, random generation, and AES/RSA encrypt/decrypt
+- Support image manipulation (resize, crop, rotate, filters, format conversion) and metadata extraction using ImageMagick
+- Allow users to write and execute custom JavaScript in workflows via a dedicated Code node with built-in helper utilities
+- Integrate all nodes with the existing credential management, binary storage, and node configuration systems
+
+## Quality Gates
+
+These commands must pass for every user story:
+- `cd webui && npm run build` - Frontend build
+- `cd build && ninja -j4` - Backend build
+
+## User Stories
+
+### US-001: MySQL Node - Connection & Schema Introspection
+As a workflow designer, I want to configure a MySQL node that connects using stored credentials and auto-detects tables and columns so that I can quickly build database queries without memorizing schema details.
+
+**Acceptance Criteria:**
+- [ ] MySQL node definition created in `nodes/` following existing node module interface
+- [ ] Node config UI allows selecting credentials from the existing credential management system
+- [ ] Connection fields: host, port, username, password, database (stored via credential system)
+- [ ] Live schema introspection fetches available tables when the node is configured in the editor
+- [ ] After selecting a table, columns are fetched and displayed for selection
+- [ ] Introspection works in real-time (requires active DB connection at design time)
+- [ ] Node appears in the workflow editor node palette under a "Database" category
+
+### US-002: MySQL Node - CRUD Operations
+As a workflow designer, I want to perform Select, Insert, Update, Delete, Upsert, and Bulk Insert operations on MySQL tables using a dropdown selector so that I can manipulate data without writing SQL.
+
+**Acceptance Criteria:**
+- [ ] Operation selector dropdown with options: Select, Insert, Update, Delete, Upsert, Bulk Insert, Custom SQL
+- [ ] **Select:** allows choosing columns, adding WHERE conditions, LIMIT, ORDER BY
+- [ ] **Insert:** allows mapping input fields to table columns
+- [ ] **Update:** allows mapping fields to columns with WHERE conditions
+- [ ] **Delete:** allows specifying WHERE conditions
+- [ ] **Upsert:** allows specifying the conflict key and update fields
+- [ ] **Bulk Insert:** accepts an array of objects and inserts multiple rows
+- [ ] **Custom SQL:** free-text SQL input with parameterized query support
+- [ ] Output format: array of objects (each row as JSON with column-name keys) plus metadata (affected rows count, last insert ID for write operations)
+- [ ] Parameterized queries used for all operations to prevent SQL injection
+
+### US-003: PostgreSQL Node - Connection & Schema Introspection
+As a workflow designer, I want to configure a PostgreSQL node with the same UX as the MySQL node so that I have a consistent experience across database types.
+
+**Acceptance Criteria:**
+- [ ] PostgreSQL node definition created in `nodes/` following existing node module interface
+- [ ] Node config UI allows selecting credentials from the existing credential management system
+- [ ] Connection fields: host, port, username, password, database (stored via credential system)
+- [ ] Live schema introspection fetches available tables when configured in the editor
+- [ ] After selecting a table, columns are fetched and displayed for selection
+- [ ] Node appears in the workflow editor node palette under a "Database" category
+
+### US-004: PostgreSQL Node - CRUD Operations
+As a workflow designer, I want the same CRUD, Upsert, Bulk Insert, and Custom SQL operations on PostgreSQL as on MySQL so that I can work with PostgreSQL data the same way.
+
+**Acceptance Criteria:**
+- [ ] Operation selector dropdown with options: Select, Insert, Update, Delete, Upsert, Bulk Insert, Custom SQL
+- [ ] **Select:** column selection, WHERE conditions, LIMIT, ORDER BY
+- [ ] **Insert:** map input fields to table columns
+- [ ] **Update:** map fields to columns with WHERE conditions
+- [ ] **Delete:** specify WHERE conditions
+- [ ] **Upsert:** uses PostgreSQL `ON CONFLICT` syntax, allows specifying conflict key and update fields
+- [ ] **Bulk Insert:** accepts array of objects, inserts multiple rows
+- [ ] **Custom SQL:** free-text SQL with parameterized query support
+- [ ] Output format: array of objects plus metadata (affected rows count, last insert ID for write operations)
+- [ ] Parameterized queries used for all operations to prevent SQL injection
+
+### US-005: RSS Reader Node - Feed Parsing & Filtering
+As a workflow designer, I want to read and parse RSS/Atom feeds with optional filtering so that I can integrate external content sources into workflows.
+
+**Acceptance Criteria:**
+- [ ] RSS Reader node definition created in `nodes/` following existing node module interface
+- [ ] Config accepts a feed URL as input
+- [ ] Parses both RSS 2.0 and Atom feed formats
+- [ ] Outputs an array of feed items with fields: title, link, description, pubDate, author, categories, guid
+- [ ] Supports filtering by keyword (matches against title and description)
+- [ ] Supports filtering by date range (items newer than a specified date)
+- [ ] Supports limiting the number of returned items (count limit)
+- [ ] Node appears in the workflow editor node palette
+
+### US-006: RSS Reader Node - Stateful New Item Detection
+As a workflow designer, I want to optionally track which RSS items have already been seen so that my workflow only processes new items on each execution.
+
+**Acceptance Criteria:**
+- [ ] Optional "Detect new items" toggle in node configuration
+- [ ] When enabled, user must select a collection from the database service for state storage
+- [ ] The selected collection must be configured as a read-write (RW) collection on the workflow before it can be selected in the node
+- [ ] On each execution, the node compares feed items against stored item GUIDs/links
+- [ ] Only new (unseen) items are output
+- [ ] Seen item identifiers are persisted to the selected collection after successful execution
+- [ ] When disabled, the node outputs all items matching filter criteria (stateless mode)
+
+### US-007: Crypto Node - Hashing & HMAC
+As a workflow designer, I want to hash data and generate HMACs so that I can verify data integrity and authenticate messages in workflows.
+
+**Acceptance Criteria:**
+- [ ] Crypto node definition created in `nodes/` following existing node module interface
+- [ ] Operation selector with categories: Hash, HMAC, Random Generation, Encrypt, Decrypt
+- [ ] **Hash** operation supports: MD5, SHA-1, SHA-256, SHA-384, SHA-512
+- [ ] Hash input accepts string or binary data
+- [ ] Hash output format configurable: hex, base64
+- [ ] **HMAC** operation supports the same algorithms with a secret key input
+- [ ] Node appears in the workflow editor node palette
+
+### US-008: Crypto Node - Random Generation
+As a workflow designer, I want to generate random values (UUIDs, tokens, passwords) so that I can create unique identifiers and secure credentials in workflows.
+
+**Acceptance Criteria:**
+- [ ] **Random Generation** sub-operation in the Crypto node
+- [ ] Supports generating: UUID v4, random hex token (configurable length), random alphanumeric string, secure password (configurable length, character sets)
+- [ ] Output is a string value passed to subsequent nodes
+
+### US-009: Crypto Node - Encrypt & Decrypt
+As a workflow designer, I want to encrypt and decrypt data using AES and RSA so that I can protect sensitive information in workflows.
+
+**Acceptance Criteria:**
+- [ ] **Encrypt** operation supports AES-256-CBC, AES-256-GCM, and RSA
+- [ ] AES requires: key (or passphrase-derived key), IV (auto-generated if not provided)
+- [ ] RSA requires: public key (PEM format) for encryption
+- [ ] **Decrypt** operation supports the same algorithms
+- [ ] RSA decrypt requires: private key (PEM format)
+- [ ] Input/output supports both string and base64-encoded binary data
+- [ ] Error output provides clear messages for invalid keys or corrupted data
+
+### US-010: Image Node - Information & Metadata
+As a workflow designer, I want to extract image information and metadata so that I can make workflow decisions based on image properties.
+
+**Acceptance Criteria:**
+- [ ] Image node definition created in `nodes/` following existing node module interface
+- [ ] Operation selector with categories: Info, Resize, Crop, Rotate, Filter, Convert Format
+- [ ] **Info** operation outputs: width, height, format, color space, file size, DPI
+- [ ] EXIF metadata extraction (camera, date taken, GPS coordinates, orientation, etc.)
+- [ ] Input accepts base64-encoded images, file paths on the runner filesystem, and URLs (auto-downloaded)
+- [ ] Uses the existing binary storage logic for passing image data between nodes
+- [ ] Uses ImageMagick for image processing
+- [ ] Node appears in the workflow editor node palette
+
+### US-011: Image Node - Manipulation (Resize, Crop, Rotate)
+As a workflow designer, I want to resize, crop, and rotate images so that I can transform images as part of automated workflows.
+
+**Acceptance Criteria:**
+- [ ] **Resize** operation: accepts target width and/or height, maintains aspect ratio option, supports percentage-based scaling
+- [ ] **Crop** operation: accepts x, y, width, height parameters for the crop region
+- [ ] **Rotate** operation: accepts angle in degrees (90, 180, 270, or arbitrary), optional background fill color
+- [ ] All operations output the result using the existing binary storage mechanism
+- [ ] Output format configurable (JPEG, PNG, WebP, GIF)
+- [ ] Quality setting available for lossy formats
+
+### US-012: Image Node - Filters & Watermark
+As a workflow designer, I want to apply filters and watermarks to images so that I can process images for various output requirements.
+
+**Acceptance Criteria:**
+- [ ] **Filter** operations: blur (configurable radius), sharpen, grayscale, sepia, brightness adjustment, contrast adjustment
+- [ ] **Watermark** operation: overlay text or image watermark, configurable position (center, corners), configurable opacity
+- [ ] Filters can be chained (apply multiple in sequence)
+- [ ] All operations use ImageMagick and output via existing binary storage
+
+### US-013: Code Node - JavaScript Execution
+As a workflow designer, I want to write custom JavaScript code in a node so that I can implement custom logic that isn't covered by built-in nodes.
+
+**Acceptance Criteria:**
+- [ ] Code node definition created in `nodes/` following existing node module interface
+- [ ] Node config provides a code editor textarea in the workflow editor for writing JavaScript
+- [ ] User's code has access to `input` (the node's input data) and `context` (workflow execution context)
+- [ ] User's code returns a value that becomes the node's output
+- [ ] Built-in helper utilities available: lodash-like collection/string/object helpers, date formatting utilities
+- [ ] Code executes in the existing QuickJS sandbox on the Runner service
+- [ ] Execution timeout configurable (default: 30 seconds)
+- [ ] Runtime errors are caught and reported as node execution failures with the error message and line number
+- [ ] Node appears in the workflow editor node palette
+
+### US-014: Node Registration & Editor Integration
+As a workflow designer, I want all new nodes to appear in the workflow editor with proper icons, categories, and configuration panels so that I can easily discover and configure them.
+
+**Acceptance Criteria:**
+- [ ] All six nodes (MySQL, PostgreSQL, RSS Reader, Crypto, Image, Code) are registered in the node system
+- [ ] Each node has a distinct icon in the editor palette
+- [ ] Nodes are categorized: Database (MySQL, PostgreSQL), Data (RSS Reader), Security (Crypto), Media (Image), Developer (Code)
+- [ ] Each node's configuration panel renders all relevant fields based on the selected operation
+- [ ] Dynamic form updates: selecting an operation shows/hides relevant fields
+- [ ] All nodes can be migrated to the database using the existing `POST /api/v1/nodes/migrate` endpoint
+
+## Functional Requirements
+- FR-1: Database nodes must use parameterized queries for all operations to prevent SQL injection
+- FR-2: Database schema introspection must query the live database when configuring nodes in the editor
+- FR-3: Database operation results must include an array of row objects and metadata (affected rows, last insert ID)
+- FR-4: RSS Reader must support both RSS 2.0 and Atom feed formats
+- FR-5: RSS stateful detection requires a pre-configured RW collection on the workflow before it can be used
+- FR-6: Crypto node must use established cryptographic libraries (not custom implementations)
+- FR-7: Image node must use ImageMagick via the Runner's C++ integration or QuickJS bindings
+- FR-8: Image node must use the existing binary storage mechanism for input and output
+- FR-9: Code node must execute in the QuickJS sandbox with a configurable timeout
+- FR-10: Code node must provide meaningful error messages including line numbers on failure
+- FR-11: All nodes must follow the existing node module interface (configSchema, inputSchema, outputSchema, execute)
+- FR-12: All nodes must be hot-reloadable by the Runner service
+
+## Non-Goals (Out of Scope)
+- MongoDB or other NoSQL database nodes (future enhancement)
+- Stored procedure execution for database nodes
+- RSS feed publishing/writing
+- Asymmetric key generation in the Crypto node (users provide their own keys)
+- Video processing or manipulation in the Image node
+- Multi-language support in the Code node (JavaScript only)
+- Visual code editor with syntax highlighting (plain textarea for now; can be enhanced later)
+- Database connection pooling (single connection per execution for now)
+
+## Technical Considerations
+- **Database connectivity in QuickJS:** MySQL and PostgreSQL client libraries are C/C++ native. These will need C++ bindings exposed to the QuickJS runtime on the Runner, similar to how other native features are exposed.
+- **ImageMagick integration:** ImageMagick's MagickWand C API can be wrapped and exposed to QuickJS as native functions on the Runner.
+- **RSS parsing:** A lightweight XML parser (e.g., pugixml or RapidXML, already available in C++) can be used for feed parsing, exposed to QuickJS.
+- **Crypto:** OpenSSL (likely already a dependency via gRPC) provides all needed cryptographic primitives. Wrap and expose to QuickJS.
+- **Binary storage:** The existing binary storage mechanism must be used for Image node input/output. Consult existing node implementations that handle binary data for the pattern.
+- **Credential system:** Database nodes must integrate with the existing credential management. Consult the current credential storage/retrieval API.
+- **Schema introspection API:** A new endpoint or WebSocket message may be needed for the editor to trigger live schema introspection through the WebServer to the Runner (which holds the DB connection logic).
+
+## Success Metrics
+- All six nodes pass the build quality gates (frontend build + backend ninja build)
+- All nodes appear in the workflow editor with proper configuration UIs
+- Database nodes can connect, introspect schema, and execute all supported operations
+- RSS Reader correctly parses sample RSS 2.0 and Atom feeds
+- Crypto node produces correct outputs for known test vectors (e.g., SHA-256 of known input)
+- Image node can read, manipulate, and output images via the binary storage system
+- Code node executes user JavaScript and returns results correctly
+- All nodes can be migrated to the database via the existing migration endpoint
+
+## Open Questions
+- What specific lodash-like helpers should be bundled with the Code node? (e.g., `_.map`, `_.filter`, `_.groupBy`, date-fns subset)
+- Should database schema introspection go through a new REST endpoint on the WebServer, or use WebSocket for real-time feedback?
+- Are there existing QuickJS native bindings in the Runner that can serve as a template for the new ImageMagick/MySQL/PostgreSQL bindings?
+- Should the RSS Reader support authenticated feeds (HTTP Basic Auth, API keys)?

+ 341 - 0
tasks/prd.json

@@ -0,0 +1,341 @@
+{
+  "name": "New Workflow Automation Nodes",
+  "branchName": "feature/new-workflow-automation-nodes",
+  "userStories": [
+    {
+      "id": "US-001",
+      "title": "MySQL Node - Connection & Schema Introspection",
+      "description": "As a workflow designer, I want to configure a MySQL node that connects using stored credentials and auto-detects tables and columns so that I can quickly build database queries without memorizing schema details.",
+      "acceptanceCriteria": [
+        "MySQL node definition created in nodes/ following existing node module interface",
+        "Node config UI allows selecting credentials from the existing credential management system",
+        "Connection fields: host, port, username, password, database (stored via credential system)",
+        "Live schema introspection fetches available tables when the node is configured in the editor",
+        "After selecting a table, columns are fetched and displayed for selection",
+        "Introspection works in real-time (requires active DB connection at design time)",
+        "Node appears in the workflow editor node palette under a Database category"
+      ],
+      "priority": 1,
+      "passes": true,
+      "dependsOn": [],
+      "labels": [
+        "database",
+        "mysql",
+        "backend",
+        "frontend"
+      ],
+      "completionNotes": "Completed by agent"
+    },
+    {
+      "id": "US-002",
+      "title": "MySQL Node - CRUD Operations",
+      "description": "As a workflow designer, I want to perform Select, Insert, Update, Delete, Upsert, and Bulk Insert operations on MySQL tables using a dropdown selector so that I can manipulate data without writing SQL.",
+      "acceptanceCriteria": [
+        "Operation selector dropdown with options: Select, Insert, Update, Delete, Upsert, Bulk Insert, Custom SQL",
+        "Select: allows choosing columns, adding WHERE conditions, LIMIT, ORDER BY",
+        "Insert: allows mapping input fields to table columns",
+        "Update: allows mapping fields to columns with WHERE conditions",
+        "Delete: allows specifying WHERE conditions",
+        "Upsert: allows specifying the conflict key and update fields",
+        "Bulk Insert: accepts an array of objects and inserts multiple rows",
+        "Custom SQL: free-text SQL input with parameterized query support",
+        "Output format: array of objects (each row as JSON with column-name keys) plus metadata (affected rows count, last insert ID for write operations)",
+        "Parameterized queries used for all operations to prevent SQL injection"
+      ],
+      "priority": 1,
+      "passes": false,
+      "dependsOn": [
+        "US-001"
+      ],
+      "labels": [
+        "database",
+        "mysql",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-003",
+      "title": "PostgreSQL Node - Connection & Schema Introspection",
+      "description": "As a workflow designer, I want to configure a PostgreSQL node with the same UX as the MySQL node so that I have a consistent experience across database types.",
+      "acceptanceCriteria": [
+        "PostgreSQL node definition created in nodes/ following existing node module interface",
+        "Node config UI allows selecting credentials from the existing credential management system",
+        "Connection fields: host, port, username, password, database (stored via credential system)",
+        "Live schema introspection fetches available tables when configured in the editor",
+        "After selecting a table, columns are fetched and displayed for selection",
+        "Node appears in the workflow editor node palette under a Database category"
+      ],
+      "priority": 1,
+      "passes": false,
+      "dependsOn": [],
+      "labels": [
+        "database",
+        "postgresql",
+        "backend",
+        "frontend"
+      ]
+    },
+    {
+      "id": "US-004",
+      "title": "PostgreSQL Node - CRUD Operations",
+      "description": "As a workflow designer, I want the same CRUD, Upsert, Bulk Insert, and Custom SQL operations on PostgreSQL as on MySQL so that I can work with PostgreSQL data the same way.",
+      "acceptanceCriteria": [
+        "Operation selector dropdown with options: Select, Insert, Update, Delete, Upsert, Bulk Insert, Custom SQL",
+        "Select: column selection, WHERE conditions, LIMIT, ORDER BY",
+        "Insert: map input fields to table columns",
+        "Update: map fields to columns with WHERE conditions",
+        "Delete: specify WHERE conditions",
+        "Upsert: uses PostgreSQL ON CONFLICT syntax, allows specifying conflict key and update fields",
+        "Bulk Insert: accepts array of objects, inserts multiple rows",
+        "Custom SQL: free-text SQL with parameterized query support",
+        "Output format: array of objects plus metadata (affected rows count, last insert ID for write operations)",
+        "Parameterized queries used for all operations to prevent SQL injection"
+      ],
+      "priority": 1,
+      "passes": false,
+      "dependsOn": [
+        "US-003"
+      ],
+      "labels": [
+        "database",
+        "postgresql",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-005",
+      "title": "RSS Reader Node - Feed Parsing & Filtering",
+      "description": "As a workflow designer, I want to read and parse RSS/Atom feeds with optional filtering so that I can integrate external content sources into workflows.",
+      "acceptanceCriteria": [
+        "RSS Reader node definition created in nodes/ following existing node module interface",
+        "Config accepts a feed URL as input",
+        "Parses both RSS 2.0 and Atom feed formats",
+        "Outputs an array of feed items with fields: title, link, description, pubDate, author, categories, guid",
+        "Supports filtering by keyword (matches against title and description)",
+        "Supports filtering by date range (items newer than a specified date)",
+        "Supports limiting the number of returned items (count limit)",
+        "Node appears in the workflow editor node palette"
+      ],
+      "priority": 2,
+      "passes": false,
+      "dependsOn": [],
+      "labels": [
+        "data",
+        "rss",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-006",
+      "title": "RSS Reader Node - Stateful New Item Detection",
+      "description": "As a workflow designer, I want to optionally track which RSS items have already been seen so that my workflow only processes new items on each execution.",
+      "acceptanceCriteria": [
+        "Optional 'Detect new items' toggle in node configuration",
+        "When enabled, user must select a collection from the database service for state storage",
+        "The selected collection must be configured as a read-write (RW) collection on the workflow before it can be selected in the node",
+        "On each execution, the node compares feed items against stored item GUIDs/links",
+        "Only new (unseen) items are output",
+        "Seen item identifiers are persisted to the selected collection after successful execution",
+        "When disabled, the node outputs all items matching filter criteria (stateless mode)"
+      ],
+      "priority": 2,
+      "passes": false,
+      "dependsOn": [
+        "US-005"
+      ],
+      "labels": [
+        "data",
+        "rss",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-007",
+      "title": "Crypto Node - Hashing & HMAC",
+      "description": "As a workflow designer, I want to hash data and generate HMACs so that I can verify data integrity and authenticate messages in workflows.",
+      "acceptanceCriteria": [
+        "Crypto node definition created in nodes/ following existing node module interface",
+        "Operation selector with categories: Hash, HMAC, Random Generation, Encrypt, Decrypt",
+        "Hash operation supports: MD5, SHA-1, SHA-256, SHA-384, SHA-512",
+        "Hash input accepts string or binary data",
+        "Hash output format configurable: hex, base64",
+        "HMAC operation supports the same algorithms with a secret key input",
+        "Node appears in the workflow editor node palette"
+      ],
+      "priority": 2,
+      "passes": false,
+      "dependsOn": [],
+      "labels": [
+        "security",
+        "crypto",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-008",
+      "title": "Crypto Node - Random Generation",
+      "description": "As a workflow designer, I want to generate random values (UUIDs, tokens, passwords) so that I can create unique identifiers and secure credentials in workflows.",
+      "acceptanceCriteria": [
+        "Random Generation sub-operation in the Crypto node",
+        "Supports generating: UUID v4, random hex token (configurable length), random alphanumeric string, secure password (configurable length, character sets)",
+        "Output is a string value passed to subsequent nodes"
+      ],
+      "priority": 2,
+      "passes": false,
+      "dependsOn": [
+        "US-007"
+      ],
+      "labels": [
+        "security",
+        "crypto",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-009",
+      "title": "Crypto Node - Encrypt & Decrypt",
+      "description": "As a workflow designer, I want to encrypt and decrypt data using AES and RSA so that I can protect sensitive information in workflows.",
+      "acceptanceCriteria": [
+        "Encrypt operation supports AES-256-CBC, AES-256-GCM, and RSA",
+        "AES requires: key (or passphrase-derived key), IV (auto-generated if not provided)",
+        "RSA requires: public key (PEM format) for encryption",
+        "Decrypt operation supports the same algorithms",
+        "RSA decrypt requires: private key (PEM format)",
+        "Input/output supports both string and base64-encoded binary data",
+        "Error output provides clear messages for invalid keys or corrupted data"
+      ],
+      "priority": 2,
+      "passes": false,
+      "dependsOn": [
+        "US-007"
+      ],
+      "labels": [
+        "security",
+        "crypto",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-010",
+      "title": "Image Node - Information & Metadata",
+      "description": "As a workflow designer, I want to extract image information and metadata so that I can make workflow decisions based on image properties.",
+      "acceptanceCriteria": [
+        "Image node definition created in nodes/ following existing node module interface",
+        "Operation selector with categories: Info, Resize, Crop, Rotate, Filter, Convert Format",
+        "Info operation outputs: width, height, format, color space, file size, DPI",
+        "EXIF metadata extraction (camera, date taken, GPS coordinates, orientation, etc.)",
+        "Input accepts base64-encoded images, file paths on the runner filesystem, and URLs (auto-downloaded)",
+        "Uses the existing binary storage logic for passing image data between nodes",
+        "Uses ImageMagick for image processing",
+        "Node appears in the workflow editor node palette"
+      ],
+      "priority": 3,
+      "passes": false,
+      "dependsOn": [],
+      "labels": [
+        "media",
+        "image",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-011",
+      "title": "Image Node - Manipulation (Resize, Crop, Rotate)",
+      "description": "As a workflow designer, I want to resize, crop, and rotate images so that I can transform images as part of automated workflows.",
+      "acceptanceCriteria": [
+        "Resize operation: accepts target width and/or height, maintains aspect ratio option, supports percentage-based scaling",
+        "Crop operation: accepts x, y, width, height parameters for the crop region",
+        "Rotate operation: accepts angle in degrees (90, 180, 270, or arbitrary), optional background fill color",
+        "All operations output the result using the existing binary storage mechanism",
+        "Output format configurable (JPEG, PNG, WebP, GIF)",
+        "Quality setting available for lossy formats"
+      ],
+      "priority": 3,
+      "passes": false,
+      "dependsOn": [
+        "US-010"
+      ],
+      "labels": [
+        "media",
+        "image",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-012",
+      "title": "Image Node - Filters & Watermark",
+      "description": "As a workflow designer, I want to apply filters and watermarks to images so that I can process images for various output requirements.",
+      "acceptanceCriteria": [
+        "Filter operations: blur (configurable radius), sharpen, grayscale, sepia, brightness adjustment, contrast adjustment",
+        "Watermark operation: overlay text or image watermark, configurable position (center, corners), configurable opacity",
+        "Filters can be chained (apply multiple in sequence)",
+        "All operations use ImageMagick and output via existing binary storage"
+      ],
+      "priority": 3,
+      "passes": false,
+      "dependsOn": [
+        "US-010"
+      ],
+      "labels": [
+        "media",
+        "image",
+        "backend"
+      ]
+    },
+    {
+      "id": "US-013",
+      "title": "Code Node - JavaScript Execution",
+      "description": "As a workflow designer, I want to write custom JavaScript code in a node so that I can implement custom logic that isn't covered by built-in nodes.",
+      "acceptanceCriteria": [
+        "Code node definition created in nodes/ following existing node module interface",
+        "Node config provides a code editor textarea in the workflow editor for writing JavaScript",
+        "User's code has access to input (the node's input data) and context (workflow execution context)",
+        "User's code returns a value that becomes the node's output",
+        "Built-in helper utilities available: lodash-like collection/string/object helpers, date formatting utilities",
+        "Code executes in the existing QuickJS sandbox on the Runner service",
+        "Execution timeout configurable (default: 30 seconds)",
+        "Runtime errors are caught and reported as node execution failures with the error message and line number",
+        "Node appears in the workflow editor node palette"
+      ],
+      "priority": 2,
+      "passes": false,
+      "dependsOn": [],
+      "labels": [
+        "developer",
+        "code",
+        "backend",
+        "frontend"
+      ]
+    },
+    {
+      "id": "US-014",
+      "title": "Node Registration & Editor Integration",
+      "description": "As a workflow designer, I want all new nodes to appear in the workflow editor with proper icons, categories, and configuration panels so that I can easily discover and configure them.",
+      "acceptanceCriteria": [
+        "All six nodes (MySQL, PostgreSQL, RSS Reader, Crypto, Image, Code) are registered in the node system",
+        "Each node has a distinct icon in the editor palette",
+        "Nodes are categorized: Database (MySQL, PostgreSQL), Data (RSS Reader), Security (Crypto), Media (Image), Developer (Code)",
+        "Each node's configuration panel renders all relevant fields based on the selected operation",
+        "Dynamic form updates: selecting an operation shows/hides relevant fields",
+        "All nodes can be migrated to the database using the existing POST /api/v1/nodes/migrate endpoint"
+      ],
+      "priority": 1,
+      "passes": false,
+      "dependsOn": [
+        "US-001",
+        "US-003",
+        "US-005",
+        "US-007",
+        "US-010",
+        "US-013"
+      ],
+      "labels": [
+        "frontend",
+        "integration"
+      ]
+    }
+  ],
+  "metadata": {
+    "updatedAt": "2026-01-28T14:56:29.294Z"
+  }
+}