Source: mongodb/models/publisher.js

/**
 * @module mongodb/models/publisher
 */

var mongoose = require('mongoose');
var deepPopulate = require('mongoose-deep-populate');
var Schema = mongoose.Schema;
var util = require('util');
var _ = require('underscore');
var mongooseApiQuery = require('mongoose-api-query');
var creativeSchema = require('./advertiser').creativeSchema;

var TreeDocument = require('./lib/treeDocument').TreeDocument;

// ---- Publisher Collection Models ---- //

var HostedCreative = mongoose.model('HostedCreative', creativeSchema);

/**
 * Mongoose Schema representing Publisher Placements
 *
 * @class
 * @type {Schema}
 */
var placementSchema = new Schema({
    name:           String,
    active:         { type: Boolean, default: false },
    description:    String,
    tstamp:         { type: Date, default: Date.now },
    h:              { type: Number, required: true },
    w:              { type: Number, required: true },
    pos:            { type: Number, required: true, max: 7 }, //see OpenRTB 2.3 section 5.4 for list & definitions
    defaultType:    { type: String, required: true, enum: ['passback','hostedCreative','psa','hide'], default: 'hide' },
    passbackTag:    { type: String, required: false },
    hostedCreatives: [creativeSchema]
});
placementSchema.plugin(deepPopulate, {});
placementSchema.plugin(mongooseApiQuery);

/**
 * Hook to apply logic on hostedCreative selection when serving up default creatives.
 *
 * Just chooses a random creative right now.
 * @returns {*}
 */
placementSchema.methods.getRandomHostedCreative = function(){
    if (this.hostedCreatives){
        return this.hostedCreatives[Math.floor(Math.random()*this.hostedCreatives.length)]
    }
};

/**
 * Cross validation to ensure that proper data has been provided with individual defaultType options
 */
placementSchema.pre('save', function(next){
    var err = null;
    switch (this.defaultType){
        case "passback":
            if (!this.passbackTag){
                err = new Error("defaultType set to 'passback' but no passbackTag was provided");
            }
            break;
        case "hostedCreative":
            if (this.hostedCreatives && this.hostedCreatives.length > 0){
                break;
            } else {
                err = new Error("defaultType set to 'hostedCreative' but no hostedCreatives provided");
            }
        //TODO: add case for PSA when that functionality gets built out
    }
    return next(err);
});

var Placement = mongoose.model('Placement',placementSchema);

/**
 * Mongoose Schema representing Publisher Pages
 *
 * @class
 * @type {Schema}
 */
var pageSchema = new Schema({
    name:           String,
    active:         { type: Boolean, default: false },
    description:    String,
    tstamp:         { type: Date, default: Date.now },
    url:            { type: String, required: true },
    placements:     [placementSchema],
    clique:         { type: String, required: false, ref: 'Clique' }//TODO: Add validators to check string against Clique._id
});
pageSchema.plugin(deepPopulate, {});
pageSchema.plugin(mongooseApiQuery);
var Page = mongoose.model('Page', pageSchema);

/**
 * Mongoose Schema representing Publisher Sites
 *
 * @class
 * @type {Schema}
 */
var siteSchema = new Schema({
    name:           String,
    active:         { type: Boolean, default: false },
    description:    String,
    logo_url:       String,
    tstamp:         { type: Date, default: Date.now },
    domain_name:    { type: String, required: true },
    pages:          [pageSchema],
    blacklist:      [{type: String}],
    bidfloor:       Number,
    clique:         { type: String, required: true, ref: 'Clique' }//TODO: Add validators to check string against Clique._id
});
// Virtual field to retrieve secure URL
siteSchema.virtual('logo_secure_url').get(function(){
    if (this.logo_url){
        return this.logo_url.replace('http://', 'https://');
    }
});
siteSchema.set('toObject', { virtuals: true });
siteSchema.set('toJSON', { virtuals: true });

siteSchema.plugin(deepPopulate, {});
siteSchema.plugin(mongooseApiQuery);

//pre-save hook to populate logo_url with parent logo_url if not provided
siteSchema.pre('save', function(next){
    if (!this.logo_url){
        // this.parent call doesn't work here for some reason
        this.logo_url = this.__parent.logo_url;
    }
    next();
});

var Site = mongoose.model('Site', siteSchema);

/**
 * Mongoose Schema representing Publishers
 *
 * @class
 * @type {Schema}
 */
var publisherSchema = new Schema({
    name:           { type: String, required: true, index: true },
    user:           [{ type: Schema.ObjectId, ref: 'User' }],
    organization:       { type: Schema.ObjectId, ref: 'Organization' },
    logo_url:       String,
    revenue_share:  { type: Number, required: true, default: 0.10 },
    website:        String,
    description:    String,
    tstamp:         { type: Date, default: Date.now },
    sites:          [siteSchema],
    cliques:        [{ type: String, required: true, ref: 'Clique' }] //TODO: Add validators to check string against Clique._id
});

// Virtual field to retrieve secure URL
publisherSchema.virtual('logo_secure_url').get(function(){
    if (this.logo_url){
        return this.logo_url.replace('http://', 'https://');
    }
});
publisherSchema.set('toObject', { virtuals: true });
publisherSchema.set('toJSON', { virtuals: true });
publisherSchema.plugin(deepPopulate, {});
publisherSchema.plugin(mongooseApiQuery);
publisherSchema.set('autoIndex', false); // performance gains
var Publisher = mongoose.model('Publisher', publisherSchema);

/**
 * [TreeDocument]{@link mongodb/models/lib/treeDocument~TreeDocument} subclass wrapping Publisher document tree.
 *
 * @param {mongoose.connection} connection DB connection object
 * @param {Object} options
 * @constructor
 */
var PublisherModels = function(connection, options){
    options = options || {};
    this.connection = connection;

    // Publisher models
    this.Publisher = this.connection.model('Publisher');
    this.Site = this.connection.model('Site');
    this.Page = this.connection.model('Page');
    this.Placement = this.connection.model('Placement');
    this.HostedCreative = this.connection.model('HostedCreative');
    var branches = [['Site','Page','Placement','HostedCreative']];
    var top_level_node = 'Publisher';
    TreeDocument.call(this, top_level_node, branches, options);
};
util.inherits(PublisherModels, TreeDocument);


/**
 * Gets placement document nested several levels deep in publisher document hierarchy.
 *
 * I'm not proud of this query function, probably could be done more efficiently
 * but I can't figure out how.
 *
 * @param placement_id ObjectID of desired placement
 * @param read set to "secondary" if reading from slave
 * @param callback callback function
 */
PublisherModels.prototype.getPlacementFromID = function(placement_id, read, callback){
    this.getNestedObjectById(placement_id, 'Placement', callback);
};

exports.PublisherModels = PublisherModels;