/*jshint laxcomma:true, smarttabs: true, node: true, unused: false, esnext: true */ 'use strict'; /** * Serves as name space for a collection of resources. i.e., api/v1, api/v2, etc * @module tastypie/lib/api * @author Eric Satterwhite * @requires util * @requires boom * @requires debug * @requires mout/lang/isObject * @requires tastypie/lib/class * @requires tastypie/lib/class/options * @example var api = new Api('api/v1') api.add('test', new tatsypie.Resource() ) // GET /api/v1/test?format=json **/ var Class = require( './class' ) // this is Class , util = require('util') , joi = require('joi') , Boom = require('boom') , isObject = require('mout/lang/isObject') , has = require('mout/object/hasOwn') , debug = require('debug')('tastypie:api') , Options = require( './class/options' ) , pkg = require('../package.json') , mime = require('./mime') , endsWithSlash = /\/$/ , trailingSlashes = /^\/|\/$/g , Api , optionValidator ; optionValidator = joi.object().keys({ serializer:joi.object().optional() ,select:joi.string().optional() }).unknown() function onPostAuth( request, reply ){ let serializer = this.options.serializer; request.info.api_name = this.basepath; // if it is a format hapi deals with, it has already parsed it if(!this.baseexp.test( request.path ) || !request.payload || isObject( request.payload ) ){ return reply.continue(); } // if it is a string, lets try to deserialize it serializer.deserialize( request.payload, request.headers['content-type'], function( err, data ){ if( data ){ request.payload = data; return reply.continue( ); } reply( err ); }); } function onPreResponse( request, reply ){ var response = request.response , query = request.query , serializer = this.options.serializer , format , fmt , ct ; fmt = request.query && request.query.format; ct = this.options.serializer.convertFormat( fmt ); if( fmt && !ct ){ return reply( Boom.unsupportedMediaType(`unsupported serialization format ${fmt}`) ); } format = (fmt && ct) ? ct : mime.determine( request, serializer.types ); debug('requested format %s', format); if( has(query,'callback') && !( has(query, 'format') ) || response.isBoom ){ return reply.continue(); } serializer.serialize( response.source, format, function( err, content ){ if( err ){ return reply( err ); } reply( content ) .code( response.statusCode ) .type( format || serializer.options.defaultFormat ); }); } /** * Provides namespaces for collections of resource ( api/v1, api/v2, etc ) * @constructor * @alias module:tastypie/lib/api * @param {Object} options api configuration options * @param {?module:tastypie/lib/serializer} [serilaizer=null] A serializer instance to handle serialization / deserializion during the lifecycle of the request - rather than at the resource level. * @example var api = new Api('api/v1'); x.use('fake', new Resource() ) */ Api = new Class( /** @lends module:NAME.Api.prototype */{ mixin: Options ,options:{ DEFAULT_LIMIT:20 ,MAX_LIMIT:500 , serializer: null , select: null } ,constructor: function( path, options ){ let results = optionValidator.validate( options ); if( results.error ){ throw results.error; } this.setOptions( options ); this.basepath = this.normalizePath( path ); this.baseexp = new RegExp( "^" + this.basepath ); this.api_cache = {}; this.url_cache = []; this.pending = []; this.register = this.plug.bind(this); this.register.attributes = { name:'tastypie' ,version:pkg.version ,multiple: true }; } , plug: function( plugin, options, next ){ var that = this; this.plg = this.options.select ? plugin.select( this.options.select ) : plugin; for( let key in this.api_cache ){ if( this.api_cache.hasOwnProperty( key ) ){ debug('registering plugin %s', key ); this._register(key, this.api_cache[ key ] ); } } this.plg.expose('name', pkg.name); this.plg.expose('version', pkg.version); debug("serializer %s", !!this.options.serializer, this.basepath ); // FIXME: I don't like this... if( this.options.serializer ){ debug("Loading serialization plugin for %s", this.basepath ); debug('adding server serializer for %s', this.basepath); this.plg.ext('onPostAuth',onPostAuth.bind( this )); this.plg.ext('onPreResponse', onPreResponse.bind( this )); } this.plg.route({ path: this.basepath ,method:'GET' ,handler: function( request, reply ){ var data = {}, current; for( let name in that.api_cache ){ if( that.api_cache.hasOwnProperty( name ) ){ current = that.api_cache[ name ]; data[ name ] = {}; for( let r of current.routes ){ let plg = r.config.plugins.tastypie; if( !plg.name ){ return; } data[name][plg.name] = r.path; } } } reply( data ); } }); this.registed = Date.now(); return next(); } , _register: function( name, resource ){ this.plg.route( resource.routes ); } /** * Mounts a resource on a route prefix * @method module:tastypie/lib/api#use * @param {String} [prefix] The route prefix in addition the the API prefix * @param {module:tastypie/lib/resource} resource A resource instance to mount at the prfix path */ ,use: function(/*prefix, resource*/){ var prefix, resource; if( arguments.length === 1){ resource = arguments[0]; prefix = arguments[0].options.name || ''; prefix = prefix.replace(endsWithSlash,''); } else { resource = arguments[1]; prefix = arguments[0]; prefix = prefix.replace(endsWithSlash,''); } this.api_cache[ prefix ] = resource; resource.api = this; resource.prefix = prefix; if( this.plg ){ this._register( prefix, resource ); } this.url_cache = []; return this; } /** * Normalizes a uri path by stripping trailing or adding leading slashed acordingly * @protected * @method module:tastypie/lib/api#normalizePath * @param {String} path The path to normalize * @returns {String} The newly formated path */ ,normalizePath: function( path ){ return util.format( '/%s' , path.replace(trailingSlashes,'') ); } }); // defines a quick url look up per API object // it is cached on first call until another // resource is registered /** * @readonly * @name urls * @instance * @memberof module:tastypie/lib/api * @property {Object} urls An object containing all know methods registed to the api instance **/ Object.defineProperties(Api.prototype,{ urls:{ get: function(){ var routes; if( !this.url_cache.length ){ routes = this.plg && this.plg.table()[0].table || []; var baseexp = this.baseexp; this.url_cache = routes .map(function( r ){ return r.path; }) .filter(function( r ){ return baseexp.test( r ); }); } return this.url_cache; } } }); module.exports = Api;