Updating Multi-Value Relations
Multi-value relations — Owners, Subs, Children, Workitems,
RegressionSuites — accept a family of syntaxes that all normalise to the same
projected form:
{
"update": {
"Owners": { "add": [ /* ... */ ], "remove": [ /* ... */ ] }
}
}
Everything below is a shorthand for filling those two arrays. Pick whichever form reads best for the job.
Adding
One Reference
All five of these are equivalent.
{ "from": "Task:1489", "update": { "Owners": "Member:20" } }
{ "from": "Task:1489", "update": { "Owners": ["Member:20"] } }
{ "from": "Task:1489", "update": { "Owners": { "add": "Member:20" } } }
{ "from": "Task:1489", "update": { "Owners": { "add": ["Member:20"] } } }
{ "from": "Task:1489", "update": { "Owners": { "Member:20": "add" } } }
Each projects to:
[
{
"@bd9598ad-66af-4cec-b03e-bf273d91bfb5": {
"oid": "Task:1489",
"update": { "Owners": { "add": ["Member:20"], "remove": [] } }
}
}
]
Several References
{ "from": "Task:1489", "update": { "Owners": ["Member:20", "Member:1575"] } }
{ "from": "Task:1489", "update": { "Owners": { "add": ["Member:20", "Member:1575"] } } }
{
"from": "Task:1489",
"update": { "Owners": { "Member:20": "add", "Member:1575": "add" } }
}
Removing
Swap add for remove. The object-scalar and object-keys forms are the most
convenient:
{ "from": "Task:1489", "update": { "Owners": { "remove": "Member:20" } } }
{ "from": "Task:1489", "update": { "Owners": { "Member:20": "remove" } } }
Object keys scale to several removals in one command:
{
"from": "Epic:1600",
"update": {
"Name": "I have zero Subs",
"Subs": { "Story:1601": "remove", "Story:1602": "remove" }
}
}
Adding and Removing in One Command
from: Story:1700
update:
Owners:
remove: Member:20
add: Member:1800
from: Story:1700
update:
Owners:
remove:
- Member:20
add:
- Member:1800
{
"from": "Story:1700",
"update": {
"Owners": { "Member:20": "remove", "Member:1800": "add" }
}
}
All three project to:
[
{
"@ca414bbc-8dc5-41ef-9626-244b80145059": {
"oid": "Story:1700",
"update": {
"Owners": { "add": ["Member:1800"], "remove": ["Member:20"] }
}
}
}
]
remove is applied together with add, so it is safe to reference the same
attribute in both — the result is the set difference plus the additions, not
an ordering-sensitive sequence of edits.
Referencing by Subquery
add and remove also accept a subquery in place of a literal list. The
subquery runs during processing and every match participates.
from: Epic:1900
update:
Subs:
add:
from: Story
filter:
- ID='Story:1901','Story:1902','Story:1903'
from: Epic:1900
update:
Subs:
remove:
from: Story
filter:
- Super='Epic:1900'
In a relation update, every asset the subquery matches is used. In an operation argument, only the first match is used. Do not assume one behavior from the other.
Mixing Existing References with New Assets
An add list may contain OID tokens and inline create commands side by side.
The new assets are created and related in the same request.
{
"from": "Epic:1850",
"update": {
"Name": "I should now be related to Story:1840 and another new Story via the Subs relation",
"Subs": [
"Story:1840",
{ "AssetType": "Story", "Name": "I am a brand new Story" }
]
}
}
The response reports the Epic in assetsModified and the new Story in
assetsCreated.
The inline create is projected once per matched asset. Under a
query-scoped update
that means one new asset for every asset the from/filter selects, each
related to its own parent — the shape to reach for when the same child
belongs on many parents.
Adding Only Where It Is Missing
Because the filter runs before the creates are projected, a filter that
excludes the parents already holding an equivalent child makes the whole
command idempotent: run it twice and the second run creates nothing.
This example puts a Link to a pull request on every listed asset that does
not already carry that URL:
{
"from": "BaseAsset",
"filter": ["ID=$oids", "-Links[URL=$url]"],
"with": {
"$oids": ["Story:1227", "Defect:1310"],
"$url": "https://github.com/example/repo/pull/12"
},
"update": {
"Links": [
{
"AssetType": "Link",
"Name": "GitHub PR",
"URL": "https://github.com/example/repo/pull/12",
"OnMenu": true
}
]
}
}
assetsModified names the parents that received a Link; a parent the
filter left out is absent from it and from assetsCreated. The check and
the write happen inside one command, so there is no window between them for
a second client to slip into — the closest thing the Bulk API offers to an
idempotency key.
- Use the array or
addform. A bare mapping directly under the relation —"Links": { "AssetType": "Link", ... }— is half-handled: the child is created and related, and the command is still reported incommandFailureswith a StructureMap error namingRelation. Only the array under the relation, oradd:with a list, is the supported syntax. - Leave the update command unaliased. In an array of commands, an alias
on an update that carries an inline create makes every projected create
fail with
The given key was not present in the dictionary; the same command with no alias, in the same batch, succeeds. Aliases on the other commands in the batch are fine. - One level only. A create nested inside an inline create is projected
after the child that needs it, so a required parent — an
Expression'sConversation, say — is not there yet and the child fails validation. An inline create also cannot reference an alias defined elsewhere in the same request. Create the dependency in its own command first and use its OID. - Match exactly in the guard filter.
-Links[URL=$url]compares the stored value;~=is full-text search and its index catches up with writes asynchronously, so a guard written with~=still sees a child created seconds ago as missing and creates it again.
Replacing One Reference with Another
To swap a reference only where it already exists, use
find and replace rather than a paired
add/remove. add/remove applies unconditionally to every matched asset;
find/replace only touches assets that already hold the find value.