Skip to content

feat(trigger-service): manual receiver + REST CRUD (TS-IMPL-015)#668

Merged
toddysm merged 2 commits into
mainfrom
ts-impl-015-manual-receiver
Jun 5, 2026
Merged

feat(trigger-service): manual receiver + REST CRUD (TS-IMPL-015)#668
toddysm merged 2 commits into
mainfrom
ts-impl-015-manual-receiver

Conversation

@toddysm
Copy link
Copy Markdown
Owner

@toddysm toddysm commented Jun 5, 2026

TS-IMPL-015 — Manual Receiver + REST CRUD

Implements the operator-facing REST surface for trigger subscriptions and a
manual :fire that drives the same normalize → match → dispatch pipeline an
inbound event takes. Closes #645.

Endpoints (api/routes/subscriptions.py)

  • POST /v1/workspaces/{ws}/triggers — create a start subscription (201)
  • GET /v1/workspaces/{ws}/triggers/{id} — read one back
  • PATCH /v1/workspaces/{ws}/triggers/{id} — amend selector / mapping / target / state
  • DELETE /v1/workspaces/{ws}/triggers/{id} — soft-delete → state expired (204)
  • POST /v1/workspaces/{ws}/triggers/{id}:fire — fire now → { runId }

Behaviour

  • Selectors validated through the CEL evaluator on create/patch → invalid → 422.
  • :fire returns the started runId; a paused/non-matching subscription →
    409 trigger.api.subscription_not_fireable; a subscription with no target
    workflow version dead-letters → 502 trigger.dispatch_failed; a
    redelivered fire collapses in the dedup window → 409 trigger.dedup_duplicate.
  • Unknown id → 404 trigger.subscription_not_found on every route.
  • RBAC delegated to the call-context middleware via require_permission
    (trigger:subscriptions:{read,write,delete,fire}).

Supporting changes

  • api/errors.py — RFC 7807 application/problem+json envelope (mirrors the
    workflow-service convention): a TriggerError handler over the locked domain
    taxonomy plus route-local trigger.api.* kinds, and a
    RequestValidationError handler.
  • storesSubscriptionReadable capability Protocol + async
    SubscriptionStore.get; backends lacking the capability raise
    SubscriptionReadUnsupportedError (the Postgres read surface lands in a later task).
  • app.py / dependencies.py — the lifespan builds a shared SelectorEvaluator
    and a Dispatcher (over an owned httpx client reaching the Workflow Service
    through Dapr, closed at shutdown); create_app gains a dispatcher injection
    seam for tests; Problem+JSON handlers + router wired in.
  • models.pyManualFireRequest / ManualFireResult.

Design constraints honoured

  • The locked SPL metadata surface is write-only for subscriptions, so read-back
    uses an optional capability Protocol satisfied by the in-process backend and
    DELETE is a soft-delete (expired is terminal — no row delete exists).
  • input_mapping placeholder resolution is forwarded literally (deferred to a
    later task); manual fire is start-only per classify.

Verification

ruff check . + ruff format --check . + mypy src tests clean.
pytest: 378 passed, 99.1% coverage (subscriptions route 98.9%,
api/errors 95.8%).

Implements TS-IMPL-015: the operator-facing REST surface for trigger
subscriptions plus a manual `:fire` that drives the same normalize → match →
dispatch pipeline an inbound event takes.

- `api/routes/subscriptions.py`: `POST/GET/PATCH/DELETE
  /v1/workspaces/{ws}/triggers[/{id}]` + `POST .../{id}:fire`. Selectors are
  validated through the CEL evaluator on create/patch (invalid → 422). DELETE
  is a soft-delete (state → expired) since the locked SPL surface has no
  row-delete. `:fire` returns the started `{runId}`; a paused/non-matching
  subscription is `409 not_fireable`, a missing target version dead-letters to
  `502`, and a redelivered fire collapses in dedup (`409 duplicate`).
- `api/errors.py`: RFC 7807 `application/problem+json` envelope mirroring the
  workflow-service convention — a `TriggerError` handler over the locked
  domain taxonomy plus route-local `trigger.api.*` kinds, and a
  `RequestValidationError` handler.
- `stores`: a `SubscriptionReadable` capability Protocol + async
  `SubscriptionStore.get` so the REST layer can read a single subscription
  back; backends without the capability raise
  `SubscriptionReadUnsupportedError` (the Postgres read surface lands later).
- `app.py`/`dependencies.py`: the lifespan now builds a shared
  `SelectorEvaluator` and a `Dispatcher` (over an owned httpx client reaching
  the Workflow Service through Dapr, closed at shutdown); `create_app` gains a
  `dispatcher` injection seam for tests, and the Problem+JSON handlers +
  router are wired in.
- `models.py`: `ManualFireRequest` / `ManualFireResult`.

RBAC is delegated to the call-context middleware via `require_permission`.
Full CRUD lifecycle, `:fire`, RBAC, and error-path tests added; 378 passed,
99% coverage.

Closes #645
Copilot AI review requested due to automatic review settings June 5, 2026 05:27
@toddysm toddysm added type:implementation Implementation work item phase:implementation Implementation phase component:trigger-service Trigger Service component labels Jun 5, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an operator-facing subscription REST API to the trigger-service, including a manual :fire endpoint that drives the same normalize → match → dispatch pipeline as inbound events, and introduces an RFC7807 Problem+JSON error envelope for route-level failures.

Changes:

  • Introduces /v1/workspaces/{ws}/triggers CRUD + /{id}:fire router and wires it into the FastAPI app lifespan (shared SelectorEvaluator, Dispatcher construction/injection).
  • Adds an optional SubscriptionReadable capability + SubscriptionStore.get() read-back adapter, plus new wire models for manual fire.
  • Adds comprehensive route tests for CRUD, selector validation, dedup behavior, and RBAC presence.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
src/services/trigger-service/tests/test_subscriptions_routes.py End-to-end tests for CRUD + :fire behavior over in-memory backend and fake workflow client.
src/services/trigger-service/src/custos_trigger/stores/subscriptions.py Adds SubscriptionStore.get() and explicit error for backends without read support.
src/services/trigger-service/src/custos_trigger/stores/base.py Introduces SubscriptionReadable Protocol as an optional read capability.
src/services/trigger-service/src/custos_trigger/stores/init.py Re-exports new store capability + error type.
src/services/trigger-service/src/custos_trigger/models.py Adds ManualFireRequest / ManualFireResult wire models.
src/services/trigger-service/src/custos_trigger/dependencies.py Adds dependency getters for dispatcher, selector evaluator, and subscription store.
src/services/trigger-service/src/custos_trigger/app.py Wires Problem+JSON handlers + subscriptions router; builds shared evaluator and dispatcher in lifespan.
src/services/trigger-service/src/custos_trigger/api/routes/subscriptions.py Implements REST CRUD + :fire logic (validate selector, match, dispatch, dedup).
src/services/trigger-service/src/custos_trigger/api/routes/init.py Exports the subscriptions router.
src/services/trigger-service/src/custos_trigger/api/errors.py Adds RFC7807 Problem+JSON envelope + exception handlers for TriggerError and validation errors.
src/services/trigger-service/src/custos_trigger/api/init.py Exposes API utilities (ProblemDetail, handler registration).
design/components/trigger-service/todos.md Marks TS-IMPL-015 as completed.

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

Comment thread src/services/trigger-service/src/custos_trigger/api/routes/subscriptions.py Outdated
Comment thread src/services/trigger-service/src/custos_trigger/api/routes/subscriptions.py Outdated
Comment thread src/services/trigger-service/src/custos_trigger/api/errors.py
Comment thread src/services/trigger-service/src/custos_trigger/api/errors.py
Comment thread src/services/trigger-service/src/custos_trigger/api/errors.py
Comment thread src/services/trigger-service/src/custos_trigger/api/errors.py
Comment thread src/services/trigger-service/src/custos_trigger/api/errors.py
- Enforce path workspace_id matches the call-context workspace on every
  subscription route (403 workspace_mismatch) as defense-in-depth against a
  mismatched path and the dev shim's unsigned header.
- PATCH can now explicitly clear optional blob fields (selector,
  targetWorkflowVersionId) via JSON null, distinguished from omission using
  model_fields_set.
- Render SubscriptionReadUnsupportedError through the Problem+JSON envelope as
  a stable trigger.api.subscription_read_unsupported / 501 instead of an
  opaque 500.
@toddysm toddysm merged commit 789d4b5 into main Jun 5, 2026
30 checks passed
@toddysm toddysm deleted the ts-impl-015-manual-receiver branch June 5, 2026 05:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component:trigger-service Trigger Service component phase:implementation Implementation phase type:implementation Implementation work item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TS-IMPL-015: Manual Receiver + REST CRUD (REQ-004)

2 participants