<commit_before>'use strict'

const co     = require('co');
const qs     = require('qs');
const parse  = require('co-body');
const debug  = require('debug')('koa-restql:middlewares');

const common = require('./common');
const getAssociationName = common.getAssociationName;
const parseQuery   = common.parseQuery;

const capitalizeFirstLetter = (string) => {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

const get = (method, model, association) => {
  return function * (next) {

    this.restql = this.restql || {};

    let params  = this.params
      , id      = params.id
      , query   = this.restql.query
      , data;

    let debugInfo = `get ${this.request.url}, using model: ${model.name}`;

    if (association) {
      debugInfo += `, with association:
        ${common.getAssociationName(association)}`;
    }

    debug(debugInfo);

    if (!query) {
      query = qs.parse(this.request.querystring, { allowDots: true });
    }

    if (!association) {
      query = parseQuery(query, model, method);

      if (id === undefined) {
        data = yield model.findAndCount({
          attributes : query._attributes,
          where      : query._where,
          order      : query._order,
          include    : query._include,
          limit      : query._limit,
          offset     : query._offset
        })

        this.response.set('X-Range',
          `objects ${query._offset}-${query._offset + data.rows.length}/${data.count}`);
        data = data.rows;
      } else {
        data = yield model.findOne({
          attributes : query._attributes,
          where      : { id },
          include    : query._include
        })

        if (!data) {
          this.throw(`${model.name} ${id} is not found`, 404);
        }
      }
    } else {
      let associationId    = params.associationId
        , associationModel = association.target
        , order;

      query = parseQuery(query, associationModel, method);

      if (!associationId) {
        let name     = getAssociationName(association)
          , isPlural = association.isMultiAssociation
          , getter   = `get${capitalizeFirstLetter(name)}`
          , counter  = `count${capitalizeFirstLetter(name)}`;

        data = yield model.findOne({
          where : { id }
        })

        if (!data) {
          this.throw(`${model.name} ${id} is not found`, 404);
        }

        let promises = [];

        promises.push(data[getter]({
          attributes : query._attributes,
          where      : query._where,
          order      : query._order,
          include    : query._include,
          through    : query._through,
          limit      : query._limit,
          offset     : query._offset
        }));

        if (isPlural) {
          promises.push(data[counter]({
            where: query._where
          }))
        }

        data = yield promises;

        if (isPlural) {
          this.response.set('X-Range',
            `objects ${query._offset}-${query._offset + data[0].length}/${data[1]}`);
        }

        data = data[0];
      } else {
        data = yield associationModel.findOne({
          attributes : query._attributes,
          where      : { id: associationId },
          include    : query._include
        })

        if (!data) {
          this.throw(`${model.name} ${id}
              association ${associationId} is not found`, 404);
        }
      }
    }

    this.body   = data;
    this.status = 200;

    yield next;
  }
}

const isSameDeletedAt = (a, b) => {
  if (a instanceof Date && b instanceof Date) {
    return a.getTime() === b.getTime();
  } else {
    return a === b;
  }
}

const getUniqueConstraintErrorFields = (model, e) => {

  let indexes    = model.options.indexes
    , attributes = model.attributes
    , fields     = {}
    , uniqueIndex, fieldsIsValid;

  if (!e.fields)
    return;

  fieldsIsValid = Object.keys(e.fields).every(key => attributes[e.fields[key]]);

  if (fieldsIsValid)
    return e.fields;

  let index = indexes.find(index => e.fields[index.name] && index.unique);

  if (index && Array.isArray(index.fields)) {
    let value = e.fields[index.name];

    switch (typeof value) {
      case 'number':
        value = [ value ];
      case 'string':
        value = value.split('-');
        break;
    }

    index.fields.forEach((field, index) => {
      fields[field] = value[index]
    })
  }

  return fields;
}

const create = (model, row, options) => {
  return function * () {
    let id      = row.id
      , message = `${model.name} unique constraint error`
      , status  = 409
      , data;

    options = options || {}

    let {
      ignoreDuplicates = false
    } = options;

    try {

      if (id) {
        delete row.id;
      }

      data =
        yield model.create(row, options);

    } catch (e) {
      if (e.name === 'SequelizeUniqueConstraintError') {

        let where        = getUniqueConstraintErrorFields(model, e)
          , attributes   = model.attributes
          , deletedAtCol = model.options.deletedAt;

        if (!deletedAtCol)
          this.throw(message, status);

        data = yield model.find({
          paranoid: false,
          where
        })

        if (!data) {
          this.throw('Sequelize goes wrong', 500);
        }

        let deletedAtVal = attributes[deletedAtCol].defaultValue;
        if (deletedAtVal === undefined) {
          deletedAtVal = null;
        }

        const upsert = (row) => {
          return function * () {
            yield model.upsert(row);
            let data = yield model.find({
              where
            });
            return data;
          }
        }

        if (!isSameDeletedAt(data[deletedAtCol], deletedAtVal)) {
          yield data.restore();
          data = yield upsert(row);
        } else {
          if (!ignoreDuplicates) {
            this.throw(message, status);
          }
          data = yield upsert(row);
        }
      } else {
        throw new Error(e);
      }
    }

    return data;
  }
}

const post = (method, model, association) => {
  return function * (next) {

    this.restql = this.restql || {};

    let body   = this.request.body || (yield parse(this))
      , params = this.params
      , query  = this.restql.query
      , data;

    let debugInfo = `post ${this.request.url}, using model: ${model.name}`;

    if (association) {
      debugInfo += `, with association:
        ${common.getAssociationName(association)}`;
    }

    debug(debugInfo);

    if (!query) {
      query = qs.parse(this.request.querystring, { allowDots: true });
    }

    const _create = (model) => {
      return function * () {
        query = parseQuery(query, model, method);
        if (Array.isArray(body)) {
          let promises = body.map(row => {
            let gen = create(model, row, {
              include          : query._include,
              ignoreDuplicates : query._ignoreDuplicates
            })
            return co(gen);
          })

          let data = yield promises;
          return data;
        } else {
          return yield create(model, body, {
            include          : query._include,
            ignoreDuplicates : query._ignoreDuplicates
          });
        }
      }
    }

    if (!association) {
      data = yield _create(model);
    } else {
      let id     = params.id
        , name   = association.options.name.singular
        , add    = `add${capitalizeFirstLetter(name)}`;

      data = yield model.findOne({
        where: { id }
      });

      if (!data) {
        this.throw(`${model.name} ${id} is not found`, 404);
      }

      let associationData = yield _create(association.target);
      yield data[add](associationData);

      data = yield association.target.findById(associationData.id);
    }

    this.body   = data;
    this.status = 201;

    yield next;
  }
}

const put = (method, model, association) => {
  return function * (next) {
    let body    = this.request.body || (yield parse(this))
      , params  = this.params
      , id      = params.id
      , include = association ? [association] : []
      , data, status;

    let debugInfo = `put ${this.request.url}, using model: ${model.name}`;

    if (association) {
      debugInfo += `, with association:
        ${common.getAssociationName(association)}`;
    }

    debug(debugInfo);

    data = yield model.findOne({
      where: { id },
      include
    })

    if (!data) {
      this.throw(`${model.name} ${id} is not found`, 404);
    }

    if (!association) {
      yield data.update(body);
    } else {
      let associationId = params.associationId;

      if (associationId) {
        /*
         * plural assocation
         */
        data = yield association.target.findOne({
          where: {
            id: associationId
          }
        });

        if (!data) {
          this.throw(`${model.name} ${id}
              association ${associationId} is not found`, 404);
        }

        delete body.id;
        yield data.update(body);
      } else {
        /*
         * singular association
         */
        let name   = association.options.name.singular
          , setter = `set${capitalizeFirstLetter(name)}`;

        let associationData = data[name];

        delete body.id;
        if (!associationData) {
          associationData = yield create(association.target, body);
          status = 201;
          yield data[setter](associationData);
        } else {
          yield associationData.update(body);
        }
        data = associationData;
      }
    }

    this.body   = data;
    this.status = status || 200;

    yield next;
  }
}

const del = (method, model, association) => {
  return function * (next) {
    let params  = this.params
      , id      = params.id
      , include = association ? [association] : []
      , data    = null;

    let debugInfo = `delete ${this.request.url}, using model: ${model.name}`;

    if (association) {
      debugInfo += `, with association:
        ${common.getAssociationName(association)}`;
    }

    debug(debugInfo);

    data = yield model.findOne({
      where: { id },
      include
    })

    if (!data) {
      this.throw(`${model.name} ${id} is not found`, 404);
    }

    if (!association) {
      yield model.destroy({
        where: {
          id: data.id
        }
      });
    } else {
      let associationId    = params.associationId
        , name             = association.options.name.singular
        , remove           = `remove${capitalizeFirstLetter(name)}`
        , associationModel = association.target
        , associationData  = null;

      if (associationId) {
        /*
         * plural assocation
         */
        associationData = yield associationModel.findOne({
          where: {
            id: associationId
          }
        });

        if (!data) {
          this.throw(`${model.name} ${id}
              association ${associationId} is not found`, 404);
        }

        let associationType = association.associationType;
        if (associationType === 'HasMany') {
          yield associationModel.destroy({
            where: {
              id: associationId
            }
          })
        } else {
          yield data[remove](associationData);
        }
      } else {
        /*
         * singular association
         */

        associationData = data[name];

        if (associationData) {
          yield associationModel.destroy({
            where: {
              id: associationData.id
            }
          });
        }
      }
    }

    this.data   = {};
    this.status = 204;

    yield next;
  }
}

module.exports.handlers = {
  get, post, put, del
};
<commit_msg>Added: this.restql.query as default query<commit_after>'use strict'

const co     = require('co');
const qs     = require('qs');
const parse  = require('co-body');
const debug  = require('debug')('koa-restql:middlewares');

const common = require('./common');
const parseQuery         = common.parseQuery;
const getAssociationName = common.getAssociationName;

const capitalizeFirstLetter = (string) => {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

const get = (method, model, association) => {
  return function * (next) {

    this.restql = this.restql || {};

    let params  = this.params
      , id      = params.id
      , query   = this.restql.query
      , data;

    let debugInfo = `get ${this.request.url}, using model: ${model.name}`;

    if (association) {
      debugInfo += `, with association:
        ${common.getAssociationName(association)}`;
    }

    debug(debugInfo);

    if (!query) {
      query = qs.parse(this.request.querystring, { allowDots: true });
    }

    if (!association) {
      query = parseQuery(query, model, method);

      if (id === undefined) {
        data = yield model.findAndCount({
          attributes : query._attributes,
          where      : query._where,
          order      : query._order,
          include    : query._include,
          limit      : query._limit,
          offset     : query._offset
        })

        this.response.set('X-Range',
          `objects ${query._offset}-${query._offset + data.rows.length}/${data.count}`);
        data = data.rows;
      } else {
        data = yield model.findOne({
          attributes : query._attributes,
          where      : { id },
          include    : query._include
        })

        if (!data) {
          this.throw(`${model.name} ${id} is not found`, 404);
        }
      }
    } else {
      let associationId    = params.associationId
        , associationModel = association.target
        , order;

      query = parseQuery(query, associationModel, method);

      if (!associationId) {
        let name     = getAssociationName(association)
          , isPlural = association.isMultiAssociation
          , getter   = `get${capitalizeFirstLetter(name)}`
          , counter  = `count${capitalizeFirstLetter(name)}`;

        data = yield model.findOne({
          where : { id }
        })

        if (!data) {
          this.throw(`${model.name} ${id} is not found`, 404);
        }

        let promises = [];

        promises.push(data[getter]({
          attributes : query._attributes,
          where      : query._where,
          order      : query._order,
          include    : query._include,
          through    : query._through,
          limit      : query._limit,
          offset     : query._offset
        }));

        if (isPlural) {
          promises.push(data[counter]({
            where: query._where
          }))
        }

        data = yield promises;

        if (isPlural) {
          this.response.set('X-Range',
            `objects ${query._offset}-${query._offset + data[0].length}/${data[1]}`);
        }

        data = data[0];
      } else {
        data = yield associationModel.findOne({
          attributes : query._attributes,
          where      : { id: associationId },
          include    : query._include
        })

        if (!data) {
          this.throw(`${model.name} ${id}
              association ${associationId} is not found`, 404);
        }
      }
    }

    this.body   = data;
    this.status = 200;

    yield next;
  }
}

const isSameDeletedAt = (a, b) => {
  if (a instanceof Date && b instanceof Date) {
    return a.getTime() === b.getTime();
  } else {
    return a === b;
  }
}

const getUniqueConstraintErrorFields = (model, e) => {

  let indexes    = model.options.indexes
    , attributes = model.attributes
    , fields     = {}
    , uniqueIndex, fieldsIsValid;

  if (!e.fields)
    return;

  fieldsIsValid = Object.keys(e.fields).every(key => attributes[e.fields[key]]);

  if (fieldsIsValid)
    return e.fields;

  let index = indexes.find(index => e.fields[index.name] && index.unique);

  if (index && Array.isArray(index.fields)) {
    let value = e.fields[index.name];

    switch (typeof value) {
      case 'number':
        value = [ value ];
      case 'string':
        value = value.split('-');
        break;
    }

    index.fields.forEach((field, index) => {
      fields[field] = value[index]
    })
  }

  return fields;
}

const create = (model, row, options) => {
  return function * () {
    let id      = row.id
      , message = `${model.name} unique constraint error`
      , status  = 409
      , data;

    options = options || {}

    let {
      ignoreDuplicates = false
    } = options;

    try {

      if (id) {
        delete row.id;
      }

      data =
        yield model.create(row, options);

    } catch (e) {
      if (e.name === 'SequelizeUniqueConstraintError') {

        let where        = getUniqueConstraintErrorFields(model, e)
          , attributes   = model.attributes
          , deletedAtCol = model.options.deletedAt;

        if (!deletedAtCol)
          this.throw(message, status);

        data = yield model.find({
          paranoid: false,
          where
        })

        if (!data) {
          this.throw('Sequelize goes wrong', 500);
        }

        let deletedAtVal = attributes[deletedAtCol].defaultValue;
        if (deletedAtVal === undefined) {
          deletedAtVal = null;
        }

        const upsert = (row) => {
          return function * () {
            yield model.upsert(row);
            let data = yield model.find({
              where
            });
            return data;
          }
        }

        if (!isSameDeletedAt(data[deletedAtCol], deletedAtVal)) {
          yield data.restore();
          data = yield upsert(row);
        } else {
          if (!ignoreDuplicates) {
            this.throw(message, status);
          }
          data = yield upsert(row);
        }
      } else {
        throw new Error(e);
      }
    }

    return data;
  }
}

const post = (method, model, association) => {
  return function * (next) {

    this.restql = this.restql || {};

    let body   = this.request.body || (yield parse(this))
      , params = this.params
      , query  = this.restql.query
      , data;

    let debugInfo = `post ${this.request.url}, using model: ${model.name}`;

    if (association) {
      debugInfo += `, with association:
        ${common.getAssociationName(association)}`;
    }

    debug(debugInfo);

    if (!query) {
      query = qs.parse(this.request.querystring, { allowDots: true });
    }

    const _create = (model) => {
      return function * () {
        query = parseQuery(query, model, method);
        if (Array.isArray(body)) {
          let promises = body.map(row => {
            let gen = create(model, row, {
              include          : query._include,
              ignoreDuplicates : query._ignoreDuplicates
            })
            return co(gen);
          })

          let data = yield promises;
          return data;
        } else {
          return yield create(model, body, {
            include          : query._include,
            ignoreDuplicates : query._ignoreDuplicates
          });
        }
      }
    }

    if (!association) {
      data = yield _create(model);
    } else {
      let id     = params.id
        , name   = association.options.name.singular
        , add    = `add${capitalizeFirstLetter(name)}`;

      data = yield model.findOne({
        where: { id }
      });

      if (!data) {
        this.throw(`${model.name} ${id} is not found`, 404);
      }

      let associationData = yield _create(association.target);
      yield data[add](associationData);

      data = yield association.target.findById(associationData.id);
    }

    this.body   = data;
    this.status = 201;

    yield next;
  }
}

const put = (method, model, association) => {
  return function * (next) {
    let body    = this.request.body || (yield parse(this))
      , params  = this.params
      , id      = params.id
      , include = association ? [association] : []
      , data, status;

    let debugInfo = `put ${this.request.url}, using model: ${model.name}`;

    if (association) {
      debugInfo += `, with association:
        ${common.getAssociationName(association)}`;
    }

    debug(debugInfo);

    data = yield model.findOne({
      where: { id },
      include
    })

    if (!data) {
      this.throw(`${model.name} ${id} is not found`, 404);
    }

    if (!association) {
      yield data.update(body);
    } else {
      let associationId = params.associationId;

      if (associationId) {
        /*
         * plural assocation
         */
        data = yield association.target.findOne({
          where: {
            id: associationId
          }
        });

        if (!data) {
          this.throw(`${model.name} ${id}
              association ${associationId} is not found`, 404);
        }

        delete body.id;
        yield data.update(body);
      } else {
        /*
         * singular association
         */
        let name   = association.options.name.singular
          , setter = `set${capitalizeFirstLetter(name)}`;

        let associationData = data[name];

        delete body.id;
        if (!associationData) {
          associationData = yield create(association.target, body);
          status = 201;
          yield data[setter](associationData);
        } else {
          yield associationData.update(body);
        }
        data = associationData;
      }
    }

    this.body   = data;
    this.status = status || 200;

    yield next;
  }
}

const del = (method, model, association) => {
  return function * (next) {
    let params  = this.params
      , id      = params.id
      , include = association ? [association] : []
      , data    = null;

    let debugInfo = `delete ${this.request.url}, using model: ${model.name}`;

    if (association) {
      debugInfo += `, with association:
        ${common.getAssociationName(association)}`;
    }

    debug(debugInfo);

    data = yield model.findOne({
      where: { id },
      include
    })

    if (!data) {
      this.throw(`${model.name} ${id} is not found`, 404);
    }

    if (!association) {
      yield model.destroy({
        where: {
          id: data.id
        }
      });
    } else {
      let associationId    = params.associationId
        , name             = association.options.name.singular
        , remove           = `remove${capitalizeFirstLetter(name)}`
        , associationModel = association.target
        , associationData  = null;

      if (associationId) {
        /*
         * plural assocation
         */
        associationData = yield associationModel.findOne({
          where: {
            id: associationId
          }
        });

        if (!data) {
          this.throw(`${model.name} ${id}
              association ${associationId} is not found`, 404);
        }

        let associationType = association.associationType;
        if (associationType === 'HasMany') {
          yield associationModel.destroy({
            where: {
              id: associationId
            }
          })
        } else {
          yield data[remove](associationData);
        }
      } else {
        /*
         * singular association
         */

        associationData = data[name];

        if (associationData) {
          yield associationModel.destroy({
            where: {
              id: associationData.id
            }
          });
        }
      }
    }

    this.data   = {};
    this.status = 204;

    yield next;
  }
}

module.exports.handlers = {
  get, post, put, del
};
<|endoftext|>


<commit_before>var budgetController = (function(){

    var Expense = function(id, description, value){
        this.id = id;
        this.description = description;
        this.value = value;
    };

    var Income = function(id, description, value){
        this.id = id;
        this.description = description;
        this.value = value;
    };

    var data = {
        allitems: {
            exp: [],
            inc: []
        },
        totals:{
            exp: 0,
            inc: 0
        },
        budget: 0,
        percentage: -1
    };

    var calculateTotal = function(type){

        var sum = 0;
        data.allitems[type].forEach(function(cur){
            sum += cur.value;
        });
        data.totals[type] = sum;
    };

    return {
        addItem: function(type, desc, val){
            var newItem, Id;

            if(data.allitems[type].length > 0){
               Id = data.allitems[type][data.allitems[type].length - 1].id + 1;
            } else {
               Id = 0;
            }
            if('inc' === type){
                newItem = new Income(Id, desc, val);
            }else{
                newItem = new Expense(Id, desc, val);
            }
            data.allitems[type].push(newItem);
            return newItem;
        },

        calculateBudget: function(){

            // calculate the total income and expenses
            calculateTotal('exp');
            calculateTotal('inc');

            // calculate the budget: income - expenses
            data.budget = data.totals.inc - data.totals.exp;

            // calculate the percentage of income that we spent
            if(data.totals.inc > 0){
               data.percentage = Math.round((data.totals.exp / data.totals.inc) * 100);
            }
        },

        getBudget: function(){

            return {
                totalinc: data.totals.inc,
                totalExp: data.totals.exp,
                percent: data.percentage,
                budget: data.budget
            }
        }
    }
})();

var uiController = (function(){

        var uiDomStrings = {
        typeClass: '.add__type',
        descClass: '.add__description',
        valueClass: '.add__value',
        btnClass: '.add__btn',
        incomeListClass: '.income__list',
        expenseListClass: '.expense__list',
        incomeDisplayClass: '.budget__income--value',
        expenseDisplayClass: '.budget__expenses--value',
        budgetPercentClass: '.budget__expenses--percentage',
        budgetValueClass: '.budget__value'
        }

    return {

        getInputData: function(){
            return {
                expenseType: document.querySelector(uiDomStrings.typeClass).value,
                expDesc: document.querySelector(uiDomStrings.descClass).value,
                expAmt: parseFloat(document.querySelector(uiDomStrings.valueClass).value)
            }
        },

        addListItem: function(obj, type){

            var html, htmlWithData, element;

            // create HTML string with place holder text
            if('inc' === type){
                element = uiDomStrings.incomeListClass;
               html = '

%description%

%value%

 <button class="item__delete--btn"></button>

';
            } else {
                element = uiDomStrings.expenseListClass;
               html = '

%description%

- %value%

%itemPercent%

 <button class="item__delete--btn"></button>

';
            }

            // replace the placeholder text with some actual data
            htmlWithData = html.replace('%id%', obj.id);
            htmlWithData = htmlWithData.replace('%value%', obj.value);
            htmlWithData = htmlWithData.replace('%description%', obj.description);

/*
            if('exp' === type){
                htmlWithData = html.replace('%itemPercent%', );
            }
*/

            // insert the html in to the DOM
            document.querySelector(element).insertAdjacentHTML('beforeend', htmlWithData);
        },

        getDomStrings: function(){
            return uiDomStrings;
        },

        clearFields: function(){

            var fields, fieldsArr;
            fields = document.querySelectorAll(uiDomStrings.descClass +','+ uiDomStrings.valueClass);
            fieldsArr = Array.prototype.slice.call(fields);

            fieldsArr.forEach(function(current, index, array){
                current.value = '';
                if(index === 0){
                   current.focus();
                }
            });
        },

        updateBudgetToUI: function(budget){

            document.querySelector(uiDomStrings.incomeDisplayClass).textContent = '+ '+budget.totalinc;
            document.querySelector(uiDomStrings.expenseDisplayClass).textContent = '- '+budget.totalExp;

            if(budget.percent > 0){
               document.querySelector(uiDomStrings.budgetPercentClass).textContent = budget.percent + '%';
            }else {
               document.querySelector(uiDomStrings.budgetPercentClass).textContent = '---';
            }

            document.querySelector(uiDomStrings.budgetValueClass).textContent = '+ '+budget.budget;
        }
    }

})();

var controller = (function(budgetCtlr, uiCtlr){

    var setupEventListeners = function (){
        var uiDomStrings = uiCtlr.getDomStrings();
        document.querySelector(uiDomStrings.btnClass).addEventListener('click', ctrlAddItem);
        document.addEventListener('keypress', function(event){
           if(event.keyCode === 13 || event.which === 13){
              ctrlAddItem();
            }
        });
    };

    var updateBudget = function(){

        // 4. calculate the budget
        budgetCtlr.calculateBudget();

        var budget = budgetCtlr.getBudget();

        // 5. display the budget to UI
        uiCtlr.updateBudgetToUI(budget);
    };

    var ctrlAddItem = function(){

       // 1. get the field input data
        var input = uiCtlr.getInputData();

        if(input.expDesc !== '' && !isNaN(input.expAmt) && input.expAmt > 0){

            // 2. add item to the budget controller
            var newItem = budgetCtlr.addItem(input.expenseType, input.expDesc, input.expAmt);

            // 3. add item to UI
            uiCtlr.addListItem(newItem, input.expenseType);

           // 4. clear fields after adding item
            uiCtlr.clearFields();

           // 5. update the budget
            updateBudget();
        }
    }

    return {

        init: function(){
            setupEventListeners();
        }
    }

})(budgetController, uiController);

controller.init();<commit_msg>updated app functionality<commit_after>var budgetController = (function(){

    var Expense = function(id, description, value){
        this.id = id;
        this.description = description;
        this.value = value;
        this.percentage = -1;
    };

    Expense.prototype.calcPercentage = function(totalIncome){

        if(totalIncome > 0){
            console.log('tptal income: '+totalIncome);
            this.percentage = Math.round((this.value / totalIncome) * 100);
            console.log('percentage: '+this.percentage);
        }
    }

    Expense.prototype.getPercentage = function(){
        return this.percentage;
    }

    var Income = function(id, description, value){
        this.id = id;
        this.description = description;
        this.value = value;
    };

    var data = {
        allitems: {
            exp: [],
            inc: []
        },
        totals:{
            exp: 0,
            inc: 0
        },
        budget: 0,
        percentage: -1
    };

    var calculateTotal = function(type){

        var sum = 0;
        data.allitems[type].forEach(function(cur){
            sum += cur.value;
        });
        data.totals[type] = sum;
    };

    return {
        addItem: function(type, desc, val){
            var newItem, Id;

            if(data.allitems[type].length > 0){
               Id = data.allitems[type][data.allitems[type].length - 1].id + 1;
            } else {
               Id = 0;
            }
            if('inc' === type){
                newItem = new Income(Id, desc, val);
            }else{
                newItem = new Expense(Id, desc, val);
            }
            data.allitems[type].push(newItem);
            return newItem;
        },

        deleteItem: function(type, id){

            var ids, index;
            ids = data.allitems[type].map(function(current){
                return current.id;
            });
            index = ids.indexOf(id);

            if(index !== -1){
                data.allitems[type].splice(index, 1);
            }
        },

        calculateBudget: function(){

            // calculate the total income and expenses
            calculateTotal('exp');
            calculateTotal('inc');

            // calculate the budget: income - expenses
            data.budget = data.totals.inc - data.totals.exp;

            // calculate the percentage of income that we spent
            if(data.totals.inc > 0){
               data.percentage = Math.round((data.totals.exp / data.totals.inc) * 100);
            }
        },

        calculatePercentages: function(){

            data.allitems['exp'].forEach(function(cur){
                cur.calcPercentage(data.totals.inc);
            });
        },

        getPercentages: function(){

            var allPerc = data.allitems.exp.map(function(cur){
                return cur.getPercentage();
            });
            return allPerc;
        },

        getBudget: function(){

            return {
                totalinc: data.totals.inc,
                totalExp: data.totals.exp,
                percent: data.percentage,
                budget: data.budget
            }
        }
    }
})();

var uiController = (function(){

        var uiDomStrings = {
            typeClass: '.add__type',
            descClass: '.add__description',
            valueClass: '.add__value',
            btnClass: '.add__btn',
            incomeListClass: '.income__list',
            expenseListClass: '.expense__list',
            incomeDisplayClass: '.budget__income--value',
            expenseDisplayClass: '.budget__expenses--value',
            budgetPercentClass: '.budget__expenses--percentage',
            budgetValueClass: '.budget__value',
            containerClass: '.container',
            expItemsPercentClass: '.item__percentage'
        }

    return {

        getInputData: function(){
            return {
                expenseType: document.querySelector(uiDomStrings.typeClass).value,
                expDesc: document.querySelector(uiDomStrings.descClass).value,
                expAmt: parseFloat(document.querySelector(uiDomStrings.valueClass).value)
            }
        },

        addListItem: function(obj, type){

            var html, htmlWithData, element;

            // create HTML string with place holder text
            if('inc' === type){
                element = uiDomStrings.incomeListClass;
               html = '

%description%

%value%

 <button class="item__delete--btn"></button>

';
            } else {
                element = uiDomStrings.expenseListClass;
               html = '

%description%

- %value%

 ---

 <button class="item__delete--btn"></button>

';
            }

            // replace the placeholder text with some actual data
            htmlWithData = html.replace('%id%', obj.id);
            htmlWithData = htmlWithData.replace('%value%', obj.value);
            htmlWithData = htmlWithData.replace('%description%', obj.description);

            // insert the html in to the DOM
            document.querySelector(element).insertAdjacentHTML('beforeend', htmlWithData);
        },

        deleteListItem: function(selectorId){

            var el;
            el = document.getElementById(selectorId);
            console.log(el.parentNode);
            el.parentNode.removeChild(el);
        },

        getDomStrings: function(){
            return uiDomStrings;
        },

        displayPercentages: function(percentages){

            var fields = document.querySelectorAll(uiDomStrings.expItemsPercentClass);

            for(var i = 0; i < fields.length; i++){
                fields[i].textContent = percentages[i] +' %';
            }
        },

        clearFields: function(){

            var fields, fieldsArr;
            fields = document.querySelectorAll(uiDomStrings.descClass +','+ uiDomStrings.valueClass);
            fieldsArr = Array.prototype.slice.call(fields);

            fieldsArr.forEach(function(current, index, array){
                current.value = '';
                if(index === 0){
                   current.focus();
                }
            });
        },

        updateBudgetToUI: function(budget){

            document.querySelector(uiDomStrings.incomeDisplayClass).textContent = '+ '+budget.totalinc;
            document.querySelector(uiDomStrings.expenseDisplayClass).textContent = '- '+budget.totalExp;

            if(budget.percent > 0){
               document.querySelector(uiDomStrings.budgetPercentClass).textContent = budget.percent + '%';
            }else {
               document.querySelector(uiDomStrings.budgetPercentClass).textContent = '---';
            }

            document.querySelector(uiDomStrings.budgetValueClass).textContent = '+ '+budget.budget;
        }
    }

})();

var controller = (function(budgetCtlr, uiCtlr){

    var setupEventListeners = function (){
        var uiDomStrings = uiCtlr.getDomStrings();
        document.querySelector(uiDomStrings.btnClass).addEventListener('click', ctrlAddItem);
        document.addEventListener('keypress', function(event){
           if(event.keyCode === 13 || event.which === 13){
              ctrlAddItem();
            }
        });

        document.querySelector(uiDomStrings.containerClass).addEventListener('click', ctrlDeleteItem);
    };

    var updateBudget = function(){

        // 4. calculate the budget
        budgetCtlr.calculateBudget();

        var budget = budgetCtlr.getBudget();

        // 5. display the budget to UI
        uiCtlr.updateBudgetToUI(budget);
    };

        var updatePercentages = function(){

        // calculate percentages
        budgetCtlr.calculatePercentages();

        // read from budget controller
        var percentages = budgetCtlr.getPercentages();

        // update UI with new percentages
        uiCtlr.displayPercentages(percentages);
    };

    var ctrlAddItem = function(){

       // 1. get the field input data
        var input = uiCtlr.getInputData();

        if(input.expDesc !== '' && !isNaN(input.expAmt) && input.expAmt > 0){

            // 2. add item to the budget controller
            var newItem = budgetCtlr.addItem(input.expenseType, input.expDesc, input.expAmt);

            // 3. add item to UI
            uiCtlr.addListItem(newItem, input.expenseType);

           // 4. clear fields after adding item
            uiCtlr.clearFields();

           // 5. update the budget
            updateBudget();

           // 6 calculate and update percentages
            updatePercentages();
        }
    };

    var ctrlDeleteItem = function(event){

        var itemId, splitId, type, id;

        itemId = event.target.parentNode.parentNode.parentNode.parentNode.id;
        if(itemId){
            splitId = itemId.split('-')
            type = splitId[0];
            id = splitId[1];

            // 1. delete the item from the data structure
            budgetCtlr.deleteItem(type.substring(0, 3), parseInt(id));

            // 2. Delete the item from the UI
            uiCtlr.deleteListItem(itemId);

            // 3. Update and show the new budget
            updateBudget();

            // 4. calculate and update percentages
            updatePercentages();
        }
    };

    return {

        init: function(){
            setupEventListeners();
        }
    };

})(budgetController, uiController);

controller.init();<|endoftext|>


<commit_before>'use strict';

const P = require('bluebird');
const domino = require('domino');
const assert = require('assert');
const underscore = require('underscore');
const uuid = require('cassandra-uuid').TimeUuid;
const HTTPError = require('./util').HTTPError;
const mUtil = {};

mUtil.CONTENT_TYPES = {
    html: { name: 'HTML', version: '2.1.0', type: 'text/html' },
    mobileSections: { name: 'mobile-sections', version: '0.14.5', type: 'application/json' },
    media: { name: 'Media', version: '1.4.5', type: 'application/json' },
    mobileHtml: { name: 'Mobile-HTML', version: '1.1.0', type: 'text/html' },
    references: { name: 'References', version: '2.0.0', type: 'application/json' },
    metadata: { name: 'Metadata', version: '1.3.0', type: 'application/json' },
    summary: { name: 'Summary', version: '1.4.1', type: 'application/json' },
    definition: { name: 'definition', version: '0.8.1', type: 'application/json' },
    random: { name: 'Random', version: '0.6.0', type: 'application/json' },
    announcements: { name: 'Announcements', version: '0.3.0', type: 'application/json' },
    onthisday: { name: 'OnThisDay', version: '0.3.3', type: 'application/json' },
    css: { name: 'CSS', version: '2.0.0', type: 'text/css' },
    javascript: { name: 'JavaScript', version: '1.0.0', type: 'text/javascript' },
    availability: { name: 'Availability', version: '1.0.1', type: 'application/json' },
    unpublished: { name: 'Unpublished', version: '0.0.0', type: 'application/json' },
    mobileHtmlOfflineResources: {
        name: 'Mobile-HTML-Offline-Resources',
        version: '1.0.0',
        type: 'application/json'
    },
    talk: { name: 'Talk', version: '0.1.1', type: 'application/json' },
    mediaList: { name: 'MediaList', version: '1.0.1', type: 'application/json' }
};

/**
 * Pass on the Content-Security-Policy HTTP header
 * @param {!Object} response the HTTPResponse object on which to set the headers
 * @param {!string} value the CSP value to set
 */
mUtil.setContentSecurityPolicy = (response, value) => {
    // Note that .set will over-write, not expand
    response.set('content-security-policy', value);
    response.set('x-content-security-policy', value);
    response.set('x-webkit-csp', value);
};

mUtil.getContentTypeString = function(spec) {
    return `${spec.type}; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/${spec.name}/${spec.version}"`;
};

mUtil.setContentType = function(res, spec) {
    if (!spec.name || !spec.version) {
        throw new HTTPError({
            status: 500,
            type: 'invalid_content_type',
            title: 'Only use the allowed content-types',
            detail: `${spec.name}/${spec.version} is not an allowed content-type!`
        });
    }

    res.type(mUtil.getContentTypeString(spec));
};

/**
 * Check if a variable is empty.
 * @param {string} val input value
 * @return {!boolean} true if val is null, undefined, an empty object, an empty array, or an empty
 * string.
 */
mUtil.isEmpty = function(val) {
    return !underscore.isNumber(val)
        && !underscore.isBoolean(val)
        && underscore.isEmpty(val);
};

mUtil.isNonempty = underscore.negate(mUtil.isEmpty);

/**
 * @param {*} val input value
 * @param {*} [fallback] the default value to assign if val is empty
 * @return {*} val if nonempty, else fallback.
*/
mUtil.defaultVal = function(val, fallback) {
    return underscore.isEmpty(val) ? fallback : val;
};

/**
 * @param {*} val input value
 * @return {*} val less empty elements.
*/
mUtil.filterEmpty = function(val) {
    if (Array.isArray(val)) {
        return val.map(mUtil.filterEmpty).filter(mUtil.isNonempty);
    }
    if (underscore.isObject(val)) {
        return underscore.pick(underscore.mapObject(val, mUtil.filterEmpty), mUtil.isNonempty);
    }
    return val;
};

/**
 * Sets the ETag header on the response object, comprised of the revision ID and
 * the time UUID. If the latter is not given, the current time stamp is used to
 * generate it.
 * @param {!Object} response The HTTPResponse object on which to set the header
 * @param {!number|string} revision The revision integer ID to use
 * @param {?string} tid      The time UUID to use; optional
 */
mUtil.setETag = function(response, revision, tid = undefined) {
    assert(['string','number'].includes(typeof revision));
    assert(['string','undefined'].includes(typeof tid));
    if (!tid) {
        tid = uuid.now().toString();
    }
    response.set('etag', `"${revision}/${tid}"`);
};

/**
 * Pass on the Vary and Content-Language HTTP headers from Parsoid if set
 * @param {!Object} response The HTTPResponse object on which to set the headers
 * @param {?number} headers  The incoming response's headers from which to read
 */
mUtil.setLanguageHeaders = (response, headers) => {
    if (headers) {
        // Downcast all keys to lower-case, as the spec doesn't require it.
        const lowerHeaders = Object.keys(headers).reduce((newHeaders, key) => {
            newHeaders[key.toLowerCase()] = headers[key];
            return newHeaders;
        }, {});

        if (lowerHeaders.vary) {
            const vary = lowerHeaders.vary.split(',')
                .map(e => e.trim())
                .filter(e => !/^[Aa]ccept$/.test(e))
                .join(', ');

            // Note that .vary will expand, not over-write
            if (vary) {
                response.vary(vary);
            }
        }
        if (lowerHeaders['content-language']) {
            // Note that .set will over-write, not expand
            response.set('Content-Language', lowerHeaders['content-language']);
        }
    }
};

/**
 * Convert mobile to canonical domain,
 * e.g., 'en.m.wikipedia.org' => 'en.wikipedia.org'
 */
mUtil.mobileToCanonical = function(domain) {
    return domain.replace('.m.', '.');
};

/**
 * Remove the top-level domain from a domain string, e.g., 'en.wikipedia.org' ->
 * 'en.wikipedia'.
 */
mUtil.removeTLD = function(domain) {
    return domain.split('.').slice(0,2).join('.');
};

/**
 * Swaps a domain's lowest-level subdomain with the provided language code.
 * This is assumed to be invalid for provided domains with two or fewer levels, and the domain is
 * returned unchanged.
 * Example: ('en.wikipedia.org', 'de') -> 'de.wikipedia.org'
 * Example: ('de.wikipedia.beta.wmflabs.org', 'ja') -> 'ja.wikipedia.beta.wmflabs.org'
 * Example: ('mediawiki.org', 'es') -> 'mediawiki.org'
 */
mUtil.domainForLangCode = (domain, code) => {
    return domain.split('.').length < 3 ? domain : `${code}${domain.slice(domain.indexOf('.'))}`;
};

mUtil.removeFragment = function(href) {
    if (href.indexOf('#') > -1) {
        return href.substring(0, href.indexOf('#'));
    }
    return href;
};

mUtil.removeLinkPrefix = function(href) {
    return href.replace(/^\.\//, '');
};

mUtil.extractDbTitleFromAnchor = function(anchor) {
    return anchor && mUtil.removeFragment(mUtil.removeLinkPrefix(anchor.getAttribute('href')));
};

mUtil.getRbPageSummaryUrl = function(restbaseTpl, domain, title) {
    const request = restbaseTpl.expand({
        request: {
            params: {
                domain,
                path: `page/summary/${encodeURIComponent(title)}` },
        }
    });
    return request.uri;
};

mUtil.throw404 = function(message) {
    throw new HTTPError({
        status: 404,
        type: 'not_found',
        title: 'Not found',
        detail: message
    });
};

mUtil.deduplicate = function(array) {
    return Array.from(new Set(array));
};

/* jslint bitwise: true */
/* eslint no-bitwise: ["error", { "allow": ["<<"] }] */
mUtil.hashCode = function(string) {
    return string.split('').reduce((prevHash, currVal) =>
        ((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
};

/**
 * Gets the data-mw-section-id for the section containing the element.
 * @param {!Element} el An Element
 * @return {?integer} the data-mw-section-id for the section containing the element
 */
mUtil.getSectionIdForElement = (el) => {
    const section = el.closest('section');
    return section && parseInt(section.getAttribute('data-mw-section-id'), 10);
};

/**
 * Historically, the MediaWiki API indicated boolean values in responses by including the property
 * with a value of "" if true, and omitting the property if false.  JSON format version 2, which
 * we always request, specifies that fields with boolean values should return true boolean 'true'
 * or 'false' values.  However, not all API modules have been updated to implement formatversion=2.
 * This method can be used to handle both cases and to guard against unexpected changes in modules
 * not yet updated.
 * @param {!Object} obj enclosing object
 * @param {!string} prop property with a boolean meaning
 * @return {!boolean} true if the value means true in either API format
 */
mUtil.mwApiTrue = (obj, prop) => {
    return obj[prop] === true || obj[prop] === '';
};

/**
 * Creates a domino Document object from an HTML text string or an array of HTML text strings.
 * Takes advantage of functionality provided by domino's internal HTMLParser implementation,
 * but not exposed in its public API via the createDocument() method, to allow parsing an HTML
 * string in chunks.
 * Returns a Promise resolving as a Document rather than synchronously parsing and returning the
 * Document as in Domino's own implementation.
 * @param {?string} html HTML string
 * @return {!Promise} a promise resolving in a Domino HTML document representing the input
 */
mUtil.createDocument = (html) => {
    if (typeof html !== 'string') {
        return P.reject(new HTTPError({
                status: 500,
                type: 'invalid_request_parameters',
                title: 'HTML input should be a string'
        }));
    }
    const MAX_PARSING_MS_PER_TICK = 200;

    function _pauseAfter(ms) {
        const start = Date.now();
        return () => (Date.now() - start) >= ms;
    }

    function _processIncremental(parser) {
        return new P((res, rej) => setImmediate(() => {
            try {
                if (parser.process(_pauseAfter(MAX_PARSING_MS_PER_TICK))) {
                    res(_processIncremental(parser));
                } else {
                    res(parser.document());
                }
            } catch (e) {
                rej(e);
            }
        }));
    }

    return P.resolve(domino.createIncrementalHTMLParser())
    .then((parser) => {
        parser.end(html);
        return _processIncremental(parser);
    });
};

module.exports = mUtil;
<commit_msg>Hygiene: Update error type string for createDocument param type check<commit_after>'use strict';

const P = require('bluebird');
const domino = require('domino');
const assert = require('assert');
const underscore = require('underscore');
const uuid = require('cassandra-uuid').TimeUuid;
const HTTPError = require('./util').HTTPError;
const mUtil = {};

mUtil.CONTENT_TYPES = {
    html: { name: 'HTML', version: '2.1.0', type: 'text/html' },
    mobileSections: { name: 'mobile-sections', version: '0.14.5', type: 'application/json' },
    media: { name: 'Media', version: '1.4.5', type: 'application/json' },
    mobileHtml: { name: 'Mobile-HTML', version: '1.1.0', type: 'text/html' },
    references: { name: 'References', version: '2.0.0', type: 'application/json' },
    metadata: { name: 'Metadata', version: '1.3.0', type: 'application/json' },
    summary: { name: 'Summary', version: '1.4.1', type: 'application/json' },
    definition: { name: 'definition', version: '0.8.1', type: 'application/json' },
    random: { name: 'Random', version: '0.6.0', type: 'application/json' },
    announcements: { name: 'Announcements', version: '0.3.0', type: 'application/json' },
    onthisday: { name: 'OnThisDay', version: '0.3.3', type: 'application/json' },
    css: { name: 'CSS', version: '2.0.0', type: 'text/css' },
    javascript: { name: 'JavaScript', version: '1.0.0', type: 'text/javascript' },
    availability: { name: 'Availability', version: '1.0.1', type: 'application/json' },
    unpublished: { name: 'Unpublished', version: '0.0.0', type: 'application/json' },
    mobileHtmlOfflineResources: {
        name: 'Mobile-HTML-Offline-Resources',
        version: '1.0.0',
        type: 'application/json'
    },
    talk: { name: 'Talk', version: '0.1.1', type: 'application/json' },
    mediaList: { name: 'MediaList', version: '1.0.1', type: 'application/json' }
};

/**
 * Pass on the Content-Security-Policy HTTP header
 * @param {!Object} response the HTTPResponse object on which to set the headers
 * @param {!string} value the CSP value to set
 */
mUtil.setContentSecurityPolicy = (response, value) => {
    // Note that .set will over-write, not expand
    response.set('content-security-policy', value);
    response.set('x-content-security-policy', value);
    response.set('x-webkit-csp', value);
};

mUtil.getContentTypeString = function(spec) {
    return `${spec.type}; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/${spec.name}/${spec.version}"`;
};

mUtil.setContentType = function(res, spec) {
    if (!spec.name || !spec.version) {
        throw new HTTPError({
            status: 500,
            type: 'invalid_content_type',
            title: 'Only use the allowed content-types',
            detail: `${spec.name}/${spec.version} is not an allowed content-type!`
        });
    }

    res.type(mUtil.getContentTypeString(spec));
};

/**
 * Check if a variable is empty.
 * @param {string} val input value
 * @return {!boolean} true if val is null, undefined, an empty object, an empty array, or an empty
 * string.
 */
mUtil.isEmpty = function(val) {
    return !underscore.isNumber(val)
        && !underscore.isBoolean(val)
        && underscore.isEmpty(val);
};

mUtil.isNonempty = underscore.negate(mUtil.isEmpty);

/**
 * @param {*} val input value
 * @param {*} [fallback] the default value to assign if val is empty
 * @return {*} val if nonempty, else fallback.
*/
mUtil.defaultVal = function(val, fallback) {
    return underscore.isEmpty(val) ? fallback : val;
};

/**
 * @param {*} val input value
 * @return {*} val less empty elements.
*/
mUtil.filterEmpty = function(val) {
    if (Array.isArray(val)) {
        return val.map(mUtil.filterEmpty).filter(mUtil.isNonempty);
    }
    if (underscore.isObject(val)) {
        return underscore.pick(underscore.mapObject(val, mUtil.filterEmpty), mUtil.isNonempty);
    }
    return val;
};

/**
 * Sets the ETag header on the response object, comprised of the revision ID and
 * the time UUID. If the latter is not given, the current time stamp is used to
 * generate it.
 * @param {!Object} response The HTTPResponse object on which to set the header
 * @param {!number|string} revision The revision integer ID to use
 * @param {?string} tid      The time UUID to use; optional
 */
mUtil.setETag = function(response, revision, tid = undefined) {
    assert(['string','number'].includes(typeof revision));
    assert(['string','undefined'].includes(typeof tid));
    if (!tid) {
        tid = uuid.now().toString();
    }
    response.set('etag', `"${revision}/${tid}"`);
};

/**
 * Pass on the Vary and Content-Language HTTP headers from Parsoid if set
 * @param {!Object} response The HTTPResponse object on which to set the headers
 * @param {?number} headers  The incoming response's headers from which to read
 */
mUtil.setLanguageHeaders = (response, headers) => {
    if (headers) {
        // Downcast all keys to lower-case, as the spec doesn't require it.
        const lowerHeaders = Object.keys(headers).reduce((newHeaders, key) => {
            newHeaders[key.toLowerCase()] = headers[key];
            return newHeaders;
        }, {});

        if (lowerHeaders.vary) {
            const vary = lowerHeaders.vary.split(',')
                .map(e => e.trim())
                .filter(e => !/^[Aa]ccept$/.test(e))
                .join(', ');

            // Note that .vary will expand, not over-write
            if (vary) {
                response.vary(vary);
            }
        }
        if (lowerHeaders['content-language']) {
            // Note that .set will over-write, not expand
            response.set('Content-Language', lowerHeaders['content-language']);
        }
    }
};

/**
 * Convert mobile to canonical domain,
 * e.g., 'en.m.wikipedia.org' => 'en.wikipedia.org'
 */
mUtil.mobileToCanonical = function(domain) {
    return domain.replace('.m.', '.');
};

/**
 * Remove the top-level domain from a domain string, e.g., 'en.wikipedia.org' ->
 * 'en.wikipedia'.
 */
mUtil.removeTLD = function(domain) {
    return domain.split('.').slice(0,2).join('.');
};

/**
 * Swaps a domain's lowest-level subdomain with the provided language code.
 * This is assumed to be invalid for provided domains with two or fewer levels, and the domain is
 * returned unchanged.
 * Example: ('en.wikipedia.org', 'de') -> 'de.wikipedia.org'
 * Example: ('de.wikipedia.beta.wmflabs.org', 'ja') -> 'ja.wikipedia.beta.wmflabs.org'
 * Example: ('mediawiki.org', 'es') -> 'mediawiki.org'
 */
mUtil.domainForLangCode = (domain, code) => {
    return domain.split('.').length < 3 ? domain : `${code}${domain.slice(domain.indexOf('.'))}`;
};

mUtil.removeFragment = function(href) {
    if (href.indexOf('#') > -1) {
        return href.substring(0, href.indexOf('#'));
    }
    return href;
};

mUtil.removeLinkPrefix = function(href) {
    return href.replace(/^\.\//, '');
};

mUtil.extractDbTitleFromAnchor = function(anchor) {
    return anchor && mUtil.removeFragment(mUtil.removeLinkPrefix(anchor.getAttribute('href')));
};

mUtil.getRbPageSummaryUrl = function(restbaseTpl, domain, title) {
    const request = restbaseTpl.expand({
        request: {
            params: {
                domain,
                path: `page/summary/${encodeURIComponent(title)}` },
        }
    });
    return request.uri;
};

mUtil.throw404 = function(message) {
    throw new HTTPError({
        status: 404,
        type: 'not_found',
        title: 'Not found',
        detail: message
    });
};

mUtil.deduplicate = function(array) {
    return Array.from(new Set(array));
};

/* jslint bitwise: true */
/* eslint no-bitwise: ["error", { "allow": ["<<"] }] */
mUtil.hashCode = function(string) {
    return string.split('').reduce((prevHash, currVal) =>
        ((prevHash << 5) - prevHash) + currVal.charCodeAt(0), 0);
};

/**
 * Gets the data-mw-section-id for the section containing the element.
 * @param {!Element} el An Element
 * @return {?integer} the data-mw-section-id for the section containing the element
 */
mUtil.getSectionIdForElement = (el) => {
    const section = el.closest('section');
    return section && parseInt(section.getAttribute('data-mw-section-id'), 10);
};

/**
 * Historically, the MediaWiki API indicated boolean values in responses by including the property
 * with a value of "" if true, and omitting the property if false.  JSON format version 2, which
 * we always request, specifies that fields with boolean values should return true boolean 'true'
 * or 'false' values.  However, not all API modules have been updated to implement formatversion=2.
 * This method can be used to handle both cases and to guard against unexpected changes in modules
 * not yet updated.
 * @param {!Object} obj enclosing object
 * @param {!string} prop property with a boolean meaning
 * @return {!boolean} true if the value means true in either API format
 */
mUtil.mwApiTrue = (obj, prop) => {
    return obj[prop] === true || obj[prop] === '';
};

/**
 * Creates a domino Document object from an HTML text string or an array of HTML text strings.
 * Takes advantage of functionality provided by domino's internal HTMLParser implementation,
 * but not exposed in its public API via the createDocument() method, to allow parsing an HTML
 * string in chunks.
 * Returns a Promise resolving as a Document rather than synchronously parsing and returning the
 * Document as in Domino's own implementation.
 * @param {?string} html HTML string
 * @return {!Promise} a promise resolving in a Domino HTML document representing the input
 */
mUtil.createDocument = (html) => {
    if (typeof html !== 'string') {
        return P.reject(new HTTPError({
                status: 500,
                type: 'invalid_parameter_type',
                title: 'HTML input should be a string'
        }));
    }
    const MAX_PARSING_MS_PER_TICK = 200;

    function _pauseAfter(ms) {
        const start = Date.now();
        return () => (Date.now() - start) >= ms;
    }

    function _processIncremental(parser) {
        return new P((res, rej) => setImmediate(() => {
            try {
                if (parser.process(_pauseAfter(MAX_PARSING_MS_PER_TICK))) {
                    res(_processIncremental(parser));
                } else {
                    res(parser.document());
                }
            } catch (e) {
                rej(e);
            }
        }));
    }

    return P.resolve(domino.createIncrementalHTMLParser())
    .then((parser) => {
        parser.end(html);
        return _processIncremental(parser);
    });
};

module.exports = mUtil;
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');
var util = require('util');
var BluebirdPromise = require('bluebird');
var Class = require('../util/class');
var Manager = require('./manager');
var HasMany = require('../relations/has_many');
var BelongsTo = require('../relations/belongs_to');
var property = require('../util/property').fn;
var attr = require('./attr').fn;
var inflection = require('../util/inflection');

_.str = require('underscore.string');

// TODO: consider how to set up the model class. we would like to get the
// schema from the db before doing anything with the model so that we can
// support getter/setter methods that are aware of the types they're working
// with and can report better errors. that may mean using a `setup` function
// like:
//
//     var Person = db.Model.extend();
//     Person.setup().then(function() {
//       Person.create().save();
//     });
//
// or on the database:
//
//     db.setup().then(function() {
//       Person.create().save();
//     });
//
// other thoughts:
//
//     db.loadModels('./path/to/dir')
//     db.prepareModels(Person, ...)
//     var Person = db.Model.extend().prepare().then('...')
//
// another idea:
//
// simply force the user to define all attributes on a model. an advantage to
// this is self-documenting code. a disadvantage is having to write more code.
// the writing more code could be solved by adding tools to the CLI that would
// allow inspection of a database to create/update models (similar to what the
// rails annotate tool does).

// REVISIT: createOrUpdate()

// REVISIT: support difference between join queries (one-to-one) and
// prefetching (one-to-many)?

// REVISIT: support difference between join queries (for actual querying) and
// prefetching (associations for use)?

// REVISIT: perform a join query when there are conditions & build all objects
// from the result data (which will include duplicates). when there are no
// conditions, simply perform multiple queries to prefetch data to avoid
// duplicate data being sent back to JS.

// TODO: some of the features that are desired like using `pk` as part of a
// `where` or having joins performed automatically based on the key in a
// `where` would require some sort of introspection of the condition &
// manipulation of the query. this may be hard to achieve. in addition, it may
// not actually lead to better code -- forcing the user to actually specify a
// `join`, for instance, leads to more clarity in the code & forces the
// developer to make a conscious decision while writing the query. make a
// decision regarding how to handle this.

/**
 * The base model class.
 *
 * ## Accessing Objects
 *
 *     Article.objects.all().then('...'); // -> select * from "articles";
 *     Article.objects.where({ pk: 1 }).fetch().then('...');
 *     // -> select * from "articles" where "articles"."id" = ?;
 *     // !> [1]
 *
 * ## Table Level Queries
 *
 *     var Article = Model.extend();
 *     Article.objects.delete(); // -> delete from "articles";
 *     Article.objects.update({ title: 'Article' });
 *     // -> update "articles" set "title" = ?;
 *     // !> ['Article']
 *
 * ## Options
 *
 *     var Article = Model.extend();
 *
 *     Article.reopenClass({
 *       tableName: 'articles',
 *     });
 *
 * ## Associations
 *
 *     var hasMany = Model.hasMany;
 *     var belongsTo = Model.belongsTo;
 *
 *     var Article = Model.extend({
 *       author: belongsTo('user', { foreignKey: 'author_id', primaryKey: 'id' })
 *     });
 *     module.exports = Article.reopenClass({ __name__: 'Article' });
 *
 *     var User = Model.extend({
 *       articles: hasMany(Article, { foreignKey: 'author_id', primaryKey: 'id' })
 *     });
 *     module.exports = User.reopenClass({ __name__: 'User' });
 *
 *     Article.objects.with('blog').fetch().then('...');
 *     // -> select * from "articles";
 *     // -> select * from "blogs" where "blog"."id" in (?, ?);
 *     // !> [3,5,8]
 *
 *     Article.objects.where({ 'blog.title[contains]': 'Azul' }).fetch().then('...');
 *     // -> select * from "articles" left join "blogs" on "articles"."blog_id" = "blog"."id"
 *     // -> where "blogs"."title" like ?;
 *     // !> ["%Whit%"];
 *
 *     Article.objects.where({ 'title[contains]': 'Azul' }).fetchOne().then('...');
 *     // throws if not exactly one record
 *
 *     var user = User.create();
 *     user.save().then('...'); // insert
 *
 *     user.save().then('...'); // no save because no changes
 *     user.name = 'Whitney';
 *     user.save().then('...'); // update
 *
 *     user.delete();
 *     user.save(); // delete
 *
 *     // note: user.destory(); // shortcut for user.delete().save()
 *
 * ### Accessing Relationships
 *
 *     User.objects.find(1).then(function(user) {
 *       user.articles; // throws because this was not `with` articles
 *     });
 *
 *     User.objects.with('articles').find(1).fetch().then(function(user) {
 *       user.articles; // okay
 *     });
 *
 *     User.objects.find(1)
 *     .then(function(user) { return user.fetch('articles'); })
 *     .then(function(user) {
 *       return bog.articles; // okay because of prior fetch
 *     });
 *
 *     Article.objects.find(1).then(function(article) {
 *       article.author; // throws because this was not `with` the author
 *     });
 *
 *     Article.objects.with('author').find(1).then(function(article) {
 *       article.author; // okay
 *     });
 *
 * ### Querying
 *
 *     Article.objects.where({ 'author.name[contains]': 'Whit' }).fetch().then('...');
 *     // -> select * from "articles"
 *     // -> left join "users" on "articles"."author_id" = "users"."id"
 *     // -> where "users"."name" like ?;
 *     // !> ["%Whit%"]
 *
 *     Article.objects.where({ 'author.name[contains]': 'Whit' })
 *       .orderBy('-name')
 *       .limit(10)
 *       .offset(20)
 *       .fetch()
 *       .then('...');
 *     // -> select * from "articles"
 *     // -> left join users on "articles"."author_id" = "users"."id"
 *     // -> where "users"."name" like ?
 *     // -> order by "users"."name" desc
 *     // -> limit 10 offset 20;
 *     // !> ["%Whit%"]
 *
 *     Article.objects.with('author', 'comments').fetch().then('...');
 *     // -> select * from "articles";
 *     // -> select * from "users" where "users"."id" in (?, ?);
 *     // !> [10,32]
 *     // -> select * from "comments" where "comments"."id" in (?, ?, ?);
 *     // !> [12,43,73]
 *
 *
 * ### Manipulating Relationships
 *
 *
 *     var article = Article.create({ title: 'Example' });
 *     user.addArticle(article).then('...');
 *     // -> insert into "articles" ("title", "author_id") values ?, ?;
 *     // !> ["Example", 1]
 *
 *     user.createArticle({ title: 'Example' }).save().then(function(article) {
 *       // ...
 *     });
 *     // -> insert into "articles" ("title", "author_id") values ?, ?;
 *     // !> ["Example", 1]
 *
 *     user.addArticles(article1, article2).then(function(articles) {
 *       // ...
 *     });
 *     // -> insert into "articles" ("title", "author_id") values (?, ?), (?, ?);
 *     // !> ["Example1", 1, "Example2", 1]
 *
 *     user.removeArticles(article1, article2).then('...');
 *     // -> update "articles" set "author_id" = null
 *     // -> where "articles"."id" in (?, ?);
 *     // !> [1, 2]
 *
 *     user.removeArticle(article1).then('...');
 *     // -> update "articles" set "author_id" = null
 *     // -> where "articles"."id" = ?;
 *     // !> [1]
 *
 *     user.clearArticles().then('...');
 *     // -> update "articles" set "author_id" = null
 *     // -> where "articles"."author_id" = ?;
 *     // !> [1]
 *
 * @public
 * @constructor Model
 */
var Model = Class.extend(/** @lends Model# */ {
  init: function(properties) {
    this._attrs = {};
    this._dirtyAttributes = {};
    this._persisted = false;
    this._inFlight = false;

    _.extend(this, properties, {
      _dirtyAttributes: {}
    });
  },

  /**
   * Each model comes by default with an `pk` attribute. By default, this
   * corresponds to the `id` column in the database. If you use a different
   * column name in your database, you can override this property:
   *
   *     MyModel.reopen({
   *       pk: attr('identifier')
   *     });
   *
   * If you prefer to access your custom column by another name, you can add an
   * additional attribute, making the column accessible by both attributes:
   *
   *     MyModel.reopen({
   *       identifier: attr('identifier')
   *     });
   *
   * @type {Integer|?}
   */
  pk: attr('id'),

  /**
   * An alias for the `pk` property.
   *
   * @type {Integer|?}
   * @see {@link Model#pk}
   */
  id: property({ property: 'pk', writable: true }),

  /**
   * Get an attribute by the database column name. This is the preferred way of
   * reading individual attributes rather than using {@link Model#attrs}.
   *
   * @method
   * @protected
   */
  getAttribute: function(attr) {
    return this.attrs[attr];
  },

  /**
   * Get an attribute by the database column name. This is the only acceptable
   * way to set attributes in `attrs`. Do set them directly.
   *
   * @method
   * @protected
   */
  setAttribute: function(attr, val) {
    this._dirtyAttributes[attr] = true;
    this._attrs[attr] = val;
  },

  /**
   * Mark an attribute as clean.
   *
   * @method
   * @protected
   */
  cleanAttribute: function(attr) {
    delete this._dirtyAttributes[attr];
  },

  /**
   * Save a model instance. Objects that are not {@link Model#persisted} will
   * cause an `insert` to occur. Objects that have been persisted, but are
   * {@link Model#dirty} will cause an `update` to occur. Otherwise, no action
   * will be taken.
   *
   * After the save occurs, the model will be marked as clean & persisted.
   *
   * Multiple saves of the same object at the same time will result in an
   * error.
   *
   * @method
   * @public
   * @param {Object} [options]
   * @param {Boolean} [options.force] Force a save of the object even if it
   * does not have any dirty attributes.
   */
  save: BluebirdPromise.method(function(options) {
    var self = this;
    var opts = options || {};
    var pk = this.pkAttr;
    var where = _.pick(this.attrs, pk);
    var attrs = _.omit(this.attrs, pk);
    var query;

    if (this.inFlight) {
      throw new Error(util.format(
        'Cannot save %s while another save is in flight.',
        this.__identity__.__name__));
    }

    if (!this.persisted) {
      query = this.__identity__.objects
      .insert(attrs).returning(pk)
      .execute()
      .tap(function(result) {
        self.id = result[0][pk];
      });
    }
    else if (this.dirty || opts.force) {
      query = this.__identity__.objects
        .where(where).update(attrs)
        .execute();
    }

    if (query) {
      this._inFlight = true;
      query = query.tap(this._afterSave.bind(this));
    }

    return BluebirdPromise.resolve(query).return(this);
  }),

  /**
   * This method is called after an object is successfully saved. It marks the
   * object as clean, persisted, and no longer in flight.
   *
   * @method
   * @protected
   */
  _afterSave: function() {
    this._dirtyAttributes = {};
    this._persisted = true;
    this._inFlight = false;
  },

  /**
   * The values of the database attributes, keyed by the actual column name.
   * This property is readonly, and altering the values the object is not
   * supported.
   *
   * @public
   * @readonly
   * @type {Object}
   */
  attrs: property(function() {
    return _.clone(this._attrs);
  }),

  /**
   * Whether this model is persisted, that is it has either been loaded from
   * the database or saved since it was created. Objects that are persisted can
   * still be {@link Model#dirty}.
   *
   * @public
   * @type {Boolean}
   */
  persisted: property(),

  /**
   * Whether properties on this object have been changed since it was created
   * or loaded from the database.
   *
   * @public
   * @type {Boolean}
   */
  dirty: property(function() {
    return !!Object.keys(this._dirtyAttributes).length;
  }),

  /**
   * An array of all attributes that are dirty on this object.
   *
   * @public
   * @type {Array}
   */
  dirtyAttributes: property(function() {
    return Object.keys(this._dirtyAttributes);
  }),

  /**
   * Whether this object is in the process of being saved.
   *
   * @protected
   * @type {Boolean}
   */
  inFlight: property()

});

Model.reopenClass(/** @lends Model */ {

  /**
   * Access to the {@link Manager} for all objects corresponding to this model.
   * This is the entry point to allow manipulation of the underlying data.
   * Subclasses can override this property to define a custom manager that
   * changes the default set of data that this model works with. Subclasses can
   * also add additional managers, allowing quick access to different data
   * sets (also sometimes referred to as scopes).
   *
   * @protected
   * @type {Manager}
   */
  objects: Manager.create(),

  /**
   * Create a database attribute. The preferred user facing function for this
   * is {@link Database#attr}.
   *
   * @private
   * @function
   * @see {@link Database#attr}
   */
  attr: attr,

  /**
   * Create a has-many relationship. The preferred user facing function for
   * this is {@link Database#hasMany}.
   *
   * @private
   * @function
   * @see {@link Database#hasMany}
   */
  hasMany: HasMany.attr(),

  /**
   * Create a belongs-to relationship. The preferred user facing function for
   * this is {@link Database#belongsTo}.
   *
   * @private
   * @function
   * @see {@link Database#belongsTo}
   */
  belongsTo: BelongsTo.attr(),

  /**
   * The name of the database table. Subclasses can override this property. It
   * defaults to the pluralized, underscored class name.
   *
   * @protected
   * @type {String}
   */
  tableName: property(function() {
    return this._tableName ||
      inflection.pluralize(_.str.underscored(this.__name__));
  }, { writable: true }),

  /**
   * A {@link Manager} will use this method to set attributes of a model when
   * it is loaded from the database via a query.
   *
   * @method
   * @protected
   * @param {Object} attrs The attributes read from the database.
   */
  load: function(attrs) {
    return _.extend(this.create({ _persisted: true }), {
      _attrs: attrs
    });
  }

});

module.exports = Model.reopenClass({ __name__: 'Model' });
<commit_msg>Updated docs to match implementation/expected implementation.<commit_after>'use strict';

var _ = require('lodash');
var util = require('util');
var BluebirdPromise = require('bluebird');
var Class = require('../util/class');
var Manager = require('./manager');
var HasMany = require('../relations/has_many');
var BelongsTo = require('../relations/belongs_to');
var property = require('../util/property').fn;
var attr = require('./attr').fn;
var inflection = require('../util/inflection');

_.str = require('underscore.string');

// TODO: consider how to set up the model class. we would like to get the
// schema from the db before doing anything with the model so that we can
// support getter/setter methods that are aware of the types they're working
// with and can report better errors. that may mean using a `setup` function
// like:
//
//     var Person = db.model('person');
//     Person.setup().then(function() {
//       Person.create().save();
//     });
//
// or on the database:
//
//     db.setup().then(function() {
//       Person.create().save();
//     });
//
// other thoughts:
//
//     db.loadModels('./path/to/dir')
//     db.prepareModels(Person, ...)
//     var Person = db.model('person').prepare().then('...')
//
// another idea:
//
// simply force the user to define all attributes on a model. an advantage to
// this is self-documenting code. a disadvantage is having to write more code.
// the writing more code could be solved by adding tools to the CLI that would
// allow inspection of a database to create/update models (similar to what the
// rails annotate tool does).

// REVISIT: createOrUpdate()

// REVISIT: support difference between join queries (one-to-one) and
// prefetching (one-to-many)?

// REVISIT: support difference between join queries (for actual querying) and
// prefetching (associations for use)?

// REVISIT: perform a join query when there are conditions & build all objects
// from the result data (which will include duplicates). when there are no
// conditions, simply perform multiple queries to prefetch data to avoid
// duplicate data being sent back to JS.

// TODO: some of the features that are desired like using `pk` as part of a
// `where` or having joins performed automatically based on the key in a
// `where` would require some sort of introspection of the condition &
// manipulation of the query. this may be hard to achieve. in addition, it may
// not actually lead to better code -- forcing the user to actually specify a
// `join`, for instance, leads to more clarity in the code & forces the
// developer to make a conscious decision while writing the query. make a
// decision regarding how to handle this.

/**
 * The base model class.
 *
 * ## Accessing Objects
 *
 *     var attr = db.attr;
 *     var Article = db.model('article', {
 *       title: attr(),
 *     });
 *
 *     var User = db.model('user', {
 *       username: attr()
 *     });
 *
 *     Article.objects.all().then('...'); // -> select * from "articles";
 *     Article.objects.where({ pk: 1 }).fetch().then('...');
 *     // -> select * from "articles" where "articles"."id" = ?;
 *     // !> [1]
 *
 *     Article.objects.where({ 'title[contains]': 'Azul' }).fetchOne().then('...');
 *     // promise is rejected if not exactly one record
 *
 *     var user = User.create();
 *     user.save().then('...'); // insert
 *
 *     user.save().then('...'); // no save because no changes
 *     user.username = 'wbyoung';
 *     user.save().then('...'); // update
 *
 *     user.delete();
 *     user.save(); // delete
 *
 *     // note: user.destory(); // shortcut for user.delete().save()
 *
 * ## Table Level Queries
 *
 *     var Article = db.model('article');
 *     Article.objects.delete(); // -> delete from "articles";
 *     Article.objects.update({ title: 'Article' });
 *     // -> update "articles" set "title" = ?;
 *     // !> ['Article']
 *
 * ## Options
 *
 *     var Article = db.model('article');
 *
 *     Article.reopenClass({
 *       tableName: 'articles',
 *     });
 *
 * ## Associations
 *
 *     var attr = db.attr;
 *     var hasMany = db.hasMany;
 *     var belongsTo = db.belongsTo;
 *
 *     var Blog = db.model('blog', {
 *       title: attr()
 *     });
 *
 *     var Article = db.model('article', {
 *       title: attr(),
 *       blog: belongsTo(),
 *       author: belongsTo('user')
 *     });
 *
 *     var User = db.model('user', {
 *       username: attr(),
 *       articles: hasMany('article', { inverse: 'author' })
 *     });
 *
 *     Article.objects.with('author').fetch().then('...');
 *     // -> select * from "articles";
 *     // -> select * from "users" where "user"."id" in (?, ?, ?) limit 3;
 *     // !> [3,5,8]
 *
 *     Article.objects.where({ 'blog.title[contains]': 'Azul' }).fetch().then('...');
 *     // -> select * from "articles" left join "blogs" on "articles"."blog_id" = "blogs"."id"
 *     // -> where "blogs"."title" like ?;
 *     // !> ["%Azul%"];
 *
 * ### Accessing Relationships
 *
 *     User.objects.find(1).then(function(user) {
 *       user.articles; // throws because this was not `with` articles
 *     });
 *
 *     User.objects.with('articles').find(1).fetch().then(function(user) {
 *       user.articles; // okay
 *     });
 *
 *     User.objects.find(1)
 *     .then(function(user) { return user.articleObjects.fetch(); })
 *     .then(function(user) {
 *       return user.articles; // okay because of prior fetch
 *     });
 *
 *     Article.objects.find(1).then(function(article) {
 *       article.author; // throws because this was not `with` the author
 *     });
 *
 *     Article.objects.with('author').find(1).then(function(article) {
 *       article.author; // okay
 *     });
 *
 * ### Querying
 *
 *     Article.objects.where({ 'author.name[contains]': 'Whit' }).fetch().then('...');
 *     // -> select * from "articles"
 *     // -> left join "users" on "articles"."author_id" = "users"."id"
 *     // -> where "users"."name" like ?;
 *     // !> ["%Whit%"]
 *
 *     Article.objects.where({ 'author.name[contains]': 'Whit' })
 *       .orderBy('-name')
 *       .limit(10)
 *       .offset(20)
 *       .fetch()
 *       .then('...');
 *     // -> select * from "articles"
 *     // -> left join users on "articles"."author_id" = "users"."id"
 *     // -> where "users"."name" like ?
 *     // -> order by "users"."name" desc
 *     // -> limit 10 offset 20;
 *     // !> ["%Whit%"]
 *
 *     Article.objects.with('author', 'comments').fetch().then('...');
 *     // -> select * from "articles";
 *     // -> select * from "users" where "users"."id" in (?, ?);
 *     // !> [10,32]
 *     // -> select * from "comments" where "comments"."id" in (?, ?, ?);
 *     // !> [12,43,73]
 *
 *
 * ### Manipulating Relationships
 *
 *
 *     var article = Article.create({ title: 'Example' });
 *     user.addArticle(article).then('...');
 *     // -> insert into "articles" ("title", "author_id") values ?, ?;
 *     // !> ["Example", 1]
 *
 *     user.createArticle({ title: 'Example' }).save().then(function(article) {
 *       // ...
 *     });
 *     // -> insert into "articles" ("title", "author_id") values ?, ?;
 *     // !> ["Example", 1]
 *
 *     user.addArticles(existingArticle1, existingArticle2).then('...');
 *     // -> update "articles" set "author_id" = ? where ."id" in (?, ?);
 *     // !> [1, 23, 84]
 *
 *     user.removeArticles(existingArticle1, existingArticle2).then('...');
 *     // -> update "articles" set "author_id" = null "id" in (?, ?);
 *     // !> [1, 2]
 *
 *     user.removeArticle(existingArticle1).then('...');
 *     // -> update "articles" set "author_id" = null "id" = ?;
 *     // !> [1]
 *
 *     user.clearArticles().then('...');
 *     // -> update "articles" set "author_id" = null "author_id" = ?;
 *     // !> [1]
 *
 * @public
 * @constructor Model
 */
var Model = Class.extend(/** @lends Model# */ {
  init: function(properties) {
    this._attrs = {};
    this._dirtyAttributes = {};
    this._persisted = false;
    this._inFlight = false;

    _.extend(this, properties, {
      _dirtyAttributes: {}
    });
  },

  /**
   * Each model comes by default with an `pk` attribute. By default, this
   * corresponds to the `id` column in the database. If you use a different
   * column name in your database, you can override this property:
   *
   *     MyModel.reopen({
   *       pk: attr('identifier')
   *     });
   *
   * If you prefer to access your custom column by another name, you can add an
   * additional attribute, making the column accessible by both attributes:
   *
   *     MyModel.reopen({
   *       identifier: attr('identifier')
   *     });
   *
   * @type {Integer|?}
   */
  pk: attr('id'),

  /**
   * An alias for the `pk` property.
   *
   * @type {Integer|?}
   * @see {@link Model#pk}
   */
  id: property({ property: 'pk', writable: true }),

  /**
   * Get an attribute by the database column name. This is the preferred way of
   * reading individual attributes rather than using {@link Model#attrs}.
   *
   * @method
   * @protected
   */
  getAttribute: function(attr) {
    return this.attrs[attr];
  },

  /**
   * Get an attribute by the database column name. This is the only acceptable
   * way to set attributes in `attrs`. Do set them directly.
   *
   * @method
   * @protected
   */
  setAttribute: function(attr, val) {
    this._dirtyAttributes[attr] = true;
    this._attrs[attr] = val;
  },

  /**
   * Mark an attribute as clean.
   *
   * @method
   * @protected
   */
  cleanAttribute: function(attr) {
    delete this._dirtyAttributes[attr];
  },

  /**
   * Save a model instance. Objects that are not {@link Model#persisted} will
   * cause an `insert` to occur. Objects that have been persisted, but are
   * {@link Model#dirty} will cause an `update` to occur. Otherwise, no action
   * will be taken.
   *
   * After the save occurs, the model will be marked as clean & persisted.
   *
   * Multiple saves of the same object at the same time will result in an
   * error.
   *
   * @method
   * @public
   * @param {Object} [options]
   * @param {Boolean} [options.force] Force a save of the object even if it
   * does not have any dirty attributes.
   */
  save: BluebirdPromise.method(function(options) {
    var self = this;
    var opts = options || {};
    var pk = this.pkAttr;
    var where = _.pick(this.attrs, pk);
    var attrs = _.omit(this.attrs, pk);
    var query;

    if (this.inFlight) {
      throw new Error(util.format(
        'Cannot save %s while another save is in flight.',
        this.__identity__.__name__));
    }

    if (!this.persisted) {
      query = this.__identity__.objects
      .insert(attrs).returning(pk)
      .execute()
      .tap(function(result) {
        self.id = result[0][pk];
      });
    }
    else if (this.dirty || opts.force) {
      query = this.__identity__.objects
        .where(where).update(attrs)
        .execute();
    }

    if (query) {
      this._inFlight = true;
      query = query.tap(this._afterSave.bind(this));
    }

    return BluebirdPromise.resolve(query).return(this);
  }),

  /**
   * This method is called after an object is successfully saved. It marks the
   * object as clean, persisted, and no longer in flight.
   *
   * @method
   * @protected
   */
  _afterSave: function() {
    this._dirtyAttributes = {};
    this._persisted = true;
    this._inFlight = false;
  },

  /**
   * The values of the database attributes, keyed by the actual column name.
   * This property is readonly, and altering the values the object is not
   * supported.
   *
   * @public
   * @readonly
   * @type {Object}
   */
  attrs: property(function() {
    return _.clone(this._attrs);
  }),

  /**
   * Whether this model is persisted, that is it has either been loaded from
   * the database or saved since it was created. Objects that are persisted can
   * still be {@link Model#dirty}.
   *
   * @public
   * @type {Boolean}
   */
  persisted: property(),

  /**
   * Whether properties on this object have been changed since it was created
   * or loaded from the database.
   *
   * @public
   * @type {Boolean}
   */
  dirty: property(function() {
    return !!Object.keys(this._dirtyAttributes).length;
  }),

  /**
   * An array of all attributes that are dirty on this object.
   *
   * @public
   * @type {Array}
   */
  dirtyAttributes: property(function() {
    return Object.keys(this._dirtyAttributes);
  }),

  /**
   * Whether this object is in the process of being saved.
   *
   * @protected
   * @type {Boolean}
   */
  inFlight: property()

});

Model.reopenClass(/** @lends Model */ {

  /**
   * Access to the {@link Manager} for all objects corresponding to this model.
   * This is the entry point to allow manipulation of the underlying data.
   * Subclasses can override this property to define a custom manager that
   * changes the default set of data that this model works with. Subclasses can
   * also add additional managers, allowing quick access to different data
   * sets (also sometimes referred to as scopes).
   *
   * @protected
   * @type {Manager}
   */
  objects: Manager.create(),

  /**
   * Create a database attribute. The preferred user facing function for this
   * is {@link Database#attr}.
   *
   * @private
   * @function
   * @see {@link Database#attr}
   */
  attr: attr,

  /**
   * Create a has-many relationship. The preferred user facing function for
   * this is {@link Database#hasMany}.
   *
   * @private
   * @function
   * @see {@link Database#hasMany}
   */
  hasMany: HasMany.attr(),

  /**
   * Create a belongs-to relationship. The preferred user facing function for
   * this is {@link Database#belongsTo}.
   *
   * @private
   * @function
   * @see {@link Database#belongsTo}
   */
  belongsTo: BelongsTo.attr(),

  /**
   * The name of the database table. Subclasses can override this property. It
   * defaults to the pluralized, underscored class name.
   *
   * @protected
   * @type {String}
   */
  tableName: property(function() {
    return this._tableName ||
      inflection.pluralize(_.str.underscored(this.__name__));
  }, { writable: true }),

  /**
   * A {@link Manager} will use this method to set attributes of a model when
   * it is loaded from the database via a query.
   *
   * @method
   * @protected
   * @param {Object} attrs The attributes read from the database.
   */
  load: function(attrs) {
    return _.extend(this.create({ _persisted: true }), {
      _attrs: attrs
    });
  }

});

module.exports = Model.reopenClass({ __name__: 'Model' });
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

module.exports = function( environment, mongoInstance ) {

  var mongoosastic = require( "mongoosastic" ),
      mongooseValidator = require( "mongoose-validator" ),
      validate = mongooseValidator.validate,
      env = environment,
      url = require( "url" ),
      Mongoose = mongoInstance,
      elasticSearchURL = env.get( "FOUNDELASTICSEARCH_URL" ) ||
                         env.get( "BONSAI_URL" ) ||
                         env.get( "ELASTIC_SEARCH_URL" );

      elasticSearchURL = url.parse( elasticSearchURL );

  mongooseValidator.extend( "isURL", function() {
    var str = this.str;
    // This is a copy of https://github.com/chriso/node-validator/blob/master/lib/validators.js#L29-L32 which
    // is used for "isUrl" with mongoose-validator. Modified to accept underscores in the hostname.
    return str.length < 2083 && str.match(/^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\_\u00a1-\uffff0-9]+-?)*[a-z\_\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i);
  });

  var Timestamp = {
    type: Number,
    es_type: "long",
    es_indexed: true,
    es_index: "not_analyzed"
  };

  var likeSchema = new Mongoose.Schema({
    userId: Number
  });

  // Schema
  var schema = new Mongoose.Schema({
    url: {
      type: String,
      required: true,
      es_indexed: true,
      validate: validate( "isURL" ),
      unique: true,
      es_index: "not_analyzed"
    },
    contenturl: {
      type: String,
      es_indexed: false,
      validate: validate( "isURL" ),
      unique: true
    },
    contentType: {
      type: String,
      es_indexed: true,
      required: true,
      es_index: "not_analyzed"
    },
    locale: {
      type: String,
      "default": "en_US",
      es_indexed: true,
      es_index: "not_analyzed"
    },
    title: {
      type: String,
      es_indexed: true,
      required: true
    },
    description: {
      type: String,
      es_indexed: true
    },
    thumbnail: {
      type: String,
      es_indexed: true,
      es_index: "not_analyzed"
    },
    author: {
      type: String,
      required: false,
      es_indexed: true,
      es_index: "not_analyzed"
    },
    email: {
      type: String,
      required: true,
      validate: validate( "isEmail" ),
      es_indexed: true,
      es_index: "not_analyzed"
    },
    published: {
      type: Boolean,
      "default": true,
      es_index: "not_analyzed"
    },
    tags: {
      type: [ String ],
      es_indexed: true,
      es_index: "not_analyzed",
      es_type: "String"
    },
    remixedFrom: {
      type: String,
      "default": null,
      es_indexed: true,
      es_index: "not_analyzed"
    },
    likes: {
      type: [ likeSchema ],
      es_indexed: true
    },
    createdAt: Timestamp,
    updatedAt: Timestamp,
    deletedAt: {
      type: Number,
      "default": null,
      es_indexed: true,
      es_type: "long"
    }
  });

  schema.set( "toJSON", { virtuals: true } );

  schema.virtual( "id" ).get(function() {
    return this._id;
  });

  schema.plugin( mongoosastic, {
    port: elasticSearchURL.port || 80,
    host: ( elasticSearchURL.auth ? elasticSearchURL.auth + "@" : "" ) + elasticSearchURL.hostname,
    hydrate: true
  });

  var Make = Mongoose.model( "Make", schema );

  Make.createMapping(function( err, mapping ) {
    if ( err ) {
      console.log( "failed to create mapping", err.toString() );
    }
  });

  Make.publicFields = [ "url", "contentType", "contenturl", "locale",
                        "title", "description", "author", "published", "tags",
                        "likes", "thumbnail", "remixedFrom" ];

  return Make;
};
<commit_msg>Bug 935170 - index contenturl in elasticsearch<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

module.exports = function( environment, mongoInstance ) {

  var mongoosastic = require( "mongoosastic" ),
      mongooseValidator = require( "mongoose-validator" ),
      validate = mongooseValidator.validate,
      env = environment,
      url = require( "url" ),
      Mongoose = mongoInstance,
      elasticSearchURL = env.get( "FOUNDELASTICSEARCH_URL" ) ||
                         env.get( "BONSAI_URL" ) ||
                         env.get( "ELASTIC_SEARCH_URL" );

      elasticSearchURL = url.parse( elasticSearchURL );

  mongooseValidator.extend( "isURL", function() {
    var str = this.str;
    // This is a copy of https://github.com/chriso/node-validator/blob/master/lib/validators.js#L29-L32 which
    // is used for "isUrl" with mongoose-validator. Modified to accept underscores in the hostname.
    return str.length < 2083 && str.match(/^(?!mailto:)(?:(?:https?|ftp):\/\/)?(?:\S+(?::\S*)?@)?(?:(?:(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\_\u00a1-\uffff0-9]+-?)*[a-z\_\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]+-?)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))|localhost)(?::\d{2,5})?(?:\/[^\s]*)?$/i);
  });

  var Timestamp = {
    type: Number,
    es_type: "long",
    es_indexed: true,
    es_index: "not_analyzed"
  };

  var likeSchema = new Mongoose.Schema({
    userId: Number
  });

  // Schema
  var schema = new Mongoose.Schema({
    url: {
      type: String,
      required: true,
      es_indexed: true,
      validate: validate( "isURL" ),
      unique: true,
      es_index: "not_analyzed"
    },
    contenturl: {
      type: String,
      es_indexed: true,
      validate: validate( "isURL" ),
      unique: true
    },
    contentType: {
      type: String,
      es_indexed: true,
      required: true,
      es_index: "not_analyzed"
    },
    locale: {
      type: String,
      "default": "en_US",
      es_indexed: true,
      es_index: "not_analyzed"
    },
    title: {
      type: String,
      es_indexed: true,
      required: true
    },
    description: {
      type: String,
      es_indexed: true
    },
    thumbnail: {
      type: String,
      es_indexed: true,
      es_index: "not_analyzed"
    },
    author: {
      type: String,
      required: false,
      es_indexed: true,
      es_index: "not_analyzed"
    },
    email: {
      type: String,
      required: true,
      validate: validate( "isEmail" ),
      es_indexed: true,
      es_index: "not_analyzed"
    },
    published: {
      type: Boolean,
      "default": true,
      es_index: "not_analyzed"
    },
    tags: {
      type: [ String ],
      es_indexed: true,
      es_index: "not_analyzed",
      es_type: "String"
    },
    remixedFrom: {
      type: String,
      "default": null,
      es_indexed: true,
      es_index: "not_analyzed"
    },
    likes: {
      type: [ likeSchema ],
      es_indexed: true
    },
    createdAt: Timestamp,
    updatedAt: Timestamp,
    deletedAt: {
      type: Number,
      "default": null,
      es_indexed: true,
      es_type: "long"
    }
  });

  schema.set( "toJSON", { virtuals: true } );

  schema.virtual( "id" ).get(function() {
    return this._id;
  });

  schema.plugin( mongoosastic, {
    port: elasticSearchURL.port || 80,
    host: ( elasticSearchURL.auth ? elasticSearchURL.auth + "@" : "" ) + elasticSearchURL.hostname,
    hydrate: true
  });

  var Make = Mongoose.model( "Make", schema );

  Make.createMapping(function( err, mapping ) {
    if ( err ) {
      console.log( "failed to create mapping", err.toString() );
    }
  });

  Make.publicFields = [ "url", "contentType", "contenturl", "locale",
                        "title", "description", "author", "published", "tags",
                        "likes", "thumbnail", "remixedFrom" ];

  return Make;
};
<|endoftext|>