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",
},

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"
        },
}

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"
                }
            ]
        }
}

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.

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).

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}}"
       },
}

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