diff --git a/backend/src/platform/isolationEngine/environment.py b/backend/src/platform/isolationEngine/environment.py index 3483304..010cd27 100644 --- a/backend/src/platform/isolationEngine/environment.py +++ b/backend/src/platform/isolationEngine/environment.py @@ -44,11 +44,38 @@ def migrate_schema(self, template_schema: str, target_schema: str) -> None: ) meta.create_all(translated) + # Ensure newer columns exist even if template was seeded before they + # were added to the ORM models. Uses IF NOT EXISTS so it's safe to + # run repeatedly. + self._ensure_box_columns(target_schema) + # Copy GIN / non-standard indexes that MetaData.reflect doesn't capture self._copy_custom_indexes(template_schema, target_schema) self._set_replica_identity(target_schema) + def _ensure_box_columns(self, schema: str) -> None: + """Add columns that may be missing from older template snapshots.""" + _columns = [ + # (table, column, SQL type, default) + ("box_folders", "path", "VARCHAR(500)", "'/'"), + ("box_files", "path", "VARCHAR(500)", "'/0/'"), + ] + with self.session_manager.base_engine.begin() as conn: + for table, col, sql_type, default in _columns: + try: + conn.execute( + text( + f"ALTER TABLE {schema}.{table} " + f"ADD COLUMN IF NOT EXISTS {col} {sql_type} " + f"DEFAULT {default}" + ) + ) + except Exception as exc: + logger.warning( + f"Could not ensure column {schema}.{table}.{col}: {exc}" + ) + def _copy_custom_indexes(self, src_schema: str, dst_schema: str) -> None: """Copy GIN trigram and other custom indexes from template to target schema.""" with self.session_manager.base_engine.begin() as conn: diff --git a/backend/src/services/box/api/routes.py b/backend/src/services/box/api/routes.py index 8ee5883..3c4068d 100644 --- a/backend/src/services/box/api/routes.py +++ b/backend/src/services/box/api/routes.py @@ -1429,6 +1429,82 @@ async def list_file_comments(request: Request) -> Response: return _error_response(e) +async def get_comment_by_id(request: Request) -> Response: + """ + GET /2.0/comments/{comment_id} + + Retrieves a specific comment. + + SDK Reference: CommentsManager.get_comment_by_id() + """ + try: + session = _session(request) + comment_id = request.path_params["comment_id"] + fields = _parse_fields(request) + + comment = ops.get_comment_by_id(session, comment_id) + if not comment: + _box_error(BoxErrorCode.NOT_FOUND, "Not Found") + + comment_data = comment.to_dict() + filtered_data = _filter_fields(comment_data, fields) + + return _json_response(filtered_data) + + except BoxAPIError as e: + return _error_response(e) + + +async def update_comment_by_id(request: Request) -> Response: + """ + PUT /2.0/comments/{comment_id} + + Updates a comment's message. + + SDK Reference: CommentsManager.update_comment_by_id() + """ + try: + session = _session(request) + comment_id = request.path_params["comment_id"] + fields = _parse_fields(request) + + body = await _parse_json_body(request) + message = body.get("message") + + comment = ops.update_comment(session, comment_id, message=message) + + comment_data = comment.to_dict() + filtered_data = _filter_fields(comment_data, fields) + + return _json_response(filtered_data) + + except BoxAPIError as e: + return _error_response(e) + + +async def delete_comment_by_id(request: Request) -> Response: + """ + DELETE /2.0/comments/{comment_id} + + Deletes a comment. + + SDK Reference: CommentsManager.delete_comment_by_id() + + Returns: + 204 No Content on success + """ + try: + session = _session(request) + comment_id = request.path_params["comment_id"] + + ops.delete_comment(session, comment_id) + + return Response(status_code=status.HTTP_204_NO_CONTENT) + + except BoxAPIError as e: + return _error_response(e) + + # Task Endpoints @@ -1567,6 +1643,102 @@ async def create_task(request: Request) -> Response: return _error_response(e) +async def get_task_by_id(request: Request) -> Response: + """ + GET /2.0/tasks/{task_id} + + Retrieves a specific task. + + SDK Reference: TasksManager.get_task_by_id() + """ + try: + session = _session(request) + task_id = request.path_params["task_id"] + fields = _parse_fields(request) + + task = ops.get_task_by_id(session, task_id) + if not task: + _box_error(BoxErrorCode.NOT_FOUND, "Not Found") + + task_data = task.to_dict() + filtered_data = _filter_fields(task_data, fields) + + return _json_response(filtered_data) + + except BoxAPIError as e: + return _error_response(e) + + +async def update_task_by_id(request: Request) -> Response: + """ + PUT /2.0/tasks/{task_id} + + Updates a task. + + SDK Reference: TasksManager.update_task_by_id() + """ + try: + session = _session(request) + task_id = request.path_params["task_id"] + fields = _parse_fields(request) + + body = await _parse_json_body(request) + + action = body.get("action") + message = body.get("message") + due_at_str = body.get("due_at") + completion_rule = body.get("completion_rule") + + due_at = None + if due_at_str: + from datetime import datetime + + try: + due_at = datetime.fromisoformat(due_at_str.replace("Z", "+00:00")) + except (ValueError, AttributeError): + _box_error(BoxErrorCode.BAD_REQUEST, "Invalid 'due_at' format") + + task = ops.update_task( + session, + task_id, + action=action, + message=message, + due_at=due_at, + completion_rule=completion_rule, + ) + + task_data = task.to_dict() + filtered_data = _filter_fields(task_data, fields) + + return _json_response(filtered_data) + + except BoxAPIError as e: + return _error_response(e) + + +async def delete_task_by_id(request: Request) -> Response: + """ + DELETE /2.0/tasks/{task_id} + + Deletes a task. + + SDK Reference: TasksManager.delete_task_by_id() + + Returns: + 204 No Content on success + """ + try: + session = _session(request) + task_id = request.path_params["task_id"] + + ops.delete_task(session, task_id) + + return Response(status_code=status.HTTP_204_NO_CONTENT) + + except BoxAPIError as e: + return _error_response(e) + + # Hub Endpoints (Requires box-version: 2025.0 header) @@ -2079,8 +2251,14 @@ async def get_collection_items(request: Request) -> Response: Route("/files/{file_id}/tasks", list_file_tasks, methods=["GET"]), # Comments Route("/comments", create_comment, methods=["POST"]), + Route("/comments/{comment_id}", get_comment_by_id, methods=["GET"]), + Route("/comments/{comment_id}", update_comment_by_id, methods=["PUT"]), + Route("/comments/{comment_id}", delete_comment_by_id, methods=["DELETE"]), # Tasks Route("/tasks", create_task, methods=["POST"]), + Route("/tasks/{task_id}", get_task_by_id, methods=["GET"]), + Route("/tasks/{task_id}", update_task_by_id, methods=["PUT"]), + Route("/tasks/{task_id}", delete_task_by_id, methods=["DELETE"]), # Hubs (requires box-version: 2025.0 header) Route("/hubs", list_hubs, methods=["GET"]), Route("/hubs", create_hub, methods=["POST"]),