Horizon

Hiperspace does not by default apply any constraints to an object property (they are all optional, except key). Constrains are implemented by Horizon views (you cannot store anything you can't retrieve), and the convention of including a Valid value calculated by applying conditions, and Deleted type Boolean to indicate that an element has been deleted SubSpace includes a Horizon parameter of all conditions that implement validation, domain access control, and domain perspectives (such as approval)

This a deliberate design decision because:

  • In a conventional database, you need to disable constraints to initially load data - Using horizon filters this can be done for a single session, but invalid data would still be filtered
  • It avoids the need for separate schema model for logging stores
  • Traditionally separate views are needed for data entry and reporting, or virtual private databases are to hide access to data that lacks permissions
  • Status based filters can be applied in a single place to hide deleted data, or unapproved data

Horizon is used rather than global filters or constraints following the event horizon of general relativity that something you cannot see doesn't necessarily mean it does not exist


Horizon filters are provided when opening a domain SubSpace, which in turn provides the predicates to the specifc SetSpace<> for each type. Filters are applied:

  • When an element is bound to a Hiperspace - objects that cannot be read with the SubSpace are not saved
  • When an element is retrieved from Hiperspace - predicates are applied after read from Hiperspace, but before return to application and caching in SetSpace<>.

Simple case

The Invoice example includes the simplest example of Horizon filters that enforce referential integrity

namespace ERP
{
    public partial class ERPSpace
    {
        private static Horizon[] constraints = new Horizon[]
        {
            new Horizon<Address>        (i => i.Valid == true),
            new Horizon<Country>        (i => i.Valid == true),
            new Horizon<Tarriff>        (i => i.Valid == true),
            new Horizon<Tax>            (i => i.Valid == true),
            new Horizon<Shipping>       (i => i.Valid == true),
            new Horizon<Product>        (i => i.Valid == true),
            new Horizon<ProductPrice>   (i => i.Valid == true),
            new Horizon<Shipping>       (i => i.Valid == true),
            new Horizon<Salesperson>    (i => i.Valid == true),
            new Horizon<Order>          (i => i.Valid == true),
        };

        public ERPSpace(HiperSpace space, DateTime? AsAt = null, DateTime? DeltaFrom = null) 
            : this(space, constraints, AsAt, DeltaFrom)        
        { }
    }
}

The Value Valid = Country = null || PostalCode = null ? false : true is evaluated before an address is added to the ERPSpace.Addresses set to prevent saving of incomplete data. In this example, it is possible for an application to override the rule (wouldn't if it was an extent) when loading.

ERP.Sales.Order includes the validation rule Valid = Customer = null || changed(Customer) using the external function %function (unary, changed, ERP.ERPFunctions.Changed, Boolean); that retrieves the prior value of Customer, and validates that it has not changed.

CQRS case

The RBAC example includes two filters that segregate query actions from command updates

        private static Horizon[] Read (RBAC.Realm realm) =>
        [
            new Horizon<RBAC.Realm>(r => r.Valid == true && r.Name == realm.Name),
            new Horizon<RBAC.UserPermission>(p => p.Valid == true && p.Deleted == false && p.Approved == true && p.owner?.Realm == realm), 
            new Horizon<RBAC.GroupPermission>(p => p.Valid == true && p.Deleted == false && p.Approved == true && p.owner?.Realm == realm),
            new Horizon<RBAC.User>(u => u.Valid == true && u.Deleted == false && u?.Realm == realm),
            new Horizon<RBAC.Group>(g => g.Valid == true && g.Deleted == false && g?.Realm == realm),
            new Horizon<RBAC.Resource>(i => i.Valid == true && i.Deleted == false),
        ];
        private static Horizon[] Write (RBAC.Realm realm) =>
        [
            new Horizon<RBAC.Realm>(r => r.Valid == true && r == realm),
            new Horizon<RBAC.UserPermission>(p => p.Valid == true && p.owner?.Realm == realm),
            new Horizon<RBAC.GroupPermission>(p => p.Valid == true && p.owner?.Realm == realm),
            new Horizon<RBAC.User>(u => u.Valid == true && u?.Realm == realm),
            new Horizon<RBAC.Group>(g => g.Valid == true && g?.Realm == realm),
            new Horizon<RBAC.Resource>(i => i.Valid == true),
        ];

        /// <summary>
        /// Construct with predefined CQRS mode
        /// </summary>
        /// <param name="mode">read or write</param>
        /// <param name="AsAt">snapshot date</param>
        public AccessSpace(HiperSpace space, Mode mode, RBAC.Realm realm, DateTime? AsAt = null) 
            : this(space, (mode == Mode.Write ? Write(realm) : (mode == Mode.Read ? Read (realm) : null)), AsAt)
        {
            _realm = realm;
        }

The read horizon hides any values that have been deleted, but also hides any permissions that haven’t been approved yet.

Copyright © Cepheis 2024