Advanced Data Preparation With Tasypie Resources

O

ne of the major components of a tastypie resource is the data preparation cycle, or hydration cycle. The hydration cycle is the aspect of a resource that is responsible for massaging raw user input into data objects suitable for saving to a data source, and vice versa.

The hydration cycle also encompasses the serialization machinery of the resource which is responsible for converting data object into standard data formats ( JSON, xml, etc ) and vice versa.

Serializtion Cycle

Each Tastypie resource has a Serializer instance which it uses internally to convert data between well formatted string and javascript objects. This behavior is defined in the serialize and deserialize resource methods A serializer class defines how
data is converted to and from a string. This is done via the to_ and from_ methods for each of the formats. For example, the default serializer defines the following methods:

  • to_json - Converts an object to a JSON string
  • from_json - Converts a JSON string to a javascript object
  • to_xml - Converts a javascript object to an XML string
  • from_xml - Converts an XML String to a javascript object

Serialization

Serialization is the act of converting a javascript object into a standard format string - JSON string, XML String, YAML String, etc. In the simplest case this is as simple as calling JSON.stringify. To add support for serialization of a new format, you must define the to_ method on a serializer class.

var tastypie = require('tastypie')
  , Class    = tastypie.Class
  , Serializer = tastypie.Serializer
  , JSONSerializer
  ;

JSONSerializer = new Class({

	inherits: Serializer
	
	,options:{
		content_types :{
			'application/json':'json'
		}
	}
	
	,to_json: function( data, options, callback ){
		callback( null, JSON.stringify( data ) );
	}

});

Deserialization

Deserialization is the act of converting a standard format string ( usually from user input ) into a javascript object. To add support for deserialization of a new format, you must define the from_ method on a serializer class.

const  tastypie = require('tastypie')
const Class    = tastypie.Class
const Serializer = tastypie.Serializer
const JSONSerializer

JSONSerializer = new Class({

  inherits: Serializer
, options: {
  content_types: {
    'application/json':'json'
  }
}

, from_json: function(data, callback) {
    callback null, JSON.parse(data));
  }

});

Hydration Cycle

The hydration cycle is the component of a resource that enforces the data contract defined by the resource and it's fields  fields. More specifically, hydration is the act of converting raw user input into a consistent data structure that is suitable to be handed off to application code. Deyhdration is the act of taking data returned from the application code and data layers and converting into a data structure suitable for serialization.

Hydration

Hydration Takes user input from a client request and re-shapes / re-maps it into a an object suitable for further processing. Commonly, this may be an ORM model for saving data to a data store or for using in additional application code. The key aspect of the hydration cycle centers around the fields defined on the resource.

The attribute property on each of the field defines how the resource will reshape the incoming user input. The attribute can be a dot ( . ) separated name path to relocate each of the values from the input. If no attribute is defined, the name of field is used.

It is important to note that a resource will ignore any data found in the request payload. It is only concerned with matching field names.

const {Resource} = require('tastypie')
 
const MyResource = Resource.extend({
  options: {
    // resource specific defaults
  }	
	
, fields: {
    shallow : { type:'int' }
  , nested  : { type:'char', attribute:'a.b.c' }
  }
})

The resource above defines two fields, one with an attribute and one with out - given a request payload as such:

{
  "shallow": 10
, "nested":"hello world"
, "fake": true
}

The result of hydration will result in an object with the following structure:

{
  "shallow": 10,
  , "a":{
  , "b":{
      "c":"hello world"	
    }
  }
}

By defining an attribute on the nested field of a.b.c, the resource created an object structure to match the name path and set the value from the user input at the final property c, and ignored the non matching field value of fake.

Per Field Hydration

Situations arise when a simple field attribute may not be enough to express the mapping of a data structure between user input and the internal data structure you want to work with. For this, the hydration process exposes method hooks for each field. By defining a resource method prefixed with hydrate and the name of the field, you have to ability to return any value you wish as the data is being reshaped. The method is passed the entire request Bundle giving you the ability to directly change the internal data structure

const {Resource} = require('tastypie')

const MyResource = Resource.extend({
  options: {
    // resource specific defaults
  }	
	
, fields: {
    shallow : { type:'int' }
  , nested  : { type:'char', attribute:'a.b.c' }
  , fake    : { type:'bool' }
  }

, hydrate_fake: function(bundle){
    if (bundle.data.shallow > 10) {
      bundle.data.fake = true;
    } else {
      bundle.data.fake = false
    }
    return bundle;
  }
})

Here the value of the fake field is dependent on the value of a different field. This soft of logic simply isn't possible with a name path style attribute, but is the same every time and a field hydration function is a good fit. At the time of writing, the per field hydration and dehydration functions are synchronous and must return a Bundle object.

Dehydration

Dehydration is the inverse of hydration, taking a javascript object representing some internal data and reshaping it for serialization for delivery to the client by casting un-serializable object, removing circular references and some general type casting.

Using the same resource as above:

const MyResource = Resource.extend({
  options: {
    // resource specific defaults
  }	
, fields: {
    shallow : { type:'int' }
  , nested  : { type:'char', attribute:'a.b.c' }
  }
})

We could pass a nested object through the dehydrate methods:

{
  "shallow": 10
 , "a":{
    "b":{
      "c":"goodbye world"	
    }
  }
}

which would yield an object ready for serialization which looks like such:

{
  "shallow": 10
, "nested":"goodbye world"
}
Per Field Dehydration

As with the hydration process, you are able to define a dehydrate method for each field to control values returned for specific fields during the dehydration process prior to data being returned to the client. The method should have a dehydrate_ prefix followed by the name of the field you want to manage.

Unlike the hydration methods, the dehydration methods are passed the individual object being dehydrated, the Bundle object for the current request, and the the final dehydrated object that is being populated. These methods must return an individual value that is intended to populate the final field value

const {Resource} = require('tastypie')

const MyResource = Resource.extend({
  options:{
    // resource specific defaults
  }	

, fields: {
    shallow : { type:'int' }
  , nested  : { type:'char', attribute:'a.b.c' }
  , fake    : { type:'bool' }
  }

, dehydrate_shallow: function(item, bundle, dehydrated) {
    var value = 0;
    value = item.fake
      ? item.shallow + 10
      : item.shallow - 10
      
    return value
  }
})

Application development quickly becomes a string of bad decisions. Bad names, bad schema, or bad business decisions all muddy the waters. The tastypie Resource serves as a consistency layer between your data sources and the client. The hydration cycle provides a flexible way to customize how data is translated.

tastypie REST node