Filtering, matching, and patching
This page defines how the Management API selects data and how bulk patches change it. It covers five building blocks:
| Building block | Used by | Selects or changes |
|---|---|---|
| Target | Immediate writes | Exactly one user's array or key-value entry. |
| Filter | Reads and bulk jobs | Records by owner fields: key, user, profile, application, and scope. |
| Tier | Bulk jobs | How far a job reaches, which decides the required filter fields. |
| Match | Reads and bulk jobs | Records by content, inside the records a filter selects. |
| Patch | Bulk patch jobs | The top-level properties of matched values. |
A filter narrows by where data is stored. A match narrows by what the stored value contains. Both only ever apply within the environment of your token.
Scope and owner IDs
Data is stored in one of three scopes. The scope decides which IDs identify the owner.
| Scope | Owner IDs | profileId | applicationId |
|---|---|---|---|
USER | userId | none | none |
PROFILE | userId, profileId | set | none |
APPLICATION | userId, profileId, applicationId | set | set |
Records returned by reads carry an owner with these fields and the key.
Targets
An immediate write names exactly one owner's data with a target.
Targets are used by setKeyValue, upsertArrayItem, and replaceArrayItems.
| Field | Required | Meaning |
|---|---|---|
userId | Always | The user that owns the data. |
scope | Always | USER, PROFILE, or APPLICATION. |
profileId | PROFILE and APPLICATION | The profile. Omit it for USER scope. |
applicationId | APPLICATION | The application. Omit it for USER and PROFILE scope. |
key | Always | The key, which must match ^[\w-]+(:[\w-]+)*$. |
The IDs must agree with the scope in both directions.
A missing required ID, an ID that the scope does not use, and a blank ID are all rejected with TARGET_CONSTRAINT.
No required ID is inferred, and no incompatible ID is silently ignored.
mutation SetPreference($input: SetKeyValueInput!) {
setKeyValue(input: $input) {
acknowledged
insertedCount
updatedCount
}
}
{
"input": {
"target": {
"userId": "5f4b8c2e-0f0a-4f43-8a5e-1b7d9d3c6a10",
"scope": "PROFILE",
"profileId": "d6a3b6f0-2d8c-4f0f-9d57-0a3a5c2f7e11",
"key": "ui:preferences"
},
"value": { "theme": "dark" }
}
}
Filters
A filter selects records by their owner fields.
Fields that are null or omitted do not narrow the selection.
Every field that you supply must hold, so each additional field narrows the result.
| Field | Matches |
|---|---|
key | Records stored under this key. |
userId | Records of this user. |
profileId | Records of this profile, in PROFILE and APPLICATION scope. |
applicationId | Records of this application, in APPLICATION scope only. |
scope | Records written with this scope. |
kind | Key-value reads only: JSON or PROGRESS. Omit it to read both kinds. |
Reads accept an empty filter, which selects every record of the environment. For bulk jobs, the tier decides which fields are required.
Scope and ID rules
Filters reject IDs that are incompatible with the selected scope, but do not require every owner ID that a write target needs.
A request whose IDs contradict its scope is rejected with FILTER_CONSTRAINT.
scope: USERcannot be combined withprofileIdorapplicationId.scope: PROFILEcannot be combined withapplicationId.scope: APPLICATIONselects only data stored with a profile and an application.- An
applicationIdalready limits the selection toAPPLICATIONscope data.
Bulk job tiers
Every bulk job takes a tier that you choose explicitly.
The tier is never inferred from the filter.
It decides which filter fields are required and how far the job reaches inside your environment.
| Tier | Reach | Required filter fields |
|---|---|---|
ENVIRONMENT | Every user in the environment, under one key. | key |
APPLICATION | Every user's APPLICATION scope data of one application, under one key. | key, applicationId |
USER | One user's data, under one key. | key, userId |
- Every bulk job needs a
key. A job cannot reach every key at once. - A required field that is missing or
nullis rejected withFILTER_CONSTRAINTbefore any job is created. - Optional fields (
userId,profileId,applicationId, andscope) narrow the selection further. - The tier is separate from the storage scope. The scope describes how a record is stored, and the tier describes how many users a job can reach.
- The tier is not an authorization boundary. Every tier requires the same permission, as described in Authentication and permissions.
Bulk key-value jobs select JSON entries only.
They take no kind field, and progress entries are never selected, even under the same key.
Matches
A match narrows a selection by content. It is optional in reads and in bulk deletes, and it is optional for each pair of a bulk patch. When you omit the match, every record that the filter selects is included.
Array item matches
An array item match has two conditions. When you give both, both must hold.
| Field | Meaning |
|---|---|
itemId | The item's identity. |
value | An object that must be a deep subset of the item's value. |
Key-value matches
A key-value match has only value, which is required.
It follows the same rules as the array item value condition.
An empty match is rejected
A match must contain at least one condition.
An empty match such as {} is rejected with MATCH_CONSTRAINT because it would silently select everything.
To select every record, omit the match instead.
Value matching rules
The value of a match is an object that must be a deep subset of the stored value.
- Every property in the match must exist in the stored value with an equal value. Extra properties in the stored value are ignored.
- Nested objects are compared as subsets too.
- Arrays are compared as a whole. The stored array must have the same length and the same elements in the same order. A match cannot select an item because an array contains one element.
- Objects inside arrays must equal the stored objects exactly, including their properties and property order.
- Leaf values are compared strictly.
The number
1does not equal the string"1", andtruedoes not equal"true". - A
nullin the match matches a property that is absent ornull. - Only objects can match.
A stored scalar value, a stored array, and every progress entry never match a
valuecondition.
Property names in a match must not be empty or __proto__, must not contain ., and must not start with $.
No object in the match may be empty.
A value that is not an object is rejected.
All of these failures use MATCH_CONSTRAINT.
| Stored value | Match value | Result |
|---|---|---|
{"status": "active", "plan": "pro"} | {"status": "active"} | Matches. Extra properties are ignored. |
{"settings": {"lang": "en", "quality": "hd"}} | {"settings": {"lang": "en"}} | Matches. Nested objects are subsets too. |
{"level": 1} | {"level": "1"} | No match. Leaf types are strict. |
{"tags": ["a", "b"]} | {"tags": ["a"]} | No match. Arrays are compared as a whole. |
{"tags": ["a", "b"]} | {"tags": ["a", "b"]} | Matches. |
{"status": "active"} | {"expiresAt": null} | Matches. null matches an absent property. |
{"status": "active"} | {} | Rejected. An empty object is not allowed. |
Item identity
The itemId condition uses the same identity rule as the rest of the service.
- An item that has an
itemIdis identified by it. - A legacy item that has no
itemIdis identified by itsvalue.id, when that is a string or a numeric ID with the same text as theitemIdyou match. For example, a legacy item with"id": 100is matched by theitemId"100". - A legacy item that has neither has no identity and is never matched by
itemId.
The itemId returned for an item follows the same rule, and it is null when an item has no identity.
The itemId follows the pattern ^[\w-]+(:[\w-]+)*$ and is at most 256 characters long.
Example: find items with a content match
This query reads the first page of items under one key, narrowed by a content match.
query FindActiveFavorites($input: ListArrayItemsInput!) {
listArrayItems(input: $input) {
nodes {
id
itemId
value
owner {
userId
scope
key
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
{
"input": {
"filter": {
"key": "favorites",
"scope": "USER"
},
"match": {
"value": {
"status": "active",
"settings": { "quality": null }
}
},
"first": 50
}
}
Matching and paging
A read examines only a limited number of stored records per page.
With a selective match, a page can therefore be shorter than first or even empty while pageInfo.hasNextPage is still true.
Keep following pageInfo.endCursor as the after argument until hasNextPage is false.
A cursor is valid only for the operation, environment, filter, and match that produced it.
Using it with a different filter or match is rejected with CURSOR_CONSTRAINT.
Treat cursors as opaque values and never build or change them.
Patches
A bulk patch job carries a list of match and patch pairs.
Each pair has three optional parts, and every pair needs at least one of set and remove.
| Field | Meaning |
|---|---|
match | Which records the pair applies to. When omitted, the pair applies to every record that the filter selects. |
set | An object whose top-level properties are added to the value or overwrite existing ones. |
remove | A list of top-level property names to remove from the value. |
What a patch changes
A patch changes the top-level properties of a value and leaves every other property as it is.
setreplaces a property as a whole. If a property holds a nested object,setreplaces the entire object and does not merge into it. To change one nested value, include the complete new object inset.removedeletes a top-level property. Removing a property that is not present changes nothing.- A property cannot be both set and removed in the same pair.
- Only object values are patched. Array items or key-value entries whose value is not an object are left unchanged, and progress entries are never selected.
- A patch never changes the item order, the sort fields,
dateModified, or the item counts of an array.
Pair limits
| Rule | Limit |
|---|---|
| Pairs in one job | At least 1, at most 10,000 by default. |
Properties in one set | At least 1, at most 50. |
Names in one remove | At least 1, at most 50. |
Property names in set and remove | Not empty or __proto__, no ., not starting with $. |
| Content of one pair | At least one of set and remove, with no property in both. |
A violation is rejected with PATCH_CONSTRAINT before a job is created.
To change more than the limit allows, split the change into several jobs and submit them one after another, because an environment can run only one job at a time.
Omit an empty set or remove instead of sending it.
Order of pairs
The job looks at each selected record once and evaluates every pair against the value as it was stored when the record was scanned. A pair's match never sees the result of an earlier pair.
When several pairs match one record, their patches are merged in the order of the list. A later pair wins on every property that it also sets or removes.
For example, with this value:
{ "status": "trial", "plan": "basic", "legacyFlag": true }
And these pairs, in this order:
| Pair | Match | Set | Remove |
|---|---|---|---|
| 1 | none (every record) | {"status": "migrated"} | ["legacyFlag"] |
| 2 | {"plan": "basic"} | {"status": "basic-plan"} |
The result is:
{ "status": "basic-plan", "plan": "basic" }
Pair 2 wins on status, because it comes later in the list.
legacyFlag is removed by pair 1, which is the only pair that touches it.
Idempotence and results
A record that already carries the patch is counted as matched but not as changed.
Before resubmitting, review how the patches affect their own match conditions.
A previous run can change which pairs match on the next run, so not every list of conditional patches is idempotent.
Per-pair matched and changed counts, and the other results of a job, are described in Monitor and cancel bulk jobs.
A patch can make a value too large.
If the merged value would exceed the maximum JSON value size, that record is not written.
It is counted under results.rejected with the reason VALUE_TOO_LARGE, and the job continues with the other records.
Submit a corrected patch, for example one that also removes properties, and run it again.
A dry run applies the same size check.
Example: patch with ordered pairs
This mutation submits a bulk patch of array items under one key.
dryRun is true by default, so the job only reports what it would change.
mutation PatchFavorites($input: BulkPatchArrayItemsInput!) {
bulkPatchArrayItems(input: $input) {
job {
id
status
dryRun
tier
}
}
}
{
"input": {
"tier": "ENVIRONMENT",
"filter": { "key": "favorites" },
"patches": [
{
"set": { "status": "migrated" },
"remove": ["legacyFlag"]
},
{
"match": { "value": { "plan": "basic" } },
"set": { "status": "basic-plan" }
}
]
}
}
To change data, set "dryRun": false in the input after you have reviewed the dry run.
See Bulk updates and deletions for the full workflow.
Bulk deletes
A bulk delete takes a filter, a tier, and an optional match.
Without a match, it deletes every record that the filter selects.
With a match, it deletes only records that match, using the rules above.
An empty match is rejected with MATCH_CONSTRAINT.
A delete has exactly one result, for its match.