Add atomic connections across canvas subgraph boundaries
This commit is contained in:
@@ -212,6 +212,7 @@ Collapsing/scaling presentation does not rewrite child content. Projection is
|
||||
|
||||
| Route | Required input / behavior |
|
||||
| --- | --- |
|
||||
| POST B/connections | `{source,target,edge,expected_versions}` + Idempotency-Key; atomically creates/reuses boundary port chains and stores one edge in the lowest common ancestor |
|
||||
| POST B/subgraphs | Parent If-Match; `{frame:{id,title,position,width,height,labelMode}}`; returns 201, child_board, parent/child snapshots and receipt |
|
||||
| POST B/subgraphs/{frame}/members | `{source_board_id,target_board_id,node_ids,expected_versions}`; parent/child or siblings coordinated by their common parent |
|
||||
| POST B/subgraphs/{frame}/dissolve | `{expected_versions}`; preserves content in the parent and archives the emptied child |
|
||||
@@ -405,6 +406,109 @@ summary are retained with mode 0600 under
|
||||
on the ECS host, in addition to the database receipt. The frontend V2 integration
|
||||
remains a separate task; use a V2-aware client for this migrated test canvas.
|
||||
|
||||
#### Creating new cross-boundary connections
|
||||
|
||||
Ordinary Canvas PATCH preserves existing frame ports and cannot add or retarget
|
||||
ports. Migration/movement-generated ports are reusable, but creating a new
|
||||
external connection to an unexposed child node requires this dedicated command:
|
||||
|
||||
`POST B/connections` with user session authorization,
|
||||
`Accept: application/vnd.foundry.canvas+json;version=2`, JSON content type and a
|
||||
UUID `Idempotency-Key`. Availability is advertised as
|
||||
`capabilities.subgraphs.connections: true`; clients must check the deployed
|
||||
capability before calling it.
|
||||
|
||||
`B` identifies the **lowest common ancestor board** of the two actual endpoints.
|
||||
The coordinator may itself be a nested board. Both endpoints must be in this
|
||||
same project/workspace and inside its active subgraph tree. A direct endpoint on
|
||||
B is valid, as are siblings and descendants up to the existing depth limit.
|
||||
When both endpoints are in the same board, use that board as B.
|
||||
|
||||
```json
|
||||
{
|
||||
"source": {"board_id":"<child-board-uuid>","node_id":"child-text","handle":"right"},
|
||||
"target": {"board_id":"<parent-board-uuid>","node_id":"parent-image","handle":"left"},
|
||||
"edge": {
|
||||
"id":"new-edge",
|
||||
"data": {
|
||||
"appearance":{"color":"#AA968B","width":2},
|
||||
"animation":{"mode":"executing","effect":"flow","direction":"forward","durationMs":1600},
|
||||
"execution":{"enabled":true}
|
||||
}
|
||||
},
|
||||
"expected_versions": {"<parent-board-uuid>":210,"<child-board-uuid>":1}
|
||||
}
|
||||
```
|
||||
|
||||
Use canonical board UUIDs for endpoints and version keys. Eligible real node
|
||||
kinds are text/script/plain-text/image/plain-image; handles must be one of
|
||||
`top/right/bottom/left`. Do not submit projected node IDs, frame IDs as generated
|
||||
content, or `port:*` handles. The server exposes a real endpoint through each
|
||||
containing frame, creating or reusing a port with the matching
|
||||
`direction/nodeId/handle` tuple. Existing matching ports retain their IDs,
|
||||
including migration-created or custom stable IDs. A source chain uses output
|
||||
ports, and a target chain uses input ports; these are distinct. No unbound port
|
||||
or writable arbitrary child reference is accepted.
|
||||
|
||||
The expected_versions map must contain **exactly every board on both paths**,
|
||||
including the endpoint boards and B. For a grandchild-to-parent connection it
|
||||
contains parent, intermediate child and grandchild; for siblings it contains
|
||||
both children and their parent. No separate If-Match header is required for this
|
||||
multi-board operation. Missing participants return 428 `version_required`;
|
||||
when a nonempty map has missing/extra participant keys, the response includes
|
||||
`details.required_board_ids`. Any stale version returns 412 `version_conflict`.
|
||||
All participants require current editor/admin access. Calling a higher ancestor
|
||||
instead of the lowest common ancestor returns 409 `connection_owner_mismatch`.
|
||||
An unrelated/invalid ownership path returns 409 `ownership_conflict`; resources
|
||||
outside the caller's scope retain the normal 403/404 behavior.
|
||||
|
||||
Only `edge.id` and optional typed V2 `edge.data` are accepted. Its appearance,
|
||||
animation, execution flag and bezier-chain path follow the normal V2 validation.
|
||||
All knots are already in B's local coordinates; port creation does not move node
|
||||
geometry or re-layout the edge. An existing edge ID in B returns 409
|
||||
`edge_id_conflict`. Graph, port and snapshot capacity failures return 413 without
|
||||
saving any ports, edges, versions or receipt.
|
||||
|
||||
Success is 201 with the coordinator's ETag and:
|
||||
|
||||
```json
|
||||
{
|
||||
"transaction_id":"<uuid>",
|
||||
"parent":{"board_id":"<coordinator>","version":211,"document":{}},
|
||||
"snapshots":[
|
||||
{"board_id":"<coordinator>","version":211,"document":{}},
|
||||
{"board_id":"<child-board-uuid>","version":2,"document":{}}
|
||||
],
|
||||
"connection":{
|
||||
"board_id":"<coordinator>",
|
||||
"edge":{"id":"new-edge","source":"<frame-id>","sourceHandle":"port:<stable-id>","target":"parent-image","targetHandle":"left","data":{}},
|
||||
"source":{"board_id":"<child-board-uuid>","node_id":"child-text","handle":"right"},
|
||||
"target":{"board_id":"<parent-board-uuid>","node_id":"parent-image","handle":"left"}
|
||||
},
|
||||
"created_ports":[{"board_id":"<coordinator>","frame_id":"<frame-id>","port_id":"<stable-id>"}],
|
||||
"undo_receipt":{"transaction_id":"<uuid>","valid_for_seconds":604800,"expires_at":"<RFC3339 timestamp>"}
|
||||
}
|
||||
```
|
||||
|
||||
The JSON above abbreviates document values and shows a one-level example; nested
|
||||
connections include every intermediate board in snapshots. The response edge is
|
||||
the stored owner-board edge, while source/target preserve the logical endpoint
|
||||
identities. created_ports contains only newly allocated ports; reused ports are
|
||||
available in the snapshots. Each participating board advances exactly one
|
||||
version (including endpoint boards whose node content remains unchanged),
|
||||
consistent with other cross-board commands. Flush pending saves before creating
|
||||
the connection, then absorb **all** returned versions/snapshots before resuming
|
||||
autosave. Retrying an identical body with the same key returns the original 201
|
||||
response and does not advance versions again. Changing the body, including its
|
||||
version map, with that key returns 409 `idempotency_conflict`.
|
||||
|
||||
Undo uses the existing `POST B/transactions/{transaction_id}/undo` endpoint with
|
||||
a new key and all current participant versions. It restores the previous edge
|
||||
and port arrays atomically; subsequent incompatible edits cause 412. Existing
|
||||
ports reused by the command remain after undo. Redo submits the command with
|
||||
fresh versions and a new key. Ordinary PATCH can continue to edit/remove edges
|
||||
using already exposed ports, but cannot retarget or directly delete the ports.
|
||||
|
||||
#### Reproducible session API examples
|
||||
|
||||
These examples require Bash, curl and jq, an editor/admin login session, and an
|
||||
|
||||
Reference in New Issue
Block a user