Bitlibs Documentation
  • Docs
  • Home
  • Help

›Modules

Getting started

  • Introduction
  • Usage
  • Modules overview

Tutorial

  • Forms

Modules

  • Datamodels
  • Datasets
  • Networking
  • Context
  • Loading
  • Pagination
  • Notifications
  • Storage
  • Utils

View layer integrations

  • Forms

Datamodels

Introduction

The Datamodels module is the heart of bitlibs. Datamodels provide abstractions around pieces of data in your application which actually represent instances of domain models. You define your own Datamodel class by stating the fields the model has, defining operations that can be performed on instances of that model and providing some configuration properties.

Use cases

The most common use cases for the Datamodels module are as follows:

  • Defining what your domain models look like, including their fields, validation logic and custom behaviour
  • Creating new instances of models using forms in the UI, validating them and saving them to a persistent store
  • Fetching a particular instance of a model from a persistent store, editing it using forms in the UI, validating it and saving it back to the persistent store
  • Fetching collections of model instances from a persistent store, using parameters for filtering, sorting, etcetera

This chapter explains how you define Datamodels, how you can use them once you have defined them and how you can extend the provided functionality by either subclassing, hooking into the behaviour at predefined points or by listening to emitted signals.

High-level overview

The Datamodels module is like an Object-Relational Mapper for your front-end applications. Datamodels have certain fields, each field being of a certain type which determines the allowed range of values. Fields are how you specify the structure of your data and how it should be validated.

Models can also be obtained from and persisted to a persistent store like an API. In this case the raw data fetched from the persistent store is deserialized into Datamodel instances and serialized back to the format accepted by the persistent store when saving those models. How models are validated is determined by model validators, and how they are (de)serialized by model serializers.

Defining Datamodels

You define the domain models in your application by creating subclasses of either the Datamodel class or of one of its subclasses. There are just two things that you are always required to define: the set of fields on the model and some model configuration properties.

If your domain models only exist 'locally' in your front-end application you can just subclass Datamodel directly. If however they also exist in some persistent store, like a datastore behind an API for example, then you will want to subclass one of its subclasses like APIModel. You can also create your own subclass, see section Subclassing Datamodel).

An example domain model definition is as follows:

import Datamodel from "lib/bitlibs/datamodels/Datamodel";
import * as fields from "lib/bitlibs/datamodels/fields";


class User extends Datamodel {
  // The model configuration properties
  static __config__ = {
    name: "user",
    primaryKey: "id"
  }

  // The fields on the model
  __fields__ = {
    id: new fields.NumberField({
      label: "ID",
      key: "id",
      type: "number",
      required: true,
      validateOn: "change",
      default: null
    }, this),
    name: new fields.StringField({
      label: "Name",
      key: "name",
      type: "text",
      required: true,
      maxLength: 50,
      validateOn: "change"
    }, this),
    salutation: new fields.MultipleChoiceField({
      label: "Salutation",
      key: "salutation",
      type: "multiplechoice",
      required: true,
      options: [
        {id: "sir", label: "Sir"},
        {id: "madam", label: "Madam"}
      ],
      freeValue: false,
      default: "sir",
      validateOn: "change"
    }, this),
    email: new fields.EmailField({
      label: "E-mail address",
      key: "email",
      type: "email",
      required: true,
      maxLength: 150,
      validateOn: "change"
    }, this),
  }
}

The different parts of this definition are explained in the following sections.

Configuration properties

The first step in defining a domain model is providing some configuration settings (metadata). The required settings differ per Datamodel subclass:

Datamodel

The Datamodel base class has only two configuration properties, both of which are required:

NameTypeDescription
nameStringThe uniquely identifiable name of the model.
primaryKeyStringThe name of the primary key field of the model. See section primary keys.

APIModel

The APIModel class has some additional configuration properties. In the table below, the ones with their name in bold are required:

NameTypeDescription
apiRootStringThe root of the RESTfull API endpoints for this model, relative to the base of the API. For example /users/. Requests to the API regarding this model are built relative to this root.
networkerNetworkerThe Networker instance to use to fetch instances of this model from and save instances to the API.
apiModelNameStringThe name of the model on the API. If responses from the API are enveloped in a property with the name of the model, this should be set to that name. If null or undefined, bitlibs will expect responses to not be enveloped.
createEndpointStringA custom create endpoint, if different from apiRoot/.
retrieveEndpointStringA custom retrieve endpoint, if different from apiRoot/<id>/.
updateEndpointStringA custom update endpoint, if different from apiRoot/<id>/.
deleteEndpointStringA custom delete endpoint, if different from apiRoot/<id>/.
readModelClassConstructorThe Datamodel class to use to deserialize responses to create calls to the API, if different from the class itself. Can be used to define a separate creation model from the read model.

An example APIModel subclass configuration using some of the optional attributes:

import APIModel from "lib/bitlibs/datamodels/APIModel";
import User from "src/models/User";
import networker from "src/network";


class RegistrationForm extends APIModel {
  // The model configuration properties
  static __config__ = {
    name: "registration_form",
    apiRoot: "/users/",
    networker: networker,
    createEndpoint: `/users/register/`,
    readModelClass: User,
    apiModelName: "user"
  }

  // The fields on the model
  __fields__ = {
    ...
  }
}

In this example we define a user registration form model which will perform a post request to the endpoint /users/register/ when saved, and deserializes the response enveloped under the key user using the User model.

Defining fields

The fields on your models are defined in the __fields__ array. There are various types of fields, as listed in the Fields section, each having their own set of configuration properties that define how the field should behave. This influences things like validation, (de)serialization and default field values. Field constructors take two arguments: the field configuration and the Datamodel instance on which the field is defined. For a complete reference of the available configuration properties see the Fields section.

Getters & setters

Fields are defined in a __fields__ property as not to pollute the namespace of the object with the field instances. When datamodels are initialized, bitlibs automatically creates getter and setter methods for all fields so you can still set and get their value using the field name: user.name returns the value for the "name" property on the user object and user.name = 'Jan' sets it.

Primary keys

There is one special field on models: the primary key field. You must define a field with the key defined as the primaryKey configuration attribute on your model.

Hierarchical models

Bitlibs also supports defining models in a hierarchical fashion to increase modularity and code reuse. You define models within other models as you define any other field by using the EmbeddedModel field type. For example:

  ...
  __fields__ = {
    address: new fields.EmbeddedModel(Address, {
      label: "Home address",
      key: "address",
      type: "embedded",
      many: false,
      required: true
    }, this)
  }
  ...

This creates a property called "address" on instances of this model, which is an instance of the Address datamodel class. In this case there is exactly one address (as denoted by the many configuration property), which should always exist (it is required).

You can create arbitrarily deep nestings of models using this strategy. If an embedded model is required its instance will always exist, otherwise it may be null if not present. When the many flag is set to true the value will be an array of instances of the embedded model's class. See the section about EmbeddedModel for more information.

Hooks

The Datamodel class provides specific hooks which you can utilise to implement additional behaviour at certain points. This is one of the ways in which the bitlibs library enables easy customisation of its functionality. Other ways are creating subclasses of the builtin classes and by listening to signals emitted by the various classes of the library.

perform_initialize

The perform_initialize hook is called directly after the model instance is fully initialised as far as the Datamodel class is concerned. It can be used to provide additional initialization logic to run. It receives one argument: the context with which the datamodel was initialised.

perform_validate

The perform_validate hook can be used to provide additional multi-field validation logic to run after all individual fields have been successfully validated. It is only executed in case the individual field validations did not yield any errors. It does not receive any arguments. When there are validation errors, this method should throw a dictionary keyed by field names with ValidationError instances as values.

perform_save

The perform_save hook is only available to subclasses of Datamodel and should implement the logic to actually save instances of that class in some way. It does not take any arguments. It is not used by APIModel: that class handles creating and updating instances using an APIPersistenceManager.

perform_delete

The perform_delete hook is also only available to subclasses of Datamodel and should implement actually deleting instances of that class. It does not take any arguments.

perform_serialize

The perform_serialize hook can be used to influence the serialization process of models. After all field values have been serialized into a JSON dictionary, the dictionary is passed to this hook for optional modification. This method is free to add, delete or modify this JSON dictionary in accordance with application requirements.

deserialize_preprocess

The deserialize_preprocess hook is handed the raw JSON dictionary to deserialize into a model instance along with the context in which the model is being deserialized. It can be used to modify this dictionary before using it to populate the fields of a model instance with it. Use cases for this are to remove, rename or add fields in the dictionary.

perform_deserialize

After a raw JSON dictionary of values has been deserialized into a datamodel instance, this hook is called to finish the deserialization process. It is passed the dictionary of context variables in which it was deserialized.

Custom attributes and methods

Besides the provided methods and hooks you are free to define any additional attributes and methods you require on your datamodel classes. Just make sure they do not clash with any field keys as this will not allow you to use the getters and setters for the fields that clash.

Using Datamodels

After you have defined your application's domain models you can start to benefit from working with instances of these models instead of with raw pieces of data. The following sections cover the most important use cases.

Creating new model instances

To create a new instance of a model class use the new static factory method. This method completely initializes the model instance, thereby also calling any custom initialization logic defined on the class. You can pass optional initial values and a context dictionary to the factory method. When creating a model, all required fields are initialised to their default values and all non-required fields are set to null, after which any provided initial values are set.

To define custom initialization logic implement the perform_initialize hook.

  let user = await User.new({name: "Jan", address: {city: "Eindhoven"}});

Obtaining model instances

To obtain model instances from a persistent store see section Retrieving models. There are various ways of obtaining a single model instance or multiple model instances:

  let jan = await User.get(1);
  let users = await User.fetch({"address.city": "Eindhoven"});
  let resultset = await User.query({"address.city": "Eindhoven"});

Model context

You can provide an arbitrary dictionary of values when creating new datamodel instances, which will be saved on the instance. This context is passed to the initialization methods of all fields defined on the model as well as the perform_initialize hook and can be used by your application to adjust the behaviour of the model depending on the context in which it is instantiated.

Modifying models

After having created - or otherwise having obtained - a model instance you can modify its field values using several different methods listed below. When modifying field values they are marked as dirty unless specified otherwise. Datamodels keep track of their dirty fields to be able to for example patch only the modified fields when saving a modified model back to an API.

Setters

The first method is by simply using the field setters of the model (see Getters & setters). You can set a field value like you would set any other attribute of an object. You can also set field values of embedded models by traversing the hierarchy of embedded models using the getters of the embedded models and the setter of the nested field to set. The field setters use the setValue method under the hood to set the field value. When using a field's setter, the field is always marked as dirty.

Examples:

user.name = "Henk";
user.address.city = "Eindhoven";

setValue

The second method is to use the setValue method directly. This method takes the name of the field to set, optionally including a dotted path through embedded models, together with the value to set for that field. It also takes a third optional argument indicating whether the field should be marked as dirty, which defaults to true.

// Change the name of the user without marking the field dirty
user.setValue("name", "Henk", false);

// Change the value of the field "city" on the embedded model "address"
user.setValue("address.city", "Eindhoven");

setValues

The third method is to set multiple field values at once using the setValues method. This takes a dictionary of field keys and values to set. When setting values on embedded models, use a nested dictionary under the key of the embedded model. It also takes a third optional argument indicating whether the fields should be marked as dirty, which defaults to true.

For example:

user.setValues({
  name: "Jan",
  address: {
    city: "Eindhoven"
  }
});

Modifying fields directly

You can also set the values of fields directly instead of via the model on which they are defined. Once you have obtained the field you want to set (see section Getting field objects), simply call its setValue method.

clearValues

To clear model values and reset all fields to their default value, use the clearValues method.

Validating models

To validate a model's field values, call the validate method. This will populate the errors attribute on the model itself and any of its fields which contain an error with instances of ValidationError. It returns a boolean indicating whether validation was successful (no errors occured).

The actual validation is performed by the model's declared ModelValidator. You are not required to call the validate method yourself: when saving models this method is always called by the Datamodel class before proceeding with the actual saving of the instance. After being validated, any errors can be inspected via the hasErrors and getErrors methods. Errors can be cleared by calling clearErrors.

You can also validate individual field instances by calling the validate method on those fields directly.

Custom validation logic

Custom save logic can be implemented using the perform_validate hook.

Saving models

To save model instances, either newly created ones or obtained modified ones, simly call the save method. This method does not take any arguments and uses the PersistenceManager defined on the model to persist it to some store. See section saving models for more information.

  let jan = await User.new({name: "Jan", address: {city: "Eindhoven"}});
  await user.save();  // Creates a new instance of this user in the persistent store

  let piet = await User.get(2);
  piet.address.city = "Rotterdam";
  await piet.save(); // Updates the model instance in the persistent store

Getting field objects

If you need to access the BaseField subclass instance for a particular field instead of the field's value, use the getField method. You can provide it with the name of the field to obtain, as well as with a dotted path through embedded models ending in the property name of the field on the inner model.

let nameField = user.getField("name");

let cityField = user.getField("address.city");

Model signals

Datamodel instances have a ModelSignaller instance which emits various signals you can listen to to hook into its functionality. You subscribe to a signal by calling the add method on the signaller's property for that signal. The signaller is defined on the signals attribute of models. You subscribe to signals of a specific datamodel instance, not to all signals of all instances of that model.

// Subscribe to the "fieldModified" signal of this user
user.signals.fieldModified.add((field, markDirty) => {
  // Do something with the modified field instance
});

The various signals declared on the ModelSignaller class are as follows:

fieldModified

The fieldModified signal is emitted whenever the value for one of the fields on a model changes. It receives one argument, namely the key of the field which was modified.

modified

The modified signal is emitted whenever any of the fields on a model change. This is emitted together with the fieldModified signal when a field value is modified directly, but also when clearing the fields on a model for example.

You subscribe to signals

Dynamically adding/removing fields

Sometimes you need to be able to dynamically add fields to or remove fields from a model "at runtime". You can do this using the addField and removeField methods. They both accept as only argument the field instance to add/remove, like you would also define it statically on the model. For example, to add an age field to a certain user instance:

let user = User.get(1);

user.addField(new NumberField({
  label: "Age",
  key: "age",
  type: "number",
  min: 18
}), user);

Working with Datamodels in forms

You can easily work with datamodel instances in forms in your UI. The main idea is to populate your form's inputs with the current field values of the model, validate the individual field instances when required (for example oninput or onblur) and display any validation errors in your HTML, and calling the save method on the model instance when the form is submitted.

For additional view layer integrations for the supported frameworks, see the forms module.

Fields

Fields contain the logic to validate and (de)serialize the attribute values of datamodels and to report any errors. You configure the behaviour of fields at define time. There are some configuration properties which are global to all field classes as well as some field-specific ones.

General configuration properties

The configuration properties that are valid for all field types are as follows. The ones with their name in bold are required, the others are optional.

NameTypeDescription
keyStringThe key of the field. This will be the name of the getter and setter methods on the datamodel on which this field is defined, and determines the key under which the fields value is serialized.
labelStringThe human-readable name of this field. Can be used to be displayed in the UI for example.
typeStringThe unique identifier of the type of field. Can be used by the UI to determine how to display the input for a certain field type, like is done by the forms module. You can assign an arbitrary type string to every field subclass, as long as you use them consistently.
requiredBoolean, FunctionWhether this field is required. Whether a field is empty/blank is determined by the field's isEmpty method. Can also be a function receiving a dictionary of current model values as argument
defaultanyThe default value of this field, if different from the field type's default
editableBooleanWhether this field value can be modified once intialised.
validateOn"change", "input"When to validate this field. Used by the forms module to validate the field either oninput or onchange.
widgetStringAny custom widget to use to represent this field in the UI, if different from the type configuration property
validateFunctionFunction which can implement additional custom validation logic for this field. Receives the field's value and a dictionary of all current model field values as arguments.
getFunctionA custom getter function for this field's value. Receives the field instance as argument.
setFunctionA custom setter function for this field's value. Receives the field instance and value to set as arguments.
serializeFunctionA custom serialize function for this field's value. Receives the field instance as argument.
shouldSerializeBooleanWhether this field should be included in serialized representations of the model on which the field is defined. Respected by the ModelSerializer class

Field types

In addition to the general configuration properties there are also some field-specific ones. The following field types are provided by bitlibs.

StringField

Represents generic textual values.

Properties:

NameTypeDescription
rowsNumberThe number of rows to use to represent this textual field in a UI
minLengthNumberThe minimum required length of the field's value
maxLengthNumberThe maximum allowed length of the field's value

NumberField

Represents generic numerical values.

Properties:

NameTypeDescription
noFloatBooleanWhether to only allow real integers, meaning no real numbers.
minNumberThe minimum allowed value
maxNumberThe maximum allowed value
stepsizeNumberThe stepsize to use in the UI in for example stepper controls for this field.

BooleanField

A value which can only either be true or false.

DateField

Represents a date.

The DateField class uses the momentjs library to parse dates.

Properties:

NameTypeDescription
formatStringThe format in which the date should be. For a list of supported formats see https://momentjs.com/docs/#/displaying/format/. If not specified, the default format is the locale default date format, as denoted by the format string "L".

TimeField

Represents a time.

The TimeField class uses the momentjs library to parse times.

Properties:

NameTypeDescription
formatStringThe format in which the time should be. For a list of supported formats see https://momentjs.com/docs/#/displaying/format/. If not specified, the default format is the locale default time format, as denoted by the format string "LT".

DateTimeField

Represents a date including a time.

The DateTimeField class uses the momentjs library to parse datetimes.

Properties:

NameTypeDescription
formatStringThe format in which the field value should be. For a list of supported

formats see https://momentjs.com/docs/#/displaying/format/. If not specified, the default format is YYYY-M-D H:mm.

EmailField

Represents email addresses.

URLField

Represents URLs.

PhoneField

Represents phone numbers.

PasswordField

Represents passwords.

ImageField

Represents base64 encoded images. Also accepts valid urls as value.

Properties:

NameTypeDescription
supportedTypesObject[]An array of supported file types. Each element in the array must include at least a mimetype attribute.

EnumField

A field having a bounded set of valid values.

Properties:

NameTypeDescription
valuesString[]An array of valid field values.

RegexField

A field whose value is validated using the provided regular expression.

Properties:

NameTypeDescription
regexStringThe regular expression to validate the field's value against.

JSONField

A field which can contain arbitrary JSON.

Optionally the field can be configured to validate its value against a given JSONSchema v6 schema.

Properties:

NameTypeDescription
schemaJSONIf provided, the JSONSchema v6 schema to validate values against.

ArrayField

A field containing an arbitrary number of homogeneous fields.

Can be used to represent multi-valued fields, where the user can specify any number of values for the field.

Properties:

NameTypeDescription
subtypeStringThe type of the field of which the ArrayField can contain an arbitrary number.
initialAmountNumberThe initial number of fields contained in this ArrayField.

An ArrayField is instantiated using an extra argument besides the field config and model instance: the class of the fields to contain. Its getValue and setValue methods also accept an extra argument: the index of the contained field for which to get/set the value. It defines two additional methods: addField and removeField, which can be used to add and remove fields to/from the array of contained fields.

MultipleChoiceField

A multiple choice field has a set of predefined values from which it can contain exactly one. It can also optionally allow other values besides those.

Properties:

NameTypeDescription
optionsObject[]An array of options, each option having id and label properties.
allowFreeValueBooleanWhether to also allow other values besides the predefined ones.

MultipleSelectField

A multiple select field has a set of predefined values from which it can contain an arbitrary number.

Properties:

NameTypeDescription
optionsObject[]An array of options, each option having id and label properties.

EmbeddedModel

An embedded model can be used to embed an instance of a certain model class as field on another model. This enables composition of models in a hierarchical manner for increased code modularity and reuse.

Properties:

NameTypeDescription
manyBooleanWhether the field contains an array of embedded models instead of a single one.

The constructor of EmbeddedModel accepts an additional argument besides the field config and model instance on which it is defined: The class of datamodel being embedded. It accepts either a single model instance or an array of them as valid values, depending on the many config property. If many is set to true, the value of the EmbeddedModel field is actually an instance of EmbeddedModelList.

RelatedModel

A related model is like and embedded model, but contains a single model ID or array of IDs instead of actual instances of models. RelatedModel fields can only be declared on APIModel subclasses.

Properties:

NameTypeDescription
manyStringWhether the field contains an array of related model IDs instead of a single one.

The constructor of RelatedModel accepts an additional argument besides the field config and model instance on which it is defined: The class of the related datamodel. If many is false, it accepts either a single model instance or model ID as value. The field value will then actually be an instance of RelatedModelInstance, which has a fetch method that can be used to get the actual instance of the related model with the corresponding ID.

If many is true, it accepts an array of either model instances or IDs as value. The field value will then actually be an instance of RelatedModelList, which contains some convenience methods amongst which a fetch method that fetches the actual instances of all models in the related model list.

Field values

Field values are stored on field instances in an internal format which differs per field type and are converted to/from their representation when setting and getting field values using the getValue and setValue methods. The internal format is also what is validated by the validate methods of fields. This allows fields to for example work with a momentjs date object instead of with the textual representation of a date and compare that to any minimum and maximum dates specified in the field's config.

Errors

Field errors are stored on the errors property, which should be accessed using the getErrors getter. They can also be set manually using the setErrors method. The errors property is an array of ValidationError objects, as fields can contain multiple errors. The getPrimaryError function returns the most significant error on the field at that time, which is the first ValidationError in the array.

Field signals

Field instances, like datamodels, emit various signals you can listen to to hook into their functionality. The signaller is defined on the signals attribute of fields. You subscribe to field signals in the same way you subscribe to Datamodel signals. The various signals are emitted using a FieldSignaller and are as follows:

modified

This signal is emitted whenever the field's value is changed using the setValue method. It receives the field instance being modified as argument as well as whether it was marked as dirty by this modification.

Subclassing BaseField

You can easily create your own, new field type by subclassing BaseField or one of the other provided fields. When doing so, the following methods are the most likely candidates for overriding:

MethodDescription
toRawThis is the method used to convert set values to their internal representation. The default implementation simply passes them through unmodified, but if you have a particular internal representation of your field's values you should override this behaviour.
toRepresentationThis is the inverse of toRaw and converts the field's internal format to the representation of the value to be used in the UI.
validateShould verify whether the current field value is valid and if not populate the errors array on the field instance. It does not return anything but receives all of the model's current field values as only argument. This method may also modify the field's value in order to "clean" it.
isFieldEmptyShould return whether the current field value is regarded as "empty".

Persistence Managers

A persistence manager persists datamodel instances to a certain persistent store and retrieves specified (collections of) objects from that store. Different types of persistent stores have different persistent managers for interfacing with them, for example the APIPersistenceManager class is used to persistent objects to and retrieve them from a RESTful API.

Every Datamodel subclass has the persistence manager it uses defined on the static attribute __manager__, which subclasses are free to override. For example, the Datamodel base class has an instance of the abstract base class PersistenceManager which does not actually implement any functionality. APIModel has an instance of an APIPersistenceManager which persists objects to and retreives them from a RESTful API.

Persistence managers use ModelSerializers to (de)serialize models to/from their store.

Retrieving models

You can retrieve instances of your datamodel classes in several ways: you can retrieve one specific model or a collection of models satisfying some criteria. These different methods are outlined below.

Getting models

When wanting to retrieve a single model by its primary key you use the static method get on your Datamodel subclass. It only takes the primary key value and an optional context dictionary as attributes. The context dictionary is passed to the constructor of the returned Datamodel instance and stored on the instance.

Fetching models

When wanting to fetch multiple models from a persistent store, use the fetch method. It takes a dictionary of parameters to pass to the persistent store to specify which objects to fetch as well as an optional context dictionary. This method directly returns the array of fetched objects.

Querying models

If you do not want to directly receive the array of fetched objects from the persistent store, use the query method. It works exactly the same as the fetch method but instead returns a ResultSet instance containing additional information about the returned set. Not all persistent stores might support this method.

ResultSet

A ResultSet contains additional information about the set of objects returned by the persistent store, which differs per store. In case of a RESTful API interfaced by an APIPersistenceManager it includes metadata about the total number of objects in the set if the result was paginated, for example.

Saving models

Saving models is done by simply calling the save method on a datamodel instance. This uses the persistence manager to either create a new object in the store or update an existing object with the same primary key as the model instance.

Custom save logic

Custom save logic can be implemented using the perform_save hook.

Deleting models

Deleting models is done by calling the delete method on a model instance. This marks the instance as deleted and also removes it from the persistent store. Note that after deleting a model instance its behaviour during continued use is undefined: you should discard the instance.

Custom delete logic

Custom delete logic can be implemented using the perform_delete hook.

Subclassing PersistenceManager

When implementing your own persistence manager to interface with a specific persistent store not already supported by bitlibs, these are the methods you need to implement:

get & perform_get

The get method is the method which gets called by the Datamodel class, and calls the perform_get method for the actual implementation. If you need to customize the way in which it calls perform_get, and/or when, you can opt to override this method directly.

If you only need to override the actual implementation of getting objects from the persistent store, you should implement perform_get. It takes the class of the model to get, the primary key value and an optional context dictionary as arguments. It should throw some error when the specified object could not be found.

fetch & perform_fetch

Like get, this method calls perform_fetch whilst also performing some other necessary steps. You can opt to override this method directly if you need.

The perform_fetch method should implement fetching certain objects according to some specified criteria. It takes the class of models to fetch, the criteria specification and an optional context dictionary as arguments.

query & perform_query

Like fetch & perform_fetch, but should return a ResultSet instance. You are not required to implement this method if your persistent store does not support it.

saveModel & perform_saveModel

Implement saving the model to the persistent store. Should both support saving new instances as well as updating/replacing existing ones based on their primary key.

deleteModel & perform_deleteModel

Should delete given model instances from the persistent store.

serializer

The serializer attribute on a persistence manager is a ModelSerializer instance to use to (de)serialize instances to/from the persistent store.

Model Validators

Model validators implement the logic of determining whether a Datamodel's current field values are valid. They also handle setting any validation errors on the model and its fields.

Validating models

Models are automatically validated using their ModelValidator (sub)class instance when they are saved by calling save. You can however also validate them at any moment you like by directly calling the validate method on either a model instance or one of its fields. The provided ModelValidator class validates models by first validating each individual field, after which it calls the model's perform_validate hook for any additional custom validation logic. The hook is only called if the individual field validation did not yield any errors.

Handling errors

If any validation errors occured, the errors property of the fields containing errors will be populated. You should access this using the getErrors and getPrimaryError methods. The hasErrors method on the datamodel will return false if there are any errors. Errors are instances of ValidationError.

Subclassing ModelValidator

There are only a couple of methods you need to override when providing your own model validator:

validateModel

This is the most important method. It receives the model to validate as only argument and should validate it in some way. Any errors that occured should be set on the model and its fields using setError. Your subclass may also choose to respect the perform_validate hook of Datamodel.

hasErrors

Should simply return whether the given datamodel is regarded invalid, meaning it has validation errors.

getErrors

Should return all validation errors, keyed by field name. Every value should be an array of ValidationError instances. Any errors not specific to any field should be keyed under the NON_FIELD_ERRORS key.

setErrors

Should set the given dictionary of errors on the provided model. The dictionary should be in the same format as returned by getErrors.

clearModelErrors

As the name suggests, should clear all errors on the model itself and all its fields.

Model Serializers

Model serializers implement serializing models to/from some representation of their values. Only their values are serialized, no other attributes. They are used by persistence managers to (de)serialize models to and from their representation as stored in the persistent store.

Serializing models

The provided ModelSerializer class implements serialization by calling the serialize method on every field that should be serialized. It respects a shouldSerialize configuration property on fields that can be used to exclude certain fields.

Deserializing models

Deserialization of models is done in a few steps:

  1. The deserialize_preprocess method on the Datamodel class to be deserialized into is called, passing it the values to be deserialized as well as any context dictionary which was used to fetch the model from its persistent store. This is the optional context parameter of the get, fetch and query methods of persistence managers.
  2. A new model instance of the specified class is initialized with the preprocessed values as initial values. The new method is also passed the context dictionary.
  3. The instantiated model instance's perform_deserialize hook is called for any custom deserialization logic to perform just after deserialization has otherwise completed.

Subclassing ModelSerializer

When subclassing ModelSerializer you only have to implement the serializeModel and deserialize methods. You can choose to call the hooks provided by datamodel in these functions but you can also introduce your own in combination with a custom Datamodel subclass for instance.

Signallers

ModelSignallers are used to emit signals about certain events pertaining datamodels. They are declared on Datamodel instances under the signaller attribute. The supported signals are the attributes on the ModelSignaller class.

Subscribing to signals

You can subscribe to signals by calling the add method on the Signal instance for that signal. See section Model signals.

Custom signallers

You can easily subclass ModelSignaller and provide your own additional signals if you want. You then have to define an instance of your custom class on your Datamodel classes.

Subclassing Datamodel

TODO

APIModel

TODO

← FormsDatasets →
  • Introduction
    • Use cases
    • High-level overview
  • Defining Datamodels
    • Configuration properties
    • Defining fields
    • Hierarchical models
    • Hooks
    • Custom attributes and methods
  • Using Datamodels
    • Creating new model instances
    • Obtaining model instances
    • Model context
    • Modifying models
    • Validating models
    • Saving models
    • Getting field objects
    • Model signals
    • Dynamically adding/removing fields
    • Working with Datamodels in forms
  • Fields
    • General configuration properties
    • Field types
    • EmbeddedModel
    • RelatedModel
    • Field values
    • Errors
    • Field signals
    • Subclassing BaseField
  • Persistence Managers
    • Retrieving models
    • Saving models
    • Deleting models
    • Subclassing PersistenceManager
    • serializer
  • Model Validators
    • Validating models
    • Handling errors
    • Subclassing ModelValidator
  • Model Serializers
    • Serializing models
    • Deserializing models
    • Subclassing ModelSerializer
  • Signallers
    • Subscribing to signals
    • Custom signallers
  • Subclassing Datamodel
    • APIModel
Bitlibs Documentation
Docs
IntroductionDatamodelsDatasetsFormsNetworkingContextLoadingPaginationNotificationsStorageUtils
Bitlibs
HomeHelpAboutUser showcase
More
Brinkmann.IT
Copyright © 2018 Brinkmann.IT