Source: lib/resource/index.js

/*jshint laxcomma: true, smarttabs: true, node:true, esnext:true */
'use strict';
/**
 * Provides the base resource for defining expressive APIs, handling serialization, deserialization, validation,
 * caching, throttling and error handling
 * @module tastypie/lib/resource
 * @author Eric Satterwhite
 * @requires class
 * @requires class/options
 * @since 0.1.0
 * @requires domain
 * @requires events
 * @requires util
 * @requires async
 * @requires url-join
 * @requires joi
 * @requires boom
 * @requires querystring
 * @requires mout/lang
 * @requires mout/string/interpolate
 * @requires debug
 * @requires tastypie/lib/class
 * @requires tastypie/lib/class/parent
 * @requires tastypie/lib/class/options
 * @requires tastypie/lib/http
 * @requires tastypie/lib/mime
 * @requires tastypie/lib/utility
 * @requires tastypie/lib/paginator
 * @requires tastypie/lib/exceptions
 * @requires tastypie/lib/cache
 * @requires tastypie/lib/throttle
 * @requires tastypie/lib/fields
 * @requires tastypie/lib/resource/detail
 * @requires tastypie/lib/resource/list
 * @requires tastypie/lib/resource/schema
 * @requires tastypie/lib/resource/validator
 **/
var domain              = require( 'domain' )                       // domain
  , events              = require( 'events' )                       // events
  , util                = require( 'util' )                         // util
  , async               = require( 'async' )
  , urljoin             = require( 'url-join' )
  , joi                 = require( 'joi' )
  , Boom                = require( 'boom' )
  , qs                  = require( 'querystring' )                  // qs module from npm
  , {get, set, merge}   = require( 'mout/object' )
  , {clone, isFunction} = require( 'mout/lang' )                    // mout clone function
  , interpolate         = require( 'mout/string/interpolate' )      // string template interpolation interpolate
  , debug               = require( 'debug' )( 'tastypie:resource' ) // debug
  , Class               = require( '../class' )                     // Class
  , Parentize           = require( '../class/parent' )
  , Options             = require( '../class/options' )             // Options
  , http                = require( '../http' )                      // http
  , mime                = require( '../mime' )                      // mime
  , paginator           = require( '../paginator' )                 // paginator
  , exceptions          = require( '../exceptions' )                // exceptions
  , Serializer          = require( '../serializer' )                // serializer
  , {annotate,attempt}  = require( '../utility' )
  , Cache               = require( '../cache' )
  , Throttle            = require( '../throttle')
  , fields              = require( '../fields' )
  , Schema              = require( './schema' )
  , Detail              = require( './detail' )
  , List                = require( './list' )
  , validators          = require( './validator' )
  , optionsSchema
  , filterschema
  , Resource
  , checks
  ;

const PATCH = 'patch';
const EMPTY_OBJECT     = {};
const mutableMethods   = ['put','post','delete', 'patch'];

filterschema = joi.alternatives().try(
    joi.any()
    , joi.array().items( joi.string() )
);

optionsSchema = joi.object().keys({
    name          : joi.string().allow(null)
  , apiname       : joi.string().allow(null)
  , includeUri    : joi.boolean()
  , pk            : joi.string()
  , limit         : joi.number()
  , returnData    : joi.boolean()
  , defaultFormat : joi.string()
  , collection    : joi.string()
  , objectTpl     : joi.any().forbidden().description("objectTpl is now simply template")
  , template      : joi.func().allow(null)
  , filtering     : joi.object().pattern(/.+/, filterschema ).allow( null )
  , ordering      : joi.array().items( joi.string() ).allow( null )
  , allowed       : joi.object()
                        .pattern(
                            /.+/
                            , joi.object({
                                'get'     :joi.boolean()
                              , 'put'     :joi.boolean()
                              , 'post'    :joi.boolean()
                              , 'delete'  :joi.boolean()
                              , 'options' :joi.boolean()
                              , 'head'    :joi.boolean()
                              , 'patch'   :joi.boolean()
                              , 'connect' :joi.boolean()
                              , 'trace' :joi.boolean()
                            }).optionalKeys('get', 'put','post', 'delete', 'options','head', 'patch', 'connect' ) )
}).unknown();

function to_json(){
    return ( this.data || EMPTY_OBJECT );
}

checks = {
    dispatch: function( bundle ){
        var httpmethod = ( bundle.req.headers['x-method-override'] || bundle.req.method ).toLowerCase();
        var actions    = this.options.allowed[ bundle.action ] || EMPTY_OBJECT;

        if( !actions[ httpmethod ] ){
            return [ new Boom.methodNotAllowed(util.format( "method not allowed - %s", httpmethod )) ];
        }

        return [null, util.format( '%s_%s', httpmethod, bundle.action ) ];
    },

    verb: function( bundle ){
        return ( bundle.req.headers['x-method-override'] || bundle.req.method ).toLowerCase();
    },

    access: function( bundle ){
        var httpmethod = ( bundle.req.headers['x-method-override'] || bundle.req.method ).toLowerCase();
        var action = bundle.action;
        var method_name = util.format('%s_%s', httpmethod, action );

        if( !this[ method_name ] ){

            return [ Boom.notImplemented( util.format( "%s is not implemented: %s ", httpmethod, bundle.req.path) ), null ];
        }

        return [null, this[ method_name ] ];
    }
};

function async_field_dehydrate(err, data, scope, field, method, ret, bundle, obj, cb){
	let _ret = ret;
	_ret[ field ] = data;

	if( method ){
		_ret[field] = method.call(scope, obj,  bundle, _ret );
	}
	
	return cb && cb( err );
}

/**
 * An easy way to pass around request, and reply objects
 * @alias Bundle
 * @typedef {Object} module:tastypie/lib/resource~Bundle
 * @property {Object} req A Hapijs request object
 * @property {Function} res A hapi reply function
 * @property {Object} data A data object representing an entity for serialization / deserialization
 * @property {Object} [object] A fully populated data entity. Mostly used internally
 **/

/**
 * An easy way to pass around request, and reply objects
 * @typedef {Object} module:tastypie/lib/resource~Nodeback as Nodeback
 * @property {Error} [err]
 * @property {Object|Object[]} [data] Data returned from an asyn operation
 **/


 /**
  * An easy way to pass around request, and reply objects
  * @typedef {Error} RequestError
  * @property {Request} req
  * @property {Reply} res
  **/


/**
 * The base resource implementation providing hooks for extension
 * @constructor
 * @tutorial resources
 * @alias module:tastypie/lib/resource
 * @mixes module:tastypie/lib/resource/schema
 * @mixes module:tastypie/lib/class/parent
 * @mixes module:tastypie/lib/resource/list
 * @mixes module:tastypie/lib/resource/detail
 * @mixes module:tastypie/lib/class/options
 * @borrows module:tastypie/lib/resource/schema#build_schema

 * @borrows module:tastypie/lib/resource/detail#dispatch_detail
 * @borrows module:tastypie/lib/resource/detail#get_detail
 * @borrows module:tastypie/lib/resource/detail#get_object
 * @borrows module:tastypie/lib/resource/detail#put_detail
 * @borrows module:tastypie/lib/resource/detail#replace_object
 * @borrows module:tastypie/lib/resource/detail#update_object

 * @borrows module:tastypie/lib/resource/list#create_object
 * @borrows module:tastypie/lib/resource/list#get_list
 * @borrows module:tastypie/lib/resource/list#get_objects
 * @borrows module:tastypie/lib/resource/list#post_list

 * @param {Object} [options] Options data configuration
 * @param {?String} [options.name=null] The primary mount path for the resource /<name>, /<name>/{pk}. This will be set by the Api instance if not set
 * @param {String} options.pk=id The field name to use as the unique identifier for each entity
 * @param {Boolean} options.includeUri=true If set to true, a field named uri will be added to the resource instance
 * @param {string} [options.callbackKey=callback] callback key to be used for jsonp responsed
 * @param {string} [options.defaultFormat='application/json'] the default serialziion format if one is not specified
 * @param {module:tastypie/lib/serializer} [options.serializer=serializer] an instance of a serializer to be used to translate data objects
 * @param {Object[]} [options.routes=null] An array of route definitions add to the resources default crud routes
 * @param {String} [options.collection=data] the name of the key to be used lists of objects on list endpoints
 * @param {String} [options.labelField=data] A field to be included in minimal responses
 * @param {Number} [options.limit=25] The maximum number of results to return per page for the list endpoint
 * @param {module:tastypie/lib/paginator} [options.paginator=paginator] a Paginator Class used to page large list
 * @param {Object} [options.cache] A cache instance to be used for response caching.
 * @param {Object} [options.cache.engine='catbox-noop'] The underlying engine type to use. This should be a requireable catbox module that is installed
 * @param {Object} [options.allowed] an object which maps HTTP method names to a boolean {get:true} This is used as a default for custom actions if non is defined
 * @param {Object} [options.allowed.methods] an object which maps HTTP method names to a boolean {get:true} This is used as a default for custom actions if non is defined
 * @param {Object} [options.allowed.list] Defines methods access for listing methods. Defaults to the definitions found in the methods object if not defined
 * @param {Object} [options.allowed.detail] Defines methods access for detail methods. Defaults to the definitions found in the methods object if not defined
 * @param {Boolean} [options.returnData=true] return data after put, post requests.
 * @param {?Object} [options.filtering=null] And object defining an array of allowable filter types for each field
 * @example vaar Resource = require('tastypie').Resource
var instnace = new Resource({
    limit:10,
    pk:'user_id',
    defaultFormat:'text/xml',
    collection:'users'
    allowed:{
        list:{
            get:true,
            put:false,
            post:false
            delete:false
        },
        detail:{
            get:true,
            put:true,
            post:false
        }
    }
})
 */
Resource = new Class({
    mixin: [events.EventEmitter, Options, Parentize, Schema, List, Detail ]
  , options: {
        name: null
      , apiname: null
      , includeUri: true
      , pk: 'id'
      , limit: 25
      , returnData: true
      , defaultFormat: 'application/json'
      , serializer: new Serializer()
      , cache: {
            engine: 'catbox-noop'
      }
      , throttle: new Throttle()
      , collection: 'data'
      , labelField:'id'
      , paginator: paginator.local
      , template: function(){}
      , filtering: null
      , ordering: null
      , allowed:{

            methods:{
                get: true
              , put: true
              , post: true
              , "delete": true
              , patch: true
              , head: true
              , options: true
            }

            , schema:{
                get: true
            }
      }
    }

    , constructor: function( options ){
        var _fields, pkattr, allowed, isValid;
        events.EventEmitter.call( this );
        this.setOptions( options );

        isValid        = optionsSchema.validate( this.options );
        allowed        = this.options.allowed;
        pkattr         = this.options.pk;
        this._uricache = null;
        this.domain    = domain.create();
        this.modified  = undefined;

        isValid.error && this.emit('error', isValid.error );
        this.domain.on( 'error', (err) => this.exception(err) );


        if( !allowed.list ){
            this.options.allowed.list = clone( allowed.methods );
        }

        if( !allowed.detail ){
            this.options.allowed.detail = clone( allowed.methods );
        }

        allowed = undefined;
        this.cache = this.cache || new Cache( this.options.cache );
        // field inheritance
        _fields    = merge( clone( get(this.constructor, 'parent.fields' ) || EMPTY_OBJECT ), this.fields );
        _fields.id = _fields.id ? _fields.id : {type:'field', attribute:pkattr, readonly:true, help:'A unique identifier for a resource instance'};

        Object
            .keys( _fields )
            .forEach(( key ) => {
                var fieldopt = _fields[key];
                if( fieldopt.type && fields[ fieldopt.type ] ){
                    fieldopt.attribute = fieldopt.hasOwnProperty('attribute') ? fieldopt.attribute : key;
                    _fields[key] = new fields[fieldopt.type]( fieldopt );
                } else{
                    fieldopt.options.attribute = fieldopt.options.hasOwnProperty('attribute') ? fieldopt.options.attribute : key;
                    _fields[ key ] = fieldopt;
                }
                _fields[key].options.name = key;
                _fields[key].augment( this, key );

                Object.defineProperty( this, key, {

                    get: function(){
                        return _fields[ key ];
                    }
                });
            });

        this.fields = _fields;

        if( this.options.includeUri ){

            this.fields.uri = this.fields.uri || new fields.CharField({readonly:true, help:'A direict URI for this resource instance'});
        }
    }

    /**
     * Defines the base urls for this resource
     * @method module:tastypie/lib/resource#base_urls
     * @return Array. And array of objects containing a route, name and handler propperty
     * @example
{
    base_urls: fuinction(){
        return [{
            path:decodeURIComponent('/api/v1/test/{action}'),
            name:"test",
            handler: this.dispatch_test.bind( this )
        }];
    }
}
     **/
    , base_urls: function base_urls( ){
        return [{
            name: 'schema'
          , path: interpolate( urljoin(  '{{apiname}}', '{{name}}', 'schema' ), this.options ).replace( /\/\//g, "/" )
          , handler: (req, reply) => this.get_schema( req, reply )
          , config:{ tags:['api'] }
        }
        , {
            name: 'detail'
          , path: interpolate( urljoin(  '{{apiname}}', '{{name}}', '{pk}' ), this.options ).replace( /\/\//g, "/" )
          , handler: (req, reply) => this.dispatch_detail( req, reply )
          , config:{ tags:['api'] }
        }
        , {
            name: 'list'
          , path: interpolate( urljoin(  '{{apiname}}', '{{name}}' ), this.options ).replace( /\/\//g, "/" )
          , handler: (req, reply) => this.dispatch_list( req, reply )
          , config:{
                tags:['api']
                , validate:{ query: validators.query }
            }
        }];
    }

    /**
     * creates an object to be used to filter data before it is returns.
     * **NOTE** this needs to be implemented to suit the the data source backend
     * @method module:tastypie/lib/resource#buildFilters
     * @param {Object} fiters An object of requested filters
     * @return {Object} filters An object of filter definitions suitable to be pased to the data backend
     **/
    , buildFilters: function buildFilters( query ){
        return query;
    }

    /**
     * Packages peices of the request in a single object for easy passing
     * @method module:tastypie/lib/resource#bundle
     * @param {Request} req A Hapijs request object request object
     * @param {Response} res A Hapijs reply object
     * @param {Object} [data={}] The data object to package
     * @param {Object} [obj] and object instance used for hydration
     * @return Object An object packaging important information about the current request
     **/
    , bundle: function bundle( req, res, data, obj ){
        return {
            req: req
            , res: res
            , data: data || {}
            , object: obj
            , toJSON: to_json
            , toKey: ( type ) => {
                return util.format(
                    "%s:%s:%s:%s:%s:%s"
                    , type
                    , req.path
                    , 'get' // only get request are cached currently.
                    , this.options.name
                    , qs.stringify( req.params || EMPTY_OBJECT )
                    , qs.stringify( req.query || EMPTY_OBJECT )
                );
            }
        };
    }

    /**
     * function used to generate a unique cache key
     * @protected
     * @method module:tastypie/lib/resource#cacheKey
     * @param {String} type
     * @param {String} uri
     * @param {String} method
     * @param {String} resource
     * @param {Object} query
     * @param {Object} params
     * @return {String} key A valid cache key for the current request
     **/
    , cacheKey: function cacheKey( type, uri, method, resource, query, params ){
        return util.format(
            "%s:%s:%s:%s:%s:%s"
          , type
          , uri
          , method.toLowerCase()
          , resource
          , qs.stringify( params || EMPTY_OBJECT )
          , qs.stringify( query || EMPTY_OBJECT )
        );
    }

    /**
     * Executes a given check against a request bundle
     * action checks can be `verb`, `dispatch`, `access`
     * @protected
     * @method module:tastypie/lib/resource#check
     * @param {String} action The http action to check
     * @param {module:tastypie/lib/resource~Bundle} bundle a request bundle
     * @return {Mixed}
     **/
    , check: function check( act, bundle ){
        return checks[ act ] && checks[act].call(this, bundle );
    }

    /**
     * A final hook to run and last deydration operations before a response is returned
     * @method module:tastypie/lib/resource#dehydrate
     * @param {Object} obj An object to dehydrate
     * @return {Object}
     **/
    , dehydrate: function dehydrate( obj ){
        return obj;
    }

    /**
     * Generates a uri for a specific object related to this resource
     * @method module:tastypie/lib/resource#dehydrate_uri
     * @param {Object} obj
     * @param {Bundle} bundle
     * @param {Objectd} result
     * @return {String} uri
     **/
    , dehydrate_uri: function( obj, bundle, result ){
        return this.to_uri( obj, bundle, result );
    }

    /**
     * converts a data string into an object
     * @method module:tastypie/lib/resource#deserialize
     * @param {String} data A string of data to be parsed
     * @param {String} format the content type ( application/json, text/xml, etc ) of the in coming data string
     * @param {module:tastypie/lib/resource~Nodeback} callback
     **/
    , deserialize: function deserialize( data, format, callback ){
        this.options.serializer.deserialize( data, format, callback );
    }

    /**
     * Primary entry point for a request into the resource. maps Http methods to resource methods
     * @method module:tastypie/lib/resource#dispatch
     * @param {String} action the resource action to route the request to
     * @param {Object|Bundle} bundle A bundle representing the current request
     **/
    , dispatch: function dispatch( action, bundle ){
        let method
          , err
          , results
          ;

        bundle.action  = action;

        [err] = this.check('dispatch', bundle );
        results = this.check('access', bundle );

        err = err ? err : results[0];

        if( err ){
            return this.exception( annotate( err, bundle ) );
        }

        method = results[1];

        if(this.throttle( bundle )){
            return this.exception( annotate( Boom.create(429), bundle ) );
        }

        this.domain.run(() => {
            let [err, result] = attempt(method, [bundle], this );
            if( err ){
                this.exception( annotate( err, bundle ) );
            }
        });
    }

    /**
     * Used to handle uncaught caugt errors during the request life cycle.
     * @method module:tastypie/lib/resource#exception
     * @param {RequestError} Error The error generated during the request
     **/
    , exception: function exception( err ){
        var reply = err.res
          , format
          , code
          ;

        if( !reply ){
            debug('resource#exception: no reply object - throwing');
            throw err;
        }

        if( err.isBoom ){
            return reply( err );
        }

        code = err.statusCode || err.code || 500;
        format = this.format( err );
        err.req = err.res = null;

        this.serialize( {error: err.name, message: err.message, statusCode: code}, format, function( serr, data ){
            var e = Boom.wrap( err, code, data.message );
            e.output.payload = data;
            reply( e );
        });
    }

    /**
     * Applies custom filtering to a given set of objects.
     * @method module:tastypie/lib/resource#filter
     * @param {Bundle} bundle A request bundle
     * @param {Object} filters An implementation specific object used to filter data sets
     * @return {Object}
     **/
    , filter: function filter( bundle, filters ){
        this.emit( 'error', new exceptions.NotImplemented( "filter" ) );
    }

    /**
     * Attempts to determine the best serialization format for a given request
     * @method module:tastypie/lib/resource#format
     * @param {Bundle|Object} bundle A bundle object or similar object
     * @param {Array} [types] An array of possible content types this resource can deal with.
     * @return {String} accepts an accepted format for the the related request
     **/
    , format: function format( bundle, types = this.options.serializer.types ){
        var fmt  = bundle.req.query && bundle.req.query.format
          , ct = this.options.serializer.convertFormat( fmt )
          ;

        if( fmt && !ct ){
            let error = Boom.unsupportedMediaType( 'Unsupported format: ' + fmt );
            return this.emit('error', annotate( error ) );
        } else if( fmt && ct ){
            return ct;
        }

        return mime.determine( bundle.req, types );
    }

    /**
     * reads a valeue from the specified cache backend by name. If nothing is found in
     * cache it wil call {@link module:tastypie/lib/resource#get_object|get_object}
     * @protected
     * @method module:tastypie/lib/resource#from_cache
     * @param {String} type of request ( list, detail, updload, etc)
     * @param {Bundle} bundle A bundle representing the current request
     * @param {Function} callback A calback function to use whkf
     **/
    , from_cache: function from_cache( type, bundle, callback ){
        var obj
          , key = this.cacheKey(
            type
          , bundle.req.path
          , bundle.req.method
          , this.options.name
          , bundle.req.query
          , bundle.req.params
        );

        obj = this.cache.get( key, ( err, obj ) => {
          if( obj ){
            return callback( err, obj );
          }
          this.get_object( bundle, ( err, obj ) => {
            this.cache.set( key, obj );
            callback( err, obj );
          });
        });

    }

	/**
	 * A hook before serialization to converte and complex objects or classes into simple serializable objects
	 * @method module:tastypie/lib/resource#full_dehydrate
	 * @param {Object} obj an object to dehydrate object
	 * @return Object An object containing only serializable data
	 **/
	, full_dehydrate: function full_dehydrate( obj, bundle, done ){
        let that = this, ret = Object.create(null), flds=this.fields;
        async.forEachOf( flds, function eachfield( fld, field, cb){

        if( !!fld.options.exclude ) return cb();
        
        fld.dehydrate( obj, function asyncFieldDehy( err, data ){
            let method       = that[ `dehydrate_${field}` ];

            ret[ field ] = data;

            if( method ){
                ret[field] = method.call(that, obj,  bundle, ret );
            }

            return cb( err );
        });
      }, ( err ) => {
          done( err, that.dehydrate( ret ) );
      });
    }

    /**
     * Responsible for converting a raw data object into a resource mapped object
     * @method module:tastypie/lib/resource#full_hydrate
     * @param {Bundle} bundle
     * @return {Bundle}
     **/
    , full_hydrate: function full_hydrate( bundle, done ){
        var Tpl        = this.options.template
          , method     = this.check( 'verb', bundle )
          , flds
          ;

        flds = method === PATCH ? ( bundle.data || {} ) : this.fields;
        bundle = this.hydrate( bundle );
        bundle.object = bundle.object || ( typeof Tpl === 'function' ? new Tpl() : Object.create( Tpl ) );

        async.forEachOf( flds, (value, fieldname, cb ) => {
            let methodname
              , method
              , attr
              , field
              ;

            field = this.fields[ fieldname ];

            if( field.options.readonly ) return cb();

            methodname = `hydrate_${fieldname}`;
            method = this[ methodname ];

            if( typeof method === 'function' ){
                bundle = method( bundle );
            }

            attr = field.options.attribute;

            if( attr ){
                field.hydrate( bundle, ( err, value ) => {
                    set( bundle.object, attr, value );
                    return cb( err, value );
                });
            } else {
                return cb( null );
            }
        }, ( err ) => {
            done( err, bundle );
        });
    }

    /**
     * Final hydration hook method. Is a noop by default
     * @method module:tastypie/lib/resource#hydrate
     * @param {Bundle} bundle
     * @return {Bundle}
     **/
    , hydrate: function( bundle ){
        return bundle;
    }

    /**
     * Attempts to determine the primary key value of the specific object related to a resource request
     * @method module:tastypie/lib/resource#pk
     * @param {Object} orig
     * @param {Bundle} bundle
     * @param {Object} result
     * @return {String|Number} pk The value at the configured primary key field of the object related to the resource
     **/
    , pk: function pk( orig, bundle, result ){
        let pk_field;

        pk_field = this.options.pk || 'id';

        return orig[pk_field] || result[pk_field] || ( bundle.data && bundle.data[pk_field] ) || null;
    }

    /**
     * A method which returns additaional urls to be added to the default uri defintion of a resource
     * @method module:tastypie/lib/resource#prepend_urls
     * @return Array an empty array
     **/
    , prepend_urls: function prepend_urls( ){
        return this.options.routes || [];
    }

    /**
     * Method to generate a response for a bundled request. Will set contnent-type and length headers
     * @chainable
     * @method module:tastypie/lib/resource#respond
     * @param {Bundle|Object} bundle A bundle or similar object
     * @param {HttpResponse|Function} cls An HttpResponse function to call to finish the request. Function should accept a response object, and data to send
     * @return Resource
     **/

    // if location is omitted
    // respond( bundle, cls, callback )
    , respond: function respond( bundle, cls, loc, cb ){
        let _cls = cls || http.ok
          , format = this.format( bundle )
          , _loc = loc
          , fn   = cb
          , that = this
          ;

        if( arguments.length === 3 && typeof loc === 'function' ){
            fn = _loc;
            _loc = '';
        }

        this.serialize( bundle.data, format, function( err, data ){
            let reply = _cls( bundle.res, data || null, format, _loc );
            let mutable = mutableMethods.indexOf( bundle.req.method.toLowerCase() ) !== -1;
            that.modified = mutable ? (new Date()).toUTCString() : that.modified;
            that.modified && reply.header('Last-Modified', that.modified );
            fn && fn( null, reply );
        });
    }

    /**
     * Converts a valid object in to a string of the specified format
     * @method module:tastypie/lib/resource#serialize
     * @param {Object} data Data object to be serialized before delivery
     * @param {String} format the
     * @param {module:tastypie/lib/resource~Nodeback} callback
     **/
    , serialize: function serialize( data, format, callback ){
        this.options.serializer.serialize( data, format, callback );
    }

    /**
     * Applies custome sorting to a given list of objects. Default applies no sorting
     * @method module:tastypie/lib/resource#sort
     * @param {Array} list The list of objects to be sorted
     * @return Array of sorted objects
     **/
    , sort: function sort( obj_list ){
        return obj_list;
    }

    /**
     * manages request throttling. Default implementation usses the remote address of the incomming request
     * @method module:tastypie/lib/resource#throttle
     * @param {module:tastypie/lib/resource~Bundle} bundle A bundle representing the current request
     * @return {Boolean} true if the request should rejected
     */
    , throttle: function throttle( bundle ){
        let thrtl
          , request_id
          , req
          ;

        req        = bundle.req;
        request_id = `${req.method.toLowerCase()}:${req.info.remoteAddress || "noaddr"}:${req.path}`;
        thrtl      = this.options.throttle.toThrottle( request_id );

        if( !thrtl ){
            this.options.throttle.incr( request_id );
        }

        return thrtl;
    }

    /**
     * returns the internal string representation of a resource
     * @method module:tastypie/lib/resource#toString
     * @return {String} String representation
     **/
    , toString: function toString( ){
        return '[object Resource]';
    }

    /**
     * creates a resource uri for a specific object
     * @method module:tastypie/lib/resource#to_uri
     * @param {Object} obj
     * @param {Bundle} bundle
     * @param {Object} result
     * @return {String}
     **/
    , to_uri: function to_uri( obj, bundle, result ){
        return urljoin(this.options.apiname, this.options.name, this.pk( obj, bundle, result ) );
    }

    /**
     * Used internally for better type detection
     * @private
     * @method module:tastypie/lib/resource#$family
     * @return {String}
     **/
    , $family: function $family( ){
        return 'resource';
    }
});

Object.defineProperties( Resource.prototype, {

    routes: {
        /**
         * @name routes
         * @property routes A mapping of uris and their associated handlers
         * @memberof module:tastypie/lib/resource#routes
         * @type Object
         **/
        get: function( ){
            var that = this;
            if( !this._uricache ){
                this._uricache = ( this.prepend_urls() || [] ).concat( this.base_urls() );
                this.actions = [];
            }
            return this._uricache.map(function( route ){
                that.actions.indexOf( route.name ) === -1 && that.actions.push( route.name );
                return {
                    path: route.path
                    , method: route.method || "*"
                    , handler: route.handler
                    , config: merge( that.options.config || {}, route.config || {}, {
                        plugins: {
                            tastypie: {
                                name: route.name
                            }
                        }
                    })
                };
            });
        }
    }

    ,api:{
        /**
         * @property urls An array of all registered uris on a given resource
         * @name urls
         * @memberof module:tastypie.resources.Resource
         * @type Array
         **/
        set: function( api ){
            this.options.apiname = api.basepath;
            for( var key of Object.keys( this.fields ) ){
                let field = this.fields[ key ];
                if( field.is_related ){
                    field.instance.setOptions({apiname: api.basepath });
                }
            }
        }
    }

    ,prefix:{
        set: function( prefix ){
            this.options.name = this.options.name || prefix;
        }
        ,get: function(){
            this.options.name;
        }
    }
    , urls: {
        /**
         * @property urls An array of all registered uris on a given resource
         * @name urls
         * @memberof module:tastypie.resources.Resource
         * @type Array
         **/
        get: function( ){
            return this.routes.map( function( uri ){
                return uri.path;
            });
        }
    }
});

Resource.defineMutator = Class.defineMutator;

/**
 * Helper method to subclass the base resource
 * @static
 * @function
 * @name extend
 * @memberof module:tastypie/lib/resource
 * @param {Object} proto An object to use as the prototyp of a new resource type
 * @return {module:tastypie/lib/resource}
 **/
Resource.extend = function( proto ){
    proto.inherits = Resource;
    return new Class( proto );
};
module.exports = Resource;