/*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;