diff --git a/app.py b/app.py index a6a52fd..2a54fd4 100644 --- a/app.py +++ b/app.py @@ -46,7 +46,8 @@ app.register_blueprint(auth, url_prefix='/auth') # Swith to inter app routing, which results in speed up from ~400ms to ~270ms # https://stackoverflow.com/questions/76886643/linking-two-not-exposed-dokku-apps -API_URL = os.environ.get('API_URL', 'http://isolator.web:5000/execute') # 'https://isolator.peterstockings.com/execute' 'http://127.0.0.1:5000/execute' +NODE_API_URL = os.environ.get('NODE_API_URL', 'http://isolator.web:5000/execute') +DENO_API_URL = os.environ.get('DENO_API_URL', 'http://deno-isolator.web:5000/execute') def map_isolator_response_to_flask_response(response): @@ -82,6 +83,9 @@ async def execute_code(): try: # Extract code and convert request to a format acceptable by Node.js app code = request.json.get('code') + runtime = request.json.get('runtime', 'node') # Default to node + api_url = DENO_API_URL if runtime == 'deno' else NODE_API_URL + request_obj = { 'method': request.method, 'headers': dict(request.headers), @@ -92,9 +96,9 @@ async def execute_code(): environment = request.json.get('environment_info') environment_json = json.loads(environment) - # Call the Node.js API asynchronously + # Call the selected isolator API asynchronously async with aiohttp.ClientSession() as session: - async with session.post(API_URL, json={'code': code, 'request': request_obj, 'environment': environment_json, 'name': "anonymous"}) as response: + async with session.post(api_url, json={'code': code, 'request': request_obj, 'environment': environment_json, 'name': "anonymous"}) as response: response_data = await response.json() # check if playground=true is in the query string @@ -122,6 +126,7 @@ async def execute_http_function(user_id, function): code = http_function['script_content'] environment_info = http_function['environment_info'] + runtime = http_function.get('runtime', 'node') # Default to node # Ensure environment is a dictionary if isinstance(environment_info, str) and environment_info: @@ -176,8 +181,9 @@ async def execute_http_function(user_id, function): request_data['text'] = request.data.decode('utf-8') # Call the Node.js API asynchronously + api_url = DENO_API_URL if runtime == 'deno' else NODE_API_URL async with aiohttp.ClientSession() as session: - async with session.post(API_URL, json={'code': code, 'request': request_data, 'environment': environment, 'name': function_name}) as response: + async with session.post(api_url, json={'code': code, 'request': request_data, 'environment': environment, 'name': function_name}) as response: response_data = await response.json() db.update_http_function_environment_info_and_invoked_count(user_id, function_name, response_data['environment']) diff --git a/db.py b/db.py index 69ecc33..f949df2 100644 --- a/db.py +++ b/db.py @@ -59,30 +59,30 @@ class DataBase(): def get_http_functions_for_user(self, user_id): http_functions = self.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number FROM http_functions WHERE user_id=%s ORDER by id DESC', [user_id]) + 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, runtime FROM http_functions WHERE user_id=%s ORDER by id DESC', [user_id]) return http_functions def get_http_function(self, user_id, name): http_function = self.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at FROM http_functions WHERE user_id=%s AND NAME=%s', [user_id, name], one=True) + 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at, runtime FROM http_functions WHERE user_id=%s AND NAME=%s', [user_id, name], one=True) return http_function def get_http_function_by_id(self, user_id, http_function_id): http_function = self.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at FROM http_functions WHERE user_id=%s AND id=%s', [user_id, http_function_id], one=True) + 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at, runtime FROM http_functions WHERE user_id=%s AND id=%s', [user_id, http_function_id], one=True) return http_function - def create_new_http_function(self, user_id, name, script_content, environment_info, is_public, log_request, log_response): + def create_new_http_function(self, user_id, name, script_content, environment_info, is_public, log_request, log_response, runtime): self.execute( - 'INSERT INTO http_functions (user_id, NAME, script_content, environment_info, is_public, log_request, log_response) VALUES (%s, %s, %s, %s, %s, %s, %s)', - [user_id, name, script_content, environment_info, is_public, log_request, log_response], + 'INSERT INTO http_functions (user_id, NAME, script_content, environment_info, is_public, log_request, log_response, runtime) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)', + [user_id, name, script_content, environment_info, is_public, log_request, log_response, runtime], commit=True ) - def edit_http_function(self, user_id, function_id, name, script_content, environment_info, is_public, log_request, log_response): + def edit_http_function(self, user_id, function_id, name, script_content, environment_info, is_public, log_request, log_response, runtime): updated_version = self.execute( - 'UPDATE http_functions SET NAME=%s, script_content=%s, environment_info=%s, is_public=%s, log_request=%s, log_response=%s WHERE user_id=%s AND id=%s RETURNING version_number', - [name, script_content, environment_info, is_public, log_request, log_response, user_id, function_id], + 'UPDATE http_functions SET NAME=%s, script_content=%s, environment_info=%s, is_public=%s, log_request=%s, log_response=%s, runtime=%s WHERE user_id=%s AND id=%s RETURNING version_number', + [name, script_content, environment_info, is_public, log_request, log_response, runtime, user_id, function_id], commit=True, one=True ) return updated_version diff --git a/routes/http.py b/routes/http.py index 487b421..91262f1 100644 --- a/routes/http.py +++ b/routes/http.py @@ -105,8 +105,7 @@ http = Blueprint('http', __name__) @login_required def overview(): user_id = current_user.id - http_functions = db.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number FROM http_functions WHERE user_id=%s ORDER by id DESC', [user_id]) + http_functions = db.get_http_functions_for_user(user_id) http_functions = create_http_functions_view_model(http_functions) if htmx: return render_block(environment, "dashboard/http_functions/overview.html", "page", http_functions=http_functions) @@ -128,12 +127,9 @@ def new(): is_public = request.json.get('is_public') log_request = request.json.get('log_request') log_response = request.json.get('log_response') + runtime = request.json.get('runtime', 'node') - db.execute( - 'INSERT INTO http_functions (user_id, NAME, script_content, environment_info, is_public, log_request, log_response) VALUES (%s, %s, %s, %s, %s, %s, %s)', - [user_id, name, script_content, environment_info, is_public, log_request, log_response], - commit=True - ) + db.create_new_http_function(user_id, name, script_content, environment_info, is_public, log_request, log_response, runtime) return jsonify({ "status": "success", @@ -155,12 +151,9 @@ def edit(function_id): is_public = request.json.get('is_public') log_request = request.json.get('log_request') log_response = request.json.get('log_response') + runtime = request.json.get('runtime', 'node') - updated_version = db.execute( - 'UPDATE http_functions SET NAME=%s, script_content=%s, environment_info=%s, is_public=%s, log_request=%s, log_response=%s WHERE user_id=%s AND id=%s RETURNING version_number', - [name, script_content, environment_info, is_public, log_request, log_response, user_id, function_id], - commit=True, one=True - ) + updated_version = db.edit_http_function(user_id, function_id, name, script_content, environment_info, is_public, log_request, log_response, runtime) return { "status": "success", "message": f'{name} updated' } except Exception as e: @@ -184,9 +177,7 @@ def delete(function_id): @login_required def logs(function_id): user_id = current_user.id - http_function = db.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at FROM http_functions WHERE user_id=%s AND id=%s', - [user_id, function_id], one=True) + http_function = db.get_http_function_by_id(user_id, function_id) if not http_function: return jsonify({'error': 'Function not found'}), 404 name = http_function['name'] @@ -199,9 +190,7 @@ def logs(function_id): @login_required def client(function_id): user_id = current_user.id - http_function = db.execute( - 'SELECT id, user_id, NAME, script_content, invoked_count, environment_info, is_public, log_request, log_response, version_number, created_at FROM http_functions WHERE user_id=%s AND id=%s', - [user_id, function_id], one=True) + http_function = db.get_http_function_by_id(user_id, function_id) if not http_function: return jsonify({'error': 'Function not found'}), 404 @@ -265,6 +254,7 @@ def editor(function_id): 'log_request': http_function['log_request'], 'log_response': http_function['log_response'], 'version_number': http_function['version_number'], + 'runtime': http_function.get('runtime', 'node'), 'user_id': user_id, 'function_id': function_id, # Add new URLs for navigation diff --git a/routes/timer.py b/routes/timer.py index bdccec1..a62218a 100644 --- a/routes/timer.py +++ b/routes/timer.py @@ -152,10 +152,10 @@ timer = Blueprint('timer', __name__) @login_required def overview(): timer_functions = db.execute(""" - SELECT id, name, code, environment, trigger_type, - frequency_minutes, run_date, next_run, - last_run, enabled, invocation_count - FROM timer_functions + SELECT id, name, code, environment, trigger_type, + frequency_minutes, run_date, next_run, + last_run, enabled, invocation_count, runtime + FROM timer_functions WHERE user_id = %s ORDER BY id DESC """, [current_user.id]) @@ -182,6 +182,7 @@ def new(): try: data = request.json trigger_type = data.get('trigger_type') + runtime = data.get('runtime', 'node') # Validate trigger type if trigger_type not in ('interval', 'date'): @@ -202,9 +203,9 @@ def new(): # Insert new timer function db.execute(""" INSERT INTO timer_functions - (name, code, environment, user_id, trigger_type, - frequency_minutes, run_date, next_run, enabled) - VALUES (%s, %s, %s::jsonb, %s, %s, %s, %s, %s, %s) + (name, code, environment, user_id, trigger_type, + frequency_minutes, run_date, next_run, enabled, runtime) + VALUES (%s, %s, %s::jsonb, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, [ data.get('name'), @@ -215,7 +216,8 @@ def new(): frequency_minutes if trigger_type == 'interval' else None, run_date if trigger_type == 'date' else None, next_run, - True + True, + runtime ], commit=True) @@ -236,10 +238,10 @@ def edit(function_id): if request.method == 'GET': # Fetch the timer function timer_function = db.execute(""" - SELECT id, name, code, environment, version_number, trigger_type, - frequency_minutes, run_date, next_run, - last_run, enabled, invocation_count - FROM timer_functions + SELECT id, name, code, environment, version_number, trigger_type, + frequency_minutes, run_date, next_run, + last_run, enabled, invocation_count, runtime + FROM timer_functions WHERE id = %s AND user_id = %s """, [function_id, current_user.id], one=True) @@ -254,7 +256,8 @@ def edit(function_id): args = { 'function_id': function_id, - 'timer_function': timer_function + 'timer_function': timer_function, + 'runtime': timer_function.get('runtime', 'node') } if htmx: @@ -265,6 +268,7 @@ def edit(function_id): try: data = request.json trigger_type = data.get('trigger_type') + runtime = data.get('runtime', 'node') # Validate trigger type if trigger_type not in ('interval', 'date'): @@ -292,7 +296,8 @@ def edit(function_id): frequency_minutes = %s, run_date = %s, next_run = %s, - enabled = %s + enabled = %s, + runtime = %s WHERE id = %s AND user_id = %s RETURNING id """, [ @@ -304,6 +309,7 @@ def edit(function_id): run_date if trigger_type == 'date' else None, next_run, data.get('is_enabled', True), # Default to True if not provided + runtime, function_id, current_user.id ], diff --git a/static/js/mithril/editor.js b/static/js/mithril/editor.js index 1342646..761e1f4 100644 --- a/static/js/mithril/editor.js +++ b/static/js/mithril/editor.js @@ -6,6 +6,7 @@ const Editor = { this.isPublic = vnode.attrs.isPublic || false; this.logRequest = vnode.attrs.logRequest || false; this.logResponse = vnode.attrs.logResponse || false; + this.runtime = vnode.attrs.runtime || "node"; // Add runtime // Only controls whether the name/version is shown (left side of header), // but we still always show the Execute button on the right side. @@ -119,7 +120,11 @@ const Editor = { const resp = await fetch(this.executeUrl, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ code, environment_info }), + body: JSON.stringify({ + code, + environment_info, + runtime: this.runtime, + }), }); if (!resp.ok) { throw new Error(`HTTP error! status: ${resp.status}`); @@ -149,6 +154,7 @@ const Editor = { is_public: this.isPublic, log_request: this.logRequest, log_response: this.logResponse, + runtime: this.runtime, }; // Create payload based on whether this is a timer function @@ -172,6 +178,7 @@ const Editor = { is_public: this.isPublic, log_request: this.logRequest, log_response: this.logResponse, + runtime: this.runtime, }; const response = await m.request({ @@ -513,6 +520,39 @@ const Editor = { m("div", { class: "flex flex-col space-y-4" }, [ // Toggles group m("div", { class: "flex flex-wrap gap-6" }, [ + // Runtime dropdown + m("div", { class: "flex flex-col" }, [ + m( + "label", + { + for: "runtime-select", + class: "mb-2 text-sm font-medium", + }, + "Runtime" + ), + m( + "select", + { + id: "runtime-select", + class: + "bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", + onchange: (e) => (this.runtime = e.target.value), + }, + [ + m( + "option", + { value: "node", selected: this.runtime === "node" }, + "Node.js" + ), + m( + "option", + { value: "deno", selected: this.runtime === "deno" }, + "Deno" + ), + ] + ), + ]), + // Public/Private toggle this.showPublicToggle && m( diff --git a/templates/dashboard/http_functions/editor.html b/templates/dashboard/http_functions/editor.html index b5c2448..7795af2 100644 --- a/templates/dashboard/http_functions/editor.html +++ b/templates/dashboard/http_functions/editor.html @@ -32,6 +32,7 @@ history_url=url_for('http.history', function_id=function_id)) }} logRequest: {{ log_request | tojson }}, logResponse: {{ log_response | tojson }}, versionNumber: {{ version_number }}, + runtime: '{{ runtime }}', executeUrl: "{{ url_for('execute_code', playground='true') }}", saveUrl: "{{ url_for('http.edit', function_id=id) if id else url_for('http.new') }}", deleteUrl: "{{ url_for('http.delete', function_id=id) if id else '' }}", diff --git a/templates/dashboard/timer_functions/edit.html b/templates/dashboard/timer_functions/edit.html index 57c3b93..ab09bbe 100644 --- a/templates/dashboard/timer_functions/edit.html +++ b/templates/dashboard/timer_functions/edit.html @@ -30,6 +30,7 @@ history_url=url_for('timer.history', function_id=function_id)) }} isEdit: true, showHeader: true, versionNumber: {{ timer_function.version_number }}, + runtime: '{{ runtime }}', isEnabled: {{ timer_function.enabled | tojson }}, executeUrl: "{{ url_for('execute_code', playground='true') }}", saveUrl: "{{ url_for('timer.edit', function_id=function_id) }}",