X2 Framework deals with the notion of records. Records are objects of a certain type, normally persisted in a database and identified by a unique id. In the code, records are represented by JSON objects. The collection of record types assembled into a record types library defines the data domain, with which the application operates. Record types define the structure of the records, record properties and their value types, relationships between different record types in the library, etc. A record type definition is a schema for the records of that type, or, using the OOP analogy, record types are like classes and records are like instances or objects of those classes.
See module's API Reference Documentation.
The x2node-records
module provides the essentials for the application record types library. The library is represented by an instance of RecordTypesLibrary
class, which can be built by the factory buildLibrary
function exported by the module. A single library instance is usually created once by the application in the beginning of its lifecycle and is used throughout the runtime. To build a library, the module must be provided with the library definition JSON object that includes the record type definitions. Here is an example:
const records = require('x2node-records');
const recordTypes = records.buildLibrary({
recordTypes: {
'Account': {
properties: {
'id': {
valueType: 'number',
role: 'id'
},
'name': {
valueType: 'string'
},
'orderRefs': {
valueType: 'ref(Order)[]'
}
}
},
'Product': {
properties: {
'id': {
valueType: 'number',
role: 'id'
},
'name': {
valueType: 'string'
}
}
},
'Order': {
properties: {
'id': {
valueType: 'number',
role: 'id'
},
'accountRef': {
valueType: 'ref(Account)'
},
'items': {
valueType: 'object[]',
properties: {
'id': {
valueType: 'number',
role: 'id'
},
'productRef': {
valueType: 'ref(Product)'
},
'quantity': {
valueType: 'number'
}
}
}
}
}
}
});
The example above defines three record types: "Account", "Product" and "Order". Explanation of the record type definitions follows.
At the minimum, every record type definition contains a properties
attribute, which is an object that provides property definitions. The keys in the properties
object are the property names and the values are objects that define the corresponding properties. Every property definition has a valueType
attribute that defines the property value type. Structurally, a property can be scalar, which means it has a single value, an array, represented by a JSON array, or a map, represented by a JSON object. Array and map properties are sometimes called collection properties to distinguish them from the scalar properties.
A property can be optional or required. When a property is optional it means a record does not have to have a value for that property. By default, scalar properties are required and array and map properties are optional (meaning they can be empty or absent altogether). To override the defaults, a property definition can have a Boolean attribute optional
that explicitly specifies the optionality.
Also, a property can be modifiable or read-only. If a property is not modifiable it does not mean it is immutable. It only means that its value cannot be changed directly and explicitely. The property modifiability may be used by other modules to enforce or hint certain logic. By default, id and view properties are assumed to be read-only and all other properties are assumed to be modifiable. To override the defaults, a property definition can include a Boolean attribute modifiable
that explicitly specifies the modifiability. If a property is a nested object and is declared read-only, all nested properties become automatically read-only as well.
A record type definition can also have a factory
attribute, in which case it is a function that is used by the framework (and perhaps the application as well) whenever it needs to create a new instance of the record type. The function takes no arguments and the record type descriptor object (described later) is provided to it as this
. For example:
class Person {
...
}
const recordTypes = records.buildLibrary({
recordTypes: {
'Person': {
properties: {
...
},
factory: function() { return new Person(); }
}
}
});
If factory
is not specified, a simple new Object()
is used to create new record instances.
Four simple value types are supported: "string", "number", "boolean" and "datetime".
This is a single JSON string value. For example:
{
...
'Person': {
properties: {
...
'firstName': {
valueType: 'string'
},
'lastName': {
valueType: 'string'
},
...
}
},
...
}
Then a record of type Person
could be:
{
"firstName": "Billy",
"lastName": "Bones"
}
This is a single JSON number value, including integer, floating point, etc. For example:
{
...
'Person': {
properties: {
...
'worth': {
valueType: 'number'
},
'numShipsServed': {
valueType: 'number'
},
...
}
},
...
}
And in a record:
{
"worth": 250000.37,
"numShipsServed": 5
}
A single Boolean value:
{
...
'Person': {
properties: {
...
'availableForHire': {
valueType: 'boolean'
},
...
}
},
...
}
And in a record:
{
"availableForHire": true
}
Represents a single date and time value. In JSON it is represented by a string in ISO 8601 format, which is the value returned by the standard Date.prototype.toISOString()
function. For example:
{
...
'Person': {
properties: {
...
'boardedOn': {
valueType: 'datetime'
},
...
}
},
...
}
Then in a record it can be:
{
"boardedOn": "1765-10-05T14:48:00.000Z"
}
The records are intended to be persistent and every record can be identified by a unique id. Therefore, every record type definition must contain one property that is used as the record id. The record id property is marked by a role
attribute in its definition with value "id". The value type of the property can only be "string" or "number". For example:
{
...
'Person': {
properties: {
'id': {
valueType: 'number',
role: 'id'
},
...
}
},
...
}
So, a record could look like:
{
"id": 35066
}
Only one id property can be specified for a record type (composite identifiers are not supported).
A property can be a nested object. For example:
{
...
'Person': {
properties: {
...
'address': {
valueType: 'object',
properties: {
'street': {
valueType: 'string'
},
'unit': {
valueType: 'string',
optional: true
},
'city': {
valueType: 'string'
},
'state': {
valueType: 'string'
},
'zip': {
valueType: 'string'
}
}
},
...
}
},
...
}
Then in a record it could be:
{
"address": {
"street": "42 W 24th St.",
"city": "New York",
"state": "NY",
"zip": "10010"
}
}
The properties
attributes in the nested object property definition follows the same rules as the one on the record type definition. Multiple nesting levels are supported as well.
As with the record types, a nested object property definition may optionally contain a factory
attribute providing a function used to create new instances of the nested object. The nested object property descriptor object (described below) is available to the custom factory function as this
.
Sometimes it is necessary to have a nested object property that can have different properties depending on its type. For example, a shopper account may have a payment method on it and different payment methods may need different sets of properties. This is called a polymorphic nested object property. Here is an example:
{
...
'Account': {
properties: {
...
'paymentInfo': {
valueType: 'object',
typePropertyName: 'type',
subtypes: {
'CREDIT_CARD': {
properties: {
'last4Digits': {
valueType: 'string'
},
'expDate': {
valueType: 'string'
}
}
},
'ACH_TRANSFER': {
properties: {
'accountType': {
valueType: 'string'
},
'last4Digits': {
valueType: 'string'
}
}
}
}
},
...
}
},
...
}
The property is made polymorphic by having the subtypes
attribute. Every polymorphic object instance must have the type property, which identifies its type. The type property name is specified by the definition's typePropertyName
required attribute. The properties of specific subtypes are defined in the subtypes
attribute where the keys are the values for the type property.
Given the definition used above, an account record with a credit card payment info could look like:
{
"paymentInfo": {
"type": "CREDIT_CARD",
"last4Digits": "3005",
"expDate": "2020-04"
}
}
And a record with an ACH transfer payment info:
{
"paymentInfo": {
"type": "ACH_TRANSFER",
"accountType": "CHECKING",
"last4Digits": "8845"
}
}
When a custom factory function is required for a polymorphic object, the factory
attribute is specified on each individual subtype definition.
It is also possible to include a properties
attribute in a polymorphic nested object definition. In that case, the properties defined there are shared by all of the subtypes.
Also, a whole record type can be made polymoprhic by having a subtypes
attribute. For example:
{
...
'Event': {
typePropertyName: 'eventType',
properties: {
'id': {
valueType: 'number',
role: 'id'
},
'happenedOn': {
valueType: 'datetime'
}
},
subtypes: {
'OPENED': {
properties: {
'openedBy': {
valueType: 'string'
}
}
},
'CLOSED': {
properties: {
'reason': {
valueType: 'string'
}
}
}
}
},
...
}
A "closed" Event record then could look like this:
{
"id": 234532546,
"happenedOn": "2017-03-15T22:30:33.000Z",
"eventType": "CLOSED",
"reason": "REJECTED"
}
Often, different record types in the application's data domain are related and can refer to each other. For example, an Order record may contain a reference to a Product record. Such references are represented by a special property value type, which identifies allowed target record type (or types, see below). A reference property value is a string comprised of the referred record type name followed by a hash sign followed by the referred record id (for example "Order#2354" or "Product#12", etc.). Here is an example:
{
...
'Order': {
properties: {
'id': {
valueType: 'number',
role: 'id'
},
...
'productRef': {
valueType: 'ref(Product)'
},
...
}
},
'Product': {
properties: {
'id': {
valueType: 'number',
role: 'id'
},
...
}
},
...
}
Then an Order record could be:
{
"id": 123,
"productRef": "Product#255"
}
That Order refers to a Product record with id 255.
References can also be polymorphic allowing a property to refer to records of different types. In the property definition, the allowed record types are separated with a pipe sign. For example:
{
...
'Account': {
properties: {
...
'lastInterestedInRef': {
valueType: 'ref(Product|Service)'
},
...
}
},
'Product': {
...
},
'Service': {
...
},
...
}
The lastInterestedInRef
property on an Account record can refer to either a Product or a Service.
So far, we've seen only scalar properties, which allow only a single value (even if the value is a nested object it is still one single object). Collection properties serve as containers of multiple values. One type of a collection property is an array. The array element value type can be any of the scalar value types discussed above. To define an array property, the value type in the property definition is suffixed with a square brackets pair. Here is an example with multiple array properties:
{
...
'Account': {
properties: {
...
'scores': {
valueType: 'number[]'
},
'phones': {
valueType: 'object[]',
properties: {
'id': {
valueType: 'number',
role: 'id'
},
'type': {
valueType: 'string'
},
'number': {
valueType: 'string'
}
}
},
'orders': {
valueType: 'ref(Order)[]'
},
...
}
},
...
}
Then a record could look like:
{
"scores": [ 3, 5.6, 10, -1, 0 ],
"phones": [
{
"id": 1,
"type": "Home",
"number": "317-255-6677"
},
{
"id": 2,
"type": "Cell",
"number": "689-567-0203"
}
],
"orders": [ "Order#25684", "Order#25722" ]
}
By default, the framework assumes that values in a non-object array are unique. This is reported via the array property's descriptor discussed below and may be used by other modules and the application to determine the logic of how they work with the array. To override the default, a allowDuplicates
attribute can be specified on the array property definition:
{
...
'Account': {
properties: {
...
'scores': {
valueType: 'number[]',
allowDuplicates: true
},
...
}
},
...
}
Another type of collection properties are maps. In the records, maps are represented as nested objects. The difference between a nested object property and a map property is that a map does not have a fixed set of nested property definitions. To define a map property, the property value type is suffixed with a curly braces pair. For example:
{
...
'Student': {
properties: {
...
'scores': {
valueType: 'number{}'
},
...
}
},
...
}
So, a student record could look like:
{
"scores": {
"MATH101": 3.6,
"BIO201": 5.0,
"ENGLISH120": 4.8
}
}
It is possible to define view properties. A view property inherits definition from another property in the same container, called the base property, and allows to selectively override the definition attributes. For example, a view property may represent a nested objects array base property as a map by overriding the value type and adding a key property definition attribute (used by a result set parser module, for example):
{
...
'Account': {
properties: {
...
'phones': {
valueType: 'object[]',
properties: {
'id': {
valueType: 'number',
role: 'id'
},
'type': {
valueType: 'string'
},
'number': {
valueType: 'string'
}
}
},
'phonesByType': {
viewOf: 'phones',
valueType: 'object{}',
keyPropertyName: 'type'
}
...
}
},
...
}
Note how the phonesByType
view property uses viewOf
attribute in its definition to refer to the base property. Also note that some definition attributes may not be overridden in a view. Those are properties
and subtypes
.
Most of the time, the views are used to override extended definition attributes used by other modules, such as scoped collection property filtering and ordering. See Extensibility.
The RecordTypesLibrary
class returned by the module's buildLibrary
function provides an API for working with the record types. The API converts the record type and property definitions passed to the buildLibrary
function to the corresponding record type and property descriptors, which are API objects exposing properties and methods to the client code. The original definitions objects are always available through the descriptors as well.
This is the top class representing the whole record types library. The following methods are exposed:
-
getRecordTypeDesc(recordTypeName)
- Get record type descriptor from the library. The argument is a string (orSymbol
) that specifies the type name. The returned object is an instance ofRecordTypeDescriptor
. If no specified record type exists, anX2UsageError
is thrown. -
hasRecordType(recordTypeName)
- Tell if the specified record type exists. Returns a Booleantrue
orfalse
. -
definition
- The original library definition object passed to thebuildLibrary
function. -
definedRecordTypeNames
- Array of strings containing names of all record types from the original library definition provided to the factory. (The callers should not modify the array!) -
refToId(recordTypeName, ref)
- Convert referenceref
to a record of the specified byrecordTypeName
record type to the record id.
Objects of this class describe anything that contains propeties. It matches the properties
attribute in various definitions. A nested object property provides a properties container to describe the nested object's properties. The RecordTypeDescriptor
class extends the PropertiesContainer
class since every record type is a properties container.
A container can also be polymorphic. There are two types of polymoprhic containers: container for a polymorphic nested object property and container for a polymorphic reference property. Any polymorphic property has such container associated with it. The container includes special pseudo-properties to describe specific subtypes. A polymorphic nested object (or polymorphic record type) container will have a pseudo-property for each subtype (in addition to the shared properties). Each such pseudo-property will be an optional scalar nested object property encapsulating the properties specific to the subtype. For a polymorphic reference container, the container will contain a property for each allowed referred record type. The property name is the referred record type name and the type is a scalar reference.
A PropertiesContainer
instance exposes the following properties and methods:
-
recordTypeName
- The name of the record type, to which the container belongs. If the container is the record type descriptor itself, this is the record type name. If the container describes a nested object property, this is the name of the record type, to which the property belongs. -
nestedPath
- Dot-separated path to the nested object property represented by the container. The path, if present, always ends with a dot. If the container is a record type descriptor, this property contains an empty string. Path to a property of a polymoprhic nested object includes the subtype name as a path element. -
idPropertyName
- Name of the id property in the container. If the container has no id property, the value isundefined
. -
allPropertyNames
- Array of names of all properties in the container. -
getPropertyDesc(propName)
- Get descriptor of the specified property. ThepropName
parameter is a string that specifies the property name (no nested paths are supported). The method returns aPropertyDescriptor
object. If no such property anX2UsageError
is thrown. -
hasProperty(propName)
- Returns a Booleantrue
orfalse
telling if the specified property exists. -
isRecordType()
- Tells if the container is aRecordTypeDescriptor
. Returns Booleantrue
orfalse
. Another way to test if a container is a record type descriptor is to check itsnestedPath
property, which is always an empty string for a record type descriptor. -
isPolymorphObject()
- Returnstrue
for a polymorphic object container. A polymorphic object container will havesubtypes
andtypePropertyName
descriptor properties as well. Plus some of the property descriptors in the container will have theirisSubtype()
method returntrue
. -
isPolymorphRef()
- Returnstrue
for a polymorphic reference container. A polymorphic reference container will also havesubtypes
descriptor property. Plus all of the property descriptors in the container will have theirisSubtype()
method returntrue
. -
isPolymorph()
- Returnstrue
for a polymorphic container. Equivalent toisPolymorphObject() || isPolymorphRef()
. -
typePropertyName
- For a polymoprhic object container, name of the property used in the record instances described by the container to indicate the record instance subtype. Note, that the property has a descriptor accessible via the container'shasProperty()
andgetPropertyDesc()
methods, but is "hidden" and is not listed in theallPropertyNames
. The descriptor of a type property is always a scalar, non-modifiable, required "string" and itsisPolymorphObjectType()
method returnstrue
. -
subtypes
- For a polymorphic object container, the list of all subtype names. For a polymoprhic reference container, the list of names of all allowed referred record types. -
definition
- The definition object used to create the container. For a record type, this is the record type definition object. For a nested object property, this is the nested object property definition object. -
parentContainer
- For a nested object property this is a reference to the parentPropertiesContainer
. For a record type descriptor, alwaysnull
. -
newRecord()
- Creates new, empty record instance for this container. If factory function was provided in the container definition, it is used. Otherwise, a simplenew Object()
is returned.
The RecordTypeDescriptor
extends the PropertiesContainer
class and provides the top descriptor of a record type. In addition to the properties and methods exposed by PropertiesContainer
, the class also exposes the following properties and methods:
-
name
- Record type name. This is the same as what's exposed by thePropertyContainer
'srecordTypeName
property. -
refToId(ref)
- Convert referenceref
to a record of ththis record type to the record id.
This is the "leaf" descriptor object representing an individual record property. The following properties and methods are exposed:
-
name
- Property name. -
container
- Reference to thePropertiesContainer
, to which the property belongs. -
containerChain
- Array ofPropertyContainer
s that leads from the record type down to this property via the chain of nested object properties, if any. The first element in the chain is the record type descriptor, the last element is the property container (same one as incontainer
descriptor property). -
definition
- The original property definition object. -
isView()
- Returns Booleantrue
if the property is a view of another property. -
viewOfDesc
- For a view property,PropertyDescriptor
of the base property. -
scalarValueType
- Describes property value type. For a scalar property this is the type of the property value itself. For an array or map property, this is the type of the array or map elements. The following values are possible: "string", "number", "boolean", "datetime", "object" or "ref". -
isScalar()
- Returns Booleantrue
if the property is scalar. -
isArray()
- Returns Booleantrue
if the property is an array. -
isMap()
- Returns Booleantrue
if the property is a map. -
optional
- Booleantrue
if the property is optional andfalse
if it is required. -
modifiable
- Booleantrue
if the property is modifiable. -
allowDuplicates
- For a non-object array property this is Booleantrue
if duplicate values are allowed. -
isId()
- Returns Booleantrue
if the property is an id property (the definition hasrole
property set to "id"). -
isPolymorphObject()
- Returns Booleantrue
if the property is a polymorphic nested object. A shortcut fornestedProperties.isPolymorphObject()
. -
isPolymorphRef()
- Returns Booleantrue
if the property is a reference with multiple targets. Note, that thescalarValueType
in this case isobject
, notref
(seenestedProperties
property description below). This is a shortcut fornestedProperties.isPolymorphRef()
. -
isSubtype()
- Returns Booleantrue
if the property is a pseudo-property representing a subtype in a polymorphic container. Such property itself can be either a nested object or a reference property. -
isPolymorphObjectType()
- Returnstrue
if the descriptor if for a polymorphic object type "hidden" property. -
isRef()
- Returns Booleantrue
if the property is a non-polymorphic reference (that is thescalarValueType
property is "ref"). -
refTarget
- Name of the referred record type for a non-polymorphic reference property. -
refTargets
- For a polymorphic reference property, array of names of all allowed referred record types. This is a shortcut fornestedProperties.subtypes
. -
nestedProperties
- For a nested object property,PropertiesContainer
of the nested object's properties. For a non-polymorphic reference property, the referredRecordTypeDescriptor
. For a polymorphic nested object, aPropertiesContainer
with pseudo-properties that use the subtype names and describe each subtype as a nested object property. The container also includes the shared properties defined in theproperties
attribute, if any. For a polymorphic reference property, also aPropertiesContainer
with pseudo-properties, each using the target record type name as its name and having a non-polymorphic reference type. -
isCalculated()
- Returns Booleantrue
if this is a calculated value property. Calculated properties are similar to view properties. They are not stored in the record persistent storage and their values are calculated on the fly based on other record property values. Calculated properties can be included when the record is read from the persistent storage and are ignored when the record is saved into the persistent storage. This flag is used in extensions and is not used in the core record types library implementation directly. Unless overridden in an extension, the method returnsfalse
for all properties. -
isRecordMetaInfo()
- Returns Booleantrue
if the property is a record meta-info property. Record meta-info properties are special propeties automatically maintained by the library that describe some technical aspects of the record, such as creation and modification timestamps, record version, etc. Meta-info properties can be included when the record is read from the persistent storage and are ignored when the record is saved into the persistent storage. This flag is used in extensions and is not used in the core record types library implementation directly. Unless overridden in an extension, the method returnsfalse
for all properties. -
isGenerated()
- Returns Booleantrue
if the property value is automatically generated when a new record is saved into the records persistent storage. This flag is used in extensions and is not used in the core record types library implementation directly. Unless overridden in an extension, the method returnsfalse
for all properties.
The x2node-records
modules provides the foundation for the record types library. More functionality to the library is added using extensions. Many of the other X2 Framework modules are such extensions themselves and must be added to the library at the time of its construction if it is to be used with those modules. Extensions may utilise additional attributes on the definitions, add properties and methods to the descriptors, impose certain constraints on the data definitions.
To use an extension it needs to be added to the records module via its with
function before the library is constructed by the buildLibrary
function. The with
function can take multiple arguments, each of which is an extension to use. The order, in which the etensions are listed is often important. Here is an example:
const records = require('x2node-records');
const rsparser = require('x2node-rsparser');
const dbos = require('x2node-dbos');
const recordTypes = records.with(rsparser, dbos).buildLibrary({
...
});
Note how the framework's modules are extensions themselves. Many of these modules will refuse to use the library unless it is extended with them.
Refer to the corresponding extensions documentation for the additional definition attributes that they use and other usage information.
This topic will be covered in the later versions of this manual.