|
22 | 22 | "options": {} |
23 | 23 | }, |
24 | 24 | "id": "pg-list-tables", |
25 | | - "name": "List Tables + Mod Counts", |
| 25 | + "name": "List Tables", |
26 | 26 | "type": "n8n-nodes-base.postgres", |
27 | 27 | "typeVersion": 2.6, |
28 | 28 | "position": [220, 0] |
29 | 29 | }, |
30 | 30 | { |
31 | 31 | "parameters": { |
32 | | - "jsCode": "const staticData = $getWorkflowStaticData('global');\nif (!staticData.mods) staticData.mods = {};\n\nconst changed = [];\nfor (const item of $input.all()) {\n const name = item.json.table_name;\n const mods = item.json.mod_count;\n if (staticData.mods[name] !== mods) {\n staticData.mods[name] = mods;\n changed.push({ json: { table_name: name } });\n }\n}\n\nif (changed.length === 0) return [{ json: { _skip: true } }];\nreturn changed;" |
| 32 | + "jsCode": "const staticData = $getWorkflowStaticData('global');\nif (!staticData.mods) staticData.mods = {};\n\nconst changed = [];\nfor (const item of $input.all()) {\n const name = item.json.table_name;\n const mods = item.json.mod_count;\n if (staticData.mods[name] !== mods) {\n staticData.mods[name] = mods;\n changed.push(name);\n }\n}\n\nif (changed.length === 0) return [{ json: { _skip: true } }];\nreturn [{ json: { _skip: false, tables: changed } }];" |
33 | 33 | }, |
34 | 34 | "id": "filter-changed", |
35 | | - "name": "Filter Changed Tables", |
| 35 | + "name": "Filter Changed", |
36 | 36 | "type": "n8n-nodes-base.code", |
37 | 37 | "typeVersion": 2, |
38 | 38 | "position": [440, 0] |
39 | 39 | }, |
40 | 40 | { |
41 | 41 | "parameters": { |
42 | 42 | "conditions": { |
43 | | - "options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"}, |
44 | | - "conditions": [{"id": "c1", "leftValue": "={{ $json._skip }}", "rightValue": true, "operator": {"type": "boolean", "operation": "notEquals"}}], |
| 43 | + "options": {"caseSensitive": true, "leftValue": "", "typeValidation": "loose"}, |
| 44 | + "conditions": [{"id": "c1", "leftValue": "={{ $json._skip }}", "rightValue": false, "operator": {"type": "boolean", "operation": "equals"}}], |
45 | 45 | "combinator": "and" |
46 | 46 | }, |
47 | 47 | "options": {} |
48 | 48 | }, |
49 | 49 | "id": "if-changed", |
50 | | - "name": "Any Changes?", |
| 50 | + "name": "Has Changes?", |
51 | 51 | "type": "n8n-nodes-base.if", |
52 | 52 | "typeVersion": 2.3, |
53 | 53 | "position": [660, 0] |
|
66 | 66 | "typeVersion": 4.2, |
67 | 67 | "position": [880, -100] |
68 | 68 | }, |
69 | | - { |
70 | | - "parameters": { |
71 | | - "jsCode": "const sheetsResponse = $('Get Sheet IDs').first().json;\nconst sheetMap = {};\nfor (const s of sheetsResponse.sheets) {\n sheetMap[s.properties.title] = s.properties.sheetId;\n}\n\nconst tables = $('Any Changes?').all().map(i => i.json.table_name);\nconst result = tables.map(t => ({\n json: { table_name: t, sheet_id: sheetMap[t] ?? null }\n}));\nreturn result;" |
72 | | - }, |
73 | | - "id": "map-sheet-ids", |
74 | | - "name": "Map Sheet IDs", |
75 | | - "type": "n8n-nodes-base.code", |
76 | | - "typeVersion": 2, |
77 | | - "position": [1100, -100] |
78 | | - }, |
79 | | - { |
80 | | - "parameters": {"options": {}}, |
81 | | - "id": "split-batches", |
82 | | - "name": "Loop Over Tables", |
83 | | - "type": "n8n-nodes-base.splitInBatches", |
84 | | - "typeVersion": 3, |
85 | | - "position": [1320, -100] |
86 | | - }, |
87 | 69 | { |
88 | 70 | "parameters": { |
89 | 71 | "operation": "executeQuery", |
90 | | - "query": "=SELECT * FROM {{ $json.table_name }}", |
| 72 | + "query": "={{ $('Filter Changed').first().json.tables.map(t => \"SELECT '\" + t + \"' as _tbl, to_jsonb(sub) as _row FROM \\\"\" + t + \"\\\" sub\").join(' UNION ALL ') }}", |
91 | 73 | "options": {} |
92 | 74 | }, |
93 | | - "id": "pg-dump-table", |
94 | | - "name": "Dump Table", |
| 75 | + "id": "pg-dump-all", |
| 76 | + "name": "Dump All Changed", |
95 | 77 | "type": "n8n-nodes-base.postgres", |
96 | 78 | "typeVersion": 2.6, |
97 | | - "position": [1540, -100] |
| 79 | + "position": [1100, -100] |
98 | 80 | }, |
99 | 81 | { |
100 | 82 | "parameters": { |
101 | | - "jsCode": "const rows = $input.all().map(i => i.json);\nconst tableName = $('Loop Over Tables').item.json.table_name;\nlet sheetId = $('Loop Over Tables').item.json.sheet_id;\n\nif (rows.length === 0) {\n return [{ json: { _empty: true } }];\n}\n\nfunction hashCode(s) {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i) | 0;\n return Math.abs(h);\n}\n\nconst headers = Object.keys(rows[0]);\nconst csvLines = [headers.join(',')];\nfor (const row of rows) {\n csvLines.push(headers.map(h => {\n const v = String(row[h] ?? '');\n return (v.includes(',') || v.includes('\"') || v.includes('\\n')) ? '\"' + v.replace(/\"/g, '\"\"') + '\"' : v;\n }).join(','));\n}\n\nconst requests = [];\n\nif (sheetId == null) {\n sheetId = hashCode(tableName);\n requests.push({ addSheet: { properties: { sheetId, title: tableName } } });\n}\nrequests.push({ updateCells: { range: { sheetId }, fields: 'userEnteredValue' } });\nrequests.push({ pasteData: { coordinate: { sheetId, rowIndex: 0, columnIndex: 0 }, data: csvLines.join('\\n'), type: 'PASTE_NORMAL', delimiter: ',' } });\n\nreturn [{ json: { _empty: false, body: { requests } } }];" |
| 83 | + "jsCode": "const sheetsData = $('Get Sheet IDs').first().json;\nconst dumpRows = $('Dump All Changed').all().map(i => i.json);\n\nconst sheetMap = {};\nfor (const s of sheetsData.sheets) {\n sheetMap[s.properties.title] = s.properties.sheetId;\n}\n\nfunction hashCode(s) {\n let h = 0;\n for (let i = 0; i < s.length; i++) h = ((h << 5) - h) + s.charCodeAt(i) | 0;\n return Math.abs(h);\n}\n\nconst grouped = {};\nfor (const r of dumpRows) {\n if (!grouped[r._tbl]) grouped[r._tbl] = [];\n grouped[r._tbl].push(r._row);\n}\n\nconst requests = [];\n\nfor (const [tableName, rows] of Object.entries(grouped)) {\n if (rows.length === 0) continue;\n\n let sheetId = sheetMap[tableName] ?? null;\n if (sheetId == null) {\n sheetId = hashCode(tableName);\n requests.push({ addSheet: { properties: { sheetId, title: tableName } } });\n }\n\n requests.push({ updateCells: { range: { sheetId }, fields: 'userEnteredValue' } });\n\n const headers = Object.keys(rows[0]);\n const csvLines = [headers.join(',')];\n for (const row of rows) {\n csvLines.push(headers.map(h => {\n const v = String(row[h] ?? '');\n return (v.includes(',') || v.includes('\"') || v.includes('\\n')) ? '\"' + v.replace(/\"/g, '\"\"') + '\"' : v;\n }).join(','));\n }\n\n requests.push({ pasteData: { coordinate: { sheetId, rowIndex: 0, columnIndex: 0 }, data: csvLines.join('\\n'), type: 'PASTE_NORMAL', delimiter: ',' } });\n}\n\nreturn [{ json: { body: { requests } } }];" |
102 | 84 | }, |
103 | | - "id": "to-csv", |
104 | | - "name": "Build CSV + Payload", |
| 85 | + "id": "build-payload", |
| 86 | + "name": "Build Batch Payload", |
105 | 87 | "type": "n8n-nodes-base.code", |
106 | 88 | "typeVersion": 2, |
107 | | - "position": [1760, -100] |
108 | | - }, |
109 | | - { |
110 | | - "parameters": { |
111 | | - "conditions": { |
112 | | - "options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"}, |
113 | | - "conditions": [{"id": "c1", "leftValue": "={{ $json._empty }}", "rightValue": true, "operator": {"type": "boolean", "operation": "notEquals"}}], |
114 | | - "combinator": "and" |
115 | | - }, |
116 | | - "options": {} |
117 | | - }, |
118 | | - "id": "if-not-empty", |
119 | | - "name": "Has Rows?", |
120 | | - "type": "n8n-nodes-base.if", |
121 | | - "typeVersion": 2.3, |
122 | | - "position": [1980, -100] |
| 89 | + "position": [1320, -100] |
123 | 90 | }, |
124 | 91 | { |
125 | 92 | "parameters": { |
|
133 | 100 | "options": {} |
134 | 101 | }, |
135 | 102 | "id": "paste-data", |
136 | | - "name": "Paste to Sheet", |
| 103 | + "name": "Paste to Sheets", |
137 | 104 | "type": "n8n-nodes-base.httpRequest", |
138 | 105 | "typeVersion": 4.2, |
139 | | - "position": [2200, -200] |
| 106 | + "position": [1540, -100] |
140 | 107 | } |
141 | 108 | ], |
142 | 109 | "connections": { |
143 | | - "Every 5 Minutes": {"main": [[{"node": "List Tables + Mod Counts", "type": "main", "index": 0}]]}, |
144 | | - "List Tables + Mod Counts": {"main": [[{"node": "Filter Changed Tables", "type": "main", "index": 0}]]}, |
145 | | - "Filter Changed Tables": {"main": [[{"node": "Any Changes?", "type": "main", "index": 0}]]}, |
146 | | - "Any Changes?": {"main": [[{"node": "Get Sheet IDs", "type": "main", "index": 0}], []]}, |
147 | | - "Get Sheet IDs": {"main": [[{"node": "Map Sheet IDs", "type": "main", "index": 0}]]}, |
148 | | - "Map Sheet IDs": {"main": [[{"node": "Loop Over Tables", "type": "main", "index": 0}]]}, |
149 | | - "Loop Over Tables": {"main": [[], [{"node": "Dump Table", "type": "main", "index": 0}]]}, |
150 | | - "Dump Table": {"main": [[{"node": "Build CSV + Payload", "type": "main", "index": 0}]]}, |
151 | | - "Build CSV + Payload": {"main": [[{"node": "Has Rows?", "type": "main", "index": 0}]]}, |
152 | | - "Has Rows?": {"main": [[{"node": "Paste to Sheet", "type": "main", "index": 0}], [{"node": "Loop Over Tables", "type": "main", "index": 0}]]}, |
153 | | - "Paste to Sheet": {"main": [[{"node": "Loop Over Tables", "type": "main", "index": 0}]]} |
| 110 | + "Every 5 Minutes": {"main": [[{"node": "List Tables", "type": "main", "index": 0}]]}, |
| 111 | + "List Tables": {"main": [[{"node": "Filter Changed", "type": "main", "index": 0}]]}, |
| 112 | + "Filter Changed": {"main": [[{"node": "Has Changes?", "type": "main", "index": 0}]]}, |
| 113 | + "Has Changes?": {"main": [[{"node": "Get Sheet IDs", "type": "main", "index": 0}], []]}, |
| 114 | + "Get Sheet IDs": {"main": [[{"node": "Dump All Changed", "type": "main", "index": 0}]]}, |
| 115 | + "Dump All Changed": {"main": [[{"node": "Build Batch Payload", "type": "main", "index": 0}]]}, |
| 116 | + "Build Batch Payload": {"main": [[{"node": "Paste to Sheets", "type": "main", "index": 0}]]}, |
| 117 | + "Paste to Sheets": {"main": [[]]} |
154 | 118 | }, |
155 | 119 | "settings": {"executionOrder": "v1"} |
156 | 120 | } |
0 commit comments