Resource Registry — API Operational Cookbook & Advanced Use Cases¶
Because the Resource Registry employs a strictly typed graph database under the hood, standard REST operations (PUT, PATCH, DELETE) have specific graph-level side effects. This section details common operational patterns and corner cases.
Context-Local Instance Forking (Copy-on-Write)¶
Scenario: A Resource instance is shared across Contexts C1 and C2. You want to update a Facet instance only for your Context (C1), branching off from the shared configuration without affecting Context C2.
Action: Send a PUT payload where the modified ConsistsOf instance and its nested Facet instance each carry a freshly client-generated id (a UUID that does not yet exist in the system), instead of the id of the shared instances.
Graph Mechanism: The backend interprets each not-yet-existing id as a request to create a new instance. Specifically, within the operational Context C1, it explicitly drops the existing ConsistsOf instance (and consequently its target Facet instance) from the Context. It then creates a brand new Facet instance and attaches it to the Resource instance via a newly created ConsistsOf instance, using the client-supplied id values. Because Contexts are strictly isolated, the original ConsistsOf and Facet instances remain completely untouched and safely attached to the shared Resource instance in Context C2.
(Both ids must be fresh — reusing the existing ConsistsOf instance’s id while only generating a new one for the Facet instance would retarget a shared ConsistsOf instance to a different Facet across Contexts, which is topologically invalid; see 09-api-instances-and-admin.md, “Single Context Update (Local Copy-on-Write)” for the full explanation).
The Cardinality Chicken-and-Egg (min: 1, max: 1)¶
Scenario: A Resource instance has a mandatory Facet instance (e.g., StateFacet with min:1, max:1). You need to completely replace this Facet instance with a new one for your Context. You cannot POST the new one first (violates max: 1) and you cannot DELETE the old one first (violates min: 1).
Action: Send a PUT request for the entire Resource instance, removing the old Facet instance from the payload array and including the new one with a freshly client-generated id (a UUID that does not yet exist in the system).
Graph Mechanism: The PUT operation acts as a “Full Replace” for the Context. The backend calculates the delta: it removes the omitted old Facet instance and attaches the new one. Validation constraints (like min/max) are strictly evaluated at the end of the transaction. Since the final state contains exactly 1 StateFacet, validation passes seamlessly.
Hierarchical Downstream Mutations (hierarchical vs syncExistingHierarchy)¶
When executing a PUT operation on a Resource instance as a role with subtree mutation scope (IS-Editor or higher), you have two powerful mechanisms to push topological changes (e.g., changes to its attached Facet instances) down a Context tree. It is crucial to understand the difference to avoid unintended data proliferation.
(Note on Parameter Conflict: The flags hierarchical=true and syncExistingHierarchy=true describe two mutually exclusive propagation strategies. If a client mistakenly passes BOTH flags on the same PUT request, the API strictly rejects the request with a 400 Bad Request to prevent ambiguity).
The Surgical Sync (?syncExistingHierarchy=true)¶
Scenario: You want to push a configuration change down to all child VREs (Contexts C_Child_1, C_Child_2) only if they have already explicitly “opted-in” by having the Resource instance shared with them.
Action: Issue a PUT /instances/{resource-type}/{instance-uuid}?syncExistingHierarchy=true targeting the parent Context.
Graph Mechanism: This acts as a safe, cascading sync. The backend applies the PUT state replacement to the Resource instance in the parent Context AND in all descendant Contexts where the Resource instance already exists. It does not implicitly share the Resource instance to child Contexts that did not already have it.
The Blanket Replication (?hierarchical=true)¶
Scenario: You want to enforce a global baseline, ensuring a specific Resource instance is present and updated across all descendant Contexts, overriding any local absences.
Action: Issue a PUT /instances/{resource-type}/{instance-uuid}?hierarchical=true targeting the parent Context.
Graph Mechanism: The backend applies the submitted Resource payload as a full replacement in the parent Context and every descendant Context within the authorization scope. A child that did not previously contain the Resource receives exactly the Resource topology described by the payload. This is payload synchronization, not an Add to Context avalanche: IsRelatedTo instances and other live topology absent from the payload do not follow.
Surgical Relation instance / Entity instance Deletion (?propagationMode=surgical)¶
Scenario: You accidentally created an IsRelatedTo instance with remove: cascade to an important target Resource instance, and you need to remove the Relation instance without triggering the avalanche that would remove the target Resource instance too.
Action: Issue a DELETE /instances/{type}/{instance-uuid}?propagationMode=surgical (where {instance-uuid} is the UUID of the Relation instance or the source Entity instance).
Graph Mechanism: This triggers a surgical non-cascading Remove from Current Context. The backend removes the targeted Relation instance (or the targeted Entity instance and its strictly required Facet instances) from the token Context, but explicitly ignores any remove propagation constraints towards external instances. As for every standard DELETE, an instance is physically deleted only when it has no remaining Context membership. This acts as a safe “undo” facility, but must be used carefully to avoid leaving detached or unlinked instances floating in the Context unintentionally.
Descriptive-Topology Closure on Direct Relation Add¶
When managing graph topologies, a strict theoretical model would dictate that an edge cannot be added to a Context unless both its source and target nodes are already present. However, the IS-Model API implements a deliberate “Developer Experience (DX) bypass” to simplify interactions.
If a client attempts to execute an Add to Context directly on a Relation instance, and the target Entity instance is missing from the destination Context, the system does not fail with a dependency error. Instead, it implicitly pulls the target Entity into the Context, actively bypassing the unpropagate constraint if present on the Relation.
Design Rationale:
This behavior is an intentional system compromise. It allows clients to share a specific connection (and its necessary downstream requirement) in a single API call, rather than forcing them to first discover the target ID, make a separate PUT call to add the target, and finally make a PUT call for the relation. While it creates a minor asymmetry with the theoretical propagation model (where unpropagate means “stop”), the necessity of preserving structural integrity (no dangling edges) combined with developer ergonomics justifies this implicit closure.
(Note: The implicit pull strictly applies to the target. If the source Resource is missing, the operation strictly fails with a 409 Conflict. This enforces the model’s unbreakable rule: the avalanche is strictly downstream. We only force the downstream pull to preserve topological closure when bypassing unpropagate. If a client wants to add the source, they must explicitly invoke Add to Context on the source instance itself. We never implicitly pull upstream. Furthermore, this implicit downstream pull triggers standard propagation rules for the target itself. If the pulled target Resource instance has mandatory Facet instances governed by add: unpropagate constraints, those Facet instances will correctly NOT be pulled. Consequently, the target Resource instance will fail its final schema validation, and the entire transaction will be predictably rejected with a 409 Conflict. The system behaves as a strict deterministic engine: it does not magically override downstream constraints to rescue a poorly modeled resource instance).
Canonical Mutation Semantics: PUT (Replace Intent) vs PATCH (Merge Intent)¶
To reduce ambiguity for client implementers, this section uses the same initial state and shows the resulting state after each method. (Note: This is a simplified structural example to demonstrate the delta mechanics; a real EService instance would require additional mandatory facets to pass strict schema validation).
PUT= replace intent on the addressed instance representation.PATCH= merge intent (partial modification).
Shared initial state (conceptual):
{
"type": "EService",
"id": "1a50a46d-2210-4a17-bd2f-22450b109264",
"consistsOf": [
{
"type": "ConsistsOf",
"id": "8bb0a45e-1110-4b17-ad3f-11450b109277",
"target": {
"type": "StateFacet",
"id": "7aa991ba-2043-4259-8d85-b6377ac778df",
"value": "created"
}
},
{
"type": "ConsistsOf",
"id": "9cc0b56f-2220-4c28-be4f-22560c200388",
"target": {
"type": "AccessPointFacet",
"id": "39bd3a5b-9737-40bc-8934-6e491aee8fed",
"endpoint": "https://old.example.org"
}
}
]
}
PUT Replace Intent (example)¶
Request headers:
Content-Type: application/jsonAccept: application/json
Client request (PUT /instances/EService/1a50a46d-2210-4a17-bd2f-22450b109264):
{
"type": "EService",
"id": "1a50a46d-2210-4a17-bd2f-22450b109264",
"consistsOf": [
{
"type": "ConsistsOf",
"id": "c11d1e44-5555-4a17-bc4e-33660a209999",
"propagationConstraint": {
"type": "PropagationConstraint",
"add": "propagate",
"remove": "cascade"
},
"target": {
"type": "StateFacet",
"id": "d22e2f55-6666-4b28-cd5f-44770b30aaaa",
"value": "ready"
}
}
]
}
(Both id values are freshly client-generated UUIDs that don’t yet exist in the system — this is a new ConsistsOf/StateFacet pair, not an update of the omitted AccessPointFacet one.)
Final state after PUT (intent):
{
"type": "EService",
"id": "1a50a46d-2210-4a17-bd2f-22450b109264",
"consistsOf": [
{
"type": "ConsistsOf",
"id": "c11d1e44-5555-4a17-bc4e-33660a209999",
"target": {
"type": "StateFacet",
"id": "d22e2f55-6666-4b28-cd5f-44770b30aaaa",
"value": "ready"
}
}
]
}
The addressed instance representation is replaced by what was sent.
Omitted parts (e.g., the AccessPointFacet instance) are not preserved by intent.
Validation (cardinality/schema constraints) is evaluated on the final post-mutation state.
PATCH Merge Intent (example)¶
Request headers:
Content-Type: application/merge-patch+jsonAccept: application/json
Client request (PATCH /instances/EService/1a50a46d-2210-4a17-bd2f-22450b109264):
{
"type": "EService",
"id": "1a50a46d-2210-4a17-bd2f-22450b109264",
"consistsOf": [
{
"type": "ConsistsOf",
"id": "8bb0a45e-1110-4b17-ad3f-11450b109277",
"target": {
"type": "StateFacet",
"id": "7aa991ba-2043-4259-8d85-b6377ac778df",
"value": "ready"
}
}
]
}
Final state after PATCH (intent):
{
"type": "EService",
"id": "1a50a46d-2210-4a17-bd2f-22450b109264",
"consistsOf": [
{
"type": "ConsistsOf",
"id": "8bb0a45e-1110-4b17-ad3f-11450b109277",
"target": {
"type": "StateFacet",
"id": "7aa991ba-2043-4259-8d85-b6377ac778df",
"value": "ready"
}
},
{
"type": "ConsistsOf",
"id": "9cc0b56f-2220-4c28-be4f-22560c200388",
"target": {
"type": "AccessPointFacet",
"id": "39bd3a5b-9737-40bc-8934-6e491aee8fed",
"endpoint": "https://old.example.org"
}
}
]
}
Only the provided property field value (for
StateFacet.value) is merged/updated.Unspecified property fields remain unchanged.
Media type rule: In this API, PATCH requests use
application/merge-patch+json.
| Method | Client sends | Server intent on addressed instance representation |
|——–|————–|—————————————————-|
| PUT | full instance representation | replace with submitted instance representation |
| PATCH | partial instance representation (delta) | merge only submitted property field values |
Moving or Renaming a Context: Previous Paths¶
A rename or a reparent changes the path of a Context and of all its descendants, while tokens issued for the old paths stay in circulation. Each outgoing path is kept in the Context’s previousPaths, so those tokens keep resolving to the same Context. The rules are in 08-api-contexts-and-types.md; the examples below show them at work. All PATCH requests use Content-Type: application/merge-patch+json.
Reparenting a VRE¶
Scenario: /gcube/old-vo/VRE1 (id 4a1b2c3d-5e6f-4708-8192-a3b4c5d6e7f8) must move under /gcube/new-vo (id b1829304-c5d6-4e7f-f8a9-b0c1d2e3f4a5).
Action: A Context whose local state is active cannot be moved or renamed (409 Conflict). Suspend it, move it, then reactivate it:
PATCH /contexts/4a1b2c3d-5e6f-4708-8192-a3b4c5d6e7f8
{ "state": "suspended" }
PATCH /contexts/4a1b2c3d-5e6f-4708-8192-a3b4c5d6e7f8
{ "parent": { "id": "b1829304-c5d6-4e7f-f8a9-b0c1d2e3f4a5" } }
Response to the move — 200 OK:
{
"type": "Context",
"id": "4a1b2c3d-5e6f-4708-8192-a3b4c5d6e7f8",
"name": "VRE1",
"path": "/gcube/new-vo/VRE1",
"previousPaths": [ "/gcube/old-vo/VRE1" ],
"state": "suspended",
"effectiveState": "suspended",
"parent": {
"type": "Context",
"id": "b1829304-c5d6-4e7f-f8a9-b0c1d2e3f4a5",
"name": "new-vo",
"path": "/gcube/new-vo",
"state": "active",
"effectiveState": "active"
},
"children": []
}
PATCH /contexts/4a1b2c3d-5e6f-4708-8192-a3b4c5d6e7f8
{ "state": "active" }
Mechanism: The move writes three events in the Context’s history (GET /contexts/{context-uuid}/history), all with how: PATCH:
| where | what |
|———|——–|
| parent | <old-vo id> -> b1829304-c5d6-4e7f-f8a9-b0c1d2e3f4a5 |
| path | /gcube/old-vo/VRE1 -> /gcube/new-vo/VRE1 |
| previousPaths | added /gcube/old-vo/VRE1 |
The suspension and the reactivation write their own state events.
Renaming a VO: the Cascade¶
Scenario: The VO /gcube/a (id 7c2e9a41-58d3-4b6f-a0e2-93f1c6d8b4a5, already suspended) has two VREs, /gcube/a/c and /gcube/a/d. It is renamed to f.
Action:
PATCH /contexts/7c2e9a41-58d3-4b6f-a0e2-93f1c6d8b4a5
{ "name": "f" }
Mechanism: In one transaction the VO becomes /gcube/f and its VREs /gcube/f/c and /gcube/f/d. Each Context keeps its own outgoing path: /gcube/a, /gcube/a/c and /gcube/a/d respectively. The history events are:
| Context | where | what | how |
|———|———|——–|——-|
| f | name | a -> f | PATCH |
| f | path | /gcube/a -> /gcube/f | PATCH |
| f | previousPaths | added /gcube/a | PATCH |
| c | path | /gcube/a/c -> /gcube/f/c | PATCH (cascade from 7c2e9a41-58d3-4b6f-a0e2-93f1c6d8b4a5) |
| c | previousPaths | added /gcube/a/c | PATCH (cascade from 7c2e9a41-58d3-4b6f-a0e2-93f1c6d8b4a5) |
d gets the same pair as c. A token bound to /gcube/a/c now resolves to c, reported by X-Resolved-Context-Path: /gcube/f/c.
A Token with an Old Path¶
Scenario: Months after the move, a client whose credentials were never reissued still presents a token for /gcube/old-vo/VRE1.
Action: GET /contexts/@current
Mechanism: No Context has that path any more; the lookup falls back to previousPaths and finds VRE1. The response is 200 OK with the same body as a direct match, plus two headers:
X-Result-Scope: previous-path
X-Resolved-Context-Path: /gcube/new-vo/VRE1
Retention has no time limit: the fallback works the same after four days or four years. The headers give the caller, and any monitoring watching X-Result-Scope, a signal that a stale path is still in use.
Replacing the Previous Paths¶
previousPaths in a PATCH replaces the whole retained set (RFC 7396): the submitted array is exactly what remains, and null or [] clears it. Each removed and each added value writes its own event (where: previousPaths, what: removed <path> or added <path>, how: PATCH).
Clearing after the transition. Once every integration uses the new path,
{ "previousPaths": [] }removes/gcube/old-vo/VRE1. From then on a token still bearing it gets404 Not FoundonGET /contexts/@current.Registering a path by hand. A pre-migration path that was never recorded, still used by external tokens, can be added:
{ "previousPaths": [ "/gcube/historic-vo/VRE1" ] }. Remember that the array is the whole set: keep the existing values in it.Replacing and moving together. In
{ "parent": { "id": "b1829304-c5d6-4e7f-f8a9-b0c1d2e3f4a5" }, "previousPaths": [] }the replacement is applied first and the move second, so the result holds only the path the move has just left behind.
Reusing a Path¶
Scenario: A new VO is created with a path that another Context still keeps in its previousPaths, e.g. /gcube/old-vo after the original VO was renamed.
Action:
PUT /contexts/c3d4e5f6-7081-4920-a3b4-c5d6e7f8a9b0
Content-Type: application/json
{
"type": "Context",
"id": "c3d4e5f6-7081-4920-a3b4-c5d6e7f8a9b0",
"name": "old-vo",
"parent": { "id": "<gcube id>" }
}
Mechanism: In the same transaction as the creation, the old value is removed from the other Context’s previousPaths, with an event on that Context: where: previousPaths, what: removed /gcube/old-vo, how: PUT (path reclaimed by c3d4e5f6-7081-4920-a3b4-c5d6e7f8a9b0). Resolution was already safe, since a current path always wins over a previous one; the removal only keeps the representation honest, so that no Context advertises a path it can no longer receive.