Keywords

Identifier

Identifier sets a unique identifier for the model that will be used instead of the UUID produced by the platform. The value is a URI pointing to one of the properties (i.e. “unique_name” in the example below). When each Asset is created, the platform will check that every new value for the identifier is unique. Setting an identifier in the Model is optional.

"identifier": "#/properties/unique_name",

Label

Label is a keyword that serves as a non-unique identifier for the model and is used solely in the user interface without any associated logic. The value consists of a URI that points to one of the properties.

To provide flexibility in defining labels, the JSON schema has been extended to allow the use of multiple labels. The label keyword is introduced, and it can be defined in two ways:

  1. As a String:

    nested in Root
    "label": "#/properties/serial_number"
    
  2. As an Object:

    nested in Root
     "label": {
            "targets": [
                "#/properties/id",
                "#/properties/name"
            ],
            "separator": "_" 
        }

Search is an optional keyword used for listing the values addressable in the search endpoint.

"search": [
    	"unique_name",
    	"type",
    	"meters",
    	"composition",
    	"manufacturer",
    	"invoice_number",
],

Authorized Groups

authorized_groups, when used at the first level, set the privileges needed to create the Asset. It identifies the entities with permission to create (and only create) the Asset. If this field is blank, everyone can create the Asset.

"authorized_groups": [
    	"/root",
    	"/root/group1",
    	"/root/group2/sub-group",
],

Note: authorized_groups can be at the first level and/or within the mutations section. When it is inside the mutations section, it indicates which groups (roles) can launch that mutation and therefore MODIFY (only modify) the Asset.

States

States define the entry point of the Finite State Machine and every other possible state. A Model does not need an FSM (i.e. more than one state), but each Model's declaration of a default_state is mandatory. States are defined as a JSON object with the following structure:

"states": {
    	"default_state": "created",
},
FieldsData TypeDescription

states

Object

A field that defines the model’s state.

default_state

String

A mandatory field that defines the default model’s state.

Transitions

Transition is nested inside the states block and is a Datome’s specific keyword used to compose the transition map and define every other state besides the default_state.

It must contain at least one state besides the default state if specified. Transitions are defined as a JSON object with the following structure:

nested in States
"transitions": {
        "send_to_quality_control": {
            "required_state": "manufacturing",
            "target_state": "quality_control"
        },
        "send_to_warehouse": {
            "required_state": "quality_control",
            "target_state": "warehouse"
        },
        "send_to_distribution": {
            "required_state": "warehouse",
            "target_state": "distribution"
        },
}
FieldsData TypeDescription

label

Object

A field that defines the label to transition to a new state.

required_state

String

A field that defines the original state in which the model must be when the transition mutation is called.

target_state

String

A field that defines the destination state.

Events

For each Asset, Datome shows the lists of the states it went through together with their timestamp. The sequence will follow the rules set in the transitions.

If there’s the need to log a situation at any point of the state's flow, we can add an Event. In the exemplary image below, the states are issued, accepted, and shipped, but an Event was logged between accepted and shipped. Authorized users can log any number of events.

If we want to add the possibility to log Events in the Model, we’ll have to include a section in “definitions” where we define the object “event” and its properties.

nested in Root
"definitions": {
  	"event": {
     	"properties": {
        	"attachment": {
           	    "format": "file",
           	    "items": {
              	    "type": "object"
           	    },
           	    "type": "array"
        	},
        	"date": {
           	    "format": "date-time",
           	    "type": "string"
        	},
        	"description": {
           	    "format": "textarea",
           	    "type": "string"
        	},
        	"title": {
           	    "type": "string"
        	}
     	},
     	"type": "object"
  	}
}

Mutations

Mutations is a Datome-specific keyword used to define a set of operations that modify the Asset’s Properties (i.e. an “update”) or State (i.e. a “transition”) or log a new Event. A Mutation also sets the privileges required to perform such changes. Declaring mutations on a Model is optional but, without a mutation, no change can be applied to an Asset after its creation. Setting the “mutations” within the Model ensures that only the defined updates, transitions or event logs can be actioned on a certain Asset by specific users. Nesting elements of Mutations are as follows:

  • Level 1: mutation name (e.g. “send_to_confirmed”)

  • Level 2: changes

    • target: a URI pointing to a certain transition in the state, update of properties or event.

    • type: equal to “transition” or “event” for such cases. It is “static” (i.e. set by the platform) or “dynamic” (i.e. set by the user) in case a change of a property

    • required: a boolean value in case it’s a dynamic change in a property

    • value: the value set by the platform in case it’s a static change in a property

  • Level 2: authorized_groups: sets which group can trigger the defined changes

  • Level 2: external_mutations: triggers a change to an Asset belonging to a different model (for details see here).

Mutations are defined as a JSON object with the following structure:

nested in Root
"mutations": {
        "add_event": {
            "authorized_groups": [
                "/root"
            ],
            "changes": [
              {
                 "target": "#/definitions/event",
                 "type": "event"
              }
            ]
        },
        "send_to_confirmed": {
            "changes": [
                {
                    "type": "transition",
                    "target": "#/states/transitions/send_to_confirmed"
                },
                {
                    "type": "dynamic", 
                    "required": true,
                    "target": "#/properties/id_technician"
                },
                {
                    "type": "dynamic",
                    "required": false,
                    "target": "#/properties/id_employee"
                },
                {
                    "type": "static",
                    "value": true,
                    "target": "#/properties/available"
                }
            ],
            "authorized_groups": [
		          "/root/group/…/admin",
		          "/root/group2/…/controller"
	          ],
            "external_mutations": [
                {
                  "model": "garment",
                  "target": "#/mutations/send_to_available"
                }
            ]
        }
}
FieldsData TypeDescription

custom name

Object

A field that defines a name to identify a mutation. This field is nested under the “mutation” keyword.

changes

Array

List of operations applied by the mutation. This field is mandatory and must contain at least one operation. This field has four possible values: 1. static: the “target” is always a property of the model. The new value is prefixed and defined within the definition of the StaticChange. Therefore, each time the mutation is executed, that specific property will always be updated with the same value. 2. dynamic: the “target” is always a property of the model. The new value is passed in the body of the API request, hence is defined by the authorized user who is calling the mutation. It contains a label “required” of type boolean. If set to true, the new value of the field must be expressed in the API request. Otherwise, it could be omitted (it answers the question, “Is there the necessity to check that in the body of the call to the Datome API there is a field (the target) valorized?”). 3. transition: the “type” used to change the state of a model. The “target” is always a state, identified by URI, defined within the "transitions" block. 4. events: the “target” is always a JSON object defined in a “definitions” block (JSON schema) inside the Model. Its behavior is equal to a DynamicChange, so the fields of the object needed to be updated have to be specified in the body of the API request.

Dynamic mutations and Events need the “params” key in the API requests payload to give the required values.

authorized_groups

Array

A field that contains the privileges needed to apply the defined mutation.

external_mutations

Array

An optional Datome-specific keyword that lets the user run a mutation that affects an Asset belonging to a different model. Whenever an external mutation is performed, the "authorized_groups" clause will be checked both on the current mutation (where the external is defined) and the one to which the external is linked.

To implement a mutation, it is necessary to contact the correct endpoint with the required input parameters via the URL and, if necessary, within the body of the API request. See the code example below:

curl --location --request POST 'https://[your_organization].datome.io/api/models/fabric/silklot00456/mutations/send_to_confirmed//' \
--header 'Authorization: Bearer ....' \
--header 'Content-Type: application/json' \
--data-raw '{
    "params": {
       "id_technician": 987,
       "id_employee": 14005
    }
}'

A mutation can potentially be implemented an infinite number of times. However, mutations that involve the execution of a state "transition" may fail if re-executed because the model's "required_state" has been altered.

External Mutations

A Model B mutation may be activated by an external mutation performed within a Model A mutation. An External mutation shall tell:

  • via the keyword model, the external Model it refers to;

  • via the keyword target, the URI of the external mutation to trigger;

"external_mutations": [
                {
                  "model": "engine",
                  "target": "#/mutations/send_to_unavailable"
                }
]

For example, if in Model Car we add an external mutation that triggers the mutation send_to_unavailable of Model Engine, we’ll set the following.

Parent model "Engine"
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "http://mangrovia.solutions/engine.json",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "notes": {
      "type": "string"
      }
  },
  "mutations": {
    "send_to_unavailable": {
      "changes": [
        {
          "type": "transition",
          "target": "#/states/transitions/send_to_unavailable"
        },
        {
          "type": "dynamic",
          "target": "#/properties/notes",
          "required": false
        }
      ]
    }
  },
  "states": {
    ...
  }
}
Child model "Car"
{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "http://mangrovia.solutions/car.json",
    "type": "object",
    "additionalProperties": false,
    "properties": {
        ...
    },
    "mutations": {
        "set_relation": {
            "changes": [
                ...
            ],
            "external_mutations": [
                {
                    "model": "engine",
                    "target": "#/mutations/send_to_unavailable"
                }
            ]
        }
    },
    "states": ...
}

Given that the mutation send_to_unavailable in Model Engine requires, among other changes, the user to input a value for a property named notes, when we call the endpoint to trigger a mutation in Car that contains an external mutation to Engine, we will also need to provide a value for notes. This will be done within keyword params as follows:

"external_mutations": {
        "engine": {
            "send_to_unavailable": {
                "engine001": {
                     "params": {
                         "notes": "invalid product"
                     }
                }
            }
        }
}

External Mutations can be nested one inside another producing what we call a Cascade Mutation.

A cascade mutation is a sequence of external mutations set in different models, triggered subsequently to create a chain of events. Each external mutation triggers the next in line, forming a sequence that propagates changes across multiple models. Ensure that each model involved is configured to handle the external mutations and is set subsequently to maintain the desired sequence of mutations in your system.

The following is an example of a Cascade Mutation in a JSON object:

curl --request POST 'https://[your_organization].datome.io/api/models/fabric/{{asset_id}}/mutations/send_to_confirm/' \
--header 'Authorization: Bearer ....' \
--header 'Content-Type: application/json' \
--data-raw '{
    "params": {
      ...
    },
    "external_mutations": {
      "{{target_model_name}}": {
        "{{target_mutation_name}}": {
          "{{target_asset_id}}": {
            "params": {
              ...
            },
            "external_mutations": {
              ...
            } 
          }
        }
      }
    }
}'

Relations

A relation keyword is nested inside a property to tell that such a property is dedicated to storing the identifier of another Asset. For example, one of the properties of a Garment can be dedicated to storing the identifier of the fabric it was made of.

Suppose the Fabric Model was built with an identifier (i.e. a unique value identifying each Asset Fabric). In that case, the relevant property of the Garment will show the identifier of the related Fabric. If Fabric doesn’t have an identifier, the UUID of the fabric will be stored under the relevant property.

AssetProperty 1Property 2Property 3

Garment 1

...

...

Fabric A identifier

Garment 2

...

...

Fabric B identifier

Relations are used to define ownership, provenance, composition etc. They can bring controls and validations to your processes.

The example above shows a one-to-one relation, but we can also have a one-to-many scenario like the following example (a Garment made out of two fabrics).

AssetProperty 1Property 2Property 3

Garment 3

...

...

Fabric A identifier, Fabric B identifier

To specify a one-to-many relation, it is mandatory to declare a list (array) of type and the Model of the Assets to link. The following is an example of Models relation code:

one-to-one    
    "{{relation_name}}": {
      "relation": {
        "model": "{{model_name}}"
      },
      "type": "string",
      "description": "Relation to a {{model_name}} model previously created"
    }
one-to-many    
    "{{relation_name}}": {
	      "type": "array",
	      "items": {
	        "type": "string"
	      }
       "relation": {
           "model": "{{model_name}}"
       },
}
FieldsData TypeDescription

model

String

The label of the relation to the Model.

description

String

The explanation of the model’s link.

relation

String

A Datome-specific keyword used inside the “properties” block to declare the type of relation and the models involved.

relation_name

String

The name of the model’s relations.

type

String

The field type that receives the relation (relation_name) must be consistent with the type of the model’s ID (numbers or string) to which it refers. The type field can be specified with numbers or strings.

Constraints

When used with a relation, the keyword constraint sets a checking rule so that a relation is only set if a condition of the related asset is met. Based on the statement, a possible use case could be Write Fabric A identifier in Property 3 of Garment 1 only if Property 5 of Fabric A equals 1.

"{{relation_name}}": {
    "relation": {
    "model": "{{model_name}}",
 "constraints": [
      {
        "target": "#/properties/{{property_name}}",
        "op": "eq",
        "value": "{{value}}"
      }
    ]
  },
  "type": "string",
  "description": "Relation to a {{model_name}} Asset previously created"
}

In the example above target sets the URI of the property to check, op sets equal as the operation, and value is the value to match.

The target can also be a state. In such a case, the URI will be #/states/{{target_state}}. In the case of multiple constraints, all of them have to be satisfied.

Dynamic properties

Dynamic properties offer a powerful way to dynamically calculate a property based on the values of other properties in related models. There are two main scenarios to consider: trigger properties and computed properties.

Trigger Properties

In this case, the "Stock" model has a computed property, "inStockQuantity," which is dynamically calculated based on the "Order" and "Restock" models. In the "Stock" model we define how each related model will affect the dynamic property. Possible operations include "add" for addition. "sub" for subtraction and "count".

Please note that the initial value of the triggered property will be set manually at the creation of the asset.

// Model: Stock
{
  ...
  "properties": {
    "inStockQuantity": {
      "type": "number",
      "minimum": 0,
      "operations": [
        {
          "model": "order",
          "operation": "sub"
        },
        {
          "model": "restock",
          "operation": "add"
        }
      ]
    }
  }
  ...
}

The related models ("order" and "restock") define the property that will affect the "inStockQuantity" property through the "operation_triggers" configuration. Here the relation between the models will also be defined.

// Models: Order and Restock

{
  ...
  "properties": {
    "sold_quantity": {
      "type": "number",
      //here we declare the Model and the property that this Model will affect
      "operation_triggers": [
        {
          "target_property": "#/properties/inStockQuantity",
          "target_relation": "#/properties/belongs_to_stock"
        }
      ]
    }, 
    //here we define the relation
    "belongs_to_instock": {
      "type": "string",
      "relation": {
        "model": "stock"
      }
    }
  }
  ...
}

In this scenario, changes to "order" and "restock" models automatically trigger updates to the "inStockQuantity" property in the "Stock" model.

Computed Properties

In the computed properties scenario, the "pallet" model has a property, "pallet_weight," which is dynamically calculated by summing the "weight" property of related "product" models.

// Model: Pallet
{
  ...
  "properties": {
    "pallet_weight": {
      "type": "number",
      "computed": {
        "operands": [
          {
            "target_relation": "#/properties/contains_product",
            "target_property": "#/properties/weight"
          }
        ],
        "op": "add"
      }
    },
    "contains_product": {
      "type": "string",
      "relation": {
        "model": "product"
      }
    }
  }
  ...
}

In this case, the "pallet_weight" property is calculated based on the sum of the "weight" property of related "product" models. The operation performed is "add". Possible operations include "add" and "count".

// Model: Product
{
  ...
  "properties": {
    "peso": {
      "type": "number"
    }
  }
  ...
}

The "product" model merely needs to declare the "weight" property, which is used in the computation of the "pallet_weight" property in the "Pallet" model.

In summary, computed properties provide a flexible way to automate calculations based on related model properties, streamlining processes and reducing manual intervention.

External Properties

External properties allow to automatically record user data when they create an asset.

By implementing an external property, you can save time and effort by avoiding the need to manually enter your information each time you make a delivery.

This not only streamlines the process but also ensures that the contact details are easily accessible without having to search for them in the carrier's information.

For instance, if you're a carrier delivering goods to a warehouse, you might want to save your contact information so that it's readily available for future deliveries.

In this example, the model "order" will have a property named "carrier mail" defined as follows:

// Model: Order

{
...
	"properties": {
		"carrier_mail": {  
			"type": "string",  
			"from_source": {  
				"uri": "datome://users/me/", 
				"target": "email"
			}  
		}
	}
...
}

where:

"uri": "datome://users/me/" refers to the user who is creating or updating the asset

and

"target": "email" defines which field we want to retrieve the value from.

Last updated