Classes are the cornerstone of Appirator. They define the schema of resource object instances that are created and manipulated through API endpoints or internally as storable and transient objects processed through scripts and passed through events.
Classes are capable of being enriched with elements and features.
Classes have 4 main sections:
Scripts used within a class declaration must be in the language defined by the module. Class properties and methods are available on associated class object instances within scripts in the same way that they would with any other object instance. The primary difference is that the properties are repeatedly tested internally to ensure that they can’t be assigned invalid values.
If a property on a resource object is modified during a CREATE or UPDATE operation, the object will be automatically saved with those values once validation and rule processing is performed on the object, which occurs when the process completes. This can be done preemptively in scripts by explicitly saving the object using the global api object.
Note that these are not referring to class properties which are shown below. These are properties of the class declaration.
name optional (when using Git sync)
The name of the class in its associated module. This is taken from the file name by default if the class is being imported as a file. It can be prefixed with the path name of the file when imported as a file where the path segments are separated with a dot. Similarly, if created object over a REST interface dot notation is permitted. This must be unique within the module.
modifiers optional
Defines modifiers for the class. These can be a combination of:
persistence is NONE.
Account is
marked as ROOT and RESOURCE then the associated default API endpoint
is /account/<id>. Root resource classes cannot have a parent.$class.name.augments optional
See Class Augmentation for a full description.
extends optional
This is essentially a traditional extends that exists in most object oriented languages. If this class is B and it extends class A, then B is an instance of A programmatically. With respect to API construction, if A is a resource, it essentially owns the endpoint path so that any instance of A can be created through its path, thus both B and A can be created and manipulated under the same path.
The value used here is the class name. If the class is in a separate module and can’t guarantee to be resolved without the full name, then the full name needs to be used, including the module name.
parent optional
The parent class of this class. This is mainly useful if the top parent
class and all children are resource classes, but they can also be used if
the classes are not resource classes. If the parent is a resource class,
the default API path would look like /parent-class-name/<parent-id>/child-class-name/<child-id>.
path optional
Overrides the default API path segment when the class is an API resource
class. By default, resource classes adopt the rightmost class name segment
for the path segment, lowercase and in dash notation. For example, if the
class name is reflect.ApplicationDomain, the default path segment is .../application-domain.
If the class is not a resource class, then this property is not used.
store optional
Defines the custom store to be used with the persistence types HYBRID or CUSTOM. See persistence for more details.
index optional
Defines how object instances of this class, if it is a resource class, will be stored in its associated Elasticsearch index. By default, each class instance is indexed in an independent index so cross referencing of objects via searches are not possible. Possible values are:
Defines how the class will be persisted in the linked store. If no store is explicitly associated with the class, the default internal store will be used. Class objects are stored by default. Possible values are:
searchMode optional
Not currently used.
validation optional
See Validation for full description.
rules optional
See Rules for full description.
properties optional
See Class Properties for full description.
methods optional
See Class Methods for full description.
handlers optional
See Class Event Handlers for full description.
migrations optional
See Class Migration Scripts for full description.
The properties section of a class is an array of class properties. Properties are available on associated objects in scripts in the same way that they would be had they been initially declared in the script, the difference being that assigning values to properties results in an initial assessment of the assignment internally. property validation does not occur until an object is being saved or explicitly validated through the global api object.
Each of these class properties have the following declarable properties.
name
The name of the property. This must start with an alpha character or underscore and can be followed by zero or more alphanumeric characters or underscores. The property name must be unique within in the class.
type optional
The type of the property. If this is not supplied, it defaults to a string. This can be one of:
Any of the above can be arrays simply adding [] to the type name, e.g.
string[] is a string array. Maps are also supported using {}, so a map
of strings (string keys and string values) is string{}. Note that this is
the only type of map supported currently but other map types will be supported
in the future, e.g. MyLookup{}.
If the type is a class reference and the class is a resource class, the instance is stored as a separate resource. If it’s not a resource class it is stored as an inner object of the outer instance.
mapto optional
Used to map an inbound property to this class property. This is useful if
the inbound JSON has a different structure than the one represented by the
enclosing class definition. If the mapto value begins with a $, it is assumed
to be a JSONPath selector. Otherwise it is assumed to be a script in the
same language as that used elsewhere in the class.
The return value of the script is the value to be assigned to the object property value. An error will occur if the value cannot be naturally converted to the same type as the property. If the type of the property is another class, the result of mapto function or JSONPath selector must be a JSON object that can be mapped into that class.
By default the value assigned to the the class property will be the inbound object property corresponding to the name of the class property.
persistence optional
Defines how the property will be persisted in the linked store. Properties are stored by default in the internal store. Possible values are:
default optional
The default value for the property if no value is supplied in the CREATE.
modifiers optional
Defines certain properties about the class property. Possible values are:
type is a PROTECTED
resource class.type is
a PUBLIC resource class.selector optional
This is an Elasticsearch query string that is used to select the subset of resource objects where the type of the property is a reference to a resource class array. When a selector is used, it is not possible to include resource objects directly via this object, rather the adding and manipulating these resource objects is performed via their own endpoints or directly through scripts. If a selector is not used, the resource objects contained in this array must have their parent set to this class.
searchMode optional
Not currently used.
validation optional
See Validation for full description.
rules optional
See Rules for full description.
The methods section of a class is an array of class method declarations. Methods are available on associated objects in scripts in the same way that they would be had they been initially declared in the script.
Each of these class methods have the following declarable properties.
name
The name of the method. This must start with an alpha character or underscore and can be followed by zero or more alphanumeric characters or underscores. The method name must be unique within in the current class definition. Methods can override methods declared in super classes in the same way that method override super methods in object oriented languages. It is possible to invoke the overridden super method using the super keyword in ECMAScript and may also be possible in other languages.
script
The method script, this is invoked when the method is called on the object. In ECMAScript developers can access the current object through the “this” keyword. In Python, “self”. Other languages have similar constructs.
context
See Call Context for more information.
api
See Global API Object for more information.
<declared input parameters>
Input parameters are declared in the same way that they’re declared in the chosen language. In ECMAScript, the declaration of the method can be one of:
name: doTheThing
script: >
(param1, param2) => {
// do something with the parameters and this object
return "the result";
}
or:
name: doTheThing
script: >
function(param1, param2) {
// do something with the parameters and this object
return "the result";
}
or alternatively without the enclosing function syntax:
name: doTheThing
script: "just return the result"
This is unspecified and is whatever the method needs to return.
The event handlers section of a class is an array of class event handler declarations. Event handlers are scripts that are run when an event registered with the handler is triggered. More than one event can be registered on an event handler. Event handlers can be triggered synchronously or asynchronously. Events captured by event handlers declared in class definitions must be triggered on the object associated with the class because the “this” must be present and refer to the event target object. For other event handling, global event handlers need to be used.
events
The list of event names (a string array) that will trigger this event handler. Custom events can be used here as long as those custom events are bound to an instance of this class. Event names are simply the operation in most cases, e.g. CREATE, READ, etc.
mode optional
One of the following:
script
The event handler script. In ECMAScript developers can access the event target object through the “this” keyword and this must be an instance of this class. In Python, “self”. Other languages have similar constructs. Event handler script take no function parameters, but there are contextual parameters available as described below.
context
See Call Context for more information.
api
See Global API Object for more information.
ECMAScripts can be declared as functions or not as shown below. Similar standards will be available for other languages as they become supported.
events: CREATE UPDATE
script: >
() => {
// handle the event
...
}
or:
events: CREATE UPDATE
script: >
function() {
// handle the event
...
}
or alternatively without the enclosing function syntax:
events: CREATE UPDATE
script: // handle the event
Return values from event handlers are ignored.
The migrations section of a class is an array of class migration script declarations. Migration scripts are used to upgrade or downgrade resource objects as they’re being read from the internal or custom stores.
Resource objects acquire their version when they are created or updated and that version comes from the version of the module or application in which they’ve been declared. When a resource object is stored under one version and the module or application hosting the associated class definition for the object is upgraded to a higher version, the next time the object is read from its store, the object will be updated with any changes required by the new class definition using its migration script if one is present.
Object downgrading can occur when there is a rollback of a module or application and in this case a reverse migration happens.
Upgrading and downgrading occurs script by script based on the “to” version number associated with the script. If the script is upgrading, earlier versions are processed first. If the script is downgrading, later versions are process first. Where a resource object is already ahead of a script in version, that script is not processed during an upgrade but it is processed during a downgrade.
The global property [TODO] defines whether or not the migrated object is automatically saved after being migrated.
direction optional
One of the following:
to
The object version to upgrade or downgrade to in <major>.<minor>.<build>
format, e.g. 2.103.1.
script
The migration script, triggered when an object needs to be upgraded or downgraded.
In ECMAScript developers can access the target resource object through the
this keyword. This must be an instance of this class. In Python, self.
Other languages have similar constructs.
context
See Call Context for more information.
api
See Global API Object for more information.
An example of an upgrade and downgrade can be seen below in ECMAScript.
direction: UPGRADE
to: 1.0.23
script: >
() => {
// upgrade the object from an earlier version
...
}
direction: DOWNGRADE
to: 0.9.01
script: >
() => {
// downgrade the object from a higher version
...
}
Return values from migration scripts are ignored.
Rules are a powerful feature that offers class developers the opportunity to mutate class behaviour based on contextualised input. For example, through rules, it is possible to deny access to class properties or entire classes if the user requesting access does not have specific permission, is not sending valid headers or any other parameter. It is possible through rules to run scripts, set up events, provide rule sensitive validation and much more.
Class-level and property level rules operate essentially the same, but the timing that they run is slightly different and the intent of actions can vary slightly.
Rules have 2 parts, a selector and an actor. Selectors are used to determine when an action will be taken, while the actor is the thing or things that will be done to the class or the property based on successful selection by the selector.
Rules selectors can be one or more of:
this
in ECMAScript, the current context and any global objects and functions.Rule actors can be one or more of:
operations optional
The list of operations used in the selector. Only one of these needs to match the operation of the context. See Operations for more information.
filter optional
A filter string. If the string begins with $ (ignoring preceding whitespace),
the filter string is assumed to be a JSONPath selector. If not, it is assumed
to be a script in the same language as that of the class scripts.
If the filter is a JSONPath, it acts on the current context and thus must begin
with $.context and it must match an element in the context to pass.
If the filter is a script, a return value of true (or truthy) is considered a pass. Scripts have access to the current context object and any global objects or functions.
permissions optional
A list of permission strings. Only one of these needs to match one of the
permissions in the context object.
access optional
One of the following:
See above for more information on access priority.
validation optional
A list of validators that will be applied to the class or the class property in addition to those already present. See Validation for more information.
script optional
A script to run. This will be run only if all validation is successful. The script has access to the current context object and any global objects or functions.
Validation can be applied to the class as a whole or to individual properties. The validation section of a class, class property or a class or class property rule contains a list of one or more inline validators or references to global validators.
phase optional
The validation phase, one of EARLY, MID or LATE. The default phase depends on the <phase of the rule?? TODO>.
name
The name of the validator. If this validator does not contain its own script,
this name must be a reference to a global validator. The name of the validator
is used in the error response prefixed by the path of the class and property
in dot notation, for example, the class com.appirator.core.reflect.Class has
a property called extends which has an inline validator called ExtendableClass.
If this validator does not pass, the validation error code path is
com.appirator.core.reflect.Class.extends.ExtendableClass. This code path
can be mapped as needed to error message strings for display to end users.
script optional
The validation script to be run. This should be in the same language as the associated module. The return value of this should be true (or truthy) if the validation has passed and false (or falsy) if the validation fails. If there is no validation script, the name of this validator must be a reference to a global validator.
params optional
An optional list of parameters for the validator if the validator is a reference to a global validator. This is a name/value mapping where the value can be JSON string, boolean or number. Parameters are passed to global validators in a params object.
Resource objects can contain other objects and those objects can contain more objects and so on. There’s no limit to the depth possible.
The objects that are nested inside resource objects and within other objects can be resource objects or plain objects. If they are plain objects, they are stored with the resource object that contains them. Any resource objects embedded into other objects are stored independently and references to those objects are embedded within their containing object. This means that it is possible to change the referenced resource object independently of is containing object.
There are specific rules on how embedded resource objects are added and removed from their enclosing classes.
TODO - write something
The following example is a class that deals with domain name registration on
an application. It is included in the YAML file ApplicationDomain.cls.yaml
in the path reflect of the module com.appirator.core. As a consequence the
class name automatically becomes reflect.ApplicationDomain and its full
name becomes com.appirator.core.reflect.ApplicationDomain.
# -----------------------------------------------------------------------------------#
# This is a domain registration used with an application. Once the domain is active
# the application can be accessed over the internet using that domain instead of the
# default domain provided through the framework.
# -----------------------------------------------------------------------------------#
parent: Application
modifiers: FINAL
properties:
# Contains the status of the domain registration.
- name: status
type: ApplicationDomainStatus
rules:
- operations: CREATE DELETE
access: DENY
# If the value is being set to PENDING, then we need to check the status
# transition. This is only allowed if the value was previously INACTIVE.
- operations: UPDATE
validation:
- name: StatusTransition
phase: MID
script: value === 'PENDING' && extant.status === 'INACTIVE'
# The name of the domain.
- name: domain
rules:
- operations: UPDATE DELETE
access: DENY
- operations: CREATE
validation:
- name: DomainName
- name: GloballyUnique
phase: LATE
script: "!value || api.applications.canLinkDomain(value)"
# The TXT value. This is generated by the framework and cannot be assigned by the
# implementation code.
- name: txt
rules:
- operations: CREATE UPDATE DELETE
access: DENY
# The number of TXT checks made so far while the registration is in a PENDING state.
# This is set by the
- name: checks
type: integer
rules:
- operations: CREATE UPDATE DELETE
access: DENY
# The time of the last TXT check.
- name: timeLastCheck
type: DateTime
rules:
- operations: CREATE UPDATE DELETE
access: DENY
handlers:
# Initiates the domain checks, starting in 5 minutes unless the domain is an internal
# subdomain.
- events: CREATE
mode: SYNC
script: >
function () {
// Generate a domain if it hasn't been set.
if (!this.domain) {
do {
this.domain = api.crypto.randomAlphaString(10, false, true) + '.appirator.com';
} while (api.applications.canLinkDomain(this.domain));
}
// If it's an internal subdomain, we can immediately activate it. Otherwise,
// we need to wait until someone has added the relevant TXT record to the domain
// record set as verification that they own the domain.
if (this.domain.endsWidth('.appirator.com')) {
this.status = 'ACTIVE';
api.applications.unlinkDomain(this.$parent.$id, this.domain);
} else {
if (this.status === 'PENDING') {
api.events.call(this.checkRegistration, 300);
}
}
}
# If there was a change from INACTIVE to PENDING, then we restart the checks.
# If the domain became active, then register it with the framework against the
# associated application. If the registration was deactivated, then remove
# the previous registration from the application.
- events: UPDATE`
mode: SYNC
script: >
function () {
if (this.status === 'PENDING' && old.status === 'INACTIVE') {
api.events.call(this.checkRegistration, 300);
} else if (this.status === 'ACTIVE' && old.status !== 'ACTIVE') {
api.applications.linkDomain(this.$parent.$id, this.domain);
} else if (this.status === 'INACTIVE' && old.status === 'ACTIVE') {
api.applications.unlinkDomain(this.$parent.$id, this.domain);
}
}
# If the domain is deleted, then we need to deregister it from the application.
- events: DELETE
mode: SYNC
script: >
function () {
if (this.status === 'ACTIVE') {
api.applications.unlinkDomain(this.$parent.$id, this.domain);
}
}
methods:
# Checks the domain validity by testing the presence of the TXT record on the domain
# registration. If the registration is complete, then this marks the domain as active.
# If it's not complete, then we need to check if we're expiring the checks. If they're
# continuing, then, we schedule the check again in 5 minutes. Note that this should
# not be invoked during a synchronous event or request because it can take a while to
# return.
- name: checkRegistration
script: >
function () {
if (this.status !== 'PENDING') {
return;
}
if (api.applications.checkDomainTxt(this.domain, this.txt)) {
this.status = 'ACTIVE';
api.events.raise('domain.DomainCheckSucceeded', this);
} else if (new Date() - 24 * 3600 * 1000 > this._metadata.timeCreated) {
api.events.schedule(this.checkRegistration, 300);
} else {
this.status = 'INACTIVE';
api.events.raise('domain.DomainCheckFailed', this);
}
}