This repository has been archived on 2026-02-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
glue/README.md
giles fc14d8323a Add README documenting glue layer architecture
Covers models, services, handlers, event flows, and guidelines
for adding new cross-domain logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 19:18:01 +00:00

123 lines
5.2 KiB
Markdown

# Glue Layer
The glue layer sits between the four domain apps (blog, market, cart, events) and encapsulates all **cross-domain logic** — the code that would otherwise create tight coupling between services.
Each app includes `glue/` as a git submodule. All apps share one PostgreSQL database, so glue services operate within the caller's existing DB transaction.
## Architecture
```
blog ──┐
market ─┤
cart ───┤──→ glue (services, handlers, models)
events ─┘
```
**Rule:** Apps never import models or write data belonging to another domain directly. Instead they call a glue service or emit a domain event.
## Models
### ContainerRelation (`models/container_relation.py`)
Generic parent/child relationship between any two entities.
- `parent_type + parent_id``child_type + child_id`
- Soft-deletable (`deleted_at`), ordered (`sort_order`)
- Unique constraint on (parent_type, parent_id, child_type, child_id)
Used to attach calendars to pages, markets to pages, etc. without FK coupling.
### MenuNode (`models/menu_node.py`)
Navigation tree nodes, scoped to a container (container_type + container_id).
- Self-referential (`parent_id`), ordered (`sort_order`), with `depth`
- Stores `label`, `slug`, `href`, `icon`, `feature_image`
## Services
### `services/relationships.py` — Container Relations
| Function | Description |
|----------|-------------|
| `attach_child()` | Create or revive a ContainerRelation (upsert). Emits `container.child_attached`. |
| `get_children()` | Query children of a container, optionally filtered by child_type. |
| `detach_child()` | Soft-delete a ContainerRelation. Emits `container.child_detached`. |
### `services/order_lifecycle.py` — Order ↔ Calendar Bridging
Cross-domain writes between **cart** and **events** domains. These run in the same transaction as the caller (not event-driven) because order creation and payment require immediate consistency.
| Function | Description |
|----------|-------------|
| `claim_entries_for_order()` | Mark pending CalendarEntries as "ordered" and set order_id. Called from checkout. |
| `confirm_entries_for_order()` | Mark ordered CalendarEntries as "provisional". Called when payment confirms. |
| `get_entries_for_order()` | Return CalendarEntries for an order. Replaces the old `Order.calendar_entries` relationship. |
### `services/cart_adoption.py` — Login Adoption
| Function | Description |
|----------|-------------|
| `adopt_session_for_user()` | Adopt anonymous CartItems + CalendarEntries for a logged-in user. Soft-deletes any existing user cart/entries first. |
### `services/navigation.py` — Navigation
| Function | Description |
|----------|-------------|
| `get_navigation_tree()` | Return top-level MenuNodes ordered by sort_order. |
| `rebuild_navigation()` | Placeholder for syncing ContainerRelations → MenuNodes. |
## Event Handlers
Handlers are registered at import time via `shared.events.register_handler()`. They're imported in `setup.py``register_glue_handlers()`, which each app calls at startup.
### `handlers/container_handlers.py`
| Event | Handler |
|-------|---------|
| `container.child_attached` | Triggers `rebuild_navigation()` |
| `container.child_detached` | Triggers `rebuild_navigation()` |
### `handlers/login_handlers.py`
| Event | Handler |
|-------|---------|
| `user.logged_in` | Calls `adopt_session_for_user()` to adopt anonymous cart/entries |
### `handlers/order_handlers.py`
| Event | Handler |
|-------|---------|
| `order.created` | Logging (placeholder for future workflows) |
| `order.paid` | Logging (placeholder for future workflows) |
## Event Flow Diagrams
### Checkout
```
cart/checkout.py
├─ create order + order items (cart domain)
├─ glue: claim_entries_for_order() ← cross-domain write (same txn)
└─ emit: order.created ← observability
```
### Payment Confirmation
```
cart/check_sumup_status.py
├─ update order.status = "paid" (cart domain)
├─ glue: confirm_entries_for_order() ← cross-domain write (same txn)
└─ emit: order.paid ← observability
```
### Login Adoption
```
blog/auth/routes.py
└─ emit: user.logged_in ← event (in last_login_at txn)
↓ (EventProcessor)
glue/handlers/login_handlers.py
└─ glue: adopt_session_for_user() ← cross-domain write (handler txn)
```
## Remaining Cross-Domain Reads (Pragmatic Exceptions)
These stay in the app code (not in glue) because they're read-only queries against the shared DB. Moving them to glue would be pure churn:
- `cart/bp/cart/services/checkout.py``resolve_page_config()` reads Calendar + MarketPlace
- `cart/bp/cart/api.py``/summary` reads CalendarEntry + Calendar + MarketPlace
- `cart/bp/cart/` → calendar_cart.py, page_cart.py read CalendarEntry/Calendar for display
## Adding New Cross-Domain Logic
1. **Cross-domain write?** → Add a function in `glue/services/`. Call it from the app code within the existing transaction.
2. **Eventually consistent?** → Emit a domain event from the originating app. Add a handler in `glue/handlers/`.
3. **Cross-domain read?** → If it's a simple query against the shared DB, keep it in the app. Only move to glue if multiple apps need the same query.