Flowfile 0.5.6__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (256) hide show
  1. flowfile/api.py +8 -6
  2. flowfile/web/static/assets/{AdminView-c2c7942b.js → AdminView-C4K1DdHI.js} +28 -33
  3. flowfile/web/static/assets/{CloudConnectionView-7a3042c6.js → CloudConnectionView-BZbPvPUL.js} +39 -50
  4. flowfile/web/static/assets/{CloudStorageReader-24c54524.css → CloudStorageReader-BDByiqPI.css} +25 -25
  5. flowfile/web/static/assets/{CloudStorageReader-709c4037.js → CloudStorageReader-DLVukNJ7.js} +30 -35
  6. flowfile/web/static/assets/{CloudStorageWriter-604c51a8.js → CloudStorageWriter-Bfi-C1QW.js} +32 -37
  7. flowfile/web/static/assets/{CloudStorageWriter-60547855.css → CloudStorageWriter-y8jL8yjG.css} +24 -24
  8. flowfile/web/static/assets/{ColumnActionInput-d63d6746.js → ColumnActionInput-BpiCApw9.js} +7 -12
  9. flowfile/web/static/assets/{ColumnSelector-0c8cd1cd.js → ColumnSelector-CEAwedI7.js} +1 -2
  10. flowfile/web/static/assets/ContextMenu-CdojQu0w.js +9 -0
  11. flowfile/web/static/assets/ContextMenu-D12mhsy1.js +9 -0
  12. flowfile/web/static/assets/ContextMenu-EWUR98va.js +9 -0
  13. flowfile/web/static/assets/{ContextMenu.vue_vue_type_script_setup_true_lang-774c517c.js → ContextMenu.vue_vue_type_script_setup_true_lang-I4rXXd6G.js} +4 -5
  14. flowfile/web/static/assets/{CrossJoin-38e5b99a.js → CrossJoin-BOFfxkJO.js} +19 -18
  15. flowfile/web/static/assets/{CrossJoin-71b4cc10.css → CrossJoin-Cmbyt9im.css} +18 -18
  16. flowfile/web/static/assets/{CustomNode-76e8f3f5.js → CustomNode-Bhpezobq.js} +12 -17
  17. flowfile/web/static/assets/{DatabaseConnectionSettings-38155669.js → DatabaseConnectionSettings-Dw3bSJKB.js} +10 -11
  18. flowfile/web/static/assets/{DatabaseReader-5bf8c75b.css → DatabaseReader-D6pUNUCs.css} +21 -21
  19. flowfile/web/static/assets/{DatabaseReader-2e549c8f.js → DatabaseReader-m87ghlw0.js} +36 -34
  20. flowfile/web/static/assets/{DatabaseView-dc877c29.js → DatabaseView-CisSAtpe.js} +30 -38
  21. flowfile/web/static/assets/{DatabaseWriter-ffb91864.js → DatabaseWriter-Bbj9JLdL.js} +33 -35
  22. flowfile/web/static/assets/{DatabaseWriter-bdcf2c8b.css → DatabaseWriter-RBqdFLj8.css} +17 -17
  23. flowfile/web/static/assets/{DesignerView-a4466dab.js → DesignerView-DemDevTQ.js} +1752 -2054
  24. flowfile/web/static/assets/{DesignerView-71d4e9a1.css → DesignerView-Dm6OzlIc.css} +209 -168
  25. flowfile/web/static/assets/{DocumentationView-979afc84.js → DocumentationView-BrC1ZR3H.js} +3 -4
  26. flowfile/web/static/assets/{ExploreData-e4b92aaf.js → ExploreData-BMKcDuRb.js} +8 -10
  27. flowfile/web/static/assets/{ExternalSource-d08e7227.js → ExternalSource-BXrNNS-f.js} +40 -42
  28. flowfile/web/static/assets/{ExternalSource-7ac7373f.css → ExternalSource-NB6WVl5R.css} +14 -14
  29. flowfile/web/static/assets/{Filter-7add806d.js → Filter-C2MjsN6P.js} +36 -33
  30. flowfile/web/static/assets/{Filter-7494ea97.css → Filter-DCMGGuGC.css} +9 -9
  31. flowfile/web/static/assets/{Formula-53d58c43.css → Formula-BYafbDj8.css} +4 -4
  32. flowfile/web/static/assets/{Formula-36ab24d2.js → Formula-ufuy4mVD.js} +27 -26
  33. flowfile/web/static/assets/{FuzzyMatch-ad6361d6.css → FuzzyMatch-BGJAwgd0.css} +42 -42
  34. flowfile/web/static/assets/{FuzzyMatch-cc01bb04.js → FuzzyMatch-BOHODq3h.js} +36 -38
  35. flowfile/web/static/assets/{GraphSolver-4fb98f3b.js → GraphSolver-B6ZzpNGO.js} +23 -21
  36. flowfile/web/static/assets/{GraphSolver-4b4d7db9.css → GraphSolver-DFN83sj3.css} +4 -4
  37. flowfile/web/static/assets/{GroupBy-b3c8f429.js → GroupBy-B9BRNcfe.js} +30 -29
  38. flowfile/web/static/assets/{Sort-4abb7fae.css → GroupBy-x4ooP5np.css} +1 -1
  39. flowfile/web/static/assets/Join-Bx_g5bZz.css +118 -0
  40. flowfile/web/static/assets/{Join-096b7b26.js → Join-DsBEy1IH.js} +48 -43
  41. flowfile/web/static/assets/{LoginView-c33a246a.js → LoginView-Ct0rhdcO.js} +1 -2
  42. flowfile/web/static/assets/{ManualInput-39111f19.css → ManualInput-DlZmtMdt.css} +48 -48
  43. flowfile/web/static/assets/{ManualInput-7307e9b1.js → ManualInput-bC4BUgnG.js} +40 -41
  44. flowfile/web/static/assets/{MultiSelect-14822c48.js → MultiSelect-DIQ8PuTC.js} +2 -2
  45. flowfile/web/static/assets/{MultiSelect.vue_vue_type_script_setup_true_lang-90c4d340.js → MultiSelect.vue_vue_type_script_setup_true_lang-BefHfqTI.js} +1 -1
  46. flowfile/web/static/assets/{NodeDesigner-5036c392.js → NodeDesigner-D39yzr2k.js} +178 -208
  47. flowfile/web/static/assets/{NodeDesigner-94cd4dd3.css → NodeDesigner-R0l6sYyY.css} +76 -76
  48. flowfile/web/static/assets/{NumericInput-15cf3b72.js → NumericInput-DMSX3oOr.js} +2 -2
  49. flowfile/web/static/assets/{NumericInput.vue_vue_type_script_setup_true_lang-91e679d7.js → NumericInput.vue_vue_type_script_setup_true_lang-d0YlVHAl.js} +1 -1
  50. flowfile/web/static/assets/{Output-1f8ed42c.js → Output-D0VoXGcW.js} +26 -34
  51. flowfile/web/static/assets/{Output-692dd25d.css → Output-DsmglIDy.css} +5 -5
  52. flowfile/web/static/assets/{Pivot-0e153f4e.js → Pivot-BnMB4sEe.js} +26 -26
  53. flowfile/web/static/assets/{Pivot-0eda81b4.css → Pivot-qKTyWxop.css} +4 -4
  54. flowfile/web/static/assets/{PivotValidation-81ec2a33.js → PivotValidation-B2lWvugt.js} +7 -9
  55. flowfile/web/static/assets/{PivotValidation-5a4f7c79.js → PivotValidation-BPlhRjpL.js} +7 -9
  56. flowfile/web/static/assets/{PolarsCode-a39f15ac.js → PolarsCode-5h0tHnWR.js} +22 -20
  57. flowfile/web/static/assets/{PopOver-ddcfe4f6.js → PopOver-BHpt5rsj.js} +5 -9
  58. flowfile/web/static/assets/{PopOver-d96599db.css → PopOver-CyYM4-rV.css} +1 -1
  59. flowfile/web/static/assets/{Read-90f366bc.css → Read-DJxkrTb_.css} +10 -10
  60. flowfile/web/static/assets/Read-TsLEFh3B.js +227 -0
  61. flowfile/web/static/assets/{RecordCount-e9048ccd.js → RecordCount-DkVixq9v.js} +18 -17
  62. flowfile/web/static/assets/{RecordId-ad02521d.js → RecordId-C2UEGlCf.js} +42 -39
  63. flowfile/web/static/assets/{SQLQueryComponent-2eeecf0b.js → SQLQueryComponent-Dr5KMoD3.js} +2 -3
  64. flowfile/web/static/assets/{Sample-9a68c23d.js → Sample-Cb3eQNmd.js} +30 -30
  65. flowfile/web/static/assets/{SecretSelector-2429f35a.js → SecretSelector-De2L2bSx.js} +3 -4
  66. flowfile/web/static/assets/{SecretsView-c6afc915.js → SecretsView-CheC9BPV.js} +13 -16
  67. flowfile/web/static/assets/{Select-fcd002b6.js → Select-CI8TloRs.js} +41 -36
  68. flowfile/web/static/assets/{SettingsSection-5ce15962.js → SettingsSection-B39ulIiI.js} +1 -2
  69. flowfile/web/static/assets/{SettingsSection-c6b1362c.js → SettingsSection-BiCc7S9h.js} +1 -2
  70. flowfile/web/static/assets/{SettingsSection-cebb91d5.js → SettingsSection-CITK_R7o.js} +2 -3
  71. flowfile/web/static/assets/{SettingsSection-26fe48d4.css → SettingsSection-D2GgY-Aq.css} +4 -4
  72. flowfile/web/static/assets/{SetupView-2d12e01f.js → SetupView-C1aXRDvp.js} +1 -2
  73. flowfile/web/static/assets/{SingleSelect-b67de4eb.js → SingleSelect-Kr_hz90m.js} +2 -2
  74. flowfile/web/static/assets/{SingleSelect.vue_vue_type_script_setup_true_lang-eedb70eb.js → SingleSelect.vue_vue_type_script_setup_true_lang-Rxht5Z5N.js} +1 -1
  75. flowfile/web/static/assets/{SliderInput-fd8134ac.js → SliderInput-CLqpCxCb.js} +1 -2
  76. flowfile/web/static/assets/{GroupBy-5792782d.css → Sort-BIt2kc_p.css} +1 -1
  77. flowfile/web/static/assets/{Sort-c005a573.js → Sort-Dnw_J6Qi.js} +25 -25
  78. flowfile/web/static/assets/{TextInput-1bb31dab.js → TextInput-wdlunIZC.js} +2 -2
  79. flowfile/web/static/assets/{TextInput.vue_vue_type_script_setup_true_lang-a51fe730.js → TextInput.vue_vue_type_script_setup_true_lang-Bcj3ywzv.js} +1 -1
  80. flowfile/web/static/assets/{TextToRows-4f363753.js → TextToRows-BhtyGWPq.js} +42 -49
  81. flowfile/web/static/assets/{TextToRows-12afb4f4.css → TextToRows-DivDOLDx.css} +9 -9
  82. flowfile/web/static/assets/{ToggleSwitch-ca0f2e5e.js → ToggleSwitch-B-6WzfFf.js} +2 -2
  83. flowfile/web/static/assets/{ToggleSwitch.vue_vue_type_script_setup_true_lang-49aa41d8.js → ToggleSwitch.vue_vue_type_script_setup_true_lang-Cj8LqT-b.js} +1 -1
  84. flowfile/web/static/assets/{UnavailableFields-f6147968.js → UnavailableFields-Yf6XSqFB.js} +2 -3
  85. flowfile/web/static/assets/{Union-c65f17b7.js → Union-CwpjeKYC.js} +20 -23
  86. flowfile/web/static/assets/{Unpivot-b6ad6427.css → Union-DQJcpp3-.css} +6 -6
  87. flowfile/web/static/assets/{Unique-a1d96fb2.js → Unique-25v3urqH.js} +75 -74
  88. flowfile/web/static/assets/{Union-d6a8d7d5.css → Unpivot-Deqh1gtI.css} +6 -6
  89. flowfile/web/static/assets/{Unpivot-c2657ff3.js → Unpivot-sYcTTXrq.js} +34 -27
  90. flowfile/web/static/assets/{UnpivotValidation-28e29a3b.js → UnpivotValidation-C5DDEKY2.js} +5 -7
  91. flowfile/web/static/assets/VueGraphicWalker-B8l1_Z92.js +131 -0
  92. flowfile/web/static/assets/VueGraphicWalker-Da_1-3me.css +21 -0
  93. flowfile/web/static/assets/{api-df48ec50.js → api-C0LvF-0C.js} +1 -1
  94. flowfile/web/static/assets/{api-ee542cf7.js → api-DaC83EO_.js} +1 -1
  95. flowfile/web/static/assets/client-C8Ygr6Gb.js +42 -0
  96. flowfile/web/static/assets/{dropDown-7576a76a.js → dropDown-D5YXaPRR.js} +7 -12
  97. flowfile/web/static/assets/{fullEditor-7583bef5.js → fullEditor-BVYnWm05.js} +300 -18
  98. flowfile/web/static/assets/genericNodeSettings-2wAu-QKn.css +75 -0
  99. flowfile/web/static/assets/genericNodeSettings-BBtW_Cpz.js +590 -0
  100. flowfile/web/static/assets/{VueGraphicWalker-2fc3ddd4.js → graphic-walker.es-VrK6vdGE.js} +92305 -89751
  101. flowfile/web/static/assets/index-BCJxPfM5.js +6693 -0
  102. flowfile/web/static/assets/{index-057d770d.js → index-CHPMUR0d.js} +150 -170
  103. flowfile/web/static/assets/index-DPkoZWq8.js +32 -0
  104. flowfile/web/static/assets/index-DnW_KC_I.js +277 -0
  105. flowfile/web/static/assets/index-UFXyfirV.css +10797 -0
  106. flowfile/web/static/assets/index-bcuE0Z0p.js +87456 -0
  107. flowfile/web/static/assets/{node.types-2c15bb7e.js → node.types-Dl4gtSW9.js} +2 -2
  108. flowfile/web/static/assets/{outputCsv-c492b15e.js → outputCsv-BELuBiJZ.js} +1 -2
  109. flowfile/web/static/assets/outputCsv-CdGkv-fN.css +2581 -0
  110. flowfile/web/static/assets/{outputExcel-13bfa10f.js → outputExcel-D0TTNM79.js} +1 -2
  111. flowfile/web/static/assets/{outputParquet-9be1523a.js → outputParquet-Cz9EbRHj.js} +1 -2
  112. flowfile/web/static/assets/{readCsv-5a49a8c9.js → readCsv-7bd3kUMI.js} +1 -2
  113. flowfile/web/static/assets/{readExcel-27c30ad8.js → readExcel-Cq8CCwIv.js} +3 -4
  114. flowfile/web/static/assets/{readParquet-c5244ad5.css → readParquet-CRDmBrsp.css} +4 -4
  115. flowfile/web/static/assets/{readParquet-446bde68.js → readParquet-DjR4mRaj.js} +4 -5
  116. flowfile/web/static/assets/{secrets.api-34431884.js → secrets.api-C9o2KE5V.js} +1 -1
  117. flowfile/web/static/assets/{selectDynamic-5754a2b1.js → selectDynamic-Bl5FVsME.js} +5 -7
  118. flowfile/web/static/assets/useNodeSettings-dMS9zmh_.js +69 -0
  119. flowfile/web/static/assets/{vue-codemirror.esm-8f46fb36.js → vue-codemirror.esm-CwaYwln0.js} +3469 -3064
  120. flowfile/web/static/assets/{vue-content-loader.es-808fe33a.js → vue-content-loader.es-CMoRXo7N.js} +3 -3
  121. flowfile/web/static/index.html +2 -3
  122. {flowfile-0.5.6.dist-info → flowfile-0.6.1.dist-info}/METADATA +2 -1
  123. flowfile-0.6.1.dist-info/RECORD +417 -0
  124. {flowfile-0.5.6.dist-info → flowfile-0.6.1.dist-info}/WHEEL +1 -1
  125. flowfile_core/auth/password.py +1 -0
  126. flowfile_core/database/init_db.py +7 -5
  127. flowfile_core/fileExplorer/funcs.py +2 -2
  128. flowfile_core/flowfile/code_generator/code_generator.py +13 -11
  129. flowfile_core/flowfile/filter_expressions.py +327 -0
  130. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +61 -59
  131. flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +3 -29
  132. flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +45 -14
  133. flowfile_core/flowfile/flow_data_engine/subprocess_operations/models.py +20 -3
  134. flowfile_core/flowfile/flow_data_engine/subprocess_operations/streaming.py +206 -0
  135. flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +146 -24
  136. flowfile_core/flowfile/flow_graph.py +504 -190
  137. flowfile_core/flowfile/flow_node/__init__.py +32 -0
  138. flowfile_core/flowfile/flow_node/executor.py +404 -0
  139. flowfile_core/flowfile/flow_node/flow_node.py +207 -106
  140. flowfile_core/flowfile/flow_node/models.py +40 -0
  141. flowfile_core/flowfile/flow_node/output_field_config_applier.py +217 -0
  142. flowfile_core/flowfile/flow_node/schema_utils.py +78 -0
  143. flowfile_core/flowfile/flow_node/state.py +155 -0
  144. flowfile_core/flowfile/history_manager.py +401 -0
  145. flowfile_core/flowfile/manage/compatibility_enhancements.py +9 -0
  146. flowfile_core/flowfile/manage/io_flowfile.py +3 -1
  147. flowfile_core/flowfile/sources/external_sources/sql_source/models.py +20 -4
  148. flowfile_core/flowfile/util/execution_orderer.py +89 -36
  149. flowfile_core/routes/auth.py +8 -9
  150. flowfile_core/routes/routes.py +320 -101
  151. flowfile_core/routes/user_defined_components.py +18 -16
  152. flowfile_core/schemas/history_schema.py +220 -0
  153. flowfile_core/schemas/input_schema.py +130 -6
  154. flowfile_core/schemas/schemas.py +9 -0
  155. flowfile_core/schemas/transform_schema.py +27 -5
  156. flowfile_core/schemas/yaml_types.py +23 -5
  157. flowfile_frame/adding_expr.py +18 -126
  158. flowfile_frame/callable_utils.py +261 -0
  159. flowfile_frame/database/connection_manager.py +0 -1
  160. flowfile_frame/expr.py +8 -4
  161. flowfile_frame/flow_frame.py +41 -41
  162. flowfile_frame/lazy.py +3 -12
  163. flowfile_frame/lazy_methods.py +5 -64
  164. flowfile_frame/utils.py +13 -32
  165. flowfile_worker/funcs.py +6 -4
  166. flowfile_worker/main.py +2 -0
  167. flowfile_worker/models.py +31 -11
  168. flowfile_worker/routes.py +60 -35
  169. flowfile_worker/spawner.py +7 -1
  170. flowfile_worker/streaming.py +335 -0
  171. flowfile/web/static/assets/ContextMenu-366bf1b4.js +0 -9
  172. flowfile/web/static/assets/ContextMenu-85cf5b44.js +0 -9
  173. flowfile/web/static/assets/ContextMenu-9d28ae6d.js +0 -9
  174. flowfile/web/static/assets/Join-28b5e18f.css +0 -109
  175. flowfile/web/static/assets/Read-39b63932.js +0 -222
  176. flowfile/web/static/assets/VueGraphicWalker-430f0b86.css +0 -6
  177. flowfile/web/static/assets/database_reader-ce1e55f3.svg +0 -24
  178. flowfile/web/static/assets/database_writer-b4ad0753.svg +0 -23
  179. flowfile/web/static/assets/element-icons-9c88a535.woff +0 -0
  180. flowfile/web/static/assets/element-icons-de5eb258.ttf +0 -0
  181. flowfile/web/static/assets/genericNodeSettings-0155288b.js +0 -136
  182. flowfile/web/static/assets/genericNodeSettings-3b2507ea.css +0 -46
  183. flowfile/web/static/assets/index-aeec439d.js +0 -38
  184. flowfile/web/static/assets/index-ca6799de.js +0 -62760
  185. flowfile/web/static/assets/index-d60c9dd4.css +0 -10777
  186. flowfile/web/static/assets/nodeInput-d478b9ac.js +0 -2
  187. flowfile/web/static/assets/outputCsv-cc84e09f.css +0 -2499
  188. flowfile-0.5.6.dist-info/RECORD +0 -407
  189. /flowfile/web/static/assets/{AdminView-f53bad23.css → AdminView-B2Dthl3u.css} +0 -0
  190. /flowfile/web/static/assets/{CloudConnectionView-cf85f943.css → CloudConnectionView-BdFYGWV7.css} +0 -0
  191. /flowfile/web/static/assets/{ColumnActionInput-c44b7aee.css → ColumnActionInput-dCasSIC9.css} +0 -0
  192. /flowfile/web/static/assets/{ColumnSelector-371637fb.css → ColumnSelector-j6sEOjo1.css} +0 -0
  193. /flowfile/web/static/assets/{CustomNode-edb9b939.css → CustomNode-VPlajG0j.css} +0 -0
  194. /flowfile/web/static/assets/{DatabaseConnectionSettings-c20a1e16.css → DatabaseConnectionSettings-B78hXYgu.css} +0 -0
  195. /flowfile/web/static/assets/{DatabaseView-6655afd6.css → DatabaseView-B-_adk1s.css} +0 -0
  196. /flowfile/web/static/assets/{DocumentationView-9ea6e871.css → DocumentationView-CL7iipFL.css} +0 -0
  197. /flowfile/web/static/assets/{ExploreData-10c5acc8.css → ExploreData-DHjv0Plr.css} +0 -0
  198. /flowfile/web/static/assets/{LoginView-d325d632.css → LoginView-DN1BXY3e.css} +0 -0
  199. /flowfile/web/static/assets/{PivotValidation-0e905b1a.css → PivotValidation-DK-FARWe.css} +0 -0
  200. /flowfile/web/static/assets/{PivotValidation-41b57ad6.css → PivotValidation-FUa9F47u.css} +0 -0
  201. /flowfile/web/static/assets/{PolarsCode-2b1f1f23.css → PolarsCode-G-gRSrSc.css} +0 -0
  202. /flowfile/web/static/assets/{SQLQueryComponent-edb90b98.css → SQLQueryComponent-oAbWw0r-.css} +0 -0
  203. /flowfile/web/static/assets/{SecretSelector-6329f743.css → SecretSelector-CJSadIZx.css} +0 -0
  204. /flowfile/web/static/assets/{SecretsView-aa291340.css → SecretsView-DbzIRAba.css} +0 -0
  205. /flowfile/web/static/assets/{SettingsSection-8f980839.css → SettingsSection-BGcJnH6q.css} +0 -0
  206. /flowfile/web/static/assets/{SettingsSection-07fbbc39.css → SettingsSection-DDWn_EGW.css} +0 -0
  207. /flowfile/web/static/assets/{SetupView-ec26f76a.css → SetupView-CI1nd-5Z.css} +0 -0
  208. /flowfile/web/static/assets/{SliderInput-f2e4f23c.css → SliderInput-BRk-q_Dk.css} +0 -0
  209. /flowfile/web/static/assets/{UnavailableFields-394a1f78.css → UnavailableFields-DRKDImKe.css} +0 -0
  210. /flowfile/web/static/assets/{Unique-2b705521.css → Unique-Absb0aON.css} +0 -0
  211. /flowfile/web/static/assets/{UnpivotValidation-d5ca3b7b.css → UnpivotValidation-DSBkFgS-.css} +0 -0
  212. /flowfile/web/static/assets/{airbyte-292aa232.png → airbyte-W0xvIXwZ.png} +0 -0
  213. /flowfile/web/static/assets/{cloud_storage_reader-aa1415d6.png → cloud_storage_reader-3GpSCk90.png} +0 -0
  214. /flowfile/web/static/assets/{cross_join-d30c0290.png → cross_join-B0qpgYoV.png} +0 -0
  215. /flowfile/web/static/assets/{dropDown-1d6acbd9.css → dropDown-CE0VF5_P.css} +0 -0
  216. /flowfile/web/static/assets/{explore_data-8a0a2861.png → explore_data-tX6olPPL.png} +0 -0
  217. /flowfile/web/static/assets/{fa-brands-400-808443ae.ttf → fa-brands-400-D1LuMI3I.ttf} +0 -0
  218. /flowfile/web/static/assets/{fa-brands-400-d7236a19.woff2 → fa-brands-400-D_cYUPeE.woff2} +0 -0
  219. /flowfile/web/static/assets/{fa-regular-400-e3456d12.woff2 → fa-regular-400-BjRzuEpd.woff2} +0 -0
  220. /flowfile/web/static/assets/{fa-regular-400-54cf6086.ttf → fa-regular-400-DZaxPHgR.ttf} +0 -0
  221. /flowfile/web/static/assets/{fa-solid-900-aa759986.woff2 → fa-solid-900-CTAAxXor.woff2} +0 -0
  222. /flowfile/web/static/assets/{fa-solid-900-d2f05935.ttf → fa-solid-900-D0aA9rwL.ttf} +0 -0
  223. /flowfile/web/static/assets/{fa-v4compatibility-0ce9033c.woff2 → fa-v4compatibility-C9RhG_FT.woff2} +0 -0
  224. /flowfile/web/static/assets/{fa-v4compatibility-30f6abf6.ttf → fa-v4compatibility-CCth-dXg.ttf} +0 -0
  225. /flowfile/web/static/assets/{filter-d7708bda.png → filter-WRdZyUOw.png} +0 -0
  226. /flowfile/web/static/assets/{formula-eeeb1611.png → formula-CgM7uHVI.png} +0 -0
  227. /flowfile/web/static/assets/{fullEditor-fe9f7e18.css → fullEditor-CmDI7T9F.css} +0 -0
  228. /flowfile/web/static/assets/{fuzzy_match-40c161b2.png → fuzzy_match-Yon3k5Tc.png} +0 -0
  229. /flowfile/web/static/assets/{graph_solver-8b7888b8.png → graph_solver-BlMrBttD.png} +0 -0
  230. /flowfile/web/static/assets/{group_by-80561fc3.png → group_by-Gici0CSS.png} +0 -0
  231. /flowfile/web/static/assets/{input_data-ab2eb678.png → input_data-BRdGecLc.png} +0 -0
  232. /flowfile/web/static/assets/{join-349043ae.png → join-BITWRu73.png} +0 -0
  233. /flowfile/web/static/assets/{manual_input-ae98f31d.png → manual_input-CFvo_EUS.png} +0 -0
  234. /flowfile/web/static/assets/{old_join-5d0eb604.png → old_join-B9bkpPqv.png} +0 -0
  235. /flowfile/web/static/assets/{output-06ec0371.png → output-Dp7-ZpC4.png} +0 -0
  236. /flowfile/web/static/assets/{outputExcel-f5d272b2.css → outputExcel-CKgRe2iT.css} +0 -0
  237. /flowfile/web/static/assets/{outputParquet-54597c3c.css → outputParquet-d7j407cK.css} +0 -0
  238. /flowfile/web/static/assets/{pivot-9660df51.png → pivot-DSxKhNlD.png} +0 -0
  239. /flowfile/web/static/assets/{polars_code-05ce5dc6.png → polars_code-DxiztZ1c.png} +0 -0
  240. /flowfile/web/static/assets/{readCsv-3bfac4c3.css → readCsv-BG-1Jilp.css} +0 -0
  241. /flowfile/web/static/assets/{readExcel-3db6b763.css → readExcel-DBQXKPtC.css} +0 -0
  242. /flowfile/web/static/assets/{record_count-dab44eb5.png → record_count-DCeaLtpS.png} +0 -0
  243. /flowfile/web/static/assets/{record_id-0b15856b.png → record_id-FeUjyIFh.png} +0 -0
  244. /flowfile/web/static/assets/{sample-693a88b5.png → sample-DeqfRiB-.png} +0 -0
  245. /flowfile/web/static/assets/{select-b0d0437a.png → select-D4JjbdjS.png} +0 -0
  246. /flowfile/web/static/assets/{selectDynamic-f2fb394f.css → selectDynamic-CjeTPUUo.css} +0 -0
  247. /flowfile/web/static/assets/{sort-2aa579f0.png → sort-DGwUG9WS.png} +0 -0
  248. /flowfile/web/static/assets/{summarize-2a099231.png → summarize-DFaNHpfp.png} +0 -0
  249. /flowfile/web/static/assets/{text_to_rows-859b29ea.png → text_to_rows-BdiAewrN.png} +0 -0
  250. /flowfile/web/static/assets/{union-2d8609f4.png → union-DCK-LSMq.png} +0 -0
  251. /flowfile/web/static/assets/{unique-1958b98a.png → unique-CdP3zZIq.png} +0 -0
  252. /flowfile/web/static/assets/{unpivot-d3cb4b5b.png → unpivot-CHttrEt8.png} +0 -0
  253. /flowfile/web/static/assets/{user-defined-icon-0ae16c90.png → user-defined-icon-BcIp2Vzo.png} +0 -0
  254. /flowfile/web/static/assets/{view-7a0f0be1.png → view-DUSRwjvq.png} +0 -0
  255. {flowfile-0.5.6.dist-info → flowfile-0.6.1.dist-info}/entry_points.txt +0 -0
  256. {flowfile-0.5.6.dist-info → flowfile-0.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,15 @@
1
1
  # app_routes/auth.py
2
2
 
3
3
  import os
4
- from typing import Optional, List
5
4
 
6
- from fastapi import APIRouter, Depends, HTTPException, status, Request, Form
5
+ from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
7
6
  from sqlalchemy.orm import Session
8
7
 
9
- from flowfile_core.auth.jwt import get_current_active_user, get_current_admin_user, create_access_token
10
- from flowfile_core.auth.models import Token, User, UserCreate, UserUpdate, ChangePassword
11
- from flowfile_core.auth.password import verify_password, get_password_hash, validate_password, PASSWORD_REQUIREMENTS
12
- from flowfile_core.database.connection import get_db
8
+ from flowfile_core.auth.jwt import create_access_token, get_current_active_user, get_current_admin_user
9
+ from flowfile_core.auth.models import ChangePassword, Token, User, UserCreate, UserUpdate
10
+ from flowfile_core.auth.password import PASSWORD_REQUIREMENTS, get_password_hash, validate_password, verify_password
13
11
  from flowfile_core.database import models as db_models
12
+ from flowfile_core.database.connection import get_db
14
13
 
15
14
  router = APIRouter()
16
15
 
@@ -19,8 +18,8 @@ router = APIRouter()
19
18
  async def login_for_access_token(
20
19
  request: Request,
21
20
  db: Session = Depends(get_db),
22
- username: Optional[str] = Form(None),
23
- password: Optional[str] = Form(None)
21
+ username: str | None = Form(None),
22
+ password: str | None = Form(None)
24
23
  ):
25
24
  # In Electron mode, auto-authenticate without requiring form data
26
25
  if os.environ.get("FLOWFILE_MODE") == "electron":
@@ -58,7 +57,7 @@ async def read_users_me(current_user=Depends(get_current_active_user)):
58
57
 
59
58
  # ============= Admin User Management Endpoints =============
60
59
 
61
- @router.get("/users", response_model=List[User])
60
+ @router.get("/users", response_model=list[User])
62
61
  async def list_users(
63
62
  current_user: User = Depends(get_current_admin_user),
64
63
  db: Session = Depends(get_db)
@@ -11,53 +11,51 @@ import inspect
11
11
  import logging
12
12
  import os
13
13
  from pathlib import Path
14
- from typing import List, Dict, Any, Optional
14
+ from typing import Any
15
15
 
16
- from fastapi import APIRouter, File, UploadFile, BackgroundTasks, HTTPException, status, Body, Depends
16
+ from fastapi import APIRouter, BackgroundTasks, Body, Depends, File, HTTPException, UploadFile, status
17
17
  from fastapi.responses import JSONResponse, Response
18
+
18
19
  # External dependencies
19
20
  from polars_expr_transformer.function_overview import get_all_expressions, get_expression_overview
20
21
  from sqlalchemy.orm import Session
21
22
 
22
23
  from flowfile_core import flow_file_handler
24
+
23
25
  # Core modules
24
26
  from flowfile_core.auth.jwt import get_current_active_user
25
27
  from flowfile_core.configs import logger
26
- from flowfile_core.configs.node_store import nodes_list, check_if_has_default_setting
28
+ from flowfile_core.configs.node_store import check_if_has_default_setting, nodes_list
27
29
  from flowfile_core.database.connection import get_db
30
+
28
31
  # File handling
29
32
  from flowfile_core.fileExplorer.funcs import (
30
- SecureFileExplorer,
31
33
  FileInfo,
34
+ SecureFileExplorer,
32
35
  get_files_from_directory,
33
- validate_file_path,
34
36
  validate_path_under_cwd,
35
37
  )
36
38
  from flowfile_core.flowfile.analytics.analytics_processor import AnalyticsProcessor
37
39
  from flowfile_core.flowfile.code_generator.code_generator import export_flow_to_polars
38
- from flowfile_core.flowfile.database_connection_manager.db_connections import (store_database_connection,
39
- get_database_connection,
40
- delete_database_connection,
41
- get_all_database_connections_interface)
40
+ from flowfile_core.flowfile.database_connection_manager.db_connections import (
41
+ delete_database_connection,
42
+ get_all_database_connections_interface,
43
+ get_database_connection,
44
+ store_database_connection,
45
+ )
42
46
  from flowfile_core.flowfile.extensions import get_instant_func_results
43
47
  from flowfile_core.flowfile.flow_graph import add_connection, delete_connection
44
48
  from flowfile_core.flowfile.sources.external_sources.sql_source.sql_source import create_sql_source_from_db_settings
45
49
  from flowfile_core.run_lock import get_flow_run_lock
46
- from flowfile_core.schemas import input_schema, schemas, output_model
50
+ from flowfile_core.schemas import input_schema, output_model, schemas
51
+ from flowfile_core.schemas.history_schema import HistoryActionType, HistoryState, OperationResponse, UndoRedoResult
47
52
  from flowfile_core.utils import excel_file_manager
48
53
  from flowfile_core.utils.fileManager import create_dir
49
54
  from flowfile_core.utils.utils import camel_case_to_snake_case
50
55
  from shared.storage_config import storage
51
56
 
52
-
53
57
  router = APIRouter(dependencies=[Depends(get_current_active_user)])
54
58
 
55
- # Initialize services
56
- file_explorer = SecureFileExplorer(
57
- start_path=storage.user_data_directory,
58
- sandbox_root=storage.user_data_directory
59
- )
60
-
61
59
 
62
60
  def get_node_model(setting_name_ref: str):
63
61
  """(Internal) Retrieves a node's Pydantic model from the input_schema module by its name."""
@@ -119,44 +117,16 @@ async def get_local_files(directory: str) -> list[FileInfo]:
119
117
  return files
120
118
 
121
119
 
122
- @router.get('/files/tree/', response_model=List[FileInfo], tags=['file manager'])
123
- async def get_current_files() -> List[FileInfo]:
124
- """Gets the contents of the file explorer's current directory."""
125
- f = file_explorer.list_contents()
126
- return f
127
-
128
-
129
- @router.post('/files/navigate_up/', response_model=str, tags=['file manager'])
130
- async def navigate_up() -> str:
131
- """Navigates the file explorer one directory level up."""
132
- file_explorer.navigate_up()
133
- return str(file_explorer.current_path)
134
-
135
-
136
- @router.post('/files/navigate_into/', response_model=str, tags=['file manager'])
137
- async def navigate_into_directory(directory_name: str) -> str:
138
- """Navigates the file explorer into a specified subdirectory."""
139
- file_explorer.navigate_into(directory_name)
140
- return str(file_explorer.current_path)
141
-
142
-
143
- @router.post('/files/navigate_to/', tags=['file manager'])
144
- async def navigate_to_directory(directory_name: str) -> str:
145
- """Navigates the file explorer to an absolute directory path."""
146
- file_explorer.navigate_to(directory_name)
147
- return str(file_explorer.current_path)
148
-
149
-
150
- @router.get('/files/current_path/', response_model=str, tags=['file manager'])
151
- async def get_current_path() -> str:
152
- """Returns the current absolute path of the file explorer."""
153
- return str(file_explorer.current_path)
120
+ @router.get('/files/default_path/', response_model=str, tags=['file manager'])
121
+ async def get_default_path() -> str:
122
+ """Returns the default starting path for the file browser (user data directory)."""
123
+ return str(storage.user_data_directory)
154
124
 
155
125
 
156
- @router.get('/files/directory_contents/', response_model=List[FileInfo], tags=['file manager'])
157
- async def get_directory_contents(directory: str, file_types: List[str] = None,
158
- include_hidden: bool = False) -> List[FileInfo]:
159
- """Gets the contents of an arbitrary directory path.
126
+ @router.get('/files/directory_contents/', response_model=list[FileInfo], tags=['file manager'])
127
+ async def get_directory_contents(directory: str, file_types: list[str] = None,
128
+ include_hidden: bool = False) -> list[FileInfo]:
129
+ """Gets the contents of a directory path.
160
130
 
161
131
  Args:
162
132
  directory: The absolute path to the directory.
@@ -174,12 +144,6 @@ async def get_directory_contents(directory: str, file_types: List[str] = None,
174
144
  HTTPException(404, 'Could not access the directory')
175
145
 
176
146
 
177
- @router.get('/files/current_directory_contents/', response_model=List[FileInfo], tags=['file manager'])
178
- async def get_current_directory_contents(file_types: List[str] = None, include_hidden: bool = False) -> List[FileInfo]:
179
- """Gets the contents of the file explorer's current directory."""
180
- return file_explorer.list_contents(file_types=file_types, show_hidden=include_hidden)
181
-
182
-
183
147
  @router.post('/files/create_directory', response_model=output_model.OutputDir, tags=['file manager'])
184
148
  def create_directory(new_directory: input_schema.NewDirectory) -> bool:
185
149
  """Creates a new directory at the specified path.
@@ -273,6 +237,10 @@ def apply_standard_layout(flow_id: int):
273
237
  raise HTTPException(status_code=404, detail="Flow not found")
274
238
  if flow.flow_settings.is_running:
275
239
  raise HTTPException(422, "Flow is running")
240
+
241
+ # Capture history BEFORE the layout change
242
+ flow.capture_history_snapshot(HistoryActionType.APPLY_LAYOUT, "Apply standard layout")
243
+
276
244
  flow.apply_layout()
277
245
 
278
246
 
@@ -309,14 +277,17 @@ def add_flow_input(input_data: input_schema.NodeDatasource):
309
277
  flow.add_datasource(input_data)
310
278
 
311
279
 
312
- @router.post('/editor/copy_node', tags=['editor'])
313
- def copy_node(node_id_to_copy_from: int, flow_id_to_copy_from: int, node_promise: input_schema.NodePromise):
280
+ @router.post('/editor/copy_node', tags=['editor'], response_model=OperationResponse)
281
+ def copy_node(node_id_to_copy_from: int, flow_id_to_copy_from: int, node_promise: input_schema.NodePromise) -> OperationResponse:
314
282
  """Copies an existing node's settings to a new node promise.
315
283
 
316
284
  Args:
317
285
  node_id_to_copy_from: The ID of the node to copy the settings from.
318
286
  flow_id_to_copy_from: The ID of the flow containing the source node.
319
287
  node_promise: A `NodePromise` representing the new node to be created.
288
+
289
+ Returns:
290
+ OperationResponse with current history state.
320
291
  """
321
292
  try:
322
293
  flow_to_copy_from = flow_file_handler.get_flow(flow_id_to_copy_from)
@@ -330,22 +301,32 @@ def copy_node(node_id_to_copy_from: int, flow_id_to_copy_from: int, node_promise
330
301
  if flow.flow_settings.is_running:
331
302
  raise HTTPException(422, "Flow is running")
332
303
 
304
+ # Capture history BEFORE the change
305
+ flow.capture_history_snapshot(
306
+ HistoryActionType.COPY_NODE,
307
+ f"Copy {node_promise.node_type} node",
308
+ node_id=node_promise.node_id
309
+ )
310
+
333
311
  if flow.get_node(node_promise.node_id) is not None:
334
312
  flow.delete_node(node_promise.node_id)
335
313
 
336
314
  if node_promise.node_type == "explore_data":
337
315
  flow.add_initial_node_analysis(node_promise)
338
- return
316
+ return OperationResponse(success=True, history=flow.get_history_state())
339
317
 
340
318
  flow.copy_node(node_promise, node_to_copy.setting_input, node_to_copy.node_type)
341
319
 
320
+ return OperationResponse(success=True, history=flow.get_history_state())
321
+
342
322
  except Exception as e:
343
323
  logger.error(e)
344
324
  raise HTTPException(422, str(e))
345
325
 
346
326
 
347
- @router.post('/editor/add_node/', tags=['editor'])
348
- def add_node(flow_id: int, node_id: int, node_type: str, pos_x: int = 0, pos_y: int = 0):
327
+ @router.post('/editor/add_node/', tags=['editor'], response_model=OperationResponse)
328
+ def add_node(flow_id: int, node_id: int, node_type: str, pos_x: int | float = 0,
329
+ pos_y: int | float = 0) -> OperationResponse | None:
349
330
  """Adds a new, unconfigured node (a "promise") to the flow graph.
350
331
 
351
332
  Args:
@@ -354,11 +335,19 @@ def add_node(flow_id: int, node_id: int, node_type: str, pos_x: int = 0, pos_y:
354
335
  node_type: The type of the node to add (e.g., 'filter', 'join').
355
336
  pos_x: The X coordinate for the node's position in the UI.
356
337
  pos_y: The Y coordinate for the node's position in the UI.
338
+
339
+ Returns:
340
+ OperationResponse with current history state.
357
341
  """
342
+ if isinstance(pos_x, float):
343
+ pos_x = int(pos_x)
344
+ if isinstance(pos_y, float):
345
+ pos_y = int(pos_y)
358
346
  flow = flow_file_handler.get_flow(flow_id)
359
347
  logger.info(f'Adding a promise for {node_type}')
360
348
  if flow.flow_settings.is_running:
361
349
  raise HTTPException(422, 'Flow is running')
350
+
362
351
  node = flow.get_node(node_id)
363
352
  if node is not None:
364
353
  flow.delete_node(node_id)
@@ -367,42 +356,94 @@ def add_node(flow_id: int, node_id: int, node_type: str, pos_x: int = 0, pos_y:
367
356
  node_type=node_type)
368
357
  if node_type == 'explore_data':
369
358
  flow.add_initial_node_analysis(node_promise)
370
- return
371
359
  else:
372
- logger.info("Adding node")
373
- flow.add_node_promise(node_promise)
374
-
375
- if check_if_has_default_setting(node_type):
376
- logger.info(f'Found standard settings for {node_type}, trying to upload them')
377
- setting_name_ref = 'node' + node_type.replace('_', '')
378
- node_model = get_node_model(setting_name_ref)
379
- add_func = getattr(flow, 'add_' + node_type)
380
- initial_settings = node_model(flow_id=flow_id, node_id=node_id, cache_results=False,
381
- pos_x=pos_x, pos_y=pos_y, node_type=node_type)
382
- add_func(initial_settings)
360
+ # Capture state BEFORE adding node (for batched history)
361
+ pre_snapshot = flow.get_flowfile_data() if flow.flow_settings.track_history else None
383
362
 
363
+ logger.info("Adding node")
364
+ # Add node without individual history tracking
365
+ flow.add_node_promise(node_promise, track_history=False)
366
+
367
+ if check_if_has_default_setting(node_type):
368
+ logger.info(f'Found standard settings for {node_type}, trying to upload them')
369
+ setting_name_ref = 'node' + node_type.replace('_', '')
370
+ node_model = get_node_model(setting_name_ref)
371
+
372
+ # Temporarily disable history tracking for initial settings
373
+ original_track_history = flow.flow_settings.track_history
374
+ flow.flow_settings.track_history = False
375
+ try:
376
+ add_func = getattr(flow, 'add_' + node_type)
377
+ initial_settings = node_model(flow_id=flow_id, node_id=node_id, cache_results=False,
378
+ pos_x=pos_x, pos_y=pos_y, node_type=node_type)
379
+ add_func(initial_settings)
380
+ finally:
381
+ flow.flow_settings.track_history = original_track_history
382
+
383
+ # Capture batched history entry for the whole add_node operation
384
+ if pre_snapshot is not None and flow.flow_settings.track_history:
385
+ from flowfile_core.schemas.history_schema import HistoryActionType
386
+ flow._history_manager.capture_if_changed(
387
+ flow,
388
+ pre_snapshot,
389
+ HistoryActionType.ADD_NODE,
390
+ f"Add {node_type} node",
391
+ node_id,
392
+ )
393
+ logger.info(f"History: Captured batched 'Add {node_type} node' entry")
394
+
395
+ logger.info(f"History state after add_node: {flow.get_history_state()}")
396
+ return OperationResponse(success=True, history=flow.get_history_state())
397
+
398
+
399
+ @router.post('/editor/delete_node/', tags=['editor'], response_model=OperationResponse)
400
+ def delete_node(flow_id: int | None, node_id: int) -> OperationResponse:
401
+ """Deletes a node from the flow graph.
384
402
 
385
- @router.post('/editor/delete_node/', tags=['editor'])
386
- def delete_node(flow_id: Optional[int], node_id: int):
387
- """Deletes a node from the flow graph."""
403
+ Returns:
404
+ OperationResponse with current history state.
405
+ """
388
406
  logger.info('Deleting node')
389
407
  flow = flow_file_handler.get_flow(flow_id)
390
408
  if flow.flow_settings.is_running:
391
409
  raise HTTPException(422, 'Flow is running')
410
+
411
+ # Capture history BEFORE the change
412
+ node = flow.get_node(node_id)
413
+ node_type = node.node_type if node else "unknown"
414
+ flow.capture_history_snapshot(HistoryActionType.DELETE_NODE, f"Delete {node_type} node", node_id=node_id)
415
+
392
416
  flow.delete_node(node_id)
393
417
 
418
+ return OperationResponse(success=True, history=flow.get_history_state())
419
+
394
420
 
395
- @router.post('/editor/delete_connection/', tags=['editor'])
396
- def delete_node_connection(flow_id: int, node_connection: input_schema.NodeConnection = None):
397
- """Deletes a connection (edge) between two nodes."""
421
+ @router.post('/editor/delete_connection/', tags=['editor'], response_model=OperationResponse)
422
+ def delete_node_connection(flow_id: int, node_connection: input_schema.NodeConnection = None) -> OperationResponse:
423
+ """Deletes a connection (edge) between two nodes.
424
+
425
+ Returns:
426
+ OperationResponse with current history state.
427
+ """
398
428
  flow_id = int(flow_id)
399
429
  logger.info(
400
430
  f'Deleting connection node {node_connection.output_connection.node_id} to node {node_connection.input_connection.node_id}')
401
431
  flow = flow_file_handler.get_flow(flow_id)
402
432
  if flow.flow_settings.is_running:
403
433
  raise HTTPException(422, 'Flow is running')
434
+
435
+ # Capture history BEFORE the change
436
+ from_id = node_connection.output_connection.node_id
437
+ to_id = node_connection.input_connection.node_id
438
+ flow.capture_history_snapshot(
439
+ HistoryActionType.DELETE_CONNECTION,
440
+ f"Delete connection {from_id} -> {to_id}"
441
+ )
442
+
404
443
  delete_connection(flow, node_connection)
405
444
 
445
+ return OperationResponse(success=True, history=flow.get_history_state())
446
+
406
447
 
407
448
  @router.post("/db_connection_lib", tags=['db_connections'])
408
449
  def create_db_connection(input_connection: input_schema.FullDatabaseConnection,
@@ -436,34 +477,49 @@ def delete_db_connection(connection_name: str,
436
477
 
437
478
 
438
479
  @router.get('/db_connection_lib', tags=['db_connections'],
439
- response_model=List[input_schema.FullDatabaseConnectionInterface])
480
+ response_model=list[input_schema.FullDatabaseConnectionInterface])
440
481
  def get_db_connections(
441
482
  db: Session = Depends(get_db),
442
- current_user=Depends(get_current_active_user)) -> List[input_schema.FullDatabaseConnectionInterface]:
483
+ current_user=Depends(get_current_active_user)) -> list[input_schema.FullDatabaseConnectionInterface]:
443
484
  """Retrieves all stored database connections for the current user (without passwords)."""
444
485
  return get_all_database_connections_interface(db, current_user.id)
445
486
 
446
487
 
447
- @router.post('/editor/connect_node/', tags=['editor'])
448
- def connect_node(flow_id: int, node_connection: input_schema.NodeConnection):
449
- """Creates a connection (edge) between two nodes in the flow graph."""
488
+ @router.post('/editor/connect_node/', tags=['editor'], response_model=OperationResponse)
489
+ def connect_node(flow_id: int, node_connection: input_schema.NodeConnection) -> OperationResponse:
490
+ """Creates a connection (edge) between two nodes in the flow graph.
491
+
492
+ Returns:
493
+ OperationResponse with current history state.
494
+ """
450
495
  flow = flow_file_handler.get_flow(flow_id)
451
496
  if flow is None:
452
497
  logger.info('could not find the flow')
453
498
  raise HTTPException(404, 'could not find the flow')
454
499
  if flow.flow_settings.is_running:
455
500
  raise HTTPException(422, 'Flow is running')
501
+
502
+ # Capture history BEFORE the change
503
+ from_id = node_connection.output_connection.node_id
504
+ to_id = node_connection.input_connection.node_id
505
+ flow.capture_history_snapshot(
506
+ HistoryActionType.ADD_CONNECTION,
507
+ f"Connect {from_id} -> {to_id}"
508
+ )
509
+
456
510
  add_connection(flow, node_connection)
457
511
 
512
+ return OperationResponse(success=True, history=flow.get_history_state())
513
+
458
514
 
459
- @router.get('/editor/expression_doc', tags=['editor'], response_model=List[output_model.ExpressionsOverview])
460
- def get_expression_doc() -> List[output_model.ExpressionsOverview]:
515
+ @router.get('/editor/expression_doc', tags=['editor'], response_model=list[output_model.ExpressionsOverview])
516
+ def get_expression_doc() -> list[output_model.ExpressionsOverview]:
461
517
  """Retrieves documentation for available Polars expressions."""
462
518
  return get_expression_overview()
463
519
 
464
520
 
465
- @router.get('/editor/expressions', tags=['editor'], response_model=List[str])
466
- def get_expressions() -> List[str]:
521
+ @router.get('/editor/expressions', tags=['editor'], response_model=list[str])
522
+ def get_expressions() -> list[str]:
467
523
  """Retrieves a list of all available Flowfile expression names."""
468
524
  return get_all_expressions()
469
525
 
@@ -517,17 +573,92 @@ def close_flow(flow_id: int, current_user=Depends(get_current_active_user)) -> N
517
573
  flow_file_handler.delete_flow(flow_id, user_id=user_id)
518
574
 
519
575
 
520
- @router.post('/update_settings/', tags=['transform'])
521
- def add_generic_settings(input_data: Dict[str, Any], node_type: str, current_user=Depends(get_current_active_user)):
576
+ # ==================== History/Undo-Redo Endpoints ====================
577
+
578
+ @router.post('/editor/undo/', tags=['editor'], response_model=UndoRedoResult)
579
+ def undo_action(flow_id: int) -> UndoRedoResult:
580
+ """Undo the last action on the flow graph.
581
+
582
+ Args:
583
+ flow_id: The ID of the flow to undo.
584
+
585
+ Returns:
586
+ UndoRedoResult indicating success or failure.
587
+ """
588
+ flow = flow_file_handler.get_flow(flow_id)
589
+ if flow is None:
590
+ raise HTTPException(404, 'Could not find the flow')
591
+ if flow.flow_settings.is_running:
592
+ raise HTTPException(422, 'Flow is running')
593
+ return flow.undo()
594
+
595
+
596
+ @router.post('/editor/redo/', tags=['editor'], response_model=UndoRedoResult)
597
+ def redo_action(flow_id: int) -> UndoRedoResult:
598
+ """Redo the last undone action on the flow graph.
599
+
600
+ Args:
601
+ flow_id: The ID of the flow to redo.
602
+
603
+ Returns:
604
+ UndoRedoResult indicating success or failure.
605
+ """
606
+ flow = flow_file_handler.get_flow(flow_id)
607
+ if flow is None:
608
+ raise HTTPException(404, 'Could not find the flow')
609
+ if flow.flow_settings.is_running:
610
+ raise HTTPException(422, 'Flow is running')
611
+ return flow.redo()
612
+
613
+
614
+ @router.get('/editor/history_status/', tags=['editor'], response_model=HistoryState)
615
+ def get_history_status(flow_id: int) -> HistoryState:
616
+ """Get the current state of the history system for a flow.
617
+
618
+ Args:
619
+ flow_id: The ID of the flow to get history status for.
620
+
621
+ Returns:
622
+ HistoryState with information about available undo/redo operations.
623
+ """
624
+ flow = flow_file_handler.get_flow(flow_id)
625
+ if flow is None:
626
+ raise HTTPException(404, 'Could not find the flow')
627
+ return flow.get_history_state()
628
+
629
+
630
+ @router.post('/editor/history_clear/', tags=['editor'])
631
+ def clear_history(flow_id: int):
632
+ """Clear all history for a flow.
633
+
634
+ Args:
635
+ flow_id: The ID of the flow to clear history for.
636
+ """
637
+ flow = flow_file_handler.get_flow(flow_id)
638
+ if flow is None:
639
+ raise HTTPException(404, 'Could not find the flow')
640
+ flow._history_manager.clear()
641
+ return {"message": "History cleared successfully"}
642
+
643
+
644
+ # ==================== End History Endpoints ====================
645
+
646
+
647
+ @router.post('/update_settings/', tags=['transform'], response_model=OperationResponse)
648
+ def add_generic_settings(input_data: dict[str, Any], node_type: str, current_user=Depends(get_current_active_user)) -> OperationResponse:
522
649
  """A generic endpoint to update the settings of any node.
523
650
 
524
651
  This endpoint dynamically determines the correct Pydantic model and update
525
652
  function based on the `node_type` parameter.
653
+
654
+ Returns:
655
+ OperationResponse with current history state.
526
656
  """
527
657
  input_data['user_id'] = current_user.id
528
658
  node_type = camel_case_to_snake_case(node_type)
529
659
  flow_id = int(input_data.get('flow_id'))
530
- logger.info(f'Updating the data for flow: {flow_id}, node {input_data["node_id"]}')
660
+ node_id = int(input_data.get('node_id'))
661
+ logger.info(f'Updating the data for flow: {flow_id}, node {node_id}')
531
662
  flow = flow_file_handler.get_flow(flow_id)
532
663
  if flow.flow_settings.is_running:
533
664
  raise HTTPException(422, 'Flow is running')
@@ -548,13 +679,16 @@ def add_generic_settings(input_data: Dict[str, Any], node_type: str, current_use
548
679
  if parsed_input is None:
549
680
  raise HTTPException(404, 'could not find the interface')
550
681
  try:
682
+ # History capture is handled by the decorator on each add_* method
551
683
  add_func(parsed_input)
552
684
  except Exception as e:
553
685
  logger.error(e)
554
686
  raise HTTPException(419, str(f'error: {e}'))
555
687
 
688
+ return OperationResponse(success=True, history=flow.get_history_state())
556
689
 
557
- @router.get('/files/available_flow_files', tags=['editor'], response_model=List[FileInfo])
690
+
691
+ @router.get('/files/available_flow_files', tags=['editor'], response_model=list[FileInfo])
558
692
  def get_list_of_saved_flows(path: str):
559
693
  """Scans a directory for saved flow files (`.flowfile`)."""
560
694
  try:
@@ -571,8 +705,8 @@ def get_list_of_saved_flows(path: str):
571
705
  return []
572
706
 
573
707
 
574
- @router.get('/node_list', response_model=List[schemas.NodeTemplate])
575
- def get_node_list() -> List[schemas.NodeTemplate]:
708
+ @router.get('/node_list', response_model=list[schemas.NodeTemplate])
709
+ def get_node_list() -> list[schemas.NodeTemplate]:
576
710
  """Retrieves the list of all available node types and their templates."""
577
711
  return nodes_list
578
712
 
@@ -612,6 +746,91 @@ def get_description_node(flow_id: int, node_id: int):
612
746
  return node.setting_input.description
613
747
 
614
748
 
749
+ @router.post('/node/reference/', tags=['editor'])
750
+ def update_reference_node(flow_id: int, node_id: int, reference: str = Body(...)):
751
+ """Updates the reference identifier for a specific node.
752
+
753
+ The reference must be:
754
+ - Lowercase only
755
+ - No spaces allowed
756
+ - Unique across all nodes in the flow
757
+ """
758
+ try:
759
+ flow = flow_file_handler.get_flow(flow_id)
760
+ node = flow.get_node(node_id)
761
+ except:
762
+ raise HTTPException(404, 'Could not find the node')
763
+ if node is None:
764
+ raise HTTPException(404, 'Could not find the node')
765
+
766
+ # Handle empty reference (allow clearing)
767
+ if reference == "" or reference is None:
768
+ node.setting_input.node_reference = None
769
+ return True
770
+
771
+ # Validate: lowercase only, no spaces
772
+ if " " in reference:
773
+ raise HTTPException(422, 'Reference cannot contain spaces')
774
+ if reference != reference.lower():
775
+ raise HTTPException(422, 'Reference must be lowercase')
776
+
777
+ # Validate: unique across all nodes in the flow
778
+ for other_node in flow.nodes:
779
+ if other_node.node_id != node_id:
780
+ other_ref = getattr(other_node.setting_input, 'node_reference', None)
781
+ if other_ref and other_ref == reference:
782
+ raise HTTPException(422, f'Reference "{reference}" is already used by another node')
783
+
784
+ node.setting_input.node_reference = reference
785
+ return True
786
+
787
+
788
+ @router.get('/node/reference', tags=['editor'])
789
+ def get_reference_node(flow_id: int, node_id: int):
790
+ """Retrieves the reference identifier for a specific node."""
791
+ try:
792
+ node = flow_file_handler.get_flow(flow_id).get_node(node_id)
793
+ except:
794
+ raise HTTPException(404, 'Could not find the node')
795
+ if node is None:
796
+ raise HTTPException(404, 'Could not find the node')
797
+ return node.setting_input.node_reference or ""
798
+
799
+
800
+ @router.get('/node/validate_reference', tags=['editor'])
801
+ def validate_node_reference(flow_id: int, node_id: int, reference: str):
802
+ """Validates if a reference is valid and unique for a node.
803
+
804
+ Returns:
805
+ Dict with 'valid' (bool) and 'error' (str or None) fields.
806
+ """
807
+ try:
808
+ flow = flow_file_handler.get_flow(flow_id)
809
+ except:
810
+ raise HTTPException(404, 'Could not find the flow')
811
+
812
+ # Handle empty reference (always valid - means use default)
813
+ if reference == "" or reference is None:
814
+ return {"valid": True, "error": None}
815
+
816
+ # Validate: lowercase only
817
+ if reference != reference.lower():
818
+ return {"valid": False, "error": "Reference must be lowercase"}
819
+
820
+ # Validate: no spaces
821
+ if " " in reference:
822
+ return {"valid": False, "error": "Reference cannot contain spaces"}
823
+
824
+ # Validate: unique across all nodes in the flow
825
+ for other_node in flow.nodes:
826
+ if other_node.node_id != node_id:
827
+ other_ref = getattr(other_node.setting_input, 'node_reference', None)
828
+ if other_ref and other_ref == reference:
829
+ return {"valid": False, "error": f'Reference "{reference}" is already used by another node'}
830
+
831
+ return {"valid": True, "error": None}
832
+
833
+
615
834
  @router.get('/node/data', response_model=output_model.TableExample, tags=['editor'])
616
835
  def get_table_example(flow_id: int, node_id: int):
617
836
  """Retrieves a data preview (schema and sample rows) for a node's output."""
@@ -620,8 +839,8 @@ def get_table_example(flow_id: int, node_id: int):
620
839
  return node.get_table_example(True)
621
840
 
622
841
 
623
- @router.get('/node/downstream_node_ids', response_model=List[int], tags=['editor'])
624
- async def get_downstream_node_ids(flow_id: int, node_id: int) -> List[int]:
842
+ @router.get('/node/downstream_node_ids', response_model=list[int], tags=['editor'])
843
+ async def get_downstream_node_ids(flow_id: int, node_id: int) -> list[int]:
625
844
  """Gets a list of all node IDs that are downstream dependencies of a given node."""
626
845
  flow = flow_file_handler.get_flow(flow_id)
627
846
  node = flow.get_node(node_id)
@@ -648,7 +867,7 @@ def save_flow(flow_id: int, flow_path: str = None):
648
867
 
649
868
 
650
869
  @router.get('/flow_data', tags=['manager'])
651
- def get_flow_frontend_data(flow_id: Optional[int] = 1):
870
+ def get_flow_frontend_data(flow_id: int | None = 1):
652
871
  """Retrieves the data needed to render the flow graph in the frontend."""
653
872
  flow = flow_file_handler.get_flow(flow_id)
654
873
  if flow is None:
@@ -657,7 +876,7 @@ def get_flow_frontend_data(flow_id: Optional[int] = 1):
657
876
 
658
877
 
659
878
  @router.get('/flow_settings', tags=['manager'], response_model=schemas.FlowSettings)
660
- def get_flow_settings(flow_id: Optional[int] = 1) -> schemas.FlowSettings:
879
+ def get_flow_settings(flow_id: int | None = 1) -> schemas.FlowSettings:
661
880
  """Retrieves the main settings for a flow."""
662
881
  flow = flow_file_handler.get_flow(flow_id)
663
882
  if flow is None: