Skip to content

feat: implement middleware management controllers - #199

Open
keshxvdayal wants to merge 11 commits into
devfrom
features/controller-implementation
Open

feat: implement middleware management controllers#199
keshxvdayal wants to merge 11 commits into
devfrom
features/controller-implementation

Conversation

@keshxvdayal

@keshxvdayal keshxvdayal commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Detailed Implementation Plan

  1. Create Middleware Controller Module
    Implement pro_tes.api.middlewares.controllers to serve the middleware management API expected by FOCA/Connexion.

  2. Add Endpoint Handlers
    Implement handlers for:

    • GET /middlewares - List configured middlewares
    • POST /middlewares - Add a new middleware
    • GET /middlewares/{middleware_id} - Get middleware details
    • PATCH /middlewares/{middleware_id} - Update middleware configuration
    • DELETE /middlewares/{middleware_id} - Remove middleware
    • PUT /middlewares/reorder - Reorder the middleware stack
  3. Connect to FOCA Database Configuration
    Use the existing FOCA database wiring in config.yaml so the controller layer reads and writes from the taskStore.middlewares collection.

  4. Implement API Behavior
    Add the required runtime behavior for:

    • middleware creation with execution order assignment
    • partial updates for name/config fields
    • deletion by middleware ID
    • full stack reordering with ordered IDs
    • response shaping with API-friendly id fields
  5. Add Unit Tests
    Create controller tests using mongomock to validate:

    • create
    • list
    • get
    • update
    • delete
    • reorder
  6. Validate the Implementation
    Run the middleware controller test suite to confirm the controller layer works as expected.

  7. Document the Completion
    Add a short status note to the middleware docs so the API documentation reflects that subtask 2 is implemented and verified.

Summary by Sourcery

Implement controller layer and routing for middleware management APIs backed by the taskStore.middlewares collection.

New Features:

  • Expose CRUD and reordering endpoints for middlewares, including listing with pagination, creation with automatic ordering, retrieval by ID, partial updates, deletion, and stack reordering.

Enhancements:

  • Add a middleware API package and wire the controller module into the existing OpenAPI/FOCA configuration for runtime use.

Documentation:

  • Update middleware documentation with implementation status, code locations, and test command for the controller layer.

Tests:

  • Add unit tests for the middleware controllers covering list, create, get, update, delete, and reorder behaviors.

Copilot AI review requested due to automatic review settings June 18, 2026 02:45
@sourcery-ai

sourcery-ai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements a new middleware management controller layer backed by the FOCA MongoDB config, exposing CRUD and reordering endpoints plus basic pagination/sorting, and documents its implementation status.

Sequence diagram for ReorderMiddlewares runtime flow

sequenceDiagram
    actor Client
    participant Connexion
    participant Controllers as ReorderMiddlewares
    participant FlaskApp as current_app
    participant FOCA as foca_db
    participant Mongo as middlewares_collection

    Client->>Connexion: PUT /middlewares/reorder
    Connexion->>Controllers: ReorderMiddlewares(body)
    Controllers->>FlaskApp: _collection()
    FlaskApp->>FOCA: config.foca.db.dbs[taskStore]
    FOCA->>Mongo: collections[middlewares]
    Mongo-->>Controllers: collection_client

    Controllers->>Mongo: find({}, {_id})
    Mongo-->>Controllers: existing_ids
    Controllers-->>Controllers: [validate ordered_ids]

    loop for each id in ordered_ids
        Controllers->>Mongo: update_one({_id: oid}, {$set: {order, updated_at}})
        Controllers->>Mongo: find_one({_id: oid}, projection)
        Mongo-->>Controllers: middleware_doc
        Controllers-->>Controllers: _doc_to_response(middleware_doc)
    end

    Controllers-->>Connexion: {message, middlewares}
    Connexion-->>Client: 200 OK + body
Loading

File-Level Changes

Change Details Files
Introduce middleware controller module implementing CRUD, pagination, filtering, and reordering operations on the middlewares collection.
  • Add a private helper to access the FOCA-configured taskStore.middlewares collection from Flask current_app.
  • Normalize MongoDB documents to API responses by stripping _id and exposing it as string id.
  • Implement ListMiddlewares with optional source filtering, configurable sort field/order, and page/page_size validation plus pagination metadata.
  • Implement AddMiddleware to append a middleware at the end of the stack, computing order as max(existing)+1 and setting RFC3339 created_at/updated_at timestamps.
  • Implement GetMiddleware with ObjectId validation and NotFound handling.
  • Implement UpdateMiddleware to partially update only name and config, refreshing updated_at, and returning the updated document.
  • Implement DeleteMiddleware that validates the ID, deletes the document, and returns an empty body with HTTP 204 semantics.
  • Implement ReorderMiddlewares which validates that the provided ordered IDs set matches existing IDs, updates order and updated_at in bulk, and returns the reordered list.
pro_tes/api/middlewares/controllers.py
Create middleware API package and document that the controller layer is implemented and wired through OpenAPI/FOCA.
  • Add pro_tes.api.middlewares package with an __all__ re-export of the controllers module to support the x-openapi-router-controller reference.
  • Extend middleware documentation with an implementation status section pointing to the controller module, config wiring, and associated unit test command.
pro_tes/api/middlewares/__init__.py
docs/middleware.md

Possibly linked issues

  • #feat: Implement Controllers for Middleware Management API: PR implements the middleware management controllers, wiring, and tests described in the issue, though some validations remain pending.
  • #Dynamic Middleware Management API Implementation: The PR implements the middleware management API, persistence, and behavior exactly as requested in the issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In ListMiddlewares, consider validating sort_by and sort_order against a small allowlist of supported fields/directions so a bad query parameter doesn’t propagate as an unhandled database error.
  • In ReorderMiddlewares, the implementation performs one update_one and one find_one per middleware; you could reduce this N+1 pattern by using a bulk update and then a single sorted query to return the updated middlewares in the new order.
  • The timestamp formatting logic (datetime.now(...).replace(...).isoformat().replace(...)) is duplicated in several functions; extracting a small helper (e.g., utc_now_iso()) would make the code easier to maintain and avoid inconsistencies.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ListMiddlewares`, consider validating `sort_by` and `sort_order` against a small allowlist of supported fields/directions so a bad query parameter doesn’t propagate as an unhandled database error.
- In `ReorderMiddlewares`, the implementation performs one `update_one` and one `find_one` per middleware; you could reduce this N+1 pattern by using a bulk update and then a single sorted query to return the updated middlewares in the new order.
- The timestamp formatting logic (`datetime.now(...).replace(...).isoformat().replace(...)`) is duplicated in several functions; extracting a small helper (e.g., `utc_now_iso()`) would make the code easier to maintain and avoid inconsistencies.

## Individual Comments

### Comment 1
<location path="pro_tes/api/middlewares/controllers.py" line_range="174-175" />
<code_context>
+
+    coll = _collection()
+    # Fetch existing ids
+    existing = list(coll.find({}, {"_id": True}))
+    existing_ids = {str(d["_id"]): d for d in existing}
+
+    if set(ordered_ids) != set(existing_ids.keys()):
</code_context>
<issue_to_address>
**suggestion:** The `existing_ids` mapping stores full docs but only keys are used; also the error message may not clearly surface invalid IDs.

Since only the keys are used later, `existing_ids` can just be a `set` of string IDs instead of a mapping to full documents. Also, when `ordered_ids` contains syntactically invalid IDs, the initial `set(ordered_ids) != set(existing_ids.keys())` check will fail with a generic mismatch message, and the more specific `Invalid id in ordered_ids` case is only reached later. If you care about clearer client feedback, consider validating that each `ordered_id` is a valid `ObjectId` string before the set comparison.

Suggested implementation:

```python
    coll = _collection()
    # Fetch existing ids
    existing = list(coll.find({}, {"_id": True}))
    existing_ids = {str(d["_id"]) for d in existing}

    # Validate that all provided ids are syntactically valid ObjectId strings
    for mid in ordered_ids:
        try:
            ObjectId(mid)
        except Exception:
            raise BadRequest(f"Invalid id in ordered_ids: {mid}")

    if set(ordered_ids) != existing_ids:
        raise BadRequest("`ordered_ids` must contain exactly all middleware ids")

```

```python
    # Apply new order
    updated_docs: List[Dict[str, Any]] = []
    now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
    for idx, mid in enumerate(ordered_ids):
        # At this point, all ids are known to be valid ObjectId strings
        oid = ObjectId(mid)

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +174 to +175
existing = list(coll.find({}, {"_id": True}))
existing_ids = {str(d["_id"]): d for d in existing}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The existing_ids mapping stores full docs but only keys are used; also the error message may not clearly surface invalid IDs.

Since only the keys are used later, existing_ids can just be a set of string IDs instead of a mapping to full documents. Also, when ordered_ids contains syntactically invalid IDs, the initial set(ordered_ids) != set(existing_ids.keys()) check will fail with a generic mismatch message, and the more specific Invalid id in ordered_ids case is only reached later. If you care about clearer client feedback, consider validating that each ordered_id is a valid ObjectId string before the set comparison.

Suggested implementation:

    coll = _collection()
    # Fetch existing ids
    existing = list(coll.find({}, {"_id": True}))
    existing_ids = {str(d["_id"]) for d in existing}

    # Validate that all provided ids are syntactically valid ObjectId strings
    for mid in ordered_ids:
        try:
            ObjectId(mid)
        except Exception:
            raise BadRequest(f"Invalid id in ordered_ids: {mid}")

    if set(ordered_ids) != existing_ids:
        raise BadRequest("`ordered_ids` must contain exactly all middleware ids")
    # Apply new order
    updated_docs: List[Dict[str, Any]] = []
    now = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
    for idx, mid in enumerate(ordered_ids):
        # At this point, all ids are known to be valid ObjectId strings
        oid = ObjectId(mid)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new FOCA/Connexion controller module to expose the Middleware Management API (CRUD + reorder) backed by the taskStore.middlewares MongoDB collection, and updates docs to reflect the new API layer.

Changes:

  • Implement pro_tes.api.middlewares.controllers with handlers for list/create/get/update/delete/reorder.
  • Add pro_tes.api.middlewares package initializer.
  • Update docs/middleware.md to describe implementation status and how to validate.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
pro_tes/api/middlewares/controllers.py New Mongo-backed controller functions implementing middleware management endpoints.
pro_tes/api/middlewares/init.py Declares the middlewares API package exports.
docs/middleware.md Documents controller implementation status and validation guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pro_tes/api/middlewares/controllers.py Outdated
Comment thread pro_tes/api/middlewares/controllers.py
Comment thread pro_tes/api/middlewares/controllers.py Outdated
Comment on lines +94 to +99
# Insert
try:
res = coll.insert_one(doc)
except Exception as exc:
logger.exception("Failed to insert middleware")
raise
Comment thread pro_tes/api/middlewares/controllers.py Outdated
Comment on lines +136 to +146
update_fields["updated_at"] = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
coll = _collection()
updated = coll.find_one_and_update(
{"_id": oid},
{"$set": update_fields},
projection={"_id": True, "name": True, "source": True, "order": True, "config": True, "created_at": True, "updated_at": True},
return_document=True,
)
if updated is None:
raise NotFound(f"Middleware with ID '{middleware_id}' not found")
return _doc_to_response(updated)
Comment thread pro_tes/api/middlewares/controllers.py Outdated
Comment thread pro_tes/api/middlewares/controllers.py Outdated
Comment thread docs/middleware.md Outdated
Comment on lines +35 to +39
def ListMiddlewares(page_size: int = 50,
page: int = 0,
sort_by: str = "order",
sort_order: str = "asc",
source: Optional[str] = None) -> Dict[str, Any]:
keshxvdayal and others added 8 commits June 18, 2026 08:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Keshav Dayal <115068840+keshxvdayal@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Keshav Dayal <115068840+keshxvdayal@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Keshav Dayal <115068840+keshxvdayal@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Keshav Dayal <115068840+keshxvdayal@users.noreply.github.com>
@keshxvdayal keshxvdayal self-assigned this Jun 18, 2026
@keshxvdayal
keshxvdayal requested a review from uniqueg June 18, 2026 03:18
@keshxvdayal keshxvdayal changed the title Implement middleware management controllers feat:Implement middleware management controllers Jun 18, 2026
@keshxvdayal keshxvdayal changed the title feat:Implement middleware management controllers feat: Implement middleware management controllers Jun 18, 2026
Signed-off-by: Keshav Dayal <115068840+keshxvdayal@users.noreply.github.com>
Signed-off-by: Keshav Dayal <115068840+keshxvdayal@users.noreply.github.com>
@uniqueg uniqueg changed the title feat: Implement middleware management controllers feat: implement middleware management controllers Aug 18, 2026
@uniqueg
uniqueg requested a lite review from Copilot August 18, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (7)

pro_tes/api/middlewares/controllers.py:90

  • This PR introduces a new controller surface (CRUD + reorder) but does not add any unit tests for these endpoints, despite the PR description stating mongomock-based controller tests would be added. Adding tests (or updating the PR description) would help prevent regressions, especially with validate_responses: true enabled for this spec.
def ListMiddlewares(page_size: int = 50,
                    page: int = 0,
                    sort_by: str = "order",
                    sort_order: str = "asc",
                    source: Optional[str] = None) -> Dict[str, Any]:
    """List middlewares with pagination and sorting."""

docs/middleware.md:44

  • The documentation notes that controller unit tests are “expected under tests/unitTest/pro_tes/api/middlewares/”, but no such tests are included in this PR. Either add the referenced tests or update this section to reflect the current state to avoid misleading readers.
## Implementation Status

The middleware management controller layer is now implemented in
[`pro_tes/api/middlewares/controllers.py`](../pro_tes/api/middlewares/controllers.py)
and wired through the OpenAPI configuration in
[`pro_tes/config.yaml`](../pro_tes/config.yaml).

Unit tests for the middleware management controllers are expected under
tests/unitTest/pro_tes/api/middlewares/ (e.g. test_controllers.py).

pro_tes/api/middlewares/controllers.py:253

  • middleware_id is converted to ObjectId for exclude_id before validating the ID format. If middleware_id is invalid, this ObjectId(...) call will raise outside the try/except below, resulting in an unintended 500 instead of a 400.
    if "name" in update_fields:
        _validate_duplicates(
            _collection(),
            name=update_fields.get("name"),
            class_path=None,
            exclude_id=ObjectId(middleware_id) if middleware_id else None,
        )

pro_tes/api/middlewares/controllers.py:101

  • source filtering only matches documents where source is an object (source.type). For fallback groups, source is an array per the OpenAPI schema, so GET /middlewares?source=... will silently exclude those entries. Consider adjusting the query to also match array sources (e.g., $or with $elemMatch on source.type).
    query: Dict[str, Any] = {}
    if source:
        query["source.type"] = source

pro_tes/api/middlewares/controllers.py:246

  • The OpenAPI spec for PATCH /middlewares/{middleware_id} defines a 409 Conflict for duplicate names, but _validate_duplicates raises BadRequest (400). Either adjust the controller to raise a 409-compatible exception (and ensure it’s mapped) or align the spec/status codes.
        _validate_duplicates(
            _collection(),
            name=update_fields.get("name"),
            class_path=None,
            exclude_id=ObjectId(middleware_id) if middleware_id else None,
        )

    try:

pro_tes/api/middlewares/controllers.py:330

  • ReorderMiddlewares appends _doc_to_response(doc) even if find_one returns None (which becomes {}), producing invalid response items when response validation is enabled. Consider asserting matched_count == 1 and/or raising if the document can’t be re-fetched, rather than returning empty objects.
        coll.update_one(
            {"_id": oid},
            {"$set": {"order": idx, "updated_at": now}},
        )
        doc = coll.find_one(
            {"_id": oid},
            {
                "_id": True,
                "name": True,
                "source": True,
                "order": True,
                "config": True,
                "created_at": True,
                "updated_at": True,
            },
        )
        updated_docs.append(_doc_to_response(doc))

pro_tes/api/middlewares/controllers.py:65

  • _derive_name is defined but unused in this module, which adds dead code and can confuse future maintenance. Either remove it or use it as the single name-derivation path (instead of duplicating derivation logic in AddMiddleware).
def _derive_name(body: Dict[str, Any]) -> Optional[str]:
    if body.get("name"):
        return body["name"]

    source_val = body.get("source")
    entry_point = None
    if isinstance(source_val, dict):
        entry_point = source_val.get("entry_point")
    elif isinstance(source_val, list):
        for item in source_val:
            if isinstance(item, dict) and item.get("entry_point"):
                entry_point = item["entry_point"]
                break

    if not entry_point:
        return None

    name = entry_point.split(".")[-1]
    name = name.split(":")[-1]
    return name

@uniqueg uniqueg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @keshxvdayal!

A few comments:

Let's think about what we originally wanted to do here: We wanted to allow a (pro)TES client (e.g., the TES dashboard) to define the middlewares that are used for a specific TES job.

The middleware management doesn't really help us here, because we're creating a race condition:

  • Request 1: Client sends one or more requests to set up the the desired middleware stack via the middleware controllers.
  • Request 2: Client sends TES request, expecting the configured middlewares to be used.

But between calls (1) and (2), there may be other calls that change the middleware setup, and so it cannot be guaranteed that call (2) actually uses the desired middleware stack.

That being said, the middleware controllers may still be useful, as long as we add an extension to /tasks that allows setting the middleware stack directly. But then the question is: Wouldn't this make more sense within FOCA, so that all FOCA-based services benefit from middleware management? 🤔

But that brings up other problems: FOCA is hopelessly outdated. It still uses Connexion 2, which is in legacy maintenance mode, so it is not clear how much longer it will be supported. IMO, it doesn't make much sense to invest any considerable time into adding features into the current FOCA (or proTES, for that matter).

The issue is that migrating to Connexion 3 is a considerable undertaking. One of the changes is a migration from WSGI and Flask (Connexion 2) to ASGI and Starlette (Connexion 3). But Starlette comes with its own middleware engine (https://starlette.dev/middleware/), which uses standardized ASGI middlewares and is certainly better than whatever we would come up with.

I see three paths forward:

(1) We keep on working on the current proTES, but only focus on what we want to achieve, i.e., setting the middleware stack via the /tasks request (as a custom proTES extension of that endpoint).
(2) We invest the time into upgrading FOCA to Connexion 3 and make use of Starlette's middleware engine, update proTES accordingly, and then add what's necessary to make use of it for our purpose (including rewriting existing middlewares).
(3) We ditch FOCA altogether (most of the services that depend on it are not maintained anymore) and build proTES and all middlewares from scratch using a fast/modern web service (e.g., written in Golang).

Given that Starlette is reasonably fast, I would tend towards option (2), as it represents a reasonable middlegroumd wrt to speed, time commitment, maintainability etc.

But it's quite some work (though perhaps not all that much with the help of agentic AI)!

Let's discuss this on a call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants