Classes

Description

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.

  • Modifiers on the class itself as well as individual properties - e,g, PUBLIC and PROTECTED.
  • They can be augmented with almost any element, which allows existing classes, including those in third-party modules, to be extended globally locally within applications.
  • They can be extended and instantiated in the same way that classes from object oriented programming languages can be extended.
  • The can be abstract and their API endpoints can also be abstract.
  • They behave like API REST resources and script objects without stubs, proxy classes, data objects or any other mirror component. One declaration is all it takes.
  • By default they are persisted automatically in Appirator’s internal store and their properties are indexed in an Elasticsearch cluster by default.
  • The internal store can be swapped with a custom store or proxy configuration for storage externally. The store can even be a hybrid store that stores part of the associated resource objects in Appirator’s internal store and part in an external custom store.
  • Persistence of associated resource objects is applicable to the entire object or just parts of it.
  • They can have parents. When the parent is a resource class, the child is automatically a resource class. As a result, child classes automatically get their own API endpoints when public.
  • They can be validated with one or more validators. Their properties can also individually be validated with one or more validators.
  • Both classes and their properties can contain rules which allow very specific access, filters, property scripts and more.

Classes have 4 main sections:

  • Properties - these contain the properties of the class including their name, type, persistence setting, search modifiers, search selectors, rules, access modifiers and validation.
  • Methods - these are like class methods in any programming language.
  • Handlers - these are class-level event handlers, similar to global event handlers but within event handler scripts, “this” refers to the instance of the class on which the event was originally triggered.
  • Migrations - these are scripts that are used to upgrade or downgrade instances of the class to and from particular versions.

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.

Properties

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:

  • RESOURCE - the class is a resource class which makes means that it is independently stored and will get its own API path segment if it is PUBLIC. Resource classes are not stored if the class persistence is NONE.
    • If the class does not have a parent or if it is not embedded into another resource class, it must have a ROOT modifier to be referenceable through an API endpoint.
    • Resource classes that are embedded into another resource class automatically aquire the embedding class as their parent unless they have a declared parent already.
    • This modifier is inherited from super classes, i.e. if this class is B and extends A where A is a resource class, then B is also a resource class even if it is not explicitly marked as one through this modifier.
    • If a class is not a resource class, instances can only be used embedded into other class instances. In this case they do not get their own API endpoint and they are stored as inner objects of the outer object instance rather than as separate objects.
  • ROOT - this requires that the class is a resource class, i.e. that it is marked with the RESOURCE modifier. ROOT identifies the resource class as a root within the application and its API, e.g. if class Account is marked as ROOT and RESOURCE then the associated default API endpoint is /account/<id>. Root resource classes cannot have a parent.
  • ABSTRACT - the class is abstract and must be extended to be instantiated. Resource classes can be abstract. When objects are being created over an abstract API endpoint, information about the subclass must be provided as metadata in the request, specifically in the property $class.name.
  • LENIENT - the API will ignore unknown properties being sent to the resource class API endpoint during CREATE and UPDATE requests.
  • FINAL - the class cannot be extended or augmented.
  • PUBLIC - this makes the class visible as an API endpoint or CRUDS endpoints. This modifier must be accompanied by a RESOURCE modifier.
  • PROTECTED - relevant only if the class is marked with a RESOURCE modifier and it makes the class inaccessible as an API endpoint. Consequently, no endpoint is allocated for it. This is the default visibilty state for a resource class.

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:

  • NONE - class instances are not indexed. This prevents object instances of the class from being searched, thus, the SEARCH operation is not available.
  • PARENT - class instances are indexed in the same index shared as their parent objects, if they have a parent object. This additionally sets up a parent/child relationship between the two objects so they can be searched via a parent/child join.

persistence optional

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:

  • NONE - class instances are not persisted. This restricts operations to CREATE only.
  • INTERNAL - class instances are persisted in the internal store.
  • HYBRID - class instances are persisted in both the internal store and the custom store defined in the class store property.
  • CUSTOM - class instances are persisted in a custom store or are proxied to an external target. The store name is defined in the class store property.

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.

Class Properties

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:

  • string
  • boolean
  • date
  • datetime
  • decimal
  • integer
  • double
  • a lookup reference
  • a class reference

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:

  • NONE - the property is not persisted.
  • INTERNAL - if the class uses a HYBRID store, this property is stored in the internal store. This is default. This has no effect if the class is not using a HYBRID store.
  • CUSTOM - if the class uses a HYBRID store, this property is stored in the custom store. This has no effect if the class is not using a HYBRID store.

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:

  • FINAL - the property cannot be extended or overridden.
  • PUBLIC - the property is accessible over the API endpoint as long as the class is a resource class and is PUBLIC. This modifier overrides the PROTECTED modifier of the class definition if the property type is a PROTECTED resource class.
  • PROTECTED - the property is inaccessible over the API endpoint even if the class is a resource class and is PUBLIC. This cannot be used on a property to lower the visibility of it, including when the property 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.

Class Methods

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.

Properties Per Method Entry

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.

Parameters

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"

Returns

This is unspecified and is whatever the method needs to return.

Class Event Handlers

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.

Properties Per Handler Entry

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:

  • SYNC - the event handler is run synchronously with the initiating process. This allows the initiating process to be interrupted by the script associated with this handler as a result of an exception being thrown.
  • ASYNC - the event handler is run asynchronously, independently of the initiating process. Consequently the initiating process cannot be interrupted by this handler. Asynchronous event handlers are guaranteed to start after the completion of the initiating process. This is the default mode.

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.

Parameters

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

Returns

Return values from event handlers are ignored.

Class Migration Scripts

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.

Properties Per Migration Entry

direction optional

One of the following:

  • DOWNGRADE - the migration should be triggered for an object downgrade.
  • UPGRADE - the migration should be triggered for an object upgrade. This is the default direction.

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.

Parameters

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

Returns

Return values from migration scripts are ignored.

Rules

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:

  • A list of operations, only one of which needs to be matching the operation of the context.
  • A filter. This is either a JSONPath selector that acts on the current context or a script that returns a boolean true or false to determine if the filter passes or not. The script has access to the target object through this in ECMAScript, the current context and any global objects and functions.
  • A list of permission strings, only one of which needs to match one of the permissions available in the current context object.
  • One, two or all three selectors can be used concurrently on a rule. Rule selection occurs once per resource object early in the resource processing and is not tested thereafter, so changes made to the context later will not have any effect on the selection process.

Rule actors can be one or more of:

  • An access grant, either ALLOW or DENY. If multiple access grants qualify in a request where more than one rule is selected, ALLOW always takes priority, so if one rule results in an ALLOW and two result in a DENY, the result is ALLOW. Rule processing order is not relevant in this case. If the result of an access decision on a class is DENY, then the entire object is denied and an access denied error response is returned (403 in the case of HTTP). If the result of an access decision on a property is DENY, then:
  • If the operation is CREATE, UPDATE or DELETE, an access denied error response is returned if the property is being set or removed.
  • If the operation is READ or SEARCH, the property is removed from the response so the caller never receives it. Through this mechanism, it’s possible to return a response to the caller but still protect the exposure of sensitive object properties.
  • A validation section allowing one or more validators. These validators are applied in addition to any validators already applied to the class or property. These validators honour normal validator phases.
  • A script to be run. Scripts are run only if all validation on the resource object passes.

Selector Properties Per Rule

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.

Actor Properties Per Rule

access optional

One of the following:

  • ALLOW - the operation is allowed on the object or the object property.
  • DENY - the operation is not allowed on the object or property, however, if the rule is a class property rule and the operation is READ or SEARCH, the property is removed from the output before returning.

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

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.

Properties Per Validator Entry

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.

Inner Objects

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.

  • If the property referring to the resource object is a singleton, i.e. not an array of objects, then it can be added directly to the enclosing object and will be stored when the enclosing object is stored if it is a new object or if it has been modified. Once that resource object obtains an identifier, it’s identifier and class information is embedded and stored with the enclosing object.
  • If the property referring to the resource object is an array and a selector is used on the property, the resource objects cannot be manipulated directly on the property itself, i.e. objects cannot be added, removed or altered. Instead these objects must be added, removed and altered independently of the enclosing object. The selector will ensure that these resource objects are returned during the retrieval of the enclosing object.
  • If the property referring to the resource object is an array and no selector is used, the resource objects in the array must have their parent set to the enclosing class in order to be selectable automatically. In this instance, these objects can be added and removed directly on the property. They can also be manipulated directly, independently of the enclosing object.

Class Augmentation

TODO - write something

Examples

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);
        }
      }