Skip to content
Alain Metge edited this page Mar 16, 2015 · 4 revisions

Hyperstore Json configuration format

Hyperstore is available as a node module, you must reference it within your project with npm.

npm install hyperstore --save

To initialize a store with a configuration object, use the following syntax :

var hyperstore = require('hyperstore');

var store = new hyperstore.Store();
var schema = {
        "id" : "LibrarySchema",
       	"Library" : {    		 // Library entity
        	"properties" : [
           		"name" : "string" 	// with property name of type string
            ],
            "references" : [
            	"books" : {
                	"end" : "Book",
                    "kind" : "1=>*"
                }
            ]
        },
        "Book" : {
        	"properties" : [
            	title : "string"
            ]
        }
};

var data = { // Library element
    "name" : "My library",
    "books" : [
    	{"title": "Book 1"},
        {"title": "Book 2"}
    ]
};

var domain = store.createDomain({
	name:"test",	// Domain name
	schema:schema,	// Domain schema
    data: {Library:data} // Domain elements - 'Library' is the schema element name of the root element
    });

You can now manipulate the domain :

var session = store.beginSession(); // Create an (optional) explicit atomic session
try {
	var lib = domain.getElements("Library").firstOrDefault(); // Get the root library
	var book = domain.create("Book"); // Create a new book
    book.title = "Book 3";
	lib.books.add(book);
	session.acceptChanges();
}
finally {
	session.close(); // All changes are validated and events are raised
}

Schema definition

A schema is a json object with a required id property ("LibrarySchema" in the previous sample) and a list of entities and/or relationships definition.

A schema can contains three kind of definitions :

  • An entity : An object with an id.
  • A relationship : Hyperstore needs a relationship definition to manage entities dependencies. By default, an implicit relationship definition is created whenever your define a reference between two entities. But since Hyperstore works like an Hypergraph, a relationship can also have dependencies. In this case, you must define it explicitely.
  • A value object : A value object is a type definition. In the DDD semantic, it's a type (simple or complex) without identity. In the Hyperstore context, it represents a type with its definition, how it can be serialized in JSon and an optional list of constraints.

All javascript primitive types are value objects.

Enities and relationships are defined directly in the schema property while valueObject are defined with specific property named types.

Value object definitions

Value objects are defined with the types property. All value object are parsing before every other definition regardless the property is defined in the object definition. A value object can define the following properties :

  • It can inherit from another value object with the type property.
  • It may have a custom serialization. Serialization is used to persist data in the event process to emit the value of a modified property. If no serialization process is defined, the json serialization mechanism will be used (or the inherit serialization process if any). To specify a custom serialization, you must define both a serialize and a deserialize methods. This methods have the following signatures :
serialize(val) { // val is the valueobject
	return val;    // return a json primitive type (string, number, json object as string)
}
deserialize(ctx) {     // A serialization context { domain:current domain, id:parent id, value:value to deserialize }
	return ctx.value;    // the deserialized value
}
  • A list of constraints can be defined with the property constraints. Constraint definitions are described in the next section.

Email value object definition sample An email value object has the following rules : 1 - It's a string. 2 - It has no specific serialization 3 - It has a specific format used to validate its value.

With this informations, we can define an email :

types : {
	"email" : {
    	  type : "string", // it's a string
          constraints : { // It has a constraint
        	"malformed email address {$value} for property {propertyName}." : {
            	check : function(value) {  // arguments are (newValue, oldValue, context)
                	var re = /xx/;			// because it's a property constraint. See below
                    return re.test(value);
                }
            }
        }
    }
}

Constraints

Before seeing how to define a constraint, we must understand how constraints work with hyperstore.

Every change in a store is made within a session. A session can be explicitely declared with the store.beginSession method or implicitely opened by hyperstore every time a action is made on a domain.

At the end of the session, hyperstore knows what elements have been involved by the change(s) and, so, validates this changes by executing constraints linked to this elements.

A constraint can be at an element level or at a property level, executed constraints are the aggregation of all elements constraints and constraints of properties belonging to this elements.

Sometimes, you can have very complex constraints consuming a lot of time to check the data and you don't want to execute them on every changes. For this purpose, hyperstore used two kinds of constraints :

  • a check constraint executed on every changes (at the end of a session). This is the default behavior.
  • a validate constraint exclusively executed manually by calling the validate method of the domain.

A constraint generates a diagnostic message which can be a warning (by default) or an error message. If an error message is emitted during a session, the session will be aborted and rollbacked.

if a session is aborted, an exception will be raised during the close method. You can change this behavior by setting the session mode to SessionMode.SilentMode (value=64).

Messages are availables in the result object returning by the session.close method. You can also subscribe to the sessionCompleted event on the store.

Constraint declaration

A constraint declaration is made in the constraints property. This property can be declared in a valueObject (this a property constraint) or on an entity/relationship (this an element constraint).

To define a constraint we need :

  • a message to display if the constraint failed
  • a condition to verify - Must be true to validate the constraint.
  • a constraint type : check or validate - (check is the default)
  • either if it will generate an error or a warning message. (warning is the default)

There is many ways to define a constraint depending if your used default values or not. The most complete definition is :

constraints: {
	"message" : {
    	check/validate : function(self, ctx) {return true/false}, // constraint condition either check or validate
        error : true
    }
}

The condition property name defines if its a check or a validate constraint. The condition signature differs if it is an element or a property constraint.

  • For an element constraint, arguments are (current_element, constraint_context)
  • For a property constraint, arguments are (new_value, old_value, constraint_context)

The constraint context provides the following members :

  • element : Current element (in the case of an element constraint, this is the same than the first argument)
  • propertyName : In the case of a property constraint, the current checked property.
  • log : A method to emit a new diagnostic message. Normally you don't need it.

Since some properties has default values (check condition and warning message), there is a more simple definition available. Just declare the condition with the diagnostic message.

constraints : {
	"message" : function(self, ctx) {...}  // A check constraint generating a warning message
}

You can have as many constraints as you want.

If you want to override the default values, you can add a $default property in the constraints definition like that :

constraints : {
	$default : {
    	kind : "validate", // override check default behavior
        error: true
    },
    "message" : ...
}

$default definition is valid only for the current constraints definition.

Entity definition

Entity definition consists to define its properties and its references. With hyperstore, this is two distincts concepts because a reference (simple or multiple) will be associated with a relationship.

A property is defined with :

  • a type which can be a primitive (string, number, bool or any ) or a valueObject.
  • some constraints in addition of the constraints defined on the type.
  • a default value

You can define an inherited entity with the extends property referencing a valid entity name.

Property definition

All properties must be define in the properties property.

The simplest way to declare a property is to specify its type. For exemple, to define an entity schema named Library with a property named name of type string.

{
    Library : {
        properties : [
            name : "string",
            constraints : {
                // element constraints
            }
        ]
    }
}

The type name must be an already defined primitive or valueObject. If you want to define more informations like constraint or default value, you must use a block definition.

{
    Library : {
        properties : [
            name : {
                type: "string",
                defaultValue ; "<empty>",
                constraints : {
                    // property constraints
                }
            }
        ]
    }
}

** Constraints with properties** Sometimes constraint depends of some properties values like a range for exemple. It's easy to define such a constraint like this :

types : {
	"range" : {
    	type : "numeric", // inherit from primitive type numeric,
        constraints : { // It has a constraint
        	"value must be between 1 and 10" : {
            	check : function(value) {  // arguments are (newValue, oldValue, context)
                    return value >= 1 && value <= 10;
                }
            }
        }
    }
}
// and the property definition will look like this :
"MyProperty" : "range"

But it's not very powerful because range are not always the same and we can not define a new valueObject for each range. To override this behavior, we can add properties on constraints.

types : {
	"range" : {
    	type : "numeric", // inherit from primitive type numeric,
        min : 1, // default value is 1
        max : 10, // default value is 10
        constraints : { // It has a constraint
        	"value must be between {$min} and {$max}" : {
            	check : function(value) {  // arguments are (newValue, oldValue, context)
                    return value >= this.min && value <= this.max;
                }
            }
        }
    }
}
// and the property definition will look like :
"MyProperty" : {type:"range, min:100, max:200}" // range between 100 and 200

Differents things are happened :

1 - The valueObject definition has changed. There is two new properties (min and max) 2 - The condition function uses directly this properties. 3 - The diagnostic message has some format pattern. {$min} means replace this by the value of the min property. 4 - The property definition overrides default values of the the min and max properties.

Constraint message We have saw a message can contain format pattern like {$min} which reference a property value of the current constraint. But you can use another pattern to include property of element being checked. By using the {name} (without $), you can reference a property value of the current cheked element. Some built-in pattern are available :

  • {propertyName} include the name of the property being checked.
  • {value} include the checked value (In a property constraint)
  • {oldValue} include the old value (In a property constraint)
  • {_id} include the hyperstore id.
  • {_identity} try to include an identity value by testing some classical identity properties : {id} else {name} else {_id}

For example, the message of the range sample could be change to : 'Value of the property {propertyName} element {_identity} must be between {$min} and {$max}'.

constraint reference If a constraint is often used, you can declare it at a global level and reference it by name. For this reason, a global constraint must provide a name with the name property.

Global constraint definitions is declared with the constraints property inside the schema object. You can reference a global constraints with the $ref property.

Calculated property

A calculated property is just a property define as a function.

Member : {
    properties : [
        FirstName : "string",
        LastName : "string",
        FullName : function(self) {
            return self.FirstName + " " + self.LastName;
        }
    ]
}

Reference definition

All properties must be define in the references property.

A reference is represented by a relationship. A relationship can have the following properties :

  • a start element
  • an end element
  • a cardinality OneToOne, OneToMany, ManyToOne and ManyToMany
  • if its an embedded relationship or not. An embedded relationship links the lifecycle of the end element to the lifecycle of the start element (the owner). If the owner is deleted, all its embedded elements will deleted too.
  • Some constraints like an entity.
  • a direction start -> end or end <- start. The latter is used for opposite reference.

Once again, there is many ways to define a reference. The more complete is the following :

Library : {
	references : [
        Books : {
            end : "Book", // end element
            kind : "1=>*", // oneToMany embedded relationship
            name : "LibraryHasBooks", // name of the created relationship
            constraints : {
               // element constraint on the relationship
            }
    	}
    ]
},
Book : {
	Library : {  // Opposite reference
        end : "Library", // end element
        kind : "*<=1", // inversed oneToMany embedded relationship
        name : "LibraryHasBooks", // name of the implicitely created relationship
    }
}

relationship is optional unless there is an opposite reference which will lust have the same relationship name. If two different names are provide, two relationships will be created and you cannot navigate in the two directions. If no mame is provide, a generic name will be created like "StartEntityName(has|References)EndEntityName". "Has" is used is the relationship is embedded, "References" else.

kind is a mnemonic representation of the cardinality and embedded property. "1=>" means embedded OneToMany relationship, for a not embedded relationship use "1->".

Relationship definition

Even if relationship are often declared implicitely, you may have to define one if you want add it properties or constraints.

You can define an inherited relationship with the extends property with a valid relationship name.

LibraryHasBooks : {
	source : "Library",
    end : "Book",
    kind : "1=>*",
    properties : [],
    references : [],
	constraint: {
    	...
    }
}