Mutability
These two keywords decide what a replace may change once a document exists. A replace is the transition an owner sends to overwrite a document with a new version of it. documentsMutable turns replaces on or off for the whole document type. immutable freezes chosen properties while the rest of the document stays editable: from the moment the document is created, or only while a condition holds, such as five minutes after creation, once the document is published, or once a value has been filled in.
Neither governs deletion, transfers or trading: see Deletion and Creation, Transfers and Trading.
documentsMutable
Whether the owner of a document may replace it. Set it to false for records that must never change after they are written: votes, receipts, name registrations.
| Where | document type |
| Value | boolean |
| Default | the contract config's documentsMutableContractDefault, which is true unless the contract says otherwise |
| Since | protocol version 1 |
| On update | Fixed (DocumentTypeUpdateError, 40212). Adding or removing the key without changing its value is refused too, as a schema change (IncompatibleDocumentTypeSchemaError, 10246). |
| Errors | InvalidDocumentTransitionActionError (10404) for a replace of a type set to false |
Example
"vote": {
"type": "object",
"documentsMutable": false,
"canBeDeleted": false,
"properties": {
"proposalId": { "type": "identifier", "position": 0 },
"choice": { "type": "string", "enum": ["yes", "no", "abstain"], "position": 1 }
},
"required": ["proposalId", "choice", "$createdAt"],
"additionalProperties": false
}
A vote is cast once and stays as cast: no replace is accepted, and with canBeDeleted: false its owner cannot take it back either.
How it works
- With
true, the document's owner may replace it. The replace carries the whole new document, which is validated against the schema as a create is, and a$revisionone higher than the stored one (InvalidDocumentRevisionError, 40106). Anyone other than the owner is refused (DocumentOwnerIdMismatchError, 40102). When the type lists$updatedAtinrequired, the replace sets it to the block's time, and likewise$updatedAtBlockHeightand$updatedAtCoreBlockHeightto the block heights. - With
false, every replace is refused (InvalidDocumentTransitionActionError, 10404). - A type whose documents cannot be replaced may still let them be transferred or sold (
transferable,tradeMode). Such a document changes owner and price, but its owner can never edit its properties. The DPNSdomaintype works this way. - A document stores a
$revisionwhen its type allows a replace, a transfer or trading, or keeps fields its moderators write (moderatorAbilities.changeFields), whose changes raise it even on a type whose documents cannot be replaced. A type that allows none of them stores none. See System Properties.
Rules at registration
- A contested index needs a type whose documents cannot be replaced (
ContestedUniqueIndexOnMutableDocumentTypeError, 10248). See Contested Indexes. - An
indexOnlytype must setdocumentsMutable: false. See Index-Only Types. immutableis only accepted when the type's documents are mutable (InvalidContractStructure, 10231).moderatorAbilities.deleteWithinon a mutable type needs$updatedAtinrequired. See Deletion.
immutable
The top-level properties a replace may not change, on a type whose documents can otherwise be replaced. Each entry is either a property name, frozen when the document is created, or a property with a condition, frozen for any replace the condition holds for. Reach for the first when part of a document is a commitment: the shop an order was placed with, the author of a post. Reach for the second when a value may change for a while and then must stand: a post's text for five minutes after it is published, an article's body once it is out of draft, a tracking code once it is filled in.
| Where | document type, on a type with documentsMutable: true |
| Value | array whose entries are a top-level property name, or { "property": <name>, "when": <condition> }; each property listed once |
| Default | empty: every property may change |
| Since | protocol version 14 |
| On update | May gain entries, with a condition or without. A property listed with a condition keeps it, or loses it to be listed without one. A property listed without a condition stays, and no property is dropped (DocumentTypeUpdateError, 40212) |
| Errors | DocumentImmutablePropertyChangedError (40128) for a replace that changes, adds or removes a frozen property |
Example
"post": {
"type": "object",
"documentsMutable": true,
"properties": {
"author": { "type": "string", "maxLength": 63, "position": 0 },
"text": { "type": "string", "maxLength": 500, "position": 1 },
"status": { "type": "string", "enum": ["draft", "published"], "position": 2 },
"body": { "type": "string", "maxLength": 5000, "position": 3 },
"trackingCode": { "type": "string", "maxLength": 40, "position": 4 },
"pinned": { "type": "boolean", "position": 5 }
},
"required": ["author", "text", "status", "$createdAt", "$updatedAt"],
"immutable": [
"author",
{
"property": "text",
"when": { "greaterThan": [{ "subtract": ["$updatedAt", "$createdAt"] }, 300000] }
},
{
"property": "body",
"when": { "equal": ["$old.status", { "const": "published" }] }
},
{ "property": "trackingCode", "when": { "present": "$old.trackingCode" } }
],
"additionalProperties": false
}
author never changes. text can be corrected for five minutes after the post is created: during a replace $updatedAt is the replace's block time, so the difference is the post's age in milliseconds. body stays editable while the stored post is a draft, including in the replace that publishes it, and is frozen after that. trackingCode can be filled in by one replace while the stored post has none, and is frozen once it holds one. pinned is not listed, so it can change at any time.
How it works
- On every replace, each property that differs from the stored document is checked against the list. A property listed by name is refused with
DocumentImmutablePropertyChangedError(40128). A property listed with a condition is refused with the same error when its condition holds for this replace. "Differs" covers a changed value, a value the stored document did not have, and a value the replace leaves out. - A condition takes the grammar of a
propertyConstraintsrule: comparisons, arithmetic,in,present,absent,anyOf,allOf,not,ifThen,ifThenElse, the system times and heights the type records,$revision, and$ownerId. It is judged, as a rule judges a replace, on the document the replace writes: its properties as the replace sets them, the stored$createdAtand$transferredAt, the replace's block as$updatedAt, and the replace's revision. - A path starting with
$old.reads the stored document instead:$old.statusis the status before the replace,$old.$updatedAtthe time of the previous update,$old.$revisionthe previous revision. ApropertyConstraintsrule and a type'sretractedWhenread it the same way; adeleteConstraintsrule, judged on the stored document itself, does not. - A property is frozen while its condition holds. If what the condition reads can change back, the property can become editable again: with
bodyfrozen by$old.status, a replace settingstatusback todraftstill reads the storedpublished, sobodystays frozen in that replace, and the next replace, reading the storeddraft, may change it. For a freeze that lasts, list what the condition reads as well, or use a condition that cannot turn back, as a document's age or a filled-in value cannot. - A condition is evaluated only when its property changes. A condition that faults, dividing by zero or overflowing, counts as holding: a fault never frees a property.
- Values are compared by their data, not their bytes: the order of an object's members and the width an integer is stored in do not count as changes. Freezing an object freezes everything inside it.
- One change is always allowed: a replace may clear a
deletableDocumentreference by id, listed without a condition, once the document it points to has been deleted. Every replace checks such a reference again, so without this the document could never be replaced again. See References. - A replace judged by
checkTxshortly before a time condition starts holding may still land in a block after that, and is then refused there and pays its fee, like any other state check. - Transfers, price updates and purchases carry no property values, so the list does not affect them.
Rules at registration
All refusals below are InvalidContractStructure (10231). The shape of the list, and what each condition reads, are checked on every parse. The other rules are checked when a contract is registered or updated.
- Only on a type whose documents are mutable. On a type with
documentsMutable: falseevery property is already frozen. - Every entry is a string or an object with exactly
propertyandwhen, and no property is listed twice. - Every listed property is a declared top-level property. System properties (
$ownerId,$createdAtand the rest) are refused, since the platform manages them. Nested paths such asmeta.authorare refused: list the object that contains them. - No listed property may be
transient: it is never stored, so every replace that supplies it while it is frozen would count as a change. - A condition reads what a
propertyConstraintsrule may read of the type: declared properties of the right kind, neither transient nor inside a transient object, system times and heights the type lists inrequired,$revision, and any of them under$old.. It stays within the node limit of a rule, and lists no condition twice. - A condition may not read a
countOforsumOftotal: it reads the document alone, so a replace judges it without reading state. immutableAllowSetting, which earlier builds used to let a frozen property be filled in once, is refused and names the replacement:{ "property": "p", "when": { "present": "$old.p" } }.- An immutable property may not hold a
deletableDocumentreference that a replace could not clear: a typed array of them, one inside an object, or one found byfindBywhose key no function computes. A single reference by id held directly by the property is allowed, but only without a condition: once cleared, a replace the condition leaves free could set it to another document. - On a type whose documents can be transferred or traded, an immutable property may not hold a
contractreference whosecontractRequirementshas anownerrequirement: after a change of owner the new owner could neither meet it nor repoint it. - A listed property, with a condition or without, may not be one of the fields only moderators write (
moderatorAbilities.changeFields). - A property listed with a condition is not fixed once written, so a
findBykey, aninListlist, a value read beside afindByfunction, or a value another type indexes through a reference may not rely on it. See References.
On update
What immutable freezes may only tighten. A property may be added, with a condition or without, and documents already stored are held to it from then on. A property listed with a condition may lose it, which freezes it whatever the condition said. A condition cannot change: whether one condition holds wherever another does cannot be told in general. A property listed without a condition stays, and no property is dropped, since documents were written on the promise that those properties would not change. The comparison is of the parsed entries, so reordering them is no change.
See also
- Immutable Properties on Mutable Document Types, for how the replace compares values
- Deletion, Creation, Transfers and Trading and History, the other keywords on what may happen to a document
- System Properties, for
$revisionand$updatedAt - transient and References, for the properties
immutablerefuses - propertyConstraints, for the grammar of a condition
- Contract Keywords, for how the summary tables read