Add support to switch between deno and nodejs function executor

This commit is contained in:
Peter Stockings
2025-07-28 15:48:18 +10:00
parent a4d8abcf5b
commit e115d06691
7 changed files with 90 additions and 46 deletions

14
app.py
View File

@@ -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'])

18
db.py
View File

@@ -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

View File

@@ -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

View File

@@ -154,7 +154,7 @@ def overview():
timer_functions = db.execute("""
SELECT id, name, code, environment, trigger_type,
frequency_minutes, run_date, next_run,
last_run, enabled, invocation_count
last_run, enabled, invocation_count, runtime
FROM timer_functions
WHERE user_id = %s
ORDER BY id DESC
@@ -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'):
@@ -203,8 +204,8 @@ def new():
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)
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)
@@ -238,7 +240,7 @@ def edit(function_id):
timer_function = db.execute("""
SELECT id, name, code, environment, version_number, trigger_type,
frequency_minutes, run_date, next_run,
last_run, enabled, invocation_count
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
],

View File

@@ -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(

View File

@@ -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 '' }}",

View File

@@ -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) }}",