Flowfile 0.3.10__py3-none-any.whl → 0.4.0__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.

Potentially problematic release.


This version of Flowfile might be problematic. Click here for more details.

Files changed (108) hide show
  1. flowfile/__init__.py +6 -1
  2. flowfile/web/static/assets/{CloudConnectionManager-d7c2c028.js → CloudConnectionManager-109ecc3c.js} +2 -2
  3. flowfile/web/static/assets/{CloudStorageReader-d467329f.js → CloudStorageReader-19cdd67a.js} +6 -6
  4. flowfile/web/static/assets/{CloudStorageWriter-071b8b00.js → CloudStorageWriter-48e0ae20.js} +6 -6
  5. flowfile/web/static/assets/ColumnSelector-47996a16.css +10 -0
  6. flowfile/web/static/assets/ColumnSelector-ecaf7c44.js +83 -0
  7. flowfile/web/static/assets/{ContextMenu-2dea5e27.js → ContextMenu-2b348c4c.js} +1 -1
  8. flowfile/web/static/assets/{ContextMenu-785554c4.js → ContextMenu-a779eed7.js} +1 -1
  9. flowfile/web/static/assets/{ContextMenu-a51e19ea.js → ContextMenu-eca26a03.js} +1 -1
  10. flowfile/web/static/assets/{CrossJoin-cf68ec7a.js → CrossJoin-a88f8142.js} +7 -7
  11. flowfile/web/static/assets/CustomNode-74a37f74.css +32 -0
  12. flowfile/web/static/assets/CustomNode-cb863dff.js +211 -0
  13. flowfile/web/static/assets/{DatabaseConnectionSettings-435c5dd8.js → DatabaseConnectionSettings-819d3267.js} +2 -2
  14. flowfile/web/static/assets/{DatabaseManager-349e33a8.js → DatabaseManager-84ee2834.js} +2 -2
  15. flowfile/web/static/assets/{DatabaseReader-8075bd28.js → DatabaseReader-060dd412.js} +9 -9
  16. flowfile/web/static/assets/{DatabaseWriter-3e2dda89.js → DatabaseWriter-7fc7750f.js} +8 -8
  17. flowfile/web/static/assets/{ExploreData-76ec698c.js → ExploreData-82c95991.js} +5 -5
  18. flowfile/web/static/assets/{ExternalSource-609a265c.js → ExternalSource-e1a6ddc7.js} +5 -5
  19. flowfile/web/static/assets/{Filter-97cff793.js → Filter-8aca894a.js} +7 -7
  20. flowfile/web/static/assets/{Formula-09de0ec9.js → Formula-e33686d9.js} +7 -7
  21. flowfile/web/static/assets/{FuzzyMatch-bdf70248.js → FuzzyMatch-abda150d.js} +8 -8
  22. flowfile/web/static/assets/{GraphSolver-0b5a0e05.js → GraphSolver-4ecad1d7.js} +6 -6
  23. flowfile/web/static/assets/{GroupBy-eaddadde.js → GroupBy-656d07f3.js} +5 -5
  24. flowfile/web/static/assets/{Join-3313371b.js → Join-b84ec849.js} +8 -8
  25. flowfile/web/static/assets/{ManualInput-e8bfc0be.js → ManualInput-346f4135.js} +4 -4
  26. flowfile/web/static/assets/MultiSelect-61b98268.js +5 -0
  27. flowfile/web/static/assets/MultiSelect.vue_vue_type_script_setup_true_lang-2a7c8312.js +63 -0
  28. flowfile/web/static/assets/NumericInput-e36602c2.js +5 -0
  29. flowfile/web/static/assets/NumericInput.vue_vue_type_script_setup_true_lang-211a1990.js +35 -0
  30. flowfile/web/static/assets/{Output-7303bb09.js → Output-eb041599.js} +6 -6
  31. flowfile/web/static/assets/{Pivot-3b1c54ef.js → Pivot-f5c774f4.js} +7 -7
  32. flowfile/web/static/assets/{PivotValidation-3bb36c8f.js → PivotValidation-26546cbc.js} +1 -1
  33. flowfile/web/static/assets/{PivotValidation-eaa819c0.js → PivotValidation-e150a24b.js} +1 -1
  34. flowfile/web/static/assets/{PolarsCode-aa12e25d.js → PolarsCode-da3a7abf.js} +5 -5
  35. flowfile/web/static/assets/{Read-a2bfc618.js → Read-0c768769.js} +8 -8
  36. flowfile/web/static/assets/{RecordCount-aa0dc082.js → RecordCount-84736276.js} +4 -4
  37. flowfile/web/static/assets/{RecordId-48ee1a3b.js → RecordId-60055e6d.js} +6 -6
  38. flowfile/web/static/assets/{SQLQueryComponent-e149dbf2.js → SQLQueryComponent-8a486004.js} +1 -1
  39. flowfile/web/static/assets/{Sample-f06cb97a.js → Sample-2d662611.js} +4 -4
  40. flowfile/web/static/assets/{SecretManager-37f34886.js → SecretManager-ef586cab.js} +2 -2
  41. flowfile/web/static/assets/{Select-b60e6c47.js → Select-2e4a6965.js} +7 -7
  42. flowfile/web/static/assets/{SettingsSection-75b6cf4f.js → SettingsSection-310b61c0.js} +1 -1
  43. flowfile/web/static/assets/{SettingsSection-e57a672e.js → SettingsSection-5634f439.js} +1 -1
  44. flowfile/web/static/assets/{SettingsSection-70e5a7b1.js → SettingsSection-7c68b19f.js} +1 -1
  45. flowfile/web/static/assets/SingleSelect-7298811a.js +5 -0
  46. flowfile/web/static/assets/SingleSelect.vue_vue_type_script_setup_true_lang-43807bad.js +62 -0
  47. flowfile/web/static/assets/SliderInput-53105476.js +40 -0
  48. flowfile/web/static/assets/SliderInput-b8fb6a8c.css +4 -0
  49. flowfile/web/static/assets/{Sort-51b1ee4d.js → Sort-4fdebe74.js} +5 -5
  50. flowfile/web/static/assets/TextInput-28366b7e.js +5 -0
  51. flowfile/web/static/assets/TextInput.vue_vue_type_script_setup_true_lang-9cad14ba.js +32 -0
  52. flowfile/web/static/assets/{TextToRows-26835f8f.js → TextToRows-73ffa692.js} +7 -7
  53. flowfile/web/static/assets/ToggleSwitch-598add30.js +5 -0
  54. flowfile/web/static/assets/ToggleSwitch.vue_vue_type_script_setup_true_lang-f620cd32.js +31 -0
  55. flowfile/web/static/assets/{UnavailableFields-88a4cd0c.js → UnavailableFields-66239e83.js} +2 -2
  56. flowfile/web/static/assets/{Union-4d0088eb.js → Union-26b10614.js} +4 -4
  57. flowfile/web/static/assets/{Unique-7d554a62.js → Unique-33b9edbb.js} +7 -7
  58. flowfile/web/static/assets/{Unpivot-4668595c.js → Unpivot-ef69d0e2.js} +6 -6
  59. flowfile/web/static/assets/{UnpivotValidation-d4f0e0e8.js → UnpivotValidation-8658388e.js} +1 -1
  60. flowfile/web/static/assets/{VueGraphicWalker-5324d566.js → VueGraphicWalker-4d7861f4.js} +1 -1
  61. flowfile/web/static/assets/{api-31e4fea6.js → api-2d1394bd.js} +1 -1
  62. flowfile/web/static/assets/{api-271ed117.js → api-c908fffe.js} +1 -1
  63. flowfile/web/static/assets/{designer-bf3d9487.js → designer-1667687d.js} +24 -16
  64. flowfile/web/static/assets/{designer-091bdc3f.css → designer-665e9408.css} +18 -18
  65. flowfile/web/static/assets/{documentation-4d0a1cea.js → documentation-5eed779e.js} +1 -1
  66. flowfile/web/static/assets/{dropDown-025888df.js → dropDown-41ebe3c2.js} +1 -1
  67. flowfile/web/static/assets/{fullEditor-1df991ec.js → fullEditor-0670d32d.js} +2 -2
  68. flowfile/web/static/assets/{genericNodeSettings-d3b2b2ac.js → genericNodeSettings-38410ebf.js} +3 -3
  69. flowfile/web/static/assets/{index-681a3ed0.css → index-50508d4d.css} +8 -0
  70. flowfile/web/static/assets/{index-d0518598.js → index-5ec791df.js} +6 -6
  71. flowfile/web/static/assets/{outputCsv-d8457527.js → outputCsv-059583b6.js} +1 -1
  72. flowfile/web/static/assets/{outputExcel-be89153e.js → outputExcel-76b1e02c.js} +1 -1
  73. flowfile/web/static/assets/{outputParquet-fabb445a.js → outputParquet-440fd4c7.js} +1 -1
  74. flowfile/web/static/assets/{readCsv-e8359522.js → readCsv-9813903a.js} +1 -1
  75. flowfile/web/static/assets/{readExcel-dabaf51b.js → readExcel-7f40d237.js} +3 -3
  76. flowfile/web/static/assets/{readParquet-e0771ef2.js → readParquet-22d56002.js} +1 -1
  77. flowfile/web/static/assets/{secretApi-ce823eee.js → secretApi-b3cb072e.js} +1 -1
  78. flowfile/web/static/assets/{selectDynamic-5476546e.js → selectDynamic-7ad95bca.js} +3 -3
  79. flowfile/web/static/assets/user-defined-icon-0ae16c90.png +0 -0
  80. flowfile/web/static/assets/{vue-codemirror.esm-9ed00d50.js → vue-codemirror.esm-b1dfaa46.js} +33 -3
  81. flowfile/web/static/assets/{vue-content-loader.es-7bca2d9b.js → vue-content-loader.es-22bac17c.js} +1 -1
  82. flowfile/web/static/index.html +2 -2
  83. {flowfile-0.3.10.dist-info → flowfile-0.4.0.dist-info}/METADATA +1 -1
  84. {flowfile-0.3.10.dist-info → flowfile-0.4.0.dist-info}/RECORD +108 -82
  85. flowfile_core/configs/node_store/__init__.py +30 -0
  86. flowfile_core/configs/node_store/nodes.py +383 -358
  87. flowfile_core/configs/node_store/user_defined_node_registry.py +193 -0
  88. flowfile_core/flowfile/flow_data_engine/flow_file_column/interface.py +4 -0
  89. flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +19 -34
  90. flowfile_core/flowfile/flow_data_engine/flow_file_column/type_registry.py +36 -0
  91. flowfile_core/flowfile/flow_graph.py +20 -1
  92. flowfile_core/flowfile/flow_node/flow_node.py +4 -4
  93. flowfile_core/flowfile/manage/open_flowfile.py +9 -1
  94. flowfile_core/flowfile/node_designer/__init__.py +47 -0
  95. flowfile_core/flowfile/node_designer/_type_registry.py +197 -0
  96. flowfile_core/flowfile/node_designer/custom_node.py +371 -0
  97. flowfile_core/flowfile/node_designer/data_types.py +146 -0
  98. flowfile_core/flowfile/node_designer/ui_components.py +277 -0
  99. flowfile_core/main.py +2 -1
  100. flowfile_core/routes/routes.py +16 -20
  101. flowfile_core/routes/user_defined_components.py +55 -0
  102. flowfile_core/schemas/input_schema.py +8 -1
  103. flowfile_core/schemas/schemas.py +6 -3
  104. flowfile_core/utils/validate_setup.py +3 -1
  105. shared/storage_config.py +17 -2
  106. {flowfile-0.3.10.dist-info → flowfile-0.4.0.dist-info}/LICENSE +0 -0
  107. {flowfile-0.3.10.dist-info → flowfile-0.4.0.dist-info}/WHEEL +0 -0
  108. {flowfile-0.3.10.dist-info → flowfile-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,277 @@
1
+ # ui_components.py - Updated ColumnSelector
2
+
3
+ from typing import List, Optional, Any, Literal, Union, Type, Tuple, Dict
4
+
5
+ from pydantic import Field, BaseModel, computed_field
6
+
7
+ from flowfile_core.flowfile.node_designer._type_registry import normalize_type_spec
8
+ # Public API import
9
+ from flowfile_core.flowfile.node_designer.data_types import DataType, TypeSpec
10
+
11
+ InputType = Literal["text", "number", "secret", "array", "date", "boolean"]
12
+
13
+
14
+ def normalize_input_to_data_types(
15
+ v: Any
16
+ ) -> Union[Literal["ALL"], List[DataType]]:
17
+ """
18
+ Normalizes a wide variety of inputs to either 'ALL' or a sorted list of DataType enums.
19
+ This function is used as a Pydantic BeforeValidator.
20
+
21
+ Args:
22
+ v: The input value to normalize. Can be a string, a list of strings,
23
+ a DataType, a TypeGroup, or a list of those.
24
+
25
+ Returns:
26
+ Either the string "ALL" or a sorted list of unique DataType enums.
27
+ """
28
+ if v == "ALL":
29
+ return "ALL"
30
+ if isinstance(v, list) and all(isinstance(item, DataType) for item in v):
31
+ return v
32
+
33
+ normalized_set = normalize_type_spec(v)
34
+
35
+ if normalized_set == set(DataType):
36
+ return "ALL"
37
+
38
+ return sorted(list(normalized_set), key=lambda x: x.value)
39
+
40
+
41
+ class FlowfileInComponent(BaseModel):
42
+ """
43
+ Base class for all UI components in the node settings panel.
44
+
45
+ This class provides the common attributes and methods that all UI components share.
46
+ It's not meant to be used directly, but rather to be inherited by specific
47
+ component classes.
48
+ """
49
+ component_type: str = Field(..., description="Type of the UI component")
50
+ value: Any = None
51
+ label: Optional[str] = None
52
+ input_type: InputType
53
+
54
+ def set_value(self, value: Any):
55
+ """
56
+ Sets the value of the component, received from the frontend.
57
+
58
+ This method is used internally by the framework to populate the component's
59
+ value when a user interacts with the UI.
60
+
61
+ Args:
62
+ value: The new value for the component.
63
+
64
+ Returns:
65
+ The component instance with the updated value.
66
+ """
67
+ self.value = value
68
+ return self
69
+
70
+
71
+ class IncomingColumns:
72
+ """
73
+ A marker class used in `SingleSelect` and `MultiSelect` components.
74
+
75
+ When `options` is set to this class, the component will be dynamically
76
+ populated with the column names from the node's input dataframe.
77
+ This allows users to select from the available columns at runtime.
78
+
79
+ Example:
80
+ class MyNodeSettings(NodeSettings):
81
+ column_to_process = SingleSelect(
82
+ label="Select a column",
83
+ options=IncomingColumns
84
+ )
85
+ """
86
+ pass
87
+
88
+
89
+ class ColumnSelector(FlowfileInComponent):
90
+ """
91
+ A UI component that allows users to select one or more columns from the
92
+ input dataframe, with an optional filter based on column data types.
93
+
94
+ This is particularly useful when a node operation should only be applied
95
+ to columns of a specific type (e.g., numeric, string, date).
96
+ """
97
+ component_type: Literal["ColumnSelector"] = "ColumnSelector"
98
+ required: bool = False
99
+ multiple: bool = False
100
+ input_type: InputType = "text"
101
+
102
+ # Normalized output: either "ALL" or list of DataType enums
103
+ data_type_filter_input: TypeSpec = Field(
104
+ default="ALL",
105
+ alias="data_types",
106
+ repr=False,
107
+ exclude=True
108
+ )
109
+
110
+ class Config:
111
+ arbitrary_types_allowed = True
112
+
113
+ @computed_field
114
+ @property
115
+ def data_types_filter(self) -> Union[Literal["ALL"], List[DataType]]:
116
+ """
117
+ A computed field that normalizes the `data_type_filter_input` into a
118
+ standardized format for the frontend.
119
+ """
120
+ return normalize_input_to_data_types(self.data_type_filter_input)
121
+
122
+ def model_dump(self, **kwargs) -> dict:
123
+ """
124
+ Overrides the default `model_dump` to ensure `data_types` is in the
125
+ correct format for the frontend.
126
+ """
127
+ data = super().model_dump(**kwargs)
128
+ if 'data_types_filter' in data and data['data_types_filter'] != "ALL":
129
+ data['data_types'] = sorted([dt.value for dt in data['data_types_filter']])
130
+ return data
131
+
132
+
133
+ class TextInput(FlowfileInComponent):
134
+ """A standard text input field for capturing string values."""
135
+ component_type: Literal["TextInput"] = "TextInput"
136
+ default: Optional[str] = ""
137
+ placeholder: Optional[str] = ""
138
+ input_type: InputType = "text"
139
+
140
+ def __init__(self, **data):
141
+ super().__init__(**data)
142
+ if self.value is None and self.default is not None:
143
+ self.value = self.default
144
+
145
+
146
+ class NumericInput(FlowfileInComponent):
147
+ """A numeric input field with optional minimum and maximum value validation."""
148
+ component_type: Literal["NumericInput"] = "NumericInput"
149
+ default: Optional[float] = None
150
+ min_value: Optional[float] = None
151
+ max_value: Optional[float] = None
152
+ input_type: InputType = "number"
153
+
154
+ def __init__(self, **data):
155
+ super().__init__(**data)
156
+ if self.value is None and self.default is not None:
157
+ self.value = self.default
158
+
159
+
160
+ class ToggleSwitch(FlowfileInComponent):
161
+ """A boolean toggle switch, typically used for enabling or disabling a feature."""
162
+ component_type: Literal["ToggleSwitch"] = "ToggleSwitch"
163
+ default: bool = False
164
+ description: Optional[str] = None
165
+ input_type: InputType = "boolean"
166
+
167
+ def __init__(self, **data):
168
+ super().__init__(**data)
169
+ if self.value is None:
170
+ self.value = self.default
171
+
172
+ def __bool__(self):
173
+ """Allows the component instance to be evaluated as a boolean."""
174
+ return bool(self.value)
175
+
176
+
177
+ class SingleSelect(FlowfileInComponent):
178
+ """
179
+ A dropdown menu for selecting a single option from a list.
180
+
181
+ The options can be a static list of strings or tuples, or they can be
182
+ dynamically populated from the input dataframe's columns by using the
183
+ `IncomingColumns` marker.
184
+ """
185
+ component_type: Literal["SingleSelect"] = "SingleSelect"
186
+ options: Union[List[Union[str, Tuple[str, Any]]], Type[IncomingColumns]]
187
+ default: Optional[Any] = None
188
+ input_type: InputType = "text"
189
+
190
+ def __init__(self, **data):
191
+ super().__init__(**data)
192
+ if self.value is None and self.default is not None:
193
+ self.value = self.default
194
+
195
+
196
+ class MultiSelect(FlowfileInComponent):
197
+ """
198
+ A multi-select dropdown for choosing multiple options from a list.
199
+
200
+ Like `SingleSelect`, the options can be static or dynamically populated
201
+ from the input columns using the `IncomingColumns` marker.
202
+ """
203
+ component_type: Literal["MultiSelect"] = "MultiSelect"
204
+ options: Union[List[Union[str, Tuple[str, Any]]], Type[IncomingColumns]]
205
+ default: List[Any] = Field(default_factory=list)
206
+ input_type: InputType = "array"
207
+
208
+ def __init__(self, **data):
209
+ super().__init__(**data)
210
+ if self.value is None:
211
+ self.value = self.default if self.default else []
212
+
213
+
214
+ class Section(BaseModel):
215
+ """
216
+ A container for grouping related UI components in the node settings panel.
217
+
218
+ Sections help organize the UI by grouping components under a common title
219
+ and description. Components can be added as keyword arguments during
220
+ initialization or afterward.
221
+
222
+ Example:
223
+ main_section = Section(
224
+ title="Main Settings",
225
+ description="Configure the primary behavior of the node.",
226
+ my_text_input=TextInput(label="Enter a value")
227
+ )
228
+ """
229
+ title: Optional[str] = None
230
+ description: Optional[str] = None
231
+ hidden: bool = False
232
+
233
+ class Config:
234
+ extra = 'allow'
235
+ arbitrary_types_allowed = True
236
+
237
+ def __init__(self, **data):
238
+ """
239
+ Initialize a Section with components as keyword arguments.
240
+ """
241
+ super().__init__(**data)
242
+
243
+ def __call__(self, **kwargs) -> 'Section':
244
+ """
245
+ Allows adding components to the section after initialization.
246
+
247
+ This makes it possible to build up a section dynamically.
248
+ """
249
+ for key, value in kwargs.items():
250
+ setattr(self, key, value)
251
+ return self
252
+
253
+ def get_components(self) -> Dict[str, FlowfileInComponent]:
254
+ """
255
+ Get all FlowfileInComponent instances from the section.
256
+
257
+ This method collects all the UI components that have been added to the
258
+ section, whether as defined fields or as extra fields.
259
+
260
+ Returns:
261
+ A dictionary mapping component names to their instances.
262
+ """
263
+ components = {}
264
+
265
+ # Get from extra fields
266
+ for key, value in getattr(self, '__pydantic_extra__', {}).items():
267
+ if isinstance(value, FlowfileInComponent):
268
+ components[key] = value
269
+
270
+ # Get from defined fields (excluding metadata)
271
+ for field_name in self.model_fields:
272
+ if field_name not in {'title', 'description', 'hidden'}:
273
+ value = getattr(self, field_name, None)
274
+ if isinstance(value, FlowfileInComponent):
275
+ components[field_name] = value
276
+
277
+ return components
flowfile_core/main.py CHANGED
@@ -18,6 +18,7 @@ from flowfile_core.routes.routes import router
18
18
  from flowfile_core.routes.public import router as public_router
19
19
  from flowfile_core.routes.logs import router as logs_router
20
20
  from flowfile_core.routes.cloud_connections import router as cloud_connections_router
21
+ from flowfile_core.routes.user_defined_components import router as user_defined_components_router
21
22
 
22
23
  from flowfile_core.configs.flow_logger import clear_all_flow_logs
23
24
  storage.cleanup_directories()
@@ -79,7 +80,7 @@ app.include_router(logs_router, tags=["logs"])
79
80
  app.include_router(auth_router, prefix="/auth", tags=["auth"])
80
81
  app.include_router(secrets_router, prefix="/secrets", tags=["secrets"])
81
82
  app.include_router(cloud_connections_router, prefix="/cloud_connections", tags=["cloud_connections"])
82
-
83
+ app.include_router(user_defined_components_router, prefix="/user_defined_components", tags=["user_defined_components"])
83
84
 
84
85
 
85
86
  @app.post("/shutdown")
@@ -12,45 +12,40 @@ import logging
12
12
  import os
13
13
  from pathlib import Path
14
14
  from typing import List, Dict, Any, Optional
15
- from sqlalchemy.orm import Session
16
15
 
17
16
  from fastapi import APIRouter, File, UploadFile, BackgroundTasks, HTTPException, status, Body, Depends
18
17
  from fastapi.responses import JSONResponse, Response
19
18
  # External dependencies
20
19
  from polars_expr_transformer.function_overview import get_all_expressions, get_expression_overview
20
+ from sqlalchemy.orm import Session
21
21
 
22
+ from flowfile_core import flow_file_handler
22
23
  # Core modules
23
24
  from flowfile_core.auth.jwt import get_current_active_user
24
25
  from flowfile_core.configs import logger
25
- from flowfile_core.configs.node_store import nodes
26
- from flowfile_core.configs.settings import IS_RUNNING_IN_DOCKER
26
+ from flowfile_core.configs.node_store import nodes_list, check_if_has_default_setting
27
+ from flowfile_core.database.connection import get_db
27
28
  # File handling
28
29
  from flowfile_core.fileExplorer.funcs import (
29
30
  SecureFileExplorer,
30
31
  FileInfo,
31
32
  get_files_from_directory
32
33
  )
33
- from flowfile_core.flowfile.flow_graph import add_connection, delete_connection
34
- from flowfile_core.flowfile.code_generator.code_generator import export_flow_to_polars
35
34
  from flowfile_core.flowfile.analytics.analytics_processor import AnalyticsProcessor
35
+ from flowfile_core.flowfile.code_generator.code_generator import export_flow_to_polars
36
+ from flowfile_core.flowfile.database_connection_manager.db_connections import (store_database_connection,
37
+ get_database_connection,
38
+ delete_database_connection,
39
+ get_all_database_connections_interface)
36
40
  from flowfile_core.flowfile.extensions import get_instant_func_results
37
- # Flow handling
38
-
41
+ from flowfile_core.flowfile.flow_graph import add_connection, delete_connection
39
42
  from flowfile_core.flowfile.sources.external_sources.sql_source.sql_source import create_sql_source_from_db_settings
40
43
  from flowfile_core.run_lock import get_flow_run_lock
41
- # Schema and models
42
-
43
- from shared.storage_config import storage
44
44
  from flowfile_core.schemas import input_schema, schemas, output_model
45
45
  from flowfile_core.utils import excel_file_manager
46
46
  from flowfile_core.utils.fileManager import create_dir
47
47
  from flowfile_core.utils.utils import camel_case_to_snake_case
48
- from flowfile_core import flow_file_handler
49
- from flowfile_core.flowfile.database_connection_manager.db_connections import (store_database_connection,
50
- get_database_connection,
51
- delete_database_connection,
52
- get_all_database_connections_interface)
53
- from flowfile_core.database.connection import get_db
48
+ from shared.storage_config import storage
54
49
 
55
50
 
56
51
  router = APIRouter(dependencies=[Depends(get_current_active_user)])
@@ -359,7 +354,7 @@ def add_node(flow_id: int, node_id: int, node_type: str, pos_x: int = 0, pos_y:
359
354
  logger.info("Adding node")
360
355
  flow.add_node_promise(node_promise)
361
356
 
362
- if nodes.check_if_has_default_setting(node_type):
357
+ if check_if_has_default_setting(node_type):
363
358
  logger.info(f'Found standard settings for {node_type}, trying to upload them')
364
359
  setting_name_ref = 'node' + node_type.replace('_', '')
365
360
  node_model = get_node_model(setting_name_ref)
@@ -545,10 +540,11 @@ def get_list_of_saved_flows(path: str):
545
540
  except:
546
541
  return []
547
542
 
548
- @router.get('/node_list', response_model=List[nodes.NodeTemplate])
549
- def get_node_list() -> List[nodes.NodeTemplate]:
543
+
544
+ @router.get('/node_list', response_model=List[schemas.NodeTemplate])
545
+ def get_node_list() -> List[schemas.NodeTemplate]:
550
546
  """Retrieves the list of all available node types and their templates."""
551
- return nodes.nodes_list
547
+ return nodes_list
552
548
 
553
549
 
554
550
  @router.get('/node', response_model=output_model.NodeData, tags=['editor'])
@@ -0,0 +1,55 @@
1
+
2
+ from typing import Dict, Any
3
+
4
+ from fastapi import APIRouter, HTTPException, Depends
5
+
6
+ from flowfile_core import flow_file_handler
7
+ # Core modules
8
+ from flowfile_core.auth.jwt import get_current_active_user
9
+ from flowfile_core.configs import logger
10
+ from flowfile_core.configs.node_store import CUSTOM_NODE_STORE
11
+ # File handling
12
+ from flowfile_core.schemas import input_schema
13
+ from flowfile_core.utils.utils import camel_case_to_snake_case
14
+
15
+ # External dependencies
16
+
17
+
18
+ router = APIRouter()
19
+
20
+
21
+ @router.get("/custom-node-schema", summary="Get a simple UI schema")
22
+ def get_simple_custom_object(flow_id: int, node_id: int):
23
+ """
24
+ This endpoint returns a hardcoded JSON object that represents the UI
25
+ for our SimpleFilterNode.
26
+ """
27
+ try:
28
+ node = flow_file_handler.get_node(flow_id=flow_id, node_id=node_id)
29
+ except Exception as e:
30
+ raise HTTPException(status_code=404, detail=str(e))
31
+ user_defined_node = CUSTOM_NODE_STORE.get(node.node_type)
32
+
33
+ if not user_defined_node:
34
+ raise HTTPException(status_code=404, detail=f"Node type '{node.node_type}' not found")
35
+ if node.is_setup:
36
+ settings = node.setting_input.settings
37
+ return user_defined_node.from_settings(settings).get_frontend_schema()
38
+ return user_defined_node().get_frontend_schema()
39
+
40
+
41
+ @router.post("/update_user_defined_node", tags=["transform"])
42
+ def update_user_defined_node(input_data: Dict[str, Any], node_type: str, current_user=Depends(get_current_active_user)):
43
+ input_data['user_id'] = current_user.id
44
+ node_type = camel_case_to_snake_case(node_type)
45
+ flow_id = int(input_data.get('flow_id'))
46
+ logger.info(f'Updating the data for flow: {flow_id}, node {input_data["node_id"]}')
47
+ flow = flow_file_handler.get_flow(flow_id)
48
+ user_defined_model = CUSTOM_NODE_STORE.get(node_type)
49
+ if not user_defined_model:
50
+ raise HTTPException(status_code=404, detail=f"Node type '{node_type}' not found")
51
+
52
+ user_defined_node_settings = input_schema.UserDefinedNode.model_validate(input_data)
53
+ initialized_model = user_defined_model.from_settings(user_defined_node_settings.settings)
54
+
55
+ flow.add_user_defined_node(custom_node=initialized_model, user_defined_node_settings=user_defined_node_settings)
@@ -1,4 +1,4 @@
1
- from typing import List, Optional, Literal, Iterator
1
+ from typing import List, Optional, Literal, Iterator, Any
2
2
  from flowfile_core.schemas import transform_schema
3
3
  from pathlib import Path
4
4
  import os
@@ -195,6 +195,8 @@ class NodeBase(BaseModel):
195
195
  description: Optional[str] = ''
196
196
  user_id: Optional[int] = None
197
197
  is_flow_output: Optional[bool] = False
198
+ is_user_defined: Optional[bool] = False # Indicator if the node is a user defined node
199
+
198
200
 
199
201
  class NodeSingleInput(NodeBase):
200
202
  """A base model for any node that takes a single data input."""
@@ -516,3 +518,8 @@ class NodeRecordCount(NodeSingleInput):
516
518
  class NodePolarsCode(NodeMultiInput):
517
519
  """Settings for a node that executes arbitrary user-provided Polars code."""
518
520
  polars_code_input: transform_schema.PolarsCodeInput
521
+
522
+
523
+ class UserDefinedNode(NodeMultiInput):
524
+ """Settings for a node that contains the user defined node information"""
525
+ settings: Any
@@ -5,6 +5,9 @@ from flowfile_core.configs.settings import OFFLOAD_TO_WORKER
5
5
  ExecutionModeLiteral = Literal['Development', 'Performance']
6
6
  ExecutionLocationsLiteral = Literal['local', 'remote']
7
7
 
8
+ # Type literals for classifying nodes.
9
+ NodeTypeLiteral = Literal['input', 'output', 'process']
10
+ TransformTypeLiteral = Literal['narrow', 'wide', 'other']
8
11
 
9
12
  def get_global_execution_location() -> ExecutionLocationsLiteral:
10
13
  """
@@ -135,11 +138,14 @@ class NodeTemplate(BaseModel):
135
138
  output: int
136
139
  image: str
137
140
  multi: bool = False
141
+ node_type: NodeTypeLiteral
142
+ transform_type: TransformTypeLiteral
138
143
  node_group: str
139
144
  prod_ready: bool = True
140
145
  can_be_start: bool = False
141
146
  drawer_title: str = "Node title"
142
147
  drawer_intro: str = "Drawer into"
148
+ custom_node: Optional[bool] = False
143
149
 
144
150
 
145
151
  class NodeInformation(BaseModel):
@@ -263,9 +269,6 @@ class VueFlowInput(BaseModel):
263
269
  node_inputs: List[NodeInput]
264
270
 
265
271
 
266
- # Type literals for classifying nodes.
267
- NodeTypeLiteral = Literal['input', 'output', 'process']
268
- TransformTypeLiteral = Literal['narrow', 'wide', 'other']
269
272
 
270
273
 
271
274
  class NodeDefault(BaseModel):
@@ -3,7 +3,7 @@ as have a component in flowfile_frontend"""
3
3
 
4
4
  from flowfile_core.schemas import input_schema
5
5
  from flowfile_core.flowfile.flow_graph import FlowGraph
6
- from flowfile_core.configs.node_store.nodes import nodes_list, NodeTemplate
6
+ from flowfile_core.configs.node_store import nodes_list, NodeTemplate
7
7
  import inspect
8
8
 
9
9
 
@@ -31,6 +31,8 @@ def validate_setup():
31
31
  Raises ValueError if any node is missing either.
32
32
  """
33
33
  for node in nodes_list:
34
+ if node.custom_node:
35
+ continue
34
36
  check_if_node_has_add_function_in_flow_graph(node)
35
37
  check_if_node_has_input_schema_definition(node)
36
38
 
shared/storage_config.py CHANGED
@@ -9,7 +9,7 @@ from typing import Optional, Literal
9
9
 
10
10
  DirectoryOptions = Literal["temp_directory", "logs_directory",
11
11
  "system_logs_directory", "database_directory",
12
- "cache_directory", "flows_directory"]
12
+ "cache_directory", "flows_directory", "user_defined_nodes_directory"]
13
13
 
14
14
 
15
15
  class FlowfileStorage:
@@ -87,6 +87,19 @@ class FlowfileStorage:
87
87
  # Local development - uploads in ~/.flowfile/uploads
88
88
  return self.base_directory / "uploads"
89
89
 
90
+ @property
91
+ def user_defined_nodes_directory(self) -> Path:
92
+ """Directory for user-defined custom nodes (user-accessible)."""
93
+ if os.environ.get("RUNNING_IN_DOCKER") == "true":
94
+ return self.user_data_directory / "user_defined_nodes"
95
+ else:
96
+ return self.base_directory / "user_defined_nodes"
97
+
98
+ @property
99
+ def user_defined_nodes_icons(self) -> Path:
100
+ """Directory for user-defined custom node icon (user-accessible)."""
101
+ return self.user_defined_nodes_directory / "icons"
102
+
90
103
  @property
91
104
  def outputs_directory(self) -> Path:
92
105
  """Directory for user outputs (user-accessible)."""
@@ -134,6 +147,8 @@ class FlowfileStorage:
134
147
  self.flows_directory,
135
148
  self.uploads_directory,
136
149
  self.outputs_directory,
150
+ self.user_defined_nodes_directory,
151
+ self.user_defined_nodes_icons,
137
152
  ]
138
153
 
139
154
  for directory in internal_directories + user_directories:
@@ -240,4 +255,4 @@ def get_logs_directory() -> str:
240
255
 
241
256
  def get_system_logs_directory() -> str:
242
257
  """Get system logs directory path as string."""
243
- return str(storage.system_logs_directory)
258
+ return str(storage.system_logs_directory)