From e7000f0b2e4f926b15be233923dfe0bebedc0198 Mon Sep 17 00:00:00 2001 From: hzhang Date: Thu, 20 Mar 2025 13:58:24 +0000 Subject: [PATCH] improve: change db schema for settings --- api/markdown.py | 12 +-- api/path.py | 18 ++-- api/setting.py | 122 +++++++++++++++++++++ api/template.py | 136 ++++++++++++++++++++++++ api/webhook.py | 25 ++--- db/models/Markdown.py | 5 +- db/models/MarkdownSetting.py | 15 +++ db/models/MarkdownTemplate.py | 18 ++++ db/models/Path.py | 4 +- db/models/PathSetting.py | 17 +++ db/models/PathTemplate.py | 18 ++++ db/models/WebhookSetting.py | 10 +- events/WebhookEventHandlers/__init__.py | 3 +- 13 files changed, 361 insertions(+), 42 deletions(-) create mode 100644 api/setting.py create mode 100644 api/template.py create mode 100644 db/models/MarkdownSetting.py create mode 100644 db/models/MarkdownTemplate.py create mode 100644 db/models/PathSetting.py create mode 100644 db/models/PathTemplate.py diff --git a/api/markdown.py b/api/markdown.py index fe6fe1b..e40da72 100644 --- a/api/markdown.py +++ b/api/markdown.py @@ -25,7 +25,7 @@ def get_markdowns(): @etag_response def get_home(): with get_db() as session: - markdown = session.query(Markdown).filter(Markdown.path_id == 1).filter(Markdown.title == "index").first() + markdown = session.query(Markdown).filter(Markdown.path_id == 1, Markdown.title == "index").first() if markdown is None: return jsonify({}), 204 return jsonify(markdown.to_dict()), 200 @@ -80,7 +80,7 @@ def create_markdown(): return jsonify({"error": "duplicate shortcut"}), 400 session.add(new_markdown) session.commit() - markdown_created.send(None, new_markdown.to_dict()) + markdown_created.send(None, payload=new_markdown.to_dict()) return jsonify(new_markdown.to_dict()), 201 except Exception as e: logger.error(f"failed to create markdown: {e}") @@ -120,7 +120,7 @@ def update_markdown(markdown_id): if 'shortcut' in data: markdown.shortcut = data.get('shortcut') session.commit() - markdown_updated.send(None, markdown.to_dict()) + markdown_updated.send(None, payload=markdown.to_dict()) return jsonify(markdown.to_dict()), 200 @markdown_bp.route('/', methods=['DELETE']) @@ -136,7 +136,7 @@ def delete_markdown(markdown_id): md = markdown.to_dict() session.delete(markdown) session.commit() - markdown_deleted.send(None, md) + markdown_deleted.send(None, payload=md) return jsonify({"message": "deleted"}), 200 @@ -155,7 +155,7 @@ def move_forward(markdown_id): previous_markdown = siblings[current_index - 1] markdown.order, previous_markdown.order = previous_markdown.order, markdown.order session.commit() - markdown_updated.send(None, markdown.to_dict()) + markdown_updated.send(None, payload=markdown.to_dict()) return jsonify(markdown.to_dict()), 200 @@ -175,7 +175,7 @@ def move_backward(markdown_id): next_markdown = siblings[current_index + 1] markdown.order, next_markdown.order = next_markdown.order, markdown.order session.commit() - markdown_updated.send(None, markdown.to_dict()) + markdown_updated.send(None, payload=markdown.to_dict()) return jsonify(markdown.to_dict()), 200 @markdown_bp.route('/search/', methods=['GET']) diff --git a/api/path.py b/api/path.py index 741ba7a..6ea9c35 100644 --- a/api/path.py +++ b/api/path.py @@ -55,7 +55,7 @@ def create_path(): new_path = Path(name=data['name'], parent_id=data['parent_id']) session.add(new_path) session.commit() - path_created.send(None, new_path.to_dict()) + path_created.send(None, payload=new_path.to_dict()) return jsonify(new_path.to_dict()), 201 @path_bp.route('/', methods=['PUT']) @@ -74,7 +74,7 @@ def update_path(path_id): path.name = data['name'] path.parent_id = data['parent_id'] session.commit() - path_updated.send(None, path.to_dict()) + path_updated.send(None, payload=path.to_dict()) return jsonify(path.to_dict()), 200 @path_bp.route('/', methods=['PATCH']) @@ -82,7 +82,7 @@ def update_path(path_id): @require_auth(roles=['admin']) def patch_path(path_id): data = request.json - if not data or 'name' not in data and 'parent_id' not in data: + if not data: return jsonify({"error": "bad request"}), 400 with get_db() as session: path = session.query(Path).get(path_id) @@ -90,13 +90,15 @@ def patch_path(path_id): return jsonify({"error": "path not found"}), 404 updated_name =data.get('name', path.name) updated_parent_id = data.get('parent_id', path.parent_id) + updated_setting_id = data.get('setting_id', path.setting_id) - if session.query(Path).filter_by(name=updated_name, parent_id=updated_parent_id).first(): + if session.query(Path).filter(Path.name==updated_name, Path.parent_id==updated_parent_id, Path.id != path_id).first(): return jsonify({"error": "Path already exists under the parent"}), 409 path.name = updated_name path.parent_id = updated_parent_id + path.setting_id = updated_setting_id session.commit() - path_updated.send(None, path.to_dict()) + path_updated.send(None, payload=path.to_dict()) return jsonify(path.to_dict()), 200 @@ -115,7 +117,7 @@ def delete_path(path_id): pth = path.to_dict() session.delete(path) session.commit() - path_deleted.send(None, pth) + path_deleted.send(None, payload=pth) return jsonify({"message": "path deleted"}), 200 @@ -134,7 +136,7 @@ def move_forward(path_id): previous_path = siblings[current_index - 1] path.order, previous_path.order = previous_path.order, path.order session.commit() - path_updated.send(None, path.to_dict()) + path_updated.send(None, payload=path.to_dict()) return jsonify(path.to_dict()), 200 @@ -154,6 +156,6 @@ def move_backward(path_id): next_path = siblings[current_index + 1] path.order, next_path.order = next_path.order, path.order session.commit() - path_updated.send(None, path.to_dict()) + path_updated.send(None, payload=path.to_dict()) return jsonify(path.to_dict()), 200 diff --git a/api/setting.py b/api/setting.py new file mode 100644 index 0000000..144a06f --- /dev/null +++ b/api/setting.py @@ -0,0 +1,122 @@ +from flask import Blueprint, jsonify, request +from api import limiter, etag_response, require_auth +from db import get_db +from db.models.MarkdownSetting import MarkdownSetting +from db.models.PathSetting import PathSetting +import api +import logging +logger = logging.getLogger(__name__) + +setting_bp = Blueprint('setting', __name__, url_prefix='/api/setting') + +@setting_bp.route('/path/', methods=['GET']) +@limiter.limit(api.get_rate_limit) +@etag_response +def get_path_setting(setting_id): + with get_db() as session: + setting = session.query(PathSetting).get(setting_id) + if setting is None: + return jsonify({}), 204 + return jsonify(setting.to_dict()), 200 + +@setting_bp.route('/path/', methods=['POST']) +@require_auth(roles=['admin']) +def create_path_setting(): + data = request.json + webhook_setting_id = data.get('webhook_setting_id') + template_setting_id = data.get('template_setting_id') + setting = PathSetting( + webhook_setting_id=webhook_setting_id, + template_setting_id=template_setting_id + ) + with get_db() as session: + try: + session.add(setting) + session.commit() + return jsonify(setting.to_dict()), 200 + except Exception: + logger.error(f"failed to create path setting") + return jsonify({"error": "failed to create setting"}), 500 + + +@setting_bp.route('/path/', methods=['PUT', 'PATCH']) +@require_auth(roles=['admin']) +def update_path_setting(setting_id): + data = request.json + with get_db() as session: + try: + setting = session.query(PathSetting).get(setting_id) + if setting is None: + return jsonify({"error": "setting not exists"}), 400 + webhook_setting_id = data.get('webhook_setting_id', setting.webhook_setting_id) + template_setting_id = data.get('template_setting_id', setting.template_setting_id) + setting.webhook_setting_id = webhook_setting_id + setting.template_setting_id = template_setting_id + session.commit() + return jsonify(setting.to_dict()), 200 + except Exception: + return jsonify({"error": "failed to update path setting"}), 500 + +@setting_bp.route('/path/', methods=['DELETE']) +@require_auth(roles=['admin']) +def delete_path_setting(setting_id): + with get_db() as session: + setting = session.query(PathSetting).get(setting_id) + if setting is None: + return jsonify({"error": "setting not exists"}), 400 + session.delete(setting) + return jsonify({"message": "deleted"}), 200 + + +@setting_bp.route('/markdown/', methods=['GET']) +@require_auth(roles=['admin']) +@etag_response +def get_markdown_path(setting_id): + with get_db() as session: + setting = session.query(MarkdownSetting).get(setting_id) + if setting is None: + return jsonify({}), 204 + return jsonify(setting.to_dict()), 200 + + +@setting_bp.route('/markdown/', methods=['POST']) +@require_auth(roles=['admin']) +def create_markdown_setting(): + data = request.json + template_setting_id = data.get('template_setting_id') + setting = MarkdownSetting(template_setting_id=template_setting_id) + try: + with get_db() as session: + session.add(setting) + session.commit() + return jsonify(setting.to_dict()), 200 + except Exception: + return jsonify({"error": "failed to create setting"}), 500 + +@setting_bp.route('/markdown/', methods=['PUT', 'PATCH']) +@require_auth(roles=['admin']) +def update_markdown_setting(setting_id): + data = request.json + try: + with get_db() as session: + setting = session.query(MarkdownSetting).get(setting_id) + if setting is None: + return jsonify({"error": "setting not exists"}), 400 + template_setting_id = data.get('template_setting_id', setting.template_setting_id) + setting.template_setting_id = template_setting_id + session.commit() + return jsonify(setting.to_dict()), 200 + except Exception: + return jsonify({"error": "failed to update setting"}), 500 + + +@setting_bp.route('/markdown/', methods=['DELETE']) +@require_auth(roles=['admin']) +def delete_markdown_setting(setting_id): + with get_db() as session: + setting = session.query(MarkdownSetting).get(setting_id) + if setting is None: + return jsonify({"error": "setting not exists"}), 400 + session.delete(setting) + session.commit() + return jsonify({"message": "deleted"}), 200 diff --git a/api/template.py b/api/template.py new file mode 100644 index 0000000..fc44c44 --- /dev/null +++ b/api/template.py @@ -0,0 +1,136 @@ +from flask import Blueprint, jsonify, request +from db import get_db +from api import require_auth, etag_response +from db.models.MarkdownTemplate import MarkdownTemplate +from db.models.PathTemplate import PathTemplate + +template_bp = Blueprint('template', __name__, url_prefix='/api/template') + +@template_bp.route('/path/', methods=['GET']) +@etag_response +def get_path_template(template_id): + with get_db() as session: + template = session.query(PathTemplate).get(template_id) + if template is None: + return jsonify({}), 204 + return jsonify(template.to_dict()), 200 + +@template_bp.route('/path/', methods=['GET']) +@etag_response +def get_path_templates(): + with get_db() as session: + templates = session.query(PathTemplate).all() + return jsonify([template.to_dict() for template in templates]), 200 + + +@template_bp.route('/path/', methods=['POST']) +@require_auth(roles=['admin']) +def create_path_template(): + data = request.json + if "title" not in data: + return jsonify({"error": "title is missing"}), 400 + title = data.get("title") + structure = data.get("structure") + template = PathTemplate(title=title, structure=structure) + try: + with get_db() as session: + session.add(template) + session.commit() + return template.to_dict(), 200 + except Exception as e: + return jsonify({"error": "failed to create path template"}), 400 + + +@template_bp.route('/path/', methods=['PUT', 'PATCH']) +@require_auth(roles=['admin']) +def update_path_template(template_id): + data = request.json + with get_db() as session: + template = session.query(PathTemplate).get(template_id) + if template is None: + return jsonify({'error': 'path template not found'}), 400 + title = data.get("title", template.title) + structure = data.get("structure", template.structure) + template.title = title + template.structure = structure + session.commit() + return jsonify(template.to_dict()), 200 + + +@template_bp.route('/path/', methods=['DELETE']) +@require_auth(roles=['admin']) +def delete_path_template(template_id): + with get_db() as session: + template = session.query(PathTemplate).get(template_id) + if template is None: + return jsonify({'error': 'path template not found'}), 400 + session.delete(template) + session.commit() + return jsonify({'message': 'deleted'}), 200 + + +@template_bp.route('/markdown/', methods=['GET']) +@etag_response +def get_markdown_template(template_id): + with get_db() as session: + template = session.query(MarkdownTemplate).get(template_id) + if template is None: + return jsonify({}), 204 + return jsonify(template.to_dict()), 200 + +@template_bp.route('/markdown/', methods=['GET']) +@etag_response +def get_markdown_templates(): + with get_db() as session: + templates = session.query(MarkdownTemplate).all() + return jsonify([template.to_dict() for template in templates]), 200 + + +@template_bp.route('/markdown/', methods=['POST']) +@require_auth(roles=['admin']) +def create_markdown_template(): + data = request.json + if "title" not in data: + return jsonify({"error": "title is missing"}), 400 + title = data.get("title") + parameters = data.get("parameters") + define = data.get("define") + template = MarkdownTemplate(title=title, parameters=parameters, define=define) + try: + with get_db() as session: + session.add(template) + session.commit() + return template.to_dict(), 200 + except Exception as e: + return jsonify({"error": "failed to create markdown template"}), 400 + + +@template_bp.route('/markdown/', methods=['PUT', 'PATCH']) +@require_auth(roles=['admin']) +def update_markdown_template(template_id): + data = request.json + with get_db() as session: + template = session.query(MarkdownTemplate).get(template_id) + if template is None: + return jsonify({'error': 'markdown template not found'}), 400 + title = data.get("title", template.title) + parameters = data.get("parameters", template.parameters) + define = data.get("define", template.define) + template.title = title + template.parameters = parameters + template.define = define + session.commit() + return jsonify(template.to_dict()), 200 + + +@template_bp.route('/markdown/', methods=['DELETE']) +@require_auth(roles=['admin']) +def delete_markdown_template(template_id): + with get_db() as session: + template = session.query(MarkdownTemplate).get(template_id) + if template is None: + return jsonify({'error': 'markdown template not found'}), 400 + session.delete(template) + session.commit() + return jsonify({'message': 'deleted'}), 200 + diff --git a/api/webhook.py b/api/webhook.py index 63885d3..6507bbd 100644 --- a/api/webhook.py +++ b/api/webhook.py @@ -1,5 +1,5 @@ from flask import Blueprint, jsonify, request -from api import require_auth, limiter +from api import require_auth from db import get_db from db.models.Webhook import Webhook from db.models.WebhookSetting import WebhookSetting @@ -70,7 +70,6 @@ def delete_webhook(webhook_id): return jsonify({'message': 'Webhook deleted'}), 200 - @webhook_bp.route('/setting/', methods=['GET']) @require_auth(roles=['admin']) def list_webhook_settings(): @@ -78,6 +77,7 @@ def list_webhook_settings(): settings = session.query(WebhookSetting).all() return jsonify([s.to_dict() for s in settings]), 200 + @webhook_bp.route('/setting/', methods=['GET']) @require_auth(roles=['admin']) def webhook_setting(setting_id): @@ -88,27 +88,12 @@ def webhook_setting(setting_id): return jsonify(setting.to_dict()), 200 -@webhook_bp.route('/setting/path/', methods=['GET']) -@require_auth(roles=['admin']) -def webhook_setting_by_path(path_id): - with get_db() as session: - setting = session.query(WebhookSetting).filter(WebhookSetting.path_id == path_id).first() - if not setting: - return jsonify({'info': 'Webhook setting not found'}), 204 - return jsonify(setting.to_dict()), 200 - @webhook_bp.route('/setting/', methods=['POST']) @require_auth(roles=['admin']) def create_webhook_setting(): data = request.json - print(data) - required = ['path_id', 'webhook_id'] - for key in required: - if key not in data: - return jsonify({'error': 'Required field missing'}), 400 with get_db() as session: setting = WebhookSetting( - path_id=data.get('path_id'), webhook_id=data.get('webhook_id'), recursive=data.get('recursive', False), additional_header=data.get('additional_header', ''), @@ -118,6 +103,8 @@ def create_webhook_setting(): session.add(setting) session.commit() return jsonify(setting.to_dict()), 201 + + @webhook_bp.route('/setting/', methods=['PUT', 'PATCH']) @require_auth(roles=['admin']) def update_webhook_setting(setting_id): @@ -126,7 +113,8 @@ def update_webhook_setting(setting_id): setting = session.query(WebhookSetting).get(setting_id) if not setting: return jsonify({'error': 'Webhook setting not found'}), 404 - + if 'webhook_id' in data: + setting.webhook_id = data['webhook_id'] if 'recursive' in data: setting.recursive = data['recursive'] if 'additional_header' in data: @@ -139,6 +127,7 @@ def update_webhook_setting(setting_id): session.commit() return jsonify(setting.to_dict()), 200 + @webhook_bp.route('/setting/', methods=['DELETE']) @require_auth(roles=['admin']) def delete_webhook_setting(setting_id): diff --git a/db/models/Markdown.py b/db/models/Markdown.py index f13337c..7900acb 100644 --- a/db/models/Markdown.py +++ b/db/models/Markdown.py @@ -1,6 +1,6 @@ import uuid -from sqlalchemy import Column, Text, Integer, String, DateTime, ForeignKey, Float, text, UniqueConstraint +from sqlalchemy import Column, Text, Integer, String, DateTime, ForeignKey, UniqueConstraint from db.models import Base import datetime @@ -13,6 +13,8 @@ class Markdown(Base): path_id = Column(Integer, ForeignKey('path.id'), nullable=False) order = Column(String(36), default=lambda: str(uuid.uuid4())) shortcut = Column(String(36), default="") + template_id = Column(Integer, ForeignKey("markdown_template.id"), nullable=True) + __table_args__ = (UniqueConstraint('path_id', 'title', name="unique_path_id_title"),) def to_dict(self): return { 'id': self.id, @@ -22,6 +24,7 @@ class Markdown(Base): 'path_id': self.path_id, 'order': self.order, 'shortcut': self.shortcut, + 'template_id': self.template_id, } __payload__ = { 'dev': [ diff --git a/db/models/MarkdownSetting.py b/db/models/MarkdownSetting.py new file mode 100644 index 0000000..dbbf3fe --- /dev/null +++ b/db/models/MarkdownSetting.py @@ -0,0 +1,15 @@ +from sqlalchemy import Column, Integer, ForeignKey + +from db.models import Base + + +class MarkdownSetting(Base): + __tablename__ = 'markdown_settings' + id = Column(Integer, primary_key=True, autoincrement=True) + template_setting_id = Column(Integer, ForeignKey('markdown_template.id'), nullable=True) + + def to_dict(self): + return { + "id": self.id, + "template_setting_id": self.template_setting_id, + } \ No newline at end of file diff --git a/db/models/MarkdownTemplate.py b/db/models/MarkdownTemplate.py new file mode 100644 index 0000000..33e3c2d --- /dev/null +++ b/db/models/MarkdownTemplate.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Text, Integer, String, UniqueConstraint +from db.models import Base + +class MarkdownTemplate(Base): + __tablename__ = 'markdown_template' + id = Column(Integer, primary_key=True) + title = Column(String(255), nullable=False) + parameters = Column(Text, nullable=True) + define = Column(Text, nullable=True) + __table_args__ = (UniqueConstraint("title", name="unique_title"),) + + def to_dict(self): + return { + 'id': self.id, + 'title': self.title, + 'parameters': self.parameters, + 'define': self.define, + } diff --git a/db/models/Path.py b/db/models/Path.py index f56d368..e9d7197 100644 --- a/db/models/Path.py +++ b/db/models/Path.py @@ -1,6 +1,6 @@ import uuid -from sqlalchemy import Column, String, Integer, ForeignKey, UniqueConstraint, text +from sqlalchemy import Column, String, Integer, ForeignKey, UniqueConstraint from db.models import Base @@ -10,6 +10,7 @@ class Path(Base): name = Column(String(50), nullable=False) parent_id = Column(Integer, ForeignKey("path.id"), nullable=True) order = Column(String(36), default=lambda: str(uuid.uuid4())) + setting_id = Column(Integer, ForeignKey("path_settings.id"), nullable=True) __table_args__ = (UniqueConstraint("parent_id", "name", name="unique_parent_id_name"), ) def to_dict(self): return { @@ -17,6 +18,7 @@ class Path(Base): "name": self.name, "parent_id": self.parent_id, "order": self.order, + "setting_id": self.setting_id } __payload__ = { diff --git a/db/models/PathSetting.py b/db/models/PathSetting.py new file mode 100644 index 0000000..6bf06af --- /dev/null +++ b/db/models/PathSetting.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, Integer, ForeignKey + +from db.models import Base + + +class PathSetting(Base): + __tablename__ = 'path_settings' + id = Column(Integer, primary_key=True, autoincrement=True) + webhook_setting_id = Column(Integer, ForeignKey('webhook_setting.id'), nullable=True) + template_setting_id = Column(Integer, ForeignKey('path_template.id'), nullable=True) + + def to_dict(self): + return { + "id": self.id, + "webhook_setting_id": self.webhook_setting_id, + "template_setting_id": self.template_setting_id, + } diff --git a/db/models/PathTemplate.py b/db/models/PathTemplate.py new file mode 100644 index 0000000..e16c1d8 --- /dev/null +++ b/db/models/PathTemplate.py @@ -0,0 +1,18 @@ +from sqlalchemy import Column, Integer, Text, UniqueConstraint, String + +from db.models import Base + + +class PathTemplate(Base): + __tablename__ = 'path_template' + id = Column(Integer, primary_key=True) + title = Column(String(255), nullable=False) + structure = Column(Text, nullable=False) + __table_args__ = (UniqueConstraint("title", name="unique_title"),) + + def to_dict(self): + return { + 'id': self.id, + 'title': self.title, + 'structure': self.structure + } diff --git a/db/models/WebhookSetting.py b/db/models/WebhookSetting.py index 6974fa5..d3a8960 100644 --- a/db/models/WebhookSetting.py +++ b/db/models/WebhookSetting.py @@ -1,23 +1,19 @@ -from sqlalchemy import Column, String, Integer, ForeignKey, UniqueConstraint, Boolean +from sqlalchemy import Column, Text, Integer, ForeignKey, UniqueConstraint, Boolean from db.models import Base class WebhookSetting(Base): __tablename__ = 'webhook_setting' id = Column(Integer, primary_key=True, autoincrement=True) - path_id = Column(Integer, ForeignKey('path.id'), nullable=False) - webhook_id = Column(Integer, ForeignKey('webhook.id'), nullable=False) + webhook_id = Column(Integer, ForeignKey('webhook.id'), nullable=True) recursive = Column(Boolean, default=False) - additional_header = Column(String(500), nullable=True) + additional_header = Column(Text, nullable=True) enabled = Column(Boolean, default=True) on_events = Column(Integer, default=0) - __table_args__ = (UniqueConstraint('webhook_id', 'path_id', name='_webhook_path_uc'),) - def to_dict(self): return { "id": self.id, - "path_id": self.path_id, "webhook_id": self.webhook_id, "recursive": self.recursive, "additional_header": self.additional_header, diff --git a/events/WebhookEventHandlers/__init__.py b/events/WebhookEventHandlers/__init__.py index 21ef414..21df737 100644 --- a/events/WebhookEventHandlers/__init__.py +++ b/events/WebhookEventHandlers/__init__.py @@ -17,7 +17,8 @@ class WebhookEventHandler(abc.ABC): def get_path_id(self, payload): pass - def __call__(self, payload): + def __call__(self, **kwargs): + payload = kwargs['payload'] path_id = self.get_path_id(payload) with db.get_db() as session: setting = self.get_setting(session, path_id)