Model API

The Model API accesses resources programmatically using collection-like constructs.

In addition to the basic CRUD programming model, Zero Resource Model (ZRM) introduces a list-function, the LCRUD model. The list function allows for listing and filtering collection members using simple conditions.

Important classes to know

The involved class types are Member, Collection, and TypeCollection:

zero.resource.Member
A class that represents a member instance in a collection. It dynamically holds data based on an instances configured type and fields definitions. Each Member instance has a unique identifier and an updated field holding the timestamp it was last updated.
zero.resource.Collection
Implements the LCRUD interface that returns Member instances.
zero.resource.Type
A class that contains all the metadata about a model declaration. It is this class that is created from the JSON definitions in the /app/models directory. Also, public methods from Collection exist on Type as well and act as a delegate for the default, unfiltered collection of a given Type.
zero.resource.TypeCollection
Implements the LCRUD iterface specifically for the zero.resource.Collection class.

The following example shows some common interactions with the Model API in Groovy:

// retrieve the default collection for model 'persons'
def collection = TypeCollection.retrieve('persons')

// create a new member in the collection, create returns Member instance
def joe = collection.create([firstname: 'Joe'])

// update member in the collection, update returns Member instance
joe.firstname = 'Joseph'
joe = collection.update(joe)
assert 'Joseph' == joe.firstname

// delete member from collection
collection.delete(joe.id)

// retrieve member from collection
def id = joe.id
joe = collection.retrieve(id)

// list all collection results
def all_people = collection.list()

// using list() with conditions
def some_people = collection.list(firstname: 'Joseph')
people = collection.list(firstname__contains: 'se')
people = collection.list(firstname__endswith: 'ph')

// paged results from 11th to 20th items in the overall collection
def paged_people = collection[10..19].list()

// paged and filtered results
def filtered_paged = collection.filter(firstname__endswith: 'ph')[201..300].list()

// excluding results
def excluded = collection.exclude(ischild: true).list()

// get a map of members
def map = collection.in([1, 3, 5])

Basic LCRUD

For these examples, the following simple resource model is defined in the /app/models/persons.json file:

{
    "fields" : {
        "firstname": {"type": "string", "max_length": 30},
        "birthdate": {"type": "date"},
        "ischild": {"type": "boolean"}
    },

    "collections" : {
        "children": {"memberFilters": {"ischild": true}}
    }
}

For this example, a collection of persons is instantiated:

import zero.resource.TypeCollection
...
Type persons = TypeCollection.retrieve('persons')

List all members

A List of all collection Member instances can be retrieved from the database with the list() method:

List<Member> everybody = persons.list()
everybody.each { person ->
    println person.firstname
}

List a subset of members

A List of the subset of Member objects can be retrieved by passing a filtering condition to the list method as an argument (see Filter Conditions section below for further details):

List<Member> somePersons = persons.list(firstname__startswith: 'Jo')
somePersons.each { person ->
    println person.birthdate
}

Create a new member

A new Member instance can be created and saved into the backing database with the create(Member) method:

Member member = new Member(firstname:'Joe', birthdate:'1978-01-21', ischild: true)
Member person = persons.create(member)

A shortcut is provided to simply send in a Map:

Member person = persons.create(firstname:'Joe', birthdate:'1978-01-21', ischild: true)

Retrieve a member

An individual collection member can be retrieved from the database with the retrieve(Object) method:

Member person = persons.retrieve(1)

Update a member

A collection member can be updated in the database with the update(Map) method:

Member member = new Member(id: '1', firstname:'Bob', birth_date:'1961-12-05')
Member result = persons.update(member)

Similar to the overloaded create() methods, update() has a convenience method for Maps:

Member result = persons.update(id: '1', firstname:'Bob', birth_date:'1961-12-05')

Delete a member

A collection member can be deleted with the delete(Object) method:

boolean result = persons.delete(1)

The delete() method returns true if the delete operation was successful, otherwise it will return false.

Filtering and excluding members

A filter or exclusion can be applied to a collection to create a new collection that is a subset of the original. Each filtered collection is a distinct collection instance that can be stored, used, and reused independent of its parent. The filter conditions can be passed in dynamically, which eliminates the need for writing a different service for each subset of the data that the client might need. The filters can be chained, therefore each filter creates a new logical subset-collection. However, data is not retrieved from the database until a collection member is actually accessed.

filter()

The filter() method is used to create a new Collection instance that only includes Members that meet the provided conditions.

def somePersons = persons.filter(firstname__startswith: 'Jo')
def fewerPersons = somePersons.filter(firstname__endswith: 'e')
....or....
def fewerPersons = persons.filter(firstname__startswith: 'Jo').filter(firstname__endswith: 'e')

exclude()

The exclude() method is the opposite of the filter() method. Whereas filter() includes Members that meet the provided conditions, exclude() excludes Members that meet the conditions.

def birthdate_not_christmas = persons.exclude(birthdate__month: 12, birthdate__day: 25)

The exclude() and filter() methods return a new instance of Collection and can therefore be chained without affecting the original Collection. These methods can be used together. For instance, the following code snippet:

def filtered_and_excluded = persons.filter(ischild: true).exclude(firstname__in: ['Phil', 'Brian'])

Filter conditions

The filter conditions are specified using the following convention: [field name][delimiter][operator]. This convention makes the Model API and HTTP API symmetric (this convention makes it easy to pass filters as URI parameters). The supported filters are:

Table 1 - Maps the type of comparison predicate to the operand and operators
Comparison Operand Operator Example
Equal to any field type equals firstname__equals: 'Joe'
Equal to (shortcut) any field   firstname: 'Joe'
Greater than any field gt firstname__gt: 'Joe'
Greater than or equal any field gte firstname__gte: 'Joe'
Less than any field lt firstname__lt: 'Joe'
Less than or equal any field lte firstname__lte: 'Joe'
Ends with string endswith firstname__endswith: 'oe'
Starts with string startswith firstname__startswith: 'Jo'
Contains string contains firstname__contains: 'Jo'
In any field in firstname__in: ['Joe', 'Bob', 'Dan']
Year equal to date, date-time year birthdate__year: '2005'
Month equal to date, date-time month birthdate__month: '12'
Day equal to date date-time day birthdate__day: '5'
After date, time date-time after birthdate__after: '2005-12-12'
Before date, time date-time before birthdate__before: '2005-12-12'
Between date, time date-time between birthdate__between: ['2005-12-12', '2006-12-12']

More than one filter condition can be combined with logical AND using a comma as a separator between the filters:

def somePersons = persons.filter(firstname__startswith: 'Jo', birthdate__before: '1990-12-12')

Paging collections

Often it is impractical and inefficient to return the full results of a collection, the Model API supports a paging scheme you can use to indicate what slice of data a list() or filter() call should return.

Paging collections uses a zero-based index. For example, the first member in a collection is indexed at 0 and the 10th at 9. Ordering of members in a collection is transient and not fixed when filters are applied.

Paging using ranges

You can specify a range of the data using the following Groovy syntax:

def paged_people = collection[10..19].list()

Paging ranges can also be combined with filters:

def filtered_paged = collection.filter(firstname__endswith: 'ph')[200..299].list()

Paging using start and count

Some clients might need to specify the start index and the page size, or count, rather than an explicit range of data. To do this, use the limit(start,count) method on a collection as shown in the following example:

def filtered_paged = collection.filter(firstname__endswith: 'ph').limit(201,100).list()

in()

The in() method takes a list of identifiers and returns a map where the key is the id of the Member and the value is the Member itself. This is useful when you have a small set of Members to operate on:

def subset = collection.in([30, 10, 32021])
def member30 = subset[30]
def member32021 = subset[32021]

orderBy()

The orderBy() method returns a new Collection instance with the orderBy attribute of the new Collection set to the parameter of the method. The method takes a list of strings and is provided to be used in the method chaining abilities of filter() and exclude(), and paging:

// SELECT * FROM persons ORDER BY birthdate DESC
def youngest_to_oldest = collection.orderBy(['-birthdate']).list()
// SELECT * FROM persons WHERE firstname LIKE '%don' ORDER BY updated
def filtered = collection.exclude(firstname__endswith: 'don').orderBy(['updated']).list()

latest()

The latest() method returns the most recently updated Member of the Collection instance. This method can be used with filter() and exclude() methods in order to refine the Collection first:

def latest = collection.latest()
def filtered_latest = collection.filter(birthdate__year: '1978').latest()

The latest() method is simply a shortcut for setting the orderBy attribute of the Collection instance, limiting the fetched size to 1 and returning the first result.

iterator()

Collection implements Java's Iterable interface and returns an Iterator of its Members:

def persons = TypeCollection.retrieve('persons')
persons.each { member ->
   println member.firstname
}

size()

The size() method will return the total matched size. If the Collection is unfiltered, the size is the total number of Members in the Collection. Calling this method has the side affect of evaluating the Collection if it has not yet been. The size is cached so subsequent calls to this method do not require accessing the database again:

def persons = TypeCollection.retrieve('persons')
def numberOfPeople = persons.size()

def numberOfSomePeople = persons.filter([first_name__startswith: 'B']).size()

Remember, size() is a method on Collection and can be chained with any method that returns a Collection as shown above with filter().

Named collections

Filters can be specified in the model declaration by defining a named collection:

{
    "fields" : { . . . },

    "collections" : {
        "children": {"memberFilters": {"ischild": true}}
    }
}

In Groovy, a named collection is accessed in the following way:

def persons = TypeCollection.retrieve('persons')
def children = persons.children.list()
def childrenNamedBob = persons.children.[0..4].list(firstname: 'Bob')

Data Validation

Zero Resource Models provides a robust data validation API. A call to Member.validate() returns a data structure that contains error information. A data structure is useful because you can decide how to represent this information back to the HTTP response:

def member = new Member('person', [firstname: 'Joe', birthdate:'1978-01-21'])
def messages = member.validate()

if(messages) {
  // do something useful with messages
} else {
  TypeCollection.retrieve('person').create(member)
}

The data structure is simply a map of lists (i.e. List<Map<String,String>>. The map prevents more than one field from having validation errors. The map's key is the field name. The value is a list of error messages. The list as values in the map prevents each field from being in violation of more than one validation rule.

The validate() method is also called in the create() or update() methods if the data is not yet validated. If a validation error occurs, the zero.resource.exceptions.ValidationException is thrown:

try {
    persons.create(firstname:'Joe', birthdate:'1978-01-21')
} catch (ValidationException e) {
    def messages = e.messages
    if (messages) {
      // do something useful with messages
    }
}

Custom Resource Event Handlers

The Model API can be easily used in resource event handlers for RESTful resource access. If the black box ZRM REST enablement doesn't suit your needs, you can use the API and implement your own RESTful resource handling while still leveraging the simplicity ZRM offers.

The following Groovy example shows a file in the /app/resources/my_json_person.groovy file illustrates how to utilize the API in order to customize you resource handling needs:

import zero.resource.TypeCollection

def onList() {

    def collection = TypeCollection.retrieve('person')

    request.status = 200

    // assign the Collection
    request.json.output = collection
    request.view = 'JSON'
    render()
}

def onCreate() {

    def collection = TypeCollection.retrieve('person')
    def json = zero.json.Json.decode(request.input[])

    def data = collection.type.fromJson(json)
    data = collection.create(data)

    locationUri = getRequestedUri(false) + '/' + data.id
    request.headers.out.Location = locationUri
    request.status = 201

    request.json.output = data
    request.view = 'JSON'
    render()
}

def onRetrieve() {

    def memberId = request.params.my_json_personId[]

    def collection = TypeCollection.retrieve('person')
    def data = collection.retrieve(id: memberId )

    request.status = 200

    request.json.output = data
    request.view = 'JSON'
    render()
}

def onUpdate() {

     def memberId = request.params.my_json_personId[]

     def collection = TypeCollection.retrieve('person')
     def json = zero.json.Json.decode(request.input[])

     def data = collection.type.fromJson(json)
     data.id = memberId
     data = collection.update(data)

     request.status = 204

 }

def onDelete() {

     def memberId = request.params.my_json_personId[]
     def collection = TypeCollection.retrieve('person')

     collection.delete(memberId)
     request.status = 204

 }

Overriding ZRM.delegate()

It is possible to delegate into the ZRM HTTP default implementation while also providing functionality. In order to do this, first create your own Resource Event Handler as shown in the previous example. Instead of completely overriding all behavior, simply call ZRM.delegate() in each method. You can provide your own custom logic before and after the ZRM.delegate() method call.

Further, you can obtain a reference to relevant objects after making the call into the ZRM black box implementation. Collection and Member objects are made available in the /event/resource zone upon completion of the ZRM.delegate() call. Consider the following example:

import zero.resource.TypeCollection

def onList() {
    ZRM.delegate()
    def collection = event.resource.collection[]
}

def onCreate() {
    ZRM.delegate()
    def collection = event.resource.collection[]
    def createdMember = event.resource.member[]
}

def onRetrieve() {
    ZRM.delegate()
    def collection = event.resource.collection[]
    def retrievedMember = event.resource.member[]
}

def onUpdate() {
    ZRM.delegate()
    def collection = event.resource.collection[]
    def updatedMember = event.resource.member[]
 }

def onDelete() {
    ZRM.delegate()
    def collection = event.resource.collection[]
 }

It is important to note that once the ZRM.delegate() method is called, all HTTP response processing has been completed. Thus, the HTTP method has been set and any response body content has been set. Logic done before the ZRM.delegate() call can affect the response, however.

Using the JSON Renderer

The example in the previous section illustrates how to use the TypeCollection, Type, Collection, and Member classes in your own resource event handler. Collection, Type, and Member classes can be assigned as JSON output and handled by custom converters provided by the zero.resource component. In fact, the black box HTTP API leverages the JSON rendering framework as well.

Version 1.1.0.0.21442