From ab7079f87ee005d95445a30d3c71d9d89a22906b Mon Sep 17 00:00:00 2001 From: Peter Stockings Date: Tue, 2 Dec 2025 15:19:20 +1100 Subject: [PATCH] Add functionality in settings to import data --- db.py | 122 ++++++++++++++- routes/settings.py | 128 +++++++++++++++ .../dashboard/settings/database_schema.html | 138 +++-------------- templates/dashboard/settings/export.html | 146 ++++++++++++++++-- 4 files changed, 406 insertions(+), 128 deletions(-) diff --git a/db.py b/db.py index 817e8f5..77d8115 100644 --- a/db.py +++ b/db.py @@ -427,4 +427,124 @@ ORDER BY invocation_time DESC""", [http_function_id]) link['created_at'] = link['created_at'].isoformat() export_data['timer_function_shared_env_links'] = timer_shared_env_links or [] - return export_data \ No newline at end of file + return export_data + + def import_http_function(self, user_id, func_data): + """Import a single HTTP function, returns (success, message, function_id)""" + try: + # Check if function with same name exists + existing = self.execute( + "SELECT id FROM http_functions WHERE user_id = %s AND name = %s", + (user_id, func_data['name']), + one=True + ) + + if existing: + return (False, f"Function '{func_data['name']}' already exists", None) + + # Insert the function + result = self.execute( + """INSERT INTO http_functions + (name, code, environment, version_number, user_id, runtime) + VALUES (%s, %s, %s, %s, %s, %s) + RETURNING id""", + ( + func_data['name'], + func_data['code'], + json.dumps(func_data.get('environment', {})), + 1, # Start at version 1 + user_id, + func_data.get('runtime', 'python') + ), + one=True, + commit=True + ) + + return (True, f"Imported function '{func_data['name']}'", result['id']) + except Exception as e: + return (False, f"Error importing '{func_data.get('name', 'unknown')}': {str(e)}", None) + + def import_timer_function(self,user_id, func_data): + """Import a single timer function, returns (success, message, function_id)""" + try: + # Check if function with same name exists + existing = self.execute( + "SELECT id FROM timer_functions WHERE user_id = %s AND name = %s", + (user_id, func_data['name']), + one=True + ) + + if existing: + return (False, f"Timer function '{func_data['name']}' already exists", None) + + # Calculate next_run based on trigger type + from routes.timer import calculate_next_run + next_run = calculate_next_run( + func_data['trigger_type'], + func_data.get('frequency_minutes'), + func_data.get('run_date'), + func_data.get('cron_expression') + ) + + # Insert the function + result = self.execute( + """INSERT INTO timer_functions + (name, code, environment, version_number, user_id, runtime, + trigger_type, frequency_minutes, run_date, cron_expression, + next_run, enabled) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id""", + ( + func_data['name'], + func_data['code'], + json.dumps(func_data.get('environment', {})), + 1, # Start at version 1 + user_id, + func_data.get('runtime', 'python'), + func_data['trigger_type'], + func_data.get('frequency_minutes'), + func_data.get('run_date'), + func_data.get('cron_expression'), + next_run, + func_data.get('enabled', True) + ), + one=True, + commit=True + ) + + return (True, f"Imported timer function '{func_data['name']}'", result['id']) + except Exception as e: + return (False, f"Error importing timer '{func_data.get('name', 'unknown')}': {str(e)}", None) + + def import_shared_environment(self, user_id, env_data): + """Import a single shared environment, returns (success, message, env_id)""" + try: + # Check if environment with same name exists + existing = self.execute( + "SELECT id FROM shared_environments WHERE user_id = %s AND name = %s", + (user_id, env_data['name']), + one=True + ) + + if existing: + return (False, f"Shared environment '{env_data['name']}' already exists", None) + + # Insert the environment + result = self.execute( + """INSERT INTO shared_environments + (name, environment, user_id, version_number) + VALUES (%s, %s, %s, %s) + RETURNING id""", + ( + env_data['name'], + json.dumps(env_data.get('environment', {})), + user_id, + 1 # Start at version 1 + ), + one=True, + commit=True + ) + + return (True, f"Imported shared environment '{env_data['name']}'", result['id']) + except Exception as e: + return (False, f"Error importing environment '{env_data.get('name', 'unknown')}': {str(e)}", None) \ No newline at end of file diff --git a/routes/settings.py b/routes/settings.py index b72b434..d7d7a07 100644 --- a/routes/settings.py +++ b/routes/settings.py @@ -285,3 +285,131 @@ def execute_query(): except Exception as e: return {"error": f"Query execution failed: {str(e)}"}, 500 + +@settings.route("/import", methods=["POST"]) +@login_required +def import_data(): + """Import user data from JSON file""" + try: + # Check if file was uploaded + if 'import_file' not in request.files: + return {"error": "No file uploaded"}, 400 + + file = request.files['import_file'] + + if file.filename == '': + return {"error": "No file selected"}, 400 + + # Validate file type + if not file.filename.endswith('.json'): + return {"error": "File must be a JSON file"}, 400 + + # Read and parse JSON + try: + file_content = file.read() + + # Check file size (max 10MB) + if len(file_content) > 10 * 1024 * 1024: + return {"error": "File too large (max 10MB)"}, 400 + + import_data = json.loads(file_content) + except json.JSONDecodeError as e: + return {"error": f"Invalid JSON format: {str(e)}"}, 400 + + # Validate structure + if not isinstance(import_data, dict): + return {"error": "Invalid data format: expected JSON object"}, 400 + + user_id = current_user.id + results = { + "http_functions": {"success": [], "skipped": [], "failed": []}, + "timer_functions": {"success": [], "skipped": [], "failed": []}, + "shared_environments": {"success": [], "skipped": [], "failed": []} + } + + # Import HTTP Functions + http_functions = import_data.get('http_functions', []) + for func in http_functions: + # Map old export column names to new import method requirements + func_data = { + 'name': func.get('name'), + 'code': func.get('script_content'), # Export uses 'script_content' + 'environment': func.get('environment_info'), # Export uses 'environment_info' + 'runtime': func.get('runtime', 'python') + } + + success, message, func_id = db.import_http_function(user_id, func_data) + + if success: + results['http_functions']['success'].append(message) + elif 'already exists' in message: + results['http_functions']['skipped'].append(message) + else: + results['http_functions']['failed'].append(message) + + # Import Timer Functions + timer_functions = import_data.get('timer_functions', []) + for func in timer_functions: + func_data = { + 'name': func.get('name'), + 'code': func.get('code'), + 'environment': func.get('environment'), + 'runtime': func.get('runtime', 'python'), + 'trigger_type': func.get('trigger_type'), + 'frequency_minutes': func.get('frequency_minutes'), + 'run_date': func.get('run_date'), + 'cron_expression': func.get('cron_expression'), + 'enabled': func.get('enabled', True) + } + + success, message, func_id = db.import_timer_function(user_id, func_data) + + if success: + results['timer_functions']['success'].append(message) + elif 'already exists' in message: + results['timer_functions']['skipped'].append(message) + else: + results['timer_functions']['failed'].append(message) + + # Import Shared Environments + shared_envs = import_data.get('shared_environments', []) + for env in shared_envs: + env_data = { + 'name': env.get('name'), + 'environment': env.get('environment') + } + + success, message, env_id = db.import_shared_environment(user_id, env_data) + + if success: + results['shared_environments']['success'].append(message) + elif 'already exists' in message: + results['shared_environments']['skipped'].append(message) + else: + results['shared_environments']['failed'].append(message) + + # Calculate totals + total_success = (len(results['http_functions']['success']) + + len(results['timer_functions']['success']) + + len(results['shared_environments']['success'])) + + total_skipped = (len(results['http_functions']['skipped']) + + len(results['timer_functions']['skipped']) + + len(results['shared_environments']['skipped'])) + + total_failed = (len(results['http_functions']['failed']) + + len(results['timer_functions']['failed']) + + len(results['shared_environments']['failed'])) + + return { + "success": True, + "results": results, + "summary": { + "total_success": total_success, + "total_skipped": total_skipped, + "total_failed": total_failed + } + } + + except Exception as e: + return {"error": f"Import failed: {str(e)}"}, 500 diff --git a/templates/dashboard/settings/database_schema.html b/templates/dashboard/settings/database_schema.html index 2b36c9c..ac28d70 100644 --- a/templates/dashboard/settings/database_schema.html +++ b/templates/dashboard/settings/database_schema.html @@ -29,7 +29,6 @@

Entity Relationship Diagram

-
@@ -54,32 +53,26 @@ erDiagram
         

Run SELECT queries on your data. Queries are automatically scoped to your user account for security.

-
-
+ class="px-4 py-2 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">Clear
- - @@ -98,7 +90,6 @@ erDiagram

Tables Overview

-
{% for table in schema_info %}
@@ -111,11 +102,8 @@ erDiagram

{{ table.columns|length }} column{{ 's' if table.columns|length != 1 else '' }} - {% if table.primary_keys %} - · {{ table.primary_keys|length }} PK - {% endif %} + {% if table.primary_keys %}· {{ table.primary_keys|length }} PK{% endif %}

-
{% for col in table.columns %}
@@ -131,14 +119,12 @@ erDiagram
{{ col.column_name }} {{ col.data_type }} - {% if col.is_nullable == 'NO' %} - * - {% endif %} + {% if col.is_nullable == 'NO' %}*{% endif + %}
{% endfor %}
- {% if table.foreign_keys %}

References:

@@ -161,71 +147,29 @@ erDiagram
- - - - - {% endblock %} \ No newline at end of file diff --git a/templates/dashboard/settings/export.html b/templates/dashboard/settings/export.html index 07b1dc0..a940121 100644 --- a/templates/dashboard/settings/export.html +++ b/templates/dashboard/settings/export.html @@ -23,7 +23,8 @@

Export Your Data

-

Download all your data in JSON format for backup or migration +

Download all your data in JSON format for backup or + migration purposes.

@@ -38,7 +39,8 @@

HTTP Functions

-

All your HTTP functions with code and settings +

All your HTTP functions with + code and settings

@@ -50,7 +52,8 @@

Timer Functions

-

All scheduled functions and their configurations +

All scheduled functions and + their configurations

@@ -62,7 +65,8 @@

Shared Environments

-

Environment variables and configurations

+

Environment variables and + configurations

@@ -73,7 +77,8 @@

API Keys

-

API key names and scopes (keys are masked)

+

API key names and scopes (keys + are masked)

@@ -84,7 +89,8 @@

Invocation Logs

-

Last 100 invocations per function

+

Last 100 invocations per + function

@@ -95,7 +101,8 @@

Version History

-

Complete version history for all functions

+

Complete version history for all + functions

@@ -109,10 +116,12 @@ clip-rule="evenodd" />
-

Important Information

+

Important + Information

@@ -130,14 +139,131 @@
+ + +
+

Import Your Data

+

Upload a previously exported JSON file to + restore your functions and environments.

+ +
+
+ + +
+ +
+

Import Behavior

+
    +
  • • Functions with duplicate names will be skipped
  • +
  • • Successfully imported items will be listed in green
  • +
  • • Skipped items will be shown in yellow with reasons
  • +
+
+ + +
+ + +
+

What to Do With Your Export

+ + {% endblock %} \ No newline at end of file