Move http endpoints into seperate route

This commit is contained in:
Peter Stockings
2025-07-21 22:05:58 +10:00
parent 917189b3d9
commit 2ec44252bb
12 changed files with 552 additions and 304 deletions

342
app.py
View File

@@ -13,6 +13,7 @@ from dotenv import load_dotenv
from routes.timer import timer from routes.timer import timer
from routes.test import test from routes.test import test
from routes.home import home from routes.home import home
from routes.http import http
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
import asyncio import asyncio
import aiohttp import aiohttp
@@ -37,6 +38,7 @@ init_app(app)
app.register_blueprint(timer, url_prefix='/timer') app.register_blueprint(timer, url_prefix='/timer')
app.register_blueprint(test, url_prefix='/test') app.register_blueprint(test, url_prefix='/test')
app.register_blueprint(home, url_prefix='/home') app.register_blueprint(home, url_prefix='/home')
app.register_blueprint(http, url_prefix='/dashboard/http_functions')
class User(UserMixin): class User(UserMixin):
def __init__(self, id, username, password_hash, created_at): def __init__(self, id, username, password_hash, created_at):
@@ -126,140 +128,6 @@ def dashboard():
http_functions = create_http_functions_view_model(http_functions) http_functions = create_http_functions_view_model(http_functions)
return render_template("dashboard/http_functions/overview.html", http_functions=http_functions) return render_template("dashboard/http_functions/overview.html", http_functions=http_functions)
@ app.route("/dashboard/http_functions", methods=["GET"])
@login_required
def dashboard_http_functions():
user_id = current_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(app.jinja_env, "dashboard/http_functions/overview.html", "page", http_functions=http_functions)
return render_template("dashboard/http_functions/overview.html", http_functions=http_functions)
@ app.route("/dashboard/http_functions/add_form", methods=["GET"])
@login_required
def get_http_function_add_form():
user_id = current_user.id
if htmx:
return render_block(app.jinja_env, 'dashboard/http_functions/new.html', 'page', user_id=user_id, name=DEFAULT_FUNCTION_NAME, script=DEFAULT_SCRIPT, environment_info=DEFAULT_ENVIRONMENT, is_public=False, log_request=True, log_response=False)
return render_template("dashboard/http_functions/new.html", user_id=user_id, name=DEFAULT_FUNCTION_NAME, script=DEFAULT_SCRIPT, environment_info=DEFAULT_ENVIRONMENT, is_public=False, log_request=True, log_response=False)
@ app.route("/dashboard/http_functions/create", methods=["POST"])
@login_required
def create_http_function():
try:
user_id = current_user.id
name = request.form.get('name')
script_content = request.form.get('script_content')
environment_info = request.form.get('environment_info')
is_public = request.form.get('is_public')
log_request = request.form.get('log_request')
log_response = request.form.get('log_response')
db.create_new_http_function(user_id, name, script_content, environment_info, is_public, log_request, log_response)
http_functions = db.get_http_functions_for_user(user_id)
http_functions = create_http_functions_view_model(http_functions)
return render_block(app.jinja_env, "dashboard/http_functions/overview.html", "page", http_functions=http_functions), 200, {"HX-Push-Url": url_for('dashboard_http_functions')}
except Exception as e:
print(e)
return { "status": "error", "message": str(e) }
@ app.route("/dashboard/http_functions/<int:function_id>/edit", methods=["POST"])
@login_required
def edit_http_function(function_id):
try:
user_id = current_user.id
name = request.json.get('name')
script_content = request.json.get('script_content')
environment_info = request.json.get('environment_info')
is_public = request.json.get('is_public')
log_request = request.json.get('log_request')
log_response = request.json.get('log_response')
updated_version = db.edit_http_function(user_id, function_id, name, script_content, environment_info, is_public, log_request, log_response)
return { "status": "success", "message": f'{name} updated' }
except Exception as e:
print(e)
return { "status": "error", "message": str(e) }
@ app.route("/dashboard/http_functions/<int:function_id>/delete", methods=["DELETE"])
@login_required
def delete_http_function(function_id):
try:
user_id = current_user.id
db.delete_http_function(user_id, function_id)
http_functions = db.get_http_functions_for_user(user_id)
http_functions = create_http_functions_view_model(http_functions)
return render_block(app.jinja_env, "dashboard/http_functions/overview.html", "page", http_functions=http_functions), 200, {"HX-Push-Url": url_for('dashboard_http_functions')}
except Exception as e:
return jsonify({"status": 'error', "message": str(e)}), 500
@ app.route("/dashboard/http_functions/<int:function_id>/logs", methods=["GET"])
@login_required
def get_http_function_logs(function_id):
user_id = current_user.id
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']
http_function_invocations = db.get_http_function_invocations(function_id)
if htmx:
return render_block(app.jinja_env, 'dashboard/http_functions/logs.html', 'page', user_id=user_id, function_id=function_id, name=name, http_function_invocations=http_function_invocations)
return render_template("dashboard/http_functions/logs.html", user_id=user_id, name=name, function_id=function_id, http_function_invocations=http_function_invocations)
@ app.route("/dashboard/http_functions/<int:function_id>/client", methods=["GET"])
@login_required
def client(function_id):
user_id = current_user.id
http_function = db.get_http_function_by_id(user_id, function_id)
if not http_function:
return jsonify({'error': 'Function not found'}), 404
http_function = create_http_function_view_model(http_function)
if htmx:
return render_block(app.jinja_env, 'dashboard/http_functions/client.html', 'page', function_id=function_id, **http_function)
return render_template("dashboard/http_functions/client.html", function_id=function_id, **http_function)
@app.route("/dashboard/http_functions/<int:function_id>/history", methods=["GET"])
@login_required
def get_http_function_history(function_id):
user_id = current_user.id
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']
version_number = http_function['version_number']
original_script = http_function['script_content'] if version_number == 1 else None
http_function_history = []
if version_number > 1:
raw_history = db.get_http_function_history(function_id)
for i in range(len(raw_history) - 1):
pre_version = raw_history[i + 1]
post_version = raw_history[i]
http_function_history.append({
'pre': pre_version['script_content'],
'post': post_version['script_content'],
'version_id': post_version['version_id'],
'version_number': post_version['version_number'],
'updated_at': post_version['updated_at']
})
if raw_history:
original_script = raw_history[-1]['script_content']
if htmx:
return render_block(app.jinja_env, 'dashboard/http_functions/history.html', 'page', user_id=user_id, function_id=function_id, name=name, http_function=http_function, http_function_history=http_function_history, original_script=original_script)
return render_template("dashboard/http_functions/history.html", user_id=user_id, name=name, function_id=function_id, http_function=http_function, http_function_history=http_function_history, original_script=original_script)
def _generate_script_from_natural_language(natural_query): def _generate_script_from_natural_language(natural_query):
"""Generates a Javascript function from natural language using Gemini REST API.""" """Generates a Javascript function from natural language using Gemini REST API."""
@@ -275,33 +143,68 @@ def _generate_script_from_natural_language(natural_query):
prompt = f""" prompt = f"""
You are an expert Javascript developer. Your task is to write a complete, production-ready Javascript async function based on the user's request. You are an expert Javascript developer. Your task is to write a complete, production-ready Javascript async function based on the user's request.
**Function Signature:**
Your function MUST have the following signature: `async (req, environment) => {{ ... }}`
- `req`: An object containing details about the incoming HTTP request (e.g., `req.method`, `req.headers`, `req.body`, `req.query`).
- `environment`: A mutable JSON object that persists across executions. You can read and write to it to maintain state.
**Environment & Constraints:** **Environment & Constraints:**
- The function will be executed in a simple, sandboxed Javascript environment. - The function will be executed in a simple, sandboxed Javascript environment.
- **DO NOT** use `require()`, `import`, or any other module loading system. The environment does not support it. - **CRITICAL**: ALL helper functions or variables MUST be defined *inside* the main `async` function. Do not define anything in the global scope.
- **DO NOT** use `require()`, `import`, or any other module loading system.
- **DO NOT** access the file system (`fs` module) or make network requests. - **DO NOT** access the file system (`fs` module) or make network requests.
- The function must be a single `async` arrow function. - You have access to a `console.log()` function for logging.
- You have access to a persistent JSON object called `environment`. You can read from and write to it. For example: `environment.my_variable = 'hello'`.
- You also have access to a `console.log()` function for logging.
**Input:** **Response Helpers:**
The function receives one argument: `req`, an object containing details about the incoming HTTP request. It has properties like `req.method`, `req.headers`, `req.body`, `req.query`, etc. You must use one of the following functions to return a response:
**Output:**
You must use one of the following helper functions to return a response:
- `HtmlResponse(body)`: Returns an HTML response. - `HtmlResponse(body)`: Returns an HTML response.
- `JsonResponse(body)`: Returns a JSON response. The `body` will be automatically stringified. - `JsonResponse(body)`: Returns a JSON response.
- `TextResponse(body)`: Returns a plain text response. - `TextResponse(body)`: Returns a plain text response.
- `RedirectResponse(url)`: Redirects the user to a different URL. - `RedirectResponse(url)`: Redirects the user.
**Example:** **Complex Example (Tic-Tac-Toe):**
```javascript ```javascript
async (req) => {{ async (req, environment) => {{
if (!environment.counter) {{ // Helper function defined INSIDE the main function
environment.counter = 0; function checkWinner(board) {{
const winConditions = [
[0, 1, 2], [3, 4, 5], [6, 7, 8],
[0, 3, 6], [1, 4, 7], [2, 5, 8],
[0, 4, 8], [2, 4, 6]
];
for (const condition of winConditions) {{
const [a, b, c] = condition;
if (board[a] && board[a] === board[b] && board[a] === board[c]) {{
return board[a];
}}
}}
return null;
}} }}
environment.counter++;
console.log("The counter is now " + environment.counter); if (!environment.board) {{
return HtmlResponse(`<h1>Counter: ${{environment.counter}}</h1>`); environment.board = Array(9).fill("");
environment.turn = "X";
environment.winner = null;
}}
if (req.method === "POST" && req.json && req.json.move !== undefined) {{
const move = parseInt(req.json.move);
if (environment.board[move] === "" && !environment.winner) {{
environment.board[move] = environment.turn;
environment.turn = environment.turn === "X" ? "O" : "X";
environment.winner = checkWinner(environment.board);
}}
}}
const boardHTML = environment.board.map((cell, index) => `<button hx-post="/f/${{req.path.split('/')[2]}}/${{req.path.split('/')[3]}}" hx-target="body" hx-swap="innerHTML" name="move" value="${{index}}">${{cell || '&nbsp;'}}</button>`).join("");
const message = environment.winner ? `Player ${{environment.winner}} wins!` : `Turn: ${{environment.turn}}`;
return HtmlResponse(`
<html>
<head><title>Tic-Tac-Toe</title><script src="https://unpkg.com/htmx.org@1.9.9"></script></head>
<body><h1>${{message}}</h1><div>${{boardHTML}}</div></body>
</html>
`);
}} }}
``` ```
@@ -414,7 +317,18 @@ def execute_http_function(user_id, function):
return jsonify({'error': 'Function not found'}), 404 return jsonify({'error': 'Function not found'}), 404
code = http_function['script_content'] code = http_function['script_content']
environment = http_function['environment_info'] environment_info = http_function['environment_info']
# Ensure environment is a dictionary
if isinstance(environment_info, str) and environment_info:
try:
environment = json.loads(environment_info)
except json.JSONDecodeError:
environment = {}
elif isinstance(environment_info, dict):
environment = environment_info
else:
environment = {}
is_public = http_function['is_public'] is_public = http_function['is_public']
log_request = http_function['log_request'] log_request = http_function['log_request']
log_response = http_function['log_response'] log_response = http_function['log_response']
@@ -538,129 +452,7 @@ def logout():
logout_user() logout_user()
return redirect(url_for('home')) return redirect(url_for('home'))
@app.route("/http_function_editor/<int:function_id>", methods=["GET"])
@login_required
def http_function_editor(function_id):
user_id = current_user.id
http_function = db.get_http_function_by_id(user_id, function_id)
if not http_function:
return jsonify({'error': 'Function not found'}), 404
# Create a view model with all necessary data for the editor
editor_data = {
'id': http_function['id'],
'name': http_function['name'],
'script_content': http_function['script_content'],
'environment_info': json.dumps(http_function['environment_info'], indent=2),
'is_public': http_function['is_public'],
'log_request': http_function['log_request'],
'log_response': http_function['log_response'],
'version_number': http_function['version_number'],
'user_id': user_id,
'function_id': function_id,
# Add new URLs for navigation
'cancel_url': url_for('dashboard_http_functions'),
'edit_url': url_for('http_function_editor', function_id=function_id),
}
if htmx:
return render_block(app.jinja_env, "dashboard/http_functions/editor.html", "page", **editor_data)
return render_template("dashboard/http_functions/editor.html", **editor_data)
@app.route("/api/http_functions", methods=["POST"])
@login_required
def api_create_http_function():
try:
user_id = current_user.id
data = request.get_json()
name = data.get('name')
script_content = data.get('script_content')
environment_info = data.get('environment_info')
is_public = data.get('is_public')
log_request = data.get('log_request')
log_response = data.get('log_response')
# Check if function with same name already exists for this user
existing_function = db.get_http_function(user_id, name)
if existing_function:
return jsonify({
"status": "error",
"message": f"A function with the name '{name}' already exists"
}), 400
http_function = db.create_new_http_function(
user_id,
name,
script_content,
environment_info,
is_public,
log_request,
log_response
)
return jsonify({
"status": "success",
"message": f'{name} created',
"function": http_function
})
except Exception as e:
return jsonify({
"status": "error",
"message": str(e)
}), 400
@app.route("/api/http_functions/<int:function_id>", methods=["POST"])
@login_required
def api_update_http_function(function_id):
try:
user_id = current_user.id
data = request.get_json()
name = data.get('name')
script_content = data.get('script_content')
environment_info = data.get('environment_info')
is_public = data.get('is_public')
log_request = data.get('log_request')
log_response = data.get('log_response')
updated_function = db.edit_http_function(
user_id,
function_id,
name,
script_content,
environment_info,
is_public,
log_request,
log_response
)
return jsonify({
"status": "success",
"message": f'{name} updated',
"function": updated_function
})
except Exception as e:
return jsonify({
"status": "error",
"message": str(e)
}), 400
@app.route("/api/http_functions/<int:function_id>", methods=["DELETE"])
@login_required
def api_delete_http_function(function_id):
try:
user_id = current_user.id
db.delete_http_function(user_id, function_id)
return jsonify({
"status": "success",
"message": "Function deleted successfully"
})
except Exception as e:
return jsonify({
"status": "error",
"message": str(e)
}), 400
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):

360
routes/http.py Normal file
View File

@@ -0,0 +1,360 @@
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from jinja2_fragments import render_block
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import current_user, login_required
from extensions import db, htmx, environment
from datetime import datetime, timezone, timedelta
import json
from services import create_http_function_view_model, create_http_functions_view_model
'''
CREATE TABLE http_function_versions (
id SERIAL PRIMARY KEY,
http_function_id INT NOT NULL,
script_content TEXT NOT NULL,
version_number INT NOT NULL,
versioned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT fk_http_function_versions
FOREIGN KEY (http_function_id)
REFERENCES http_functions (id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
CREATE OR REPLACE FUNCTION fn_http_functions_versioning()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
next_version INT;
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO http_function_versions (http_function_id, script_content, version_number)
VALUES (NEW.id, NEW.script_content, 1);
UPDATE http_functions
SET version_number = 1
WHERE id = NEW.id;
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
IF NEW.script_content IS DISTINCT FROM OLD.script_content THEN
SELECT COALESCE(MAX(version_number), 0) + 1
INTO next_version
FROM http_function_versions
WHERE http_function_id = NEW.id;
INSERT INTO http_function_versions (http_function_id, script_content, version_number)
VALUES (NEW.id, NEW.script_content, next_version);
UPDATE http_functions
SET version_number = next_version
WHERE id = NEW.id;
END IF;
RETURN NEW;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER tr_http_functions_versioning
AFTER INSERT OR UPDATE
ON http_functions
FOR EACH ROW
EXECUTE PROCEDURE fn_http_functions_versioning();
'''
DEFAULT_SCRIPT = """async (req) => {
console.log(`Method:${req.method}`)
console.log(`Generating ${environment.lines} random lines...`)
let svgContent = "";
for (let i = 0; i < environment.lines; i++) {
console.log(i)
let pathD = "M2 " + Math.random() * 79;
let circles = "";
for (let x = 12; x <= 202; x += 10) {
let y = Math.random() * 79
pathD += ` L${x} ${y}`;
circles += `<circle cx="${x}" cy="${y}" r="1"></circle>`;
}
let pathColor = `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`;
svgContent += `
<g style="fill: ${pathColor}; stroke: ${pathColor};">
<path d="${pathD}" fill="none" stroke="${pathColor}"></path>
${circles}
</g>`;
}
return HtmlResponse(`<svg viewBox="0 0 204 79" preserveAspectRatio="none">${svgContent}</svg>`);
}"""
DEFAULT_ENVIRONMENT = """{
"lines": 3
}"""
http = Blueprint('http', __name__)
@http.route("/", methods=["GET"])
@login_required
def dashboard_http_functions():
user_id = current_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)
return render_template("dashboard/http_functions/overview.html", http_functions=http_functions)
@http.route("/add_form", methods=["GET"])
@login_required
def get_http_function_add_form():
user_id = current_user.id
if htmx:
return render_block(environment, 'dashboard/http_functions/new.html', 'page', user_id=user_id, name='foo', script=DEFAULT_SCRIPT, environment_info=DEFAULT_ENVIRONMENT, is_public=False, log_request=True, log_response=False)
return render_template("dashboard/http_functions/new.html", user_id=user_id, name='foo', script=DEFAULT_SCRIPT, environment_info=DEFAULT_ENVIRONMENT, is_public=False, log_request=True, log_response=False)
@http.route("/create", methods=["POST"])
@login_required
def create_http_function():
try:
user_id = current_user.id
name = request.form.get('name')
script_content = request.form.get('script_content')
environment_info = request.form.get('environment_info')
is_public = request.form.get('is_public')
log_request = request.form.get('log_request')
log_response = request.form.get('log_response')
db.create_new_http_function(user_id, name, script_content, environment_info, is_public, log_request, log_response)
http_functions = db.get_http_functions_for_user(user_id)
http_functions = create_http_functions_view_model(http_functions)
return render_block(environment, "dashboard/http_functions/overview.html", "page", http_functions=http_functions), 200, {"HX-Push-Url": url_for('http.dashboard_http_functions')}
except Exception as e:
print(e)
return { "status": "error", "message": str(e) }
@http.route("/<int:function_id>/edit", methods=["POST"])
@login_required
def edit_http_function(function_id):
try:
user_id = current_user.id
name = request.json.get('name')
script_content = request.json.get('script_content')
environment_info = request.json.get('environment_info')
is_public = request.json.get('is_public')
log_request = request.json.get('log_request')
log_response = request.json.get('log_response')
updated_version = db.edit_http_function(user_id, function_id, name, script_content, environment_info, is_public, log_request, log_response)
return { "status": "success", "message": f'{name} updated' }
except Exception as e:
print(e)
return { "status": "error", "message": str(e) }
@http.route("/<int:function_id>/delete", methods=["DELETE"])
@login_required
def delete_http_function(function_id):
try:
user_id = current_user.id
db.delete_http_function(user_id, function_id)
http_functions = db.get_http_functions_for_user(user_id)
http_functions = create_http_functions_view_model(http_functions)
return render_block(environment, "dashboard/http_functions/overview.html", "page", http_functions=http_functions), 200, {"HX-Push-Url": url_for('http.dashboard_http_functions')}
except Exception as e:
return jsonify({"status": 'error', "message": str(e)}), 500
@http.route("/<int:function_id>/logs", methods=["GET"])
@login_required
def get_http_function_logs(function_id):
user_id = current_user.id
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']
http_function_invocations = db.get_http_function_invocations(function_id)
if htmx:
return render_block(environment, 'dashboard/http_functions/logs.html', 'page', user_id=user_id, function_id=function_id, name=name, http_function_invocations=http_function_invocations)
return render_template("dashboard/http_functions/logs.html", user_id=user_id, name=name, function_id=function_id, http_function_invocations=http_function_invocations)
@http.route("/<int:function_id>/client", methods=["GET"])
@login_required
def client(function_id):
user_id = current_user.id
http_function = db.get_http_function_by_id(user_id, function_id)
if not http_function:
return jsonify({'error': 'Function not found'}), 404
http_function = create_http_function_view_model(http_function)
if htmx:
return render_block(environment, 'dashboard/http_functions/client.html', 'page', function_id=function_id, **http_function)
return render_template("dashboard/http_functions/client.html", function_id=function_id, **http_function)
@http.route("/<int:function_id>/history", methods=["GET"])
@login_required
def get_http_function_history(function_id):
user_id = current_user.id
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']
version_number = http_function['version_number']
original_script = http_function['script_content'] if version_number == 1 else None
http_function_history = []
if version_number > 1:
raw_history = db.get_http_function_history(function_id)
for i in range(len(raw_history) - 1):
pre_version = raw_history[i + 1]
post_version = raw_history[i]
http_function_history.append({
'pre': pre_version['script_content'],
'post': post_version['script_content'],
'version_id': post_version['version_id'],
'version_number': post_version['version_number'],
'updated_at': post_version['updated_at']
})
if raw_history:
original_script = raw_history[-1]['script_content']
if htmx:
return render_block(environment, 'dashboard/http_functions/history.html', 'page', user_id=user_id, function_id=function_id, name=name, http_function=http_function, http_function_history=http_function_history, original_script=original_script)
return render_template("dashboard/http_functions/history.html", user_id=user_id, name=name, function_id=function_id, http_function=http_function, http_function_history=http_function_history, original_script=original_script)
@http.route("/editor/<int:function_id>", methods=["GET"])
@login_required
def http_function_editor(function_id):
user_id = current_user.id
http_function = db.get_http_function_by_id(user_id, function_id)
if not http_function:
return jsonify({'error': 'Function not found'}), 404
# Create a view model with all necessary data for the editor
editor_data = {
'id': http_function['id'],
'name': http_function['name'],
'script_content': http_function['script_content'],
'environment_info': json.dumps(http_function['environment_info'], indent=2),
'is_public': http_function['is_public'],
'log_request': http_function['log_request'],
'log_response': http_function['log_response'],
'version_number': http_function['version_number'],
'user_id': user_id,
'function_id': function_id,
# Add new URLs for navigation
'cancel_url': url_for('http.dashboard_http_functions'),
'edit_url': url_for('http.http_function_editor', function_id=function_id),
}
if htmx:
return render_block(environment, "dashboard/http_functions/editor.html", "page", **editor_data)
return render_template("dashboard/http_functions/editor.html", **editor_data)
@http.route("/api", methods=["POST"])
@login_required
def api_create_http_function():
try:
user_id = current_user.id
data = request.get_json()
name = data.get('name')
script_content = data.get('script_content')
environment_info = data.get('environment_info')
is_public = data.get('is_public')
log_request = data.get('log_request')
log_response = data.get('log_response')
# Check if function with same name already exists for this user
existing_function = db.get_http_function(user_id, name)
if existing_function:
return jsonify({
"status": "error",
"message": f"A function with the name '{name}' already exists"
}), 400
http_function = db.create_new_http_function(
user_id,
name,
script_content,
environment_info,
is_public,
log_request,
log_response
)
return jsonify({
"status": "success",
"message": f'{name} created',
"function": http_function
})
except Exception as e:
return jsonify({
"status": "error",
"message": str(e)
}), 400
@http.route("/api/<int:function_id>", methods=["POST"])
@login_required
def api_update_http_function(function_id):
try:
user_id = current_user.id
data = request.get_json()
name = data.get('name')
script_content = data.get('script_content')
environment_info = data.get('environment_info')
is_public = data.get('is_public')
log_request = data.get('log_request')
log_response = data.get('log_response')
updated_function = db.edit_http_function(
user_id,
function_id,
name,
script_content,
environment_info,
is_public,
log_request,
log_response
)
return jsonify({
"status": "success",
"message": f'{name} updated',
"function": updated_function
})
except Exception as e:
return jsonify({
"status": "error",
"message": str(e)
}), 400
@http.route("/api/<int:function_id>", methods=["DELETE"])
@login_required
def api_delete_http_function(function_id):
try:
user_id = current_user.id
db.delete_http_function(user_id, function_id)
return jsonify({
"status": "success",
"message": "Function deleted successfully"
})
except Exception as e:
return jsonify({
"status": "error",
"message": str(e)
}), 400

View File

@@ -0,0 +1,96 @@
const FunctionHistory = {
oninit: function(vnode) {
// Initialize indices in state
vnode.state.leftIndex = vnode.attrs.versions.length - 1; // Earliest
vnode.state.rightIndex = 0; // Latest
},
view: function (vnode) {
const versions = vnode.attrs.versions;
return m("div", [
m("div.flex.gap-4.mb-4", [
m("div.flex-1", [
m("label.block.text-sm.font-medium.text-gray-700", "Left Version"),
m("select.mt-1.block.w-full.rounded-md.border-gray-300.shadow-sm", {
onchange: (e) => {
vnode.state.leftIndex = parseInt(e.target.value);
vnode.state.leftVersion = versions[vnode.state.leftIndex];
vnode.state.updateDiff();
},
value: vnode.state.leftIndex
}, versions.map((v, idx) =>
m("option", { value: idx }, `Version ${v.version_number} (${new Date(v.versioned_at).toLocaleString()})`)
))
]),
m("div.flex-1", [
m("label.block.text-sm.font-medium.text-gray-700", "Right Version"),
m("select.mt-1.block.w-full.rounded-md.border-gray-300.shadow-sm", {
onchange: (e) => {
vnode.state.rightIndex = parseInt(e.target.value);
vnode.state.rightVersion = versions[vnode.state.rightIndex];
vnode.state.updateDiff();
},
value: vnode.state.rightIndex
}, versions.map((v, idx) =>
m("option", { value: idx }, `Version ${v.version_number} (${new Date(v.versioned_at).toLocaleString()})`)
))
])
]),
m("div", {
id: "diff-container",
style: "height: 500px; position: relative;"
})
]);
},
oncreate: function (vnode) {
const versions = vnode.attrs.versions;
// Initialize with the earliest and most recent versions if available
if (versions.length >= 2) {
vnode.state.leftVersion = versions[versions.length - 1]; // Earliest version
vnode.state.rightVersion = versions[0]; // Latest version
} else if (versions.length === 1) {
vnode.state.leftVersion = versions[0];
vnode.state.rightVersion = versions[0];
}
vnode.state.updateDiff = function () {
if (vnode.state.aceDiffer) {
// Clean up previous instance
vnode.state.aceDiffer.destroy();
}
vnode.state.aceDiffer = new AceDiff({
element: '#diff-container',
mode: "ace/mode/javascript",
left: {
content: vnode.state.leftVersion ? vnode.state.leftVersion.script : "",
editable: false,
},
right: {
content: vnode.state.rightVersion ? vnode.state.rightVersion.script : "",
editable: false,
}
});
// Configure both editors
const editors = vnode.state.aceDiffer.getEditors();
['left', 'right'].forEach(side => {
editors[side].setOptions({
maxLines: 20,
autoScrollEditorIntoView: true,
});
editors[side].session.setOption("useWorker", false);
});
};
// Initial diff setup
vnode.state.updateDiff();
},
onremove: function (vnode) {
// Clean up AceDiff when component is removed
if (vnode.state.aceDiffer) {
vnode.state.aceDiffer.destroy();
}
}
};
export default FunctionHistory;

View File

@@ -33,7 +33,7 @@
Home Home
</a><a </a><a
class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-500 transition-all hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50 cursor-pointer" class="flex items-center gap-3 rounded-lg px-3 py-2 text-gray-500 transition-all hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-50 cursor-pointer"
data-id="15" hx-get="{{ url_for('dashboard_http_functions') }}" hx-target="#container" data-id="15" hx-get="{{ url_for('http.dashboard_http_functions') }}" hx-target="#container"
hx-swap="innerHTML" hx-push-url="true"><svg xmlns="http://www.w3.org/2000/svg" width="18" hx-swap="innerHTML" hx-push-url="true"><svg xmlns="http://www.w3.org/2000/svg" width="18"
height="18" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" height="18" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
class="size-6"> class="size-6">

View File

@@ -270,7 +270,7 @@
class="inline-flex items-center justify-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200"> class="inline-flex items-center justify-center px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200">
View Timer Functions View Timer Functions
</a> </a>
<a href="{{ url_for('dashboard_http_functions') }}" <a href="{{ url_for('http.dashboard_http_functions') }}"
class="inline-flex items-center justify-center px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors duration-200"> class="inline-flex items-center justify-center px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors duration-200">
View HTTP Functions View HTTP Functions
</a> </a>

View File

@@ -8,10 +8,10 @@ show_edit_form=True,
show_logs=True, show_logs=True,
show_client=True, show_client=True,
show_history=True, show_history=True,
edit_url=url_for('http_function_editor', function_id=function_id), edit_url=url_for('http.http_function_editor', function_id=function_id),
cancel_url=url_for('dashboard_http_functions'), cancel_url=url_for('http.dashboard_http_functions'),
logs_url=url_for('get_http_function_logs', function_id=function_id), logs_url=url_for('http.get_http_function_logs', function_id=function_id),
history_url=url_for('get_http_function_history', function_id=function_id)) }} history_url=url_for('http.get_http_function_history', function_id=function_id)) }}
<div class="mx-auto w-full pt-4" id="client-u{{ user_id }}-f{{ function_id }}"> <div class="mx-auto w-full pt-4" id="client-u{{ user_id }}-f{{ function_id }}">
</div> </div>

View File

@@ -10,8 +10,8 @@ show_client=True,
show_history=True, show_history=True,
edit_url=edit_url, edit_url=edit_url,
cancel_url=cancel_url, cancel_url=cancel_url,
logs_url=url_for('get_http_function_logs', function_id=function_id), logs_url=url_for('http.get_http_function_logs', function_id=function_id),
history_url=url_for('get_http_function_history', function_id=function_id)) }} history_url=url_for('http.get_http_function_history', function_id=function_id)) }}
<div id="app" class="p-1"> <div id="app" class="p-1">
@@ -33,8 +33,8 @@ history_url=url_for('get_http_function_history', function_id=function_id)) }}
logResponse: {{ log_response | tojson }}, logResponse: {{ log_response | tojson }},
versionNumber: {{ version_number }}, versionNumber: {{ version_number }},
executeUrl: "{{ url_for('execute_code', playground='true') }}", executeUrl: "{{ url_for('execute_code', playground='true') }}",
saveUrl: "{{ url_for('api_update_http_function', function_id=id) if id else url_for('api_create_http_function') }}", saveUrl: "{{ url_for('http.api_update_http_function', function_id=id) if id else url_for('http.api_create_http_function') }}",
deleteUrl: "{{ url_for('api_delete_http_function', function_id=id) if id else '' }}", deleteUrl: "{{ url_for('http.api_delete_http_function', function_id=id) if id else '' }}",
showDeleteButton: true showDeleteButton: true
}) })
}) })

View File

@@ -42,8 +42,8 @@
{% if show_client|default(false, true) %} {% if show_client|default(false, true) %}
<button <button
class="group flex flex-col items-center {% if active_tab == 'client' %}text-blue-600{% else %}text-gray-500 hover:text-blue-600{% endif %}" class="group flex flex-col items-center {% if active_tab == 'client' %}text-blue-600{% else %}text-gray-500 hover:text-blue-600{% endif %}"
hx-get="{{ url_for('client', function_id=function_id) }}" hx-target="#container" hx-swap="innerHTML" hx-get="{{ url_for('http.client', function_id=function_id) }}" hx-target="#container"
hx-push-url="true"> hx-swap="innerHTML" hx-push-url="true">
<div <div
class="p-2 rounded-lg {% if active_tab == 'client' %}bg-blue-50{% else %}group-hover:bg-blue-50{% endif %}"> class="p-2 rounded-lg {% if active_tab == 'client' %}bg-blue-50{% else %}group-hover:bg-blue-50{% endif %}">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" <svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"

View File

@@ -8,10 +8,10 @@ show_edit_form=True,
show_logs=True, show_logs=True,
show_client=True, show_client=True,
show_history=True, show_history=True,
edit_url=url_for('http_function_editor', function_id=function_id), edit_url=url_for('http.http_function_editor', function_id=function_id),
cancel_url=url_for('dashboard_http_functions'), cancel_url=url_for('http.dashboard_http_functions'),
logs_url=url_for('get_http_function_logs', function_id=function_id), logs_url=url_for('http.get_http_function_logs', function_id=function_id),
history_url=url_for('get_http_function_history', function_id=function_id)) }} history_url=url_for('http.get_http_function_history', function_id=function_id)) }}
<!-- Timeline --> <!-- Timeline -->
<div> <div>

View File

@@ -8,10 +8,10 @@ show_edit_form=True,
show_logs=True, show_logs=True,
show_client=True, show_client=True,
show_history=True, show_history=True,
edit_url=url_for('http_function_editor', function_id=function_id), edit_url=url_for('http.http_function_editor', function_id=function_id),
cancel_url=url_for('dashboard_http_functions'), cancel_url=url_for('http.dashboard_http_functions'),
logs_url=url_for('get_http_function_logs', function_id=function_id), logs_url=url_for('http.get_http_function_logs', function_id=function_id),
history_url=url_for('get_http_function_history', function_id=function_id)) }} history_url=url_for('http.get_http_function_history', function_id=function_id)) }}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden"> <div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">

View File

@@ -9,8 +9,8 @@ show_refresh=False,
show_logs=False, show_logs=False,
show_client=False, show_client=False,
show_link=False, show_link=False,
dashboardUrl=url_for('dashboard_http_functions'), dashboardUrl=url_for('http.dashboard_http_functions'),
cancel_url=url_for('dashboard_http_functions'), cancel_url=url_for('http.dashboard_http_functions'),
title='New HTTP Function') title='New HTTP Function')
}} }}
@@ -32,9 +32,9 @@ title='New HTTP Function')
logRequest: {{ log_request | tojson }}, logRequest: {{ log_request | tojson }},
logResponse: {{ log_response | tojson }}, logResponse: {{ log_response | tojson }},
executeUrl: "{{ url_for('execute_code', playground='true') }}", executeUrl: "{{ url_for('execute_code', playground='true') }}",
saveUrl: "{{ url_for('api_create_http_function') }}", saveUrl: "{{ url_for('http.api_create_http_function') }}",
showDeleteButton: false, showDeleteButton: false,
dashboardUrl: "{{ url_for('dashboard_http_functions') }}" dashboardUrl: "{{ url_for('http.dashboard_http_functions') }}"
}) })
}) })
</script> </script>

View File

@@ -7,7 +7,7 @@
<h1 class="text-2xl font-bold text-gray-900">HTTP Functions</h1> <h1 class="text-2xl font-bold text-gray-900">HTTP Functions</h1>
<button <button
class="inline-flex items-center px-4 py-2 ml-auto bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors duration-200" class="inline-flex items-center px-4 py-2 ml-auto bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors duration-200"
hx-get="{{ url_for('get_http_function_add_form') }}" hx-target="#container" hx-swap="innerHTML" hx-get="{{ url_for('http.get_http_function_add_form') }}" hx-target="#container" hx-swap="innerHTML"
hx-push-url="true"> hx-push-url="true">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" class="w-5 h-5 mr-2"> stroke="currentColor" class="w-5 h-5 mr-2">
@@ -34,7 +34,7 @@
<tr class="hover:bg-gray-50"> <tr class="hover:bg-gray-50">
<td class="px-6 py-4"> <td class="px-6 py-4">
<div class="flex items-center gap-2 cursor-pointer" <div class="flex items-center gap-2 cursor-pointer"
hx-get="{{ url_for('http_function_editor', function_id=function.id) }}" hx-get="{{ url_for('http.http_function_editor', function_id=function.id) }}"
hx-target="#container" hx-swap="innerHTML" hx-push-url="true"> hx-target="#container" hx-swap="innerHTML" hx-push-url="true">
<span class="font-medium text-gray-900">{{ function.name }}</span> <span class="font-medium text-gray-900">{{ function.name }}</span>
<span <span
@@ -76,7 +76,7 @@
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-indigo-700 bg-indigo-50 rounded-md hover:bg-indigo-100 transition-colors duration-200" class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-indigo-700 bg-indigo-50 rounded-md hover:bg-indigo-100 transition-colors duration-200"
hx-get="{{ url_for('get_http_function_logs', function_id=function.id) }}" hx-get="{{ url_for('http.get_http_function_logs', function_id=function.id) }}"
hx-target="#container" hx-swap="innerHTML" hx-push-url="true"> hx-target="#container" hx-swap="innerHTML" hx-push-url="true">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1.5" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1.5" fill="none"
viewBox="0 0 24 24" stroke="currentColor"> viewBox="0 0 24 24" stroke="currentColor">
@@ -87,8 +87,8 @@
</button> </button>
<button <button
class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-indigo-700 bg-indigo-50 rounded-md hover:bg-indigo-100 transition-colors duration-200" class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-indigo-700 bg-indigo-50 rounded-md hover:bg-indigo-100 transition-colors duration-200"
hx-get="{{ url_for('client', function_id=function.id) }}" hx-target="#container" hx-get="{{ url_for('http.client', function_id=function.id) }}"
hx-swap="innerHTML" hx-push-url="true"> hx-target="#container" hx-swap="innerHTML" hx-push-url="true">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1.5" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1.5" fill="none"
viewBox="0 0 24 24" stroke="currentColor"> viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"