Skip to content

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.

The Shared Instance Mutation (Global Effect for Facets and Relations)

Scenario: An instance (e.g., a Facet instance or an IsRelatedTo instance) is shared across multiple Contexts (e.g., Context C1 and Context C2). You want to update a field (e.g., a specific property of the Facet instance, or the propagationConstraint of the Relation instance), and you want that update to be reflected for everyone. Action: Send a PUT or PATCH containing the modified payload, explicitly including its existing id. Graph Mechanism: The backend resolves the existing instance using the provided ID and updates its property fields. Because fields are intrinsically bound to the single underlying instance, the change is immediately visible globally across all Contexts where that instance is shared. (This applies equally to Encrypted fields and structural system fields like propagationConstraint).

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 (sync)

When executing a PUT operation on a Resource instance as a role with subtree mutation scope (IS-Editor or higher), sync selects the Contexts that get the same result as the token Context (see sync). It is crucial to understand the difference between its values to avoid unintended data proliferation.

The Surgical Sync (?sync=existing)

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}?sync=existing 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 (?sync=tree)

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}?sync=tree 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.

Aligning One Context to Its Parent (?sync={context-uuid})

Scenario: A Resource instance has a Facet instance of a type with min:1, max:1 (e.g., a StateFacet) shared by the Contexts where it holds. One VRE replaced it with its own, with a PUT on the VRE token. Now the VRE must take the shared one back. Action: Read the Resource instance as the parent Context sees it, and issue PUT /instances/{resource-type}/{instance-uuid}?sync={vre-uuid} with it, with a token of the parent. Graph Mechanism: The write is on the parent, which receives what it already holds and is unchanged; the VRE is aligned to it, so the shared Facet instance joins the VRE and the VRE's own one leaves it, and is deleted with its last Context. The cardinality constraints are checked at the end, in one transaction. The VRE cannot do it from its own token: the shared Facet instance is not visible there, and naming it returns 403 Forbidden. Every Facet instance the VRE has alone leaves it too: the synchronization replaces the representation of the VRE, it does not merge into it.

The Situation of One Context

Scenario: A Resource instance is shared by many Contexts, and in one of them something is wrong: a service is published as an EService instance in a VO and its VREs, and in one VRE it cannot work (a backend it needs is missing there). The IS must say so for that VRE only, with the reason, and stop saying it when the VRE is fine again. Two ways the API supports, both certified by the tests of the service:

  • A Facet of that Context only, of a type the Resource has once. The VRE replaces the shared StateFacet with its own one, inactive, with a PUT on the VRE token (The Cardinality Chicken-and-Egg); when it is fine again, the parent aligns it with ?sync={vre-uuid} (Aligning One Context to Its Parent). A discovery by state in the VRE then finds the service inactive. The cost: the Contexts hold different StateFacet instances, every change of the general state must reach each of them, the synchronization removes every other Facet instance the VRE has alone (its history, for instance), and a hierarchical read from the VO shows one StateFacet per Context that has its own.
  • The general state, plus an event of that Context only. The EService instance keeps one StateFacet, shared by every Context: its general state. The situation of the VRE is an EventFacet present in the VRE only (what the state, when the moment, why the reason, where the VRE), added with a PUT on the VRE token and removed with another one when the VRE is fine again; being in no other Context, it is then deleted. No synchronization, and a change of the general state is one write. A discovery by state finds the general state also in the VRE: whoever must know the situation of a Context reads its events. Choice of the Information System: the second, for the state of the services published by SmartGears (the whole case, with the instances as each way leaves them: A Case: the State of a Service in One Context): the StateFacet is the general state, and a Context adds an event only when its situation differs. The first stays a valid pattern where the value that differs per Context is itself the property a discovery must filter on.

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/json
  • Accept: 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": "active"
      }
    }
  ]
}

(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": "active"
      }
    }
  ]
}
  • 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+json
  • Accept: 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": "active"
      }
    }
  ]
}

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": "active"
      }
    },
    {
      "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 gets 404 Not Found on GET /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.