A view of Cordova, Spain

Verifying Persistence

  • By Artem V. Shamsutdinov
  • Early 2020

The first (to be) Airport application Votecube was originally written with Firestore as the backend. While it's own central backend will be moved from Firebase to another solution, writing it with that technology exposed me to its Security Rules.

These rules are in place because by default Firebase is accessed from a client browser. Hence there need to be a special set of rules that had to exist which would validate the newly entered data. After some time I was able to translate some of these concepts into Airport.

First of all, basic data format validation (type, size) is already covered by JPA annotations. On top of that all User/Agent information can also be easily verified. Basically if the record is being created then the user information cannot be passed in (it is automatically filled in by Airport). Same goes for creation and update timestamps - these are tracked internally by the modification history (which is mandatory in Airport). And if a record is being referenced then it's easy to always verify that the Id it is referenced by exists in the local database. If it doesn't then how was it referenced from the local client (and used for persistence) in the first place?

However one important part was missing from the spec: Given an object graph passed into create/update/save operation which parts of it should be created and or referenced at all?

The perfect way to describe these relations is a JSON tree, not unlike the one used in the entity select statement. Given the following two entities:

@Entity()
export class Parent {

    @Id()
    @GeneratedValue()
    key: number;

    value: string;

    @OneToMany({cascade: CascadeType.DELETE, mappedBy: 'parent'})
    children: Child<[];
}

@Entity()
export class Child {

    @Id()
    @GeneratedValue()
    key: number;

    value: string;

    @ManyToOne()
    parent: Parent;
}

It is then possible to construct such trees with ease:

export class ParentDao extends BaseParentDao {

    @Persist<ParentGraph>({
        value: Y,
        child: [{
            value: Y
        }, ANOTHER(0, 2)] || null
    })
    static createWithChildren = this.create;
}

export class ChildDao extends BaseChildDao {

    @Persist<ChildGraph>({
        value: Y,
        parent: {
            id: Y
        }
    })
    static createWithParentReference = this.create;
}

Note that because the @Persist decorators are processed at build time they:

  • Allow to specify OR conditions and allow for alternative data shapes
  • Allow to specify exactly how many records are expected in an input array
  • Allow to specify a range of how many input records are expected
  • Remove overhead to the client schema library - all information is in package.json and is loaded by Airport framework directly
  • The methods themselves do not need to be specified, only assigned the core operation. They are then assigned an id (based on name) which is used to look up the matching persistence graph.

With this modification in place the cascade rules for persist operations become obsolete. They are now needed only for delete operations. And for now the only way to perform a delete on an entity is to pass an object (or a stub with its ID) to be deleted. Then the framework will then traverse the cascade rules and delete everything (that is stored locally) accordingly. Of course one can still write a manual delete query with a where clause (that will also delete only the matching local records).