<commit_before>import Promise from 'bluebird';
import config from 'config';
import debugLib from 'debug';
import { keyBy, pick } from 'lodash';
import moment from 'moment';

import MemberRoles from '../server/constants/roles.ts';
import emailLib from '../server/lib/email';
import { getBackersStats, getHostedCollectives, sumTransactions } from '../server/lib/hostlib';
import { stripHTML } from '../server/lib/sanitize-html';
import { getTransactions } from '../server/lib/transactions';
import { exportToPDF } from '../server/lib/utils';
import models, { Op, sequelize } from '../server/models';

const debug = debugLib('hostreport');

const summary = {
  totalHosts: 0,
  totalActiveHosts: 0,
  totalCollectives: 0,
  totalActiveCollectives: 0,
  numberTransactions: 0,
  numberDonations: 0,
  numberPaidExpenses: 0,
  hosts: [],
};

async function HostReport(year, month, hostId) {
  let startDate, endDate;

  const d = new Date();
  d.setFullYear(year);
  let yearlyReport = false;
  if (typeof month === 'number') {
    d.setMonth(month);
    startDate = new Date(d.getFullYear(), d.getMonth(), 1);
    endDate = new Date(d.getFullYear(), d.getMonth() + 1, 1);
  } else {
    // yearly report
    month = '';
    yearlyReport = true;
    startDate = new Date(d.getFullYear(), 0, 1);
    endDate = new Date(d.getFullYear() + 1, 0, 1);
  }

  const endDateIncluded = moment(endDate).subtract(1, 'days').toDate();

  const dateRange = {
    createdAt: { [Op.gte]: startDate, [Op.lt]: endDate },
  };

  const emailTemplate = yearlyReport ? 'host.yearlyreport' : 'host.monthlyreport'; // NOTE: this will be later converted to 'host.report'
  const reportName = yearlyReport ? `${year} Yearly Host Report` : `${year}/${month + 1} Monthly Host Report`;
  const dateFormat = yearlyReport ? 'YYYY' : 'YYYYMM';
  const csvFilename = `${moment(d).format(dateFormat)}-transactions.csv`;
  const pdfFilename = `${moment(d).format(dateFormat)}-expenses.pdf`;
  console.log('startDate', startDate, 'endDate', endDate);

  year = year || startDate.getFullYear();

  let previewCondition = '';
  if (process.env.DEBUG && process.env.DEBUG.match(/preview/)) {
    previewCondition = 'AND c.id IN (11004, 9804, 9802, 9801)';
  } // open source collective host, wwcode host, brusselstogether, changex
  // previewCondition = "AND c.id IN (9802)"; // brusselstogether

  if (hostId) {
    previewCondition = `AND c.id = ${hostId}`;
  }

  if (process.env.SLUGS) {
    const slugs = process.env.SLUGS.split(',');
    previewCondition = `AND c.slug IN ('${slugs.join("','")}')`;
  }
  if (process.env.SKIP_SLUGS) {
    const slugs = process.env.SKIP_SLUGS.split(',');
    previewCondition = `AND c.slug NOT IN ('${slugs.join("','")}')`;
  }

  const getHostStats = (host, collectiveids) => {
    // Since collectives can change host,
    // we don't fetch transactions based on the CollectiveId but based on the HostCollectiveId
    // at the time of the transaction
    const where = { HostCollectiveId: host.id };
    const whereWithDateRange = { ...where, ...dateRange };

    const payoutProcessorFeesQuery = service => {
      // When using Sequelize query.include[].where it performs the condition on the join and it doesn't work
      // that's why have to do the query this this way.
      if (service === 'paypal') {
        where['$PaymentMethod.service$'] = 'paypal';
      } else {
        where['$PaymentMethod.service$'] = {
          [Op.or]: { [Op.ne]: 'paypal', [Op.is]: null },
        };
      }

      return {
        where: { ...where, ...dateRange, type: 'DEBIT' },
        raw: true,
        include: [
          {
            model: models.PaymentMethod,
            attributes: [[sequelize.fn('MAX', sequelize.col('service')), 'service']],
          },
        ],
      };
    };

    return Promise.props({
      balance: sumTransactions(
        'netAmountInCollectiveCurrency',
        { where: { ...where, createdAt: { [Op.lt]: endDate } } },
        host.currency,
      ), // total host balance
      delta: sumTransactions('netAmountInCollectiveCurrency', { where: whereWithDateRange }, host.currency), // delta host balance last month
      totalAmountDonations: sumTransactions(
        'amount',
        { where: { ...whereWithDateRange, type: 'CREDIT' } },
        host.currency,
      ), // total donations last month
      totalNetAmountReceivedForCollectives: sumTransactions(
        'netAmountInCollectiveCurrency',
        { where: { ...whereWithDateRange, type: 'CREDIT' } },
        host.currency,
      ), // total net amount received last month
      totalAmountPaidExpenses: sumTransactions(
        'netAmountInCollectiveCurrency',
        { where: { ...whereWithDateRange, type: 'DEBIT' } },
        host.currency,
      ), // total net amount paid out last month
      totalTaxAmountCollected: sumTransactions(
        'taxAmount',
        { where: whereWithDateRange, type: 'CREDIT' },
        host.currency,
      ), // total tax collected
      totalTaxAmountPaid: sumTransactions('taxAmount', { where: whereWithDateRange, type: 'DEBIT' }, host.currency), // total tax paid
      // We only count HostFees on CREDIT transactions with an Order and on DEBIT transactions with an Expense
      // (because the host fee of the destination host is recorded in the DEBIT transaction when making a donation to another host)
      totalHostFees: sumTransactions(
        'hostFeeInHostCurrency',
        {
          where: {
            ...whereWithDateRange,
            [Op.or]: [
              { type: 'CREDIT', OrderId: { [Op.ne]: null } },
              { type: 'DEBIT', ExpenseId: { [Op.ne]: null } },
            ],
          },
        },
        host.currency,
      ),
      backers: getBackersStats(startDate, endDate, collectiveids),
      platformFees: sumTransactions(
        'platformFeeInHostCurrency',
        { where: { ...whereWithDateRange, type: 'CREDIT' } },
        host.currency,
      ),
      paymentProcessorFees: sumTransactions(
        'paymentProcessorFeeInHostCurrency',
        { where: { ...whereWithDateRange, type: 'CREDIT' } },
        host.currency,
      ), // total stripe fees
      payoutProcessorFeesPaypal: sumTransactions(
        'paymentProcessorFeeInHostCurrency',
        payoutProcessorFeesQuery('paypal'),
        host.currency,
      ), // total paypal fees
      payoutProcessorFeesOther: sumTransactions(
        'paymentProcessorFeeInHostCurrency',
        payoutProcessorFeesQuery('other'),
        host.currency,
      ), // total other payout processor fees (manually recorded)
    });
  };

  const processHost = host => {
    summary.totalHosts++;
    console.log('>>> Processing host', host.slug);
    const data = {},
      attachments = [];
    const note = 'using fxrate of the day of the transaction as provided by the ECB. Your effective fxrate may vary.';
    const expensesPerPage = 30; // number of expenses per page of the Table Of Content (for PDF export)

    let collectivesById = {};
    let page = 1;
    let currentPage = 0;

    data.host = host.info;
    data.collective = host.info;
    data.reportDate = endDate;
    data.reportName = reportName;
    data.month = !yearlyReport && moment(startDate).format('MMMM');
    data.year = year;
    data.startDate = startDate;
    data.endDate = endDate;
    data.endDateIncluded = endDateIncluded;
    data.config = pick(config, 'host');
    data.maxSlugSize = 0;
    data.notes = null;
    data.expensesPerPage = [[]];
    data.stats = {
      numberPaidExpenses: 0,
    };

    const getHostAdminsEmails = host => {
      if (host.type === 'USER') {
        return models.User.findAll({ where: { CollectiveId: host.id } }).map(u => u.email);
      }
      return models.Member.findAll({
        where: {
          CollectiveId: host.id,
          role: { [Op.or]: [MemberRoles.ADMIN, MemberRoles.ACCOUNTANT] },
        },
      }).map(
        admin => {
          return models.User.findOne({
            attributes: ['email'],
            where: { CollectiveId: admin.MemberCollectiveId },
          }).then(user => user.email);
        },
        { concurrency: 1 },
      );
    };

    const processTransaction = transaction => {
      const t = {
        ...transaction.info,
        Expense: transaction.Expense
          ? {
              ...transaction.Expense.info,
              items: transaction.Expense.items.map(item => item.dataValues),
            }
          : null,
      };
      t.collective = collectivesById[t.CollectiveId].dataValues;
      t.collective.shortSlug = t.collective.slug.replace(/^wwcode-?(.)/, '$1');
      t.notes = t.Expense && t.Expense.privateMessage && stripHTML(t.Expense.privateMessage);
      if (t.data && t.data.fxrateSource) {
        t.notes = t.notes ? `${t.notes} (${note})` : note;
        data.notes = note;
      }

      // We prepare expenses for the PDF export
      if (t.type === 'DEBIT' && t.ExpenseId) {
        t.page = page++;
        data.stats.numberPaidExpenses++;
        if ((page - 1) % expensesPerPage === 0) {
          currentPage++;
          data.expensesPerPage[currentPage] = [];
        }
        data.expensesPerPage[currentPage].push(t);
      }

      data.maxSlugSize = Math.max(data.maxSlugSize, t.collective.shortSlug.length + 1);
      if (!t.description) {
        return transaction.getSource().then(source => {
          if (!source) {
            console.log('>>> no source found for ', transaction);
            return t;
          }
          t.description = source.description;
          return t;
        });
      } else {
        return Promise.resolve(t);
      }
    };

    return getHostedCollectives(host.id, endDate)
      .tap(collectives => {
        collectivesById = keyBy(collectives, 'id');
        data.stats.totalCollectives = collectives.filter(c => c.type === 'COLLECTIVE').length;
        summary.totalCollectives += data.stats.totalCollectives;
        console.log(`>>> processing ${data.stats.totalCollectives} collectives`);
      })
      .then(() =>
        getTransactions(Object.keys(collectivesById), startDate, endDate, {
          include: [
            {
              model: models.Expense,
              include: [
                'fromCollective',
                {
                  model: models.ExpenseItem,
                  as: 'items',
                  where: {
                    url: { [Op.not]: null },
                  },
                },
              ],
            },
            {
              model: models.User,
              as: 'createdByUser',
            },
          ],
        }),
      )
      .tap(transactions => {
        if (!transactions || transactions.length == 0) {
          throw new Error('No transaction found');
        }
        console.log(`>>> processing ${transactions.length} transactions`);
      })
      .map(processTransaction)
      .tap(transactions => {
        const csv = models.Transaction.exportCSV(transactions, collectivesById);
        attachments.push({
          filename: `${host.slug}-${csvFilename}`,
          content: csv,
        });
      })
      .then(transactions => (data.transactions = transactions))
      .then(() => {
        // Don't generate PDF in email if it's the yearly report
        if (yearlyReport || process.env.SKIP_PDF) {
          return;
        }
        return exportToPDF('expenses', data, {
          paper: host.currency === 'USD' ? 'Letter' : 'A4',
        });
      })
      .then(pdf => {
        if (pdf) {
          attachments.push({
            filename: `${host.slug}-${pdfFilename}`,
            content: pdf,
          });
          data.expensesPdf = true;
        }
      })
      .then(() => getHostStats(host, Object.keys(collectivesById)))
      .then(stats => {
        stats.totalAmountPaid = {
          totalInHostCurrency:
            stats.totalAmountPaidExpenses.totalInHostCurrency +
            stats.paymentProcessorFees.totalInHostCurrency +
            stats.platformFees.totalInHostCurrency,
        };
        stats.totalAmountSpent = {
          totalInHostCurrency:
            stats.totalAmountPaidExpenses.totalInHostCurrency +
            stats.payoutProcessorFeesPaypal.totalInHostCurrency +
            stats.payoutProcessorFeesOther.totalInHostCurrency,
        };
        stats.totalHostRevenue = {
          totalInHostCurrency: stats.totalHostFees.totalInHostCurrency,
        };
        // Total net amount received on the host's bank account = total amount - payment processor fees and platform fees
        stats.totalNetAmountReceived = {
          totalInHostCurrency:
            stats.totalAmountDonations.totalInHostCurrency +
            stats.paymentProcessorFees.totalInHostCurrency +
            stats.platformFees.totalInHostCurrency,
        };

        data.stats = {
          ...data.stats,
          ...stats,
          totalActiveCollectives: Object.keys(keyBy(data.transactions, 'CollectiveId')).length,
          numberTransactions: data.transactions.length,
          numberDonations: data.transactions.length - data.stats.numberPaidExpenses,
        };
        summary.hosts.push({
          host: { name: host.name, slug: host.slug, currency: host.currency },
          stats: data.stats,
        });
        summary.totalActiveHosts++;
        summary.totalActiveCollectives += data.stats.totalActiveCollectives;
        summary.numberTransactions += data.stats.numberTransactions;
        summary.numberDonations += data.stats.numberDonations;
        summary.numberPaidExpenses += data.stats.numberPaidExpenses;
        summary.totalAmountPaidExpenses += data.stats.totalAmountPaidExpenses;

        // Don't send transactions in email if there is more than 1000
        if (data.transactions.length > 1000) {
          delete data.transactions;
        }
      })
      .then(() => getHostAdminsEmails(host))
      .then(admins => sendEmail(admins, data, attachments))
      .catch(e => {
        console.error(`Error in processing host ${host.slug}:`, e.message);
        debug(e);
      });
  };

  const sendEmail = (recipients, data, attachments) => {
    debug('Sending email to ', recipients);
    if (!recipients || recipients.length === 0) {
      console.error('Unable to send host report for ', data.host.slug, 'No recipient to send to');
      return;
    }
    // debug("email data transactions", data.transactions);
    debug('email data stats', data.stats);
    debug('email data stats.backers', data.stats.backers);
    const options = { attachments };
    return emailLib.send(emailTemplate, recipients, data, options);
  };

  const query = `
  with "hosts" as (SELECT DISTINCT "HostCollectiveId" AS id FROM "Collectives" WHERE "deletedAt" IS NULL AND "isActive" IS TRUE AND "HostCollectiveId" IS NOT NULL)
  SELECT c.* FROM "Collectives" c WHERE c.id IN (SELECT h.id FROM hosts h) ${previewCondition}
  `;

  const hosts = await sequelize.query(query, {
    model: models.Collective,
    type: sequelize.QueryTypes.SELECT,
  });
  console.log(`Preparing the ${reportName} for ${hosts.length} hosts`);

  return Promise.map(hosts, processHost, { concurrency: 1 }).then(() => {
    console.log('>>> All done. Exiting.');
    process.exit(0);
  });
}

export default HostReport;
<commit_msg>protect host report against pdf generation/sending issues<commit_after>import Promise from 'bluebird';
import config from 'config';
import debugLib from 'debug';
import { keyBy, pick } from 'lodash';
import moment from 'moment';

import MemberRoles from '../server/constants/roles.ts';
import emailLib from '../server/lib/email';
import { getBackersStats, getHostedCollectives, sumTransactions } from '../server/lib/hostlib';
import { stripHTML } from '../server/lib/sanitize-html';
import { getTransactions } from '../server/lib/transactions';
import { exportToPDF } from '../server/lib/utils';
import models, { Op, sequelize } from '../server/models';

const debug = debugLib('hostreport');

const summary = {
  totalHosts: 0,
  totalActiveHosts: 0,
  totalCollectives: 0,
  totalActiveCollectives: 0,
  numberTransactions: 0,
  numberDonations: 0,
  numberPaidExpenses: 0,
  hosts: [],
};

async function HostReport(year, month, hostId) {
  let startDate, endDate;

  const d = new Date();
  d.setFullYear(year);
  let yearlyReport = false;
  if (typeof month === 'number') {
    d.setMonth(month);
    startDate = new Date(d.getFullYear(), d.getMonth(), 1);
    endDate = new Date(d.getFullYear(), d.getMonth() + 1, 1);
  } else {
    // yearly report
    month = '';
    yearlyReport = true;
    startDate = new Date(d.getFullYear(), 0, 1);
    endDate = new Date(d.getFullYear() + 1, 0, 1);
  }

  const endDateIncluded = moment(endDate).subtract(1, 'days').toDate();

  const dateRange = {
    createdAt: { [Op.gte]: startDate, [Op.lt]: endDate },
  };

  const emailTemplate = yearlyReport ? 'host.yearlyreport' : 'host.monthlyreport'; // NOTE: this will be later converted to 'host.report'
  const reportName = yearlyReport ? `${year} Yearly Host Report` : `${year}/${month + 1} Monthly Host Report`;
  const dateFormat = yearlyReport ? 'YYYY' : 'YYYYMM';
  const csvFilename = `${moment(d).format(dateFormat)}-transactions.csv`;
  const pdfFilename = `${moment(d).format(dateFormat)}-expenses.pdf`;
  console.log('startDate', startDate, 'endDate', endDate);

  year = year || startDate.getFullYear();

  let previewCondition = '';
  if (process.env.DEBUG && process.env.DEBUG.match(/preview/)) {
    previewCondition = 'AND c.id IN (11004, 9804, 9802, 9801)';
  } // open source collective host, wwcode host, brusselstogether, changex
  // previewCondition = "AND c.id IN (9802)"; // brusselstogether

  if (hostId) {
    previewCondition = `AND c.id = ${hostId}`;
  }

  if (process.env.SLUGS) {
    const slugs = process.env.SLUGS.split(',');
    previewCondition = `AND c.slug IN ('${slugs.join("','")}')`;
  }
  if (process.env.SKIP_SLUGS) {
    const slugs = process.env.SKIP_SLUGS.split(',');
    previewCondition = `AND c.slug NOT IN ('${slugs.join("','")}')`;
  }

  const getHostStats = (host, collectiveids) => {
    // Since collectives can change host,
    // we don't fetch transactions based on the CollectiveId but based on the HostCollectiveId
    // at the time of the transaction
    const where = { HostCollectiveId: host.id };
    const whereWithDateRange = { ...where, ...dateRange };

    const payoutProcessorFeesQuery = service => {
      // When using Sequelize query.include[].where it performs the condition on the join and it doesn't work
      // that's why have to do the query this this way.
      if (service === 'paypal') {
        where['$PaymentMethod.service$'] = 'paypal';
      } else {
        where['$PaymentMethod.service$'] = {
          [Op.or]: { [Op.ne]: 'paypal', [Op.is]: null },
        };
      }

      return {
        where: { ...where, ...dateRange, type: 'DEBIT' },
        raw: true,
        include: [
          {
            model: models.PaymentMethod,
            attributes: [[sequelize.fn('MAX', sequelize.col('service')), 'service']],
          },
        ],
      };
    };

    return Promise.props({
      balance: sumTransactions(
        'netAmountInCollectiveCurrency',
        { where: { ...where, createdAt: { [Op.lt]: endDate } } },
        host.currency,
      ), // total host balance
      delta: sumTransactions('netAmountInCollectiveCurrency', { where: whereWithDateRange }, host.currency), // delta host balance last month
      totalAmountDonations: sumTransactions(
        'amount',
        { where: { ...whereWithDateRange, type: 'CREDIT' } },
        host.currency,
      ), // total donations last month
      totalNetAmountReceivedForCollectives: sumTransactions(
        'netAmountInCollectiveCurrency',
        { where: { ...whereWithDateRange, type: 'CREDIT' } },
        host.currency,
      ), // total net amount received last month
      totalAmountPaidExpenses: sumTransactions(
        'netAmountInCollectiveCurrency',
        { where: { ...whereWithDateRange, type: 'DEBIT' } },
        host.currency,
      ), // total net amount paid out last month
      totalTaxAmountCollected: sumTransactions(
        'taxAmount',
        { where: whereWithDateRange, type: 'CREDIT' },
        host.currency,
      ), // total tax collected
      totalTaxAmountPaid: sumTransactions('taxAmount', { where: whereWithDateRange, type: 'DEBIT' }, host.currency), // total tax paid
      // We only count HostFees on CREDIT transactions with an Order and on DEBIT transactions with an Expense
      // (because the host fee of the destination host is recorded in the DEBIT transaction when making a donation to another host)
      totalHostFees: sumTransactions(
        'hostFeeInHostCurrency',
        {
          where: {
            ...whereWithDateRange,
            [Op.or]: [
              { type: 'CREDIT', OrderId: { [Op.ne]: null } },
              { type: 'DEBIT', ExpenseId: { [Op.ne]: null } },
            ],
          },
        },
        host.currency,
      ),
      backers: getBackersStats(startDate, endDate, collectiveids),
      platformFees: sumTransactions(
        'platformFeeInHostCurrency',
        { where: { ...whereWithDateRange, type: 'CREDIT' } },
        host.currency,
      ),
      paymentProcessorFees: sumTransactions(
        'paymentProcessorFeeInHostCurrency',
        { where: { ...whereWithDateRange, type: 'CREDIT' } },
        host.currency,
      ), // total stripe fees
      payoutProcessorFeesPaypal: sumTransactions(
        'paymentProcessorFeeInHostCurrency',
        payoutProcessorFeesQuery('paypal'),
        host.currency,
      ), // total paypal fees
      payoutProcessorFeesOther: sumTransactions(
        'paymentProcessorFeeInHostCurrency',
        payoutProcessorFeesQuery('other'),
        host.currency,
      ), // total other payout processor fees (manually recorded)
    });
  };

  const processHost = host => {
    summary.totalHosts++;
    console.log('>>> Processing host', host.slug);
    const data = {},
      attachments = [];
    const note = 'using fxrate of the day of the transaction as provided by the ECB. Your effective fxrate may vary.';
    const expensesPerPage = 30; // number of expenses per page of the Table Of Content (for PDF export)

    let collectivesById = {};
    let page = 1;
    let currentPage = 0;

    data.host = host.info;
    data.collective = host.info;
    data.reportDate = endDate;
    data.reportName = reportName;
    data.month = !yearlyReport && moment(startDate).format('MMMM');
    data.year = year;
    data.startDate = startDate;
    data.endDate = endDate;
    data.endDateIncluded = endDateIncluded;
    data.config = pick(config, 'host');
    data.maxSlugSize = 0;
    data.notes = null;
    data.expensesPerPage = [[]];
    data.stats = {
      numberPaidExpenses: 0,
    };

    const getHostAdminsEmails = host => {
      if (host.type === 'USER') {
        return models.User.findAll({ where: { CollectiveId: host.id } }).map(u => u.email);
      }
      return models.Member.findAll({
        where: {
          CollectiveId: host.id,
          role: { [Op.or]: [MemberRoles.ADMIN, MemberRoles.ACCOUNTANT] },
        },
      }).map(
        admin => {
          return models.User.findOne({
            attributes: ['email'],
            where: { CollectiveId: admin.MemberCollectiveId },
          }).then(user => user.email);
        },
        { concurrency: 1 },
      );
    };

    const processTransaction = transaction => {
      const t = {
        ...transaction.info,
        Expense: transaction.Expense
          ? {
              ...transaction.Expense.info,
              items: transaction.Expense.items.map(item => item.dataValues),
            }
          : null,
      };
      t.collective = collectivesById[t.CollectiveId].dataValues;
      t.collective.shortSlug = t.collective.slug.replace(/^wwcode-?(.)/, '$1');
      t.notes = t.Expense && t.Expense.privateMessage && stripHTML(t.Expense.privateMessage);
      if (t.data && t.data.fxrateSource) {
        t.notes = t.notes ? `${t.notes} (${note})` : note;
        data.notes = note;
      }

      // We prepare expenses for the PDF export
      if (t.type === 'DEBIT' && t.ExpenseId) {
        t.page = page++;
        data.stats.numberPaidExpenses++;
        if ((page - 1) % expensesPerPage === 0) {
          currentPage++;
          data.expensesPerPage[currentPage] = [];
        }
        data.expensesPerPage[currentPage].push(t);
      }

      data.maxSlugSize = Math.max(data.maxSlugSize, t.collective.shortSlug.length + 1);
      if (!t.description) {
        return transaction.getSource().then(source => {
          if (!source) {
            console.log('>>> no source found for ', transaction);
            return t;
          }
          t.description = source.description;
          return t;
        });
      } else {
        return Promise.resolve(t);
      }
    };

    return getHostedCollectives(host.id, endDate)
      .tap(collectives => {
        collectivesById = keyBy(collectives, 'id');
        data.stats.totalCollectives = collectives.filter(c => c.type === 'COLLECTIVE').length;
        summary.totalCollectives += data.stats.totalCollectives;
        console.log(`>>> processing ${data.stats.totalCollectives} collectives`);
      })
      .then(() =>
        getTransactions(Object.keys(collectivesById), startDate, endDate, {
          include: [
            {
              model: models.Expense,
              include: [
                'fromCollective',
                {
                  model: models.ExpenseItem,
                  as: 'items',
                  where: {
                    url: { [Op.not]: null },
                  },
                },
              ],
            },
            {
              model: models.User,
              as: 'createdByUser',
            },
          ],
        }),
      )
      .tap(transactions => {
        if (!transactions || transactions.length == 0) {
          throw new Error('No transaction found');
        }
        console.log(`>>> processing ${transactions.length} transactions`);
      })
      .map(processTransaction)
      .tap(transactions => {
        const csv = models.Transaction.exportCSV(transactions, collectivesById);
        attachments.push({
          filename: `${host.slug}-${csvFilename}`,
          content: csv,
        });
      })
      .then(transactions => (data.transactions = transactions))
      .then(() => {
        // Don't generate PDF in email if it's the yearly report
        if (yearlyReport || process.env.SKIP_PDF) {
          return;
        }
        return exportToPDF('expenses', data, {
          paper: host.currency === 'USD' ? 'Letter' : 'A4',
        }).catch(error => {
          console.error(error);
          return;
        });
      })
      .then(pdf => {
        // Mailgun limit is 25MB
        if (pdf && pdf.length < 24000000) {
          attachments.push({
            filename: `${host.slug}-${pdfFilename}`,
            content: pdf,
          });
          data.expensesPdf = true;
        }
      })
      .then(() => getHostStats(host, Object.keys(collectivesById)))
      .then(stats => {
        stats.totalAmountPaid = {
          totalInHostCurrency:
            stats.totalAmountPaidExpenses.totalInHostCurrency +
            stats.paymentProcessorFees.totalInHostCurrency +
            stats.platformFees.totalInHostCurrency,
        };
        stats.totalAmountSpent = {
          totalInHostCurrency:
            stats.totalAmountPaidExpenses.totalInHostCurrency +
            stats.payoutProcessorFeesPaypal.totalInHostCurrency +
            stats.payoutProcessorFeesOther.totalInHostCurrency,
        };
        stats.totalHostRevenue = {
          totalInHostCurrency: stats.totalHostFees.totalInHostCurrency,
        };
        // Total net amount received on the host's bank account = total amount - payment processor fees and platform fees
        stats.totalNetAmountReceived = {
          totalInHostCurrency:
            stats.totalAmountDonations.totalInHostCurrency +
            stats.paymentProcessorFees.totalInHostCurrency +
            stats.platformFees.totalInHostCurrency,
        };

        data.stats = {
          ...data.stats,
          ...stats,
          totalActiveCollectives: Object.keys(keyBy(data.transactions, 'CollectiveId')).length,
          numberTransactions: data.transactions.length,
          numberDonations: data.transactions.length - data.stats.numberPaidExpenses,
        };
        summary.hosts.push({
          host: { name: host.name, slug: host.slug, currency: host.currency },
          stats: data.stats,
        });
        summary.totalActiveHosts++;
        summary.totalActiveCollectives += data.stats.totalActiveCollectives;
        summary.numberTransactions += data.stats.numberTransactions;
        summary.numberDonations += data.stats.numberDonations;
        summary.numberPaidExpenses += data.stats.numberPaidExpenses;
        summary.totalAmountPaidExpenses += data.stats.totalAmountPaidExpenses;

        // Don't send transactions in email if there is more than 1000
        if (data.transactions.length > 1000) {
          delete data.transactions;
        }
      })
      .then(() => getHostAdminsEmails(host))
      .then(admins => sendEmail(admins, data, attachments))
      .catch(e => {
        console.error(`Error in processing host ${host.slug}:`, e.message);
        debug(e);
      });
  };

  const sendEmail = (recipients, data, attachments) => {
    debug('Sending email to ', recipients);
    if (!recipients || recipients.length === 0) {
      console.error('Unable to send host report for ', data.host.slug, 'No recipient to send to');
      return;
    }
    // debug("email data transactions", data.transactions);
    debug('email data stats', data.stats);
    debug('email data stats.backers', data.stats.backers);
    const options = { attachments };
    return emailLib.send(emailTemplate, recipients, data, options);
  };

  const query = `
  with "hosts" as (SELECT DISTINCT "HostCollectiveId" AS id FROM "Collectives" WHERE "deletedAt" IS NULL AND "isActive" IS TRUE AND "HostCollectiveId" IS NOT NULL)
  SELECT c.* FROM "Collectives" c WHERE c.id IN (SELECT h.id FROM hosts h) ${previewCondition}
  `;

  const hosts = await sequelize.query(query, {
    model: models.Collective,
    type: sequelize.QueryTypes.SELECT,
  });
  console.log(`Preparing the ${reportName} for ${hosts.length} hosts`);

  return Promise.map(hosts, processHost, { concurrency: 1 }).then(() => {
    console.log('>>> All done. Exiting.');
    process.exit(0);
  });
}

export default HostReport;
<|endoftext|>


<commit_before>var StringPrep = require('node-stringprep').StringPrep;
var NamePrep = new StringPrep('nameprep');

Template['layout_header'].events({
  'keyup #search-input': function(event) {
      Session.set('searched', NamePrep.prepare(event.target.value));
  }
})

Template['layout_header'].helpers({
  'backgroundDataUrl': function(){
    var searched = Session.get('name');
    if (searched && searched.length > 0) {
      // generate a pattern for the name
      return GeoPattern.generate(searched).toDataUrl();
    } else {
      // Otherwise generate a new pattern everyday
      return GeoPattern.generate(new Date().toISOString().substr(0,10)).toDataUrl();
    }
  },
  'returnedName': function() {
    var name = Session.get('name');
    if (name) window.location.hash = name;
    return name;
  },
  'disabled': function() {
    return (Session.get('name') && Session.get('name').length > 0 && Session.get('name').length < 7) ? 'invalid-name' : '';
  }
});
<commit_msg>Throttle lookups<commit_after>var StringPrep = require('node-stringprep').StringPrep;
var NamePrep = new StringPrep('nameprep');

Template['layout_header'].events({
  'keyup #search-input': function(event, template) {
    if (template.lookupTimeout) {
      clearTimeout(template.lookupTimeout);
    }
    template.lookupTimeout = setTimeout(function() {
      Session.set('searched', NamePrep.prepare(event.target.value));
    }, 400);
  }
})

Template['layout_header'].helpers({
  'backgroundDataUrl': function(){
    var searched = Session.get('name');
    if (searched && searched.length > 0) {
      // generate a pattern for the name
      return GeoPattern.generate(searched).toDataUrl();
    } else {
      // Otherwise generate a new pattern everyday
      return GeoPattern.generate(new Date().toISOString().substr(0,10)).toDataUrl();
    }
  },
  'returnedName': function() {
    var name = Session.get('name');
    if (name) window.location.hash = name;
    return name;
  },
  'disabled': function() {
    return (Session.get('name') && Session.get('name').length > 0 && Session.get('name').length < 7) ? 'invalid-name' : '';
  }
});
<|endoftext|>


<commit_before>import { AGREGAR_PEDIDO, ELIMINAR_PEDIDO } from "../actions/panel";

export default function panel(state = [], action) {
  switch (action.type) {
    case AGREGAR_PEDIDO:
      return [
        ...state,
        action.pedido
      ];
    case ELIMINAR_PEDIDO:
      return state.filter((pedido) => action.id !== pedido.id);
    default:
      return state;
  }
}
<commit_msg>Se cambia sintaxis en el filtro de pedidos<commit_after>import { AGREGAR_PEDIDO, ELIMINAR_PEDIDO } from "../actions/panel";

export default function panel(state = [], action) {
  switch (action.type) {
    case AGREGAR_PEDIDO:
      return [
        ...state,
        action.pedido
      ];
    case ELIMINAR_PEDIDO:
      return state.filter((pedido) => pedido.id !== action.id);
    default:
      return state;
  }
}
<|endoftext|>


<commit_before>(function () {

    angular
        .module('app')
        .controller('PatientController', [
            '$scope', '$stateParams', 'Restangular', '$mdToast', '$translate', 'AuthService',
            PatientController
        ]);

    function PatientController($scope, $stateParams, Restangular, $mdToast, $translate, AuthService) {
        var vm = this;
        var user = AuthService.getUser();

        if (user.role == 'patient') {
            $scope.user = user.person;
            $scope.patient_email = user.email;
        } else {
            Restangular.one('patients', $stateParams.userId).get()
                .then(function (response) {
                    $scope.user = response;
                    return Restangular.one('users', response.user_id).get();
                })
                .then(function (user) {
                    $scope.patient_email = user.email;
                });
        }

        $scope.update = function () {
            Restangular.one('patients', $stateParams.userId).get()
                .then(function (doctor) {
                    for (var property in patient) {
                        if ($scope.patient.hasOwnProperty(property)) {
                            patient[property] = $scope.patient[property];
                        }
                    }
                    return patient;
                })
                .then(function (editedPatient) {
                    return editedPatient.put();
                })
                .then(function (response) {
                        AuthService.updateUser();
                        $scope.showSimpleToast($translate.instant('DONE'));
                    },
                    function (errors) {
                        // TODO differrent typer of erroros
                    });
        };

        $scope.showSimpleToast = function (text) {
            $mdToast.show(
                $mdToast.simple()
                    .textContent(text)
                    .position('top right')
                    .hideDelay(3000)
            );
        };
    }

})();
<commit_msg>redirect to 404 if patient not found<commit_after>(function () {

    angular
        .module('app')
        .controller('PatientController', [
            '$scope', '$stateParams', 'Restangular', '$mdToast', '$translate', 'AuthService', '$state',
            PatientController
        ]);

    function PatientController($scope, $stateParams, Restangular, $mdToast, $translate, AuthService, $state) {
        var vm = this;
        var user = AuthService.getUser();

        if (user.role == 'patient') {
            $scope.user = user.person;
            $scope.patient_email = user.email;
        } else {
            Restangular.one('patients', $stateParams.userId).get()
                .then(function (response) {
                        $scope.user = response;
                        return Restangular.one('users', response.user_id).get();
                    },
                    function (response) {
                        if(response.status === 404){
                            $state.go('404')
                        }
                        if(response.status === 403){
                            $state.go('403')
                        }
                    })
                .then(function (user) {
                    $scope.patient_email = user.email;
                });
        }

        $scope.update = function () {
            Restangular.one('patients', $stateParams.userId).get()
                .then(function (doctor) {
                    for (var property in patient) {
                        if ($scope.patient.hasOwnProperty(property)) {
                            patient[property] = $scope.patient[property];
                        }
                    }
                    return patient;
                })
                .then(function (editedPatient) {
                    return editedPatient.put();
                })
                .then(function (response) {
                        AuthService.updateUser();
                        $scope.showSimpleToast($translate.instant('DONE'));
                    },
                    function (errors) {
                        // TODO differrent typer of erroros
                    });
        };

        $scope.showSimpleToast = function (text) {
            $mdToast.show(
                $mdToast.simple()
                    .textContent(text)
                    .position('top right')
                    .hideDelay(3000)
            );
        };
    }

})();
<|endoftext|>


<commit_before>/*!
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*!
 * @module datastore/query
 */

'use strict';

var arrify = require('arrify');

/**
 * Build a Query object.
 *
 * **Queries should be built with
 * {module:datastore/dataset#createQuery} and run via
 * {module:datastore/dataset#runQuery}.**
 *
 * @resource [Datastore Queries]{@link http://goo.gl/Cag0r6}
 *
 * @constructor
 * @alias module:datastore/query
 *
 * @param {string=} namespace - Namespace to query entities from.
 * @param {string} kind - Kind to query.
 *
 * @example
 * var gcloud = require('gcloud')({
 *   keyFilename: '/path/to/keyfile.json',
 *   projectId: 'grape-spaceship-123'
 * });
 *
 * var datastore = gcloud.datastore();
 * var query = datastore.createQuery('AnimalNamespace', 'Lion');
 */
function Query(namespace, kinds) {
  if (!kinds) {
    kinds = namespace;
    namespace = null;
  }

  this.namespace = namespace || null;
  this.kinds = kinds;

  this.filters = [];
  this.orders = [];
  this.groupByVal = [];
  this.selectVal = [];

  // pagination
  this.autoPaginateVal = true;
  this.startVal = null;
  this.endVal = null;
  this.limitVal = -1;
  this.offsetVal = -1;
}

/**
 * @param {boolean=} autoPaginateVal - Have pagination handled automatically.
 *     Default: true.
 * @return {module:datastore/query}
 *
 * @example
 * // Retrieve a list of people related to person "1234",
 * // disabling auto pagination
 * var query = datastore.createQuery('Person')
 *   .hasAncestor(datastore.key(['Person', 1234]))
 *   .autoPaginate(false);
 *
 * function callback(err, entities, nextQuery, apiResponse) {
 *   if (nextQuery) {
 *     // More results might exist, so we'll manually fetch them
 *     datastore.runQuery(nextQuery, callback);
 *   }
 * }
 *
 * datastore.runQuery(query, callback);
 */
Query.prototype.autoPaginate = function(autoPaginateVal) {
  this.autoPaginateVal = autoPaginateVal !== false;
  return this;
};

/**
 * Datastore allows querying on properties. Supported comparison operators
 * are `=`, `<`, `>`, `<=`, and `>=`. "Not equal" and `IN` operators are
 * currently not supported.
 *
 * *To filter by ancestors, see {module:datastore/query#hasAncestor}.*
 *
 * @resource [Datastore Filters]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Filters}
 *
 * @param {string} property - The field name.
 * @param {string=} operator - Operator (=, <, >, <=, >=). Default: `=`.
 * @param {*} value - Value to compare property to.
 * @return {module:datastore/query}
 *
 * @example
 * //-
 * // List all companies that are located in California.
 * //-
 * var caliQuery = query.filter('state', 'CA');
 *
 * //-
 * // List all companies named Google that have less than 400 employees.
 * //-
 * var companyQuery = query
 *   .filter('name', 'Google')
 *   .filter('size', '<', 400);
 *
 * //-
 * // To filter by key, use `__key__` for the property name. Filter on keys
 * // stored as properties is not currently supported.
 * //-
 * var key = datastore.key(['Company', 'Google']);
 * var keyQuery = query.filter('__key__', key);
 */
Query.prototype.filter = function(property, operator, value) {
  if (arguments.length === 2) {
    value = operator;
    operator = '=';
  }

  // TODO: Add filter validation.
  this.filters.push({
    name: property.trim(),
    op: operator.trim(),
    val: value
  });
  return this;
};

/**
 * Filter a query by ancestors.
 *
 * @resource [Datastore Ancestor Filters]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Ancestor_filters}
 *
 * @param {Key} key - Key object to filter by.
 * @return {module:datastore/query}
 *
 * @example
 * var ancestoryQuery = query.hasAncestor(datastore.key(['Parent', 123]));
 */
Query.prototype.hasAncestor = function(key) {
  this.filters.push({ name: '__key__', op: 'HAS_ANCESTOR', val: key });
  return this;
};

/**
 * Sort the results by a property name in ascending or descending order. By
 * default, an ascending sort order will be used.
 *
 * @resource [Datastore Sort Orders]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Sort_orders}
 *
 * @param {string} property - The property to order by.
 * @param {object=} options - Options object.
 * @param {boolean} options.descending - Sort the results by a property name
 *     in descending order. Default: `false`.
 * @return {module:datastore/query}
 *
 * @example
 * // Sort by size ascendingly.
 * var companiesAscending = companyQuery.order('size');
 *
 * // Sort by size descendingly.
 * var companiesDescending = companyQuery.order('size', {
 *   descending: true
 * });
 */
Query.prototype.order = function(property, options) {
  var sign = options && options.descending ? '-' : '+';

  this.orders.push({ name: property, sign: sign });
  return this;
};

/**
 * Group query results by a list of properties.
 *
 * @param {array} properties - Properties to group by.
 * @return {module:datastore/query}
 *
 * @example
 * var groupedQuery = companyQuery.groupBy(['name', 'size']);
 */
Query.prototype.groupBy = function(fieldNames) {
  this.groupByVal = arrify(fieldNames);
  return this;
};

/**
 * Retrieve only select properties from the matched entities.
 *
 * Queries that select a subset of properties are called Projection Queries.
 *
 * @resource [Projection Queries]{@link https://cloud.google.com/datastore/docs/concepts/projectionqueries}
 *
 * @param {string|string[]} fieldNames - Properties to return from the matched
 *     entities.
 * @return {module:datastore/query}
 *
 * @example
 * // Only retrieve the name property.
 * var selectQuery = companyQuery.select('name');
 *
 * // Only retrieve the name and size properties.
 * var selectQuery = companyQuery.select(['name', 'size']);
 */
Query.prototype.select = function(fieldNames) {
  this.selectVal = arrify(fieldNames);
  return this;
};

/**
 * Set a starting cursor to a query.
 *
 * @resource [Query Cursors]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Query_cursors}
 *
 * @param {string} cursorToken - The starting cursor token.
 * @return {module:datastore/query}
 *
 * @example
 * var cursorToken = 'X';
 *
 * // Retrieve results starting from cursorToken.
 * var startQuery = companyQuery.start(cursorToken);
 */
Query.prototype.start = function(start) {
  this.startVal = start;
  return this;
};

/**
 * Set an ending cursor to a query.
 *
 * @resource [Query Cursors]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Query_cursors}
 *
 * @param {string} cursorToken - The ending cursor token.
 * @return {module:datastore/query}
 *
 * @example
 * var cursorToken = 'X';
 *
 * // Retrieve results limited to the extent of cursorToken.
 * var endQuery = companyQuery.end(cursorToken);
 */
Query.prototype.end = function(end) {
  this.endVal = end;
  return this;
};

/**
 * Set a limit on a query.
 *
 * @resource [Query Limits]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Retrieving_results}
 *
 * @param {number} n - The number of results to limit the query to.
 * @return {module:datastore/query}
 *
 * @example
 * // Limit the results to 10 entities.
 * var limitQuery = companyQuery.limit(10);
 */
Query.prototype.limit = function(n) {
  this.limitVal = n;
  return this;
};

/**
 * Set an offset on a query.
 *
 * @resource [Query Offsets]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Retrieving_results}
 *
 * @param {number} n - The offset to start from after the start cursor.
 * @return {module:datastore/query}
 *
 * @example
 * // Start from the 101st result.
 * var offsetQuery = companyQuery.offset(100);
 */
Query.prototype.offset = function(n) {
  this.offsetVal = n;
  return this;
};

module.exports = Query;
<commit_msg>datastore: docs: remove dataset references<commit_after>/*!
 * Copyright 2014 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*!
 * @module datastore/query
 */

'use strict';

var arrify = require('arrify');

/**
 * Build a Query object.
 *
 * **Queries should be built with {module:datastore#createQuery} and run via
 * {module:datastore#runQuery}.**
 *
 * @resource [Datastore Queries]{@link http://goo.gl/Cag0r6}
 *
 * @constructor
 * @alias module:datastore/query
 *
 * @param {string=} namespace - Namespace to query entities from.
 * @param {string} kind - Kind to query.
 *
 * @example
 * var gcloud = require('gcloud')({
 *   keyFilename: '/path/to/keyfile.json',
 *   projectId: 'grape-spaceship-123'
 * });
 *
 * var datastore = gcloud.datastore();
 * var query = datastore.createQuery('AnimalNamespace', 'Lion');
 */
function Query(namespace, kinds) {
  if (!kinds) {
    kinds = namespace;
    namespace = null;
  }

  this.namespace = namespace || null;
  this.kinds = kinds;

  this.filters = [];
  this.orders = [];
  this.groupByVal = [];
  this.selectVal = [];

  // pagination
  this.autoPaginateVal = true;
  this.startVal = null;
  this.endVal = null;
  this.limitVal = -1;
  this.offsetVal = -1;
}

/**
 * @param {boolean=} autoPaginateVal - Have pagination handled automatically.
 *     Default: true.
 * @return {module:datastore/query}
 *
 * @example
 * // Retrieve a list of people related to person "1234",
 * // disabling auto pagination
 * var query = datastore.createQuery('Person')
 *   .hasAncestor(datastore.key(['Person', 1234]))
 *   .autoPaginate(false);
 *
 * function callback(err, entities, nextQuery, apiResponse) {
 *   if (nextQuery) {
 *     // More results might exist, so we'll manually fetch them
 *     datastore.runQuery(nextQuery, callback);
 *   }
 * }
 *
 * datastore.runQuery(query, callback);
 */
Query.prototype.autoPaginate = function(autoPaginateVal) {
  this.autoPaginateVal = autoPaginateVal !== false;
  return this;
};

/**
 * Datastore allows querying on properties. Supported comparison operators
 * are `=`, `<`, `>`, `<=`, and `>=`. "Not equal" and `IN` operators are
 * currently not supported.
 *
 * *To filter by ancestors, see {module:datastore/query#hasAncestor}.*
 *
 * @resource [Datastore Filters]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Filters}
 *
 * @param {string} property - The field name.
 * @param {string=} operator - Operator (=, <, >, <=, >=). Default: `=`.
 * @param {*} value - Value to compare property to.
 * @return {module:datastore/query}
 *
 * @example
 * //-
 * // List all companies that are located in California.
 * //-
 * var caliQuery = query.filter('state', 'CA');
 *
 * //-
 * // List all companies named Google that have less than 400 employees.
 * //-
 * var companyQuery = query
 *   .filter('name', 'Google')
 *   .filter('size', '<', 400);
 *
 * //-
 * // To filter by key, use `__key__` for the property name. Filter on keys
 * // stored as properties is not currently supported.
 * //-
 * var key = datastore.key(['Company', 'Google']);
 * var keyQuery = query.filter('__key__', key);
 */
Query.prototype.filter = function(property, operator, value) {
  if (arguments.length === 2) {
    value = operator;
    operator = '=';
  }

  // TODO: Add filter validation.
  this.filters.push({
    name: property.trim(),
    op: operator.trim(),
    val: value
  });
  return this;
};

/**
 * Filter a query by ancestors.
 *
 * @resource [Datastore Ancestor Filters]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Ancestor_filters}
 *
 * @param {Key} key - Key object to filter by.
 * @return {module:datastore/query}
 *
 * @example
 * var ancestoryQuery = query.hasAncestor(datastore.key(['Parent', 123]));
 */
Query.prototype.hasAncestor = function(key) {
  this.filters.push({ name: '__key__', op: 'HAS_ANCESTOR', val: key });
  return this;
};

/**
 * Sort the results by a property name in ascending or descending order. By
 * default, an ascending sort order will be used.
 *
 * @resource [Datastore Sort Orders]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Sort_orders}
 *
 * @param {string} property - The property to order by.
 * @param {object=} options - Options object.
 * @param {boolean} options.descending - Sort the results by a property name
 *     in descending order. Default: `false`.
 * @return {module:datastore/query}
 *
 * @example
 * // Sort by size ascendingly.
 * var companiesAscending = companyQuery.order('size');
 *
 * // Sort by size descendingly.
 * var companiesDescending = companyQuery.order('size', {
 *   descending: true
 * });
 */
Query.prototype.order = function(property, options) {
  var sign = options && options.descending ? '-' : '+';

  this.orders.push({ name: property, sign: sign });
  return this;
};

/**
 * Group query results by a list of properties.
 *
 * @param {array} properties - Properties to group by.
 * @return {module:datastore/query}
 *
 * @example
 * var groupedQuery = companyQuery.groupBy(['name', 'size']);
 */
Query.prototype.groupBy = function(fieldNames) {
  this.groupByVal = arrify(fieldNames);
  return this;
};

/**
 * Retrieve only select properties from the matched entities.
 *
 * Queries that select a subset of properties are called Projection Queries.
 *
 * @resource [Projection Queries]{@link https://cloud.google.com/datastore/docs/concepts/projectionqueries}
 *
 * @param {string|string[]} fieldNames - Properties to return from the matched
 *     entities.
 * @return {module:datastore/query}
 *
 * @example
 * // Only retrieve the name property.
 * var selectQuery = companyQuery.select('name');
 *
 * // Only retrieve the name and size properties.
 * var selectQuery = companyQuery.select(['name', 'size']);
 */
Query.prototype.select = function(fieldNames) {
  this.selectVal = arrify(fieldNames);
  return this;
};

/**
 * Set a starting cursor to a query.
 *
 * @resource [Query Cursors]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Query_cursors}
 *
 * @param {string} cursorToken - The starting cursor token.
 * @return {module:datastore/query}
 *
 * @example
 * var cursorToken = 'X';
 *
 * // Retrieve results starting from cursorToken.
 * var startQuery = companyQuery.start(cursorToken);
 */
Query.prototype.start = function(start) {
  this.startVal = start;
  return this;
};

/**
 * Set an ending cursor to a query.
 *
 * @resource [Query Cursors]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Query_cursors}
 *
 * @param {string} cursorToken - The ending cursor token.
 * @return {module:datastore/query}
 *
 * @example
 * var cursorToken = 'X';
 *
 * // Retrieve results limited to the extent of cursorToken.
 * var endQuery = companyQuery.end(cursorToken);
 */
Query.prototype.end = function(end) {
  this.endVal = end;
  return this;
};

/**
 * Set a limit on a query.
 *
 * @resource [Query Limits]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Retrieving_results}
 *
 * @param {number} n - The number of results to limit the query to.
 * @return {module:datastore/query}
 *
 * @example
 * // Limit the results to 10 entities.
 * var limitQuery = companyQuery.limit(10);
 */
Query.prototype.limit = function(n) {
  this.limitVal = n;
  return this;
};

/**
 * Set an offset on a query.
 *
 * @resource [Query Offsets]{@link https://cloud.google.com/datastore/docs/concepts/queries#Datastore_Retrieving_results}
 *
 * @param {number} n - The offset to start from after the start cursor.
 * @return {module:datastore/query}
 *
 * @example
 * // Start from the 101st result.
 * var offsetQuery = companyQuery.offset(100);
 */
Query.prototype.offset = function(n) {
  this.offsetVal = n;
  return this;
};

module.exports = Query;
<|endoftext|>


<commit_before>'use strict';

/* Charting helpers */

angular.module('probytes.charts', [])
  .value('barChart', function(element, data) {
    var maxTextWidth = function(strings, className) {
      var svg = d3.select(element[0]).append("svg")
          .attr('width', 0)
          .attr('height', 0);

      var lines = svg.append('g');
      lines.selectAll('.get-text-width')
          .data(strings)
        .enter().append('text')
          .attr('class', className)
          .text(_.identity);

      var width = lines.node().getBBox().width;
      svg.remove();

      return width;
    };

    var elementWidth     = $(element[0]).innerWidth(),
        maxHostnameWidth = maxTextWidth(_(data).map(function(d) { return d.hostname }), 'hostname'),
        margin           = {top: 30, right: 45, bottom: 10, left: maxHostnameWidth + 10 },
        rowHeight        = 30,
        width            = elementWidth - margin.left - margin.right,
        height           = (data.length * rowHeight) - margin.top - margin.bottom;

    var x = d3.scale.linear()
        .domain([0, d3.max(data, function(d) { return d.bytes })])
        .range([0, width]);

    var y = d3.scale.ordinal()
        .rangeRoundBands([0, height], .2);

    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("top");

    var yAxis = d3.svg.axis()
        .orient("left");

    d3.select(element[0]).html('');
    var svg = d3.select(element[0]).append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
      .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // x.domain(d3.extent(data, function(d) { return d.bytes; })).nice();
    y.domain(data.map(function(d) { return d.hostname; }));

    var bars = svg.selectAll(".bar")
      .data(data);

    bars.enter().append("rect")
        .attr("class", 'bar')
        .attr("x", function(d) { return x(0); })
        .attr("y", function(d) { return y(d.hostname); })
        .attr("width", function(d) { return Math.abs(x(d.bytes) - x(0)); })
        .attr("height", y.rangeBand());

    bars.enter().append("text")
        .attr("class", "hostname")
        .attr("x", -10)
        .attr("y", function(d) { return y(d.hostname); })
        // .attr("dy", y.rangeBand() / 2)
        .attr("dy", "1.15em")
        .attr("text-anchor", "end")
        .text(function(d) { return d.hostname; });

    svg.append("g")
        .attr("class", "x axis")
        .call(xAxis);

    svg.append("text")
        .attr("class", "x axis-label")
        .attr("text-anchor", "end")
        .attr("x", width + margin.right - 5)
        .attr("y", -9)
        .text("[GiB]");

    svg.append("g")
        .attr("class", "y axis")
      .append("line")
        .attr("x1", x(0))
        .attr("x2", x(0))
        .attr("y2", height);
  })
  .value('pieChart', function(element, data) {
    var elementWidth = $(element[0]).innerWidth(),
        width = elementWidth,
        height = elementWidth,
        radius = Math.min(width, height) / 2;

    var color = d3.scale.ordinal()
        .range(["#a05d56", "#d0743c", "#ff8c00", "#98abc5", "#8a89a6", "#7b6888", "#6b486b"]);

    var arc = d3.svg.arc()
        .outerRadius(radius - 10)
        .innerRadius(0);

    var pie = d3.layout.pie()
        .sort(null)
        .value(function(d) { return d.bytes; });

    d3.select(element[0]).html('');
    var svg = d3.select(element[0]).append("svg")
        .attr("width", width)
        .attr("height", height)
      .append("g")
        .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

    var arcs = svg.selectAll(".arc")
        .data(pie(data));

    arcs
      .enter().append("g")
        .attr("class", "arc")
      .append("path")
        .attr("d", arc)
        .style("fill", function(d) { return color(d.data.hostname); })
        .style("stroke", "#eee");

    arcs
      .enter().append("g")
        .attr("class", "arc")
      .append("text")
        .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
        .attr("dy", ".45em")
        .style("text-anchor", "middle")
        .text(function(d) { return d.data.hostname; });
  });
<commit_msg>pie chart legend<commit_after>'use strict';

/* Charting helpers */

angular.module('probytes.charts', [])
  .value('barChart', function(element, data) {
    var maxTextWidth = function(strings, className) {
      var svg = d3.select(element[0]).append("svg")
          .attr('width', 0)
          .attr('height', 0);

      var lines = svg.append('g');
      lines.selectAll('.get-text-width')
          .data(strings)
        .enter().append('text')
          .attr('class', className)
          .text(_.identity);

      var width = lines.node().getBBox().width;
      svg.remove();

      return width;
    };

    var elementWidth     = $(element[0]).innerWidth(),
        maxHostnameWidth = maxTextWidth(_(data).map(function(d) { return d.hostname }), 'hostname'),
        margin           = {top: 30, right: 45, bottom: 10, left: maxHostnameWidth + 10 },
        rowHeight        = 30,
        width            = elementWidth - margin.left - margin.right,
        height           = (data.length * rowHeight) - margin.top - margin.bottom;

    var x = d3.scale.linear()
        .domain([0, d3.max(data, function(d) { return d.bytes })])
        .range([0, width]);

    var y = d3.scale.ordinal()
        .rangeRoundBands([0, height], .2);

    var xAxis = d3.svg.axis()
        .scale(x)
        .orient("top");

    var yAxis = d3.svg.axis()
        .orient("left");

    d3.select(element[0]).html('');
    var svg = d3.select(element[0]).append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
      .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    // x.domain(d3.extent(data, function(d) { return d.bytes; })).nice();
    y.domain(data.map(function(d) { return d.hostname; }));

    var bars = svg.selectAll(".bar")
      .data(data);

    bars.enter().append("rect")
        .attr("class", 'bar')
        .attr("x", function(d) { return x(0); })
        .attr("y", function(d) { return y(d.hostname); })
        .attr("width", function(d) { return Math.abs(x(d.bytes) - x(0)); })
        .attr("height", y.rangeBand());

    bars.enter().append("text")
        .attr("class", "hostname")
        .attr("x", -10)
        .attr("y", function(d) { return y(d.hostname); })
        // .attr("dy", y.rangeBand() / 2)
        .attr("dy", "1.15em")
        .attr("text-anchor", "end")
        .text(function(d) { return d.hostname; });

    svg.append("g")
        .attr("class", "x axis")
        .call(xAxis);

    svg.append("text")
        .attr("class", "x axis-label")
        .attr("text-anchor", "end")
        .attr("x", width + margin.right - 5)
        .attr("y", -9)
        .text("[GiB]");

    svg.append("g")
        .attr("class", "y axis")
      .append("line")
        .attr("x1", x(0))
        .attr("x2", x(0))
        .attr("y2", height);
  })
  .value('pieChart', function(element, data) {
    var elementWidth    = $(element[0]).innerWidth(),
        width           = elementWidth,
        chartHeight     = elementWidth,
        legendTopMargin = 20,
        legendRowHeight = 30,
        legendBoxSize   = 15,
        legendHeight    = legendTopMargin + (data.length * legendRowHeight),
        radius          = Math.min(width, chartHeight) / 2;

    var color = d3.scale.ordinal()
        .range(["#a05d56", "#d0743c", "#ff8c00", "#98abc5", "#8a89a6", "#7b6888", "#6b486b"]);

    var arc = d3.svg.arc()
        .outerRadius(radius - 10)
        .innerRadius(0);

    var pie = d3.layout.pie()
        .sort(null)
        .value(function(d) { return d.bytes; });

    d3.select(element[0]).html('');
    var svg = d3.select(element[0]).append("svg")
        .attr("width", width)
        .attr("height", chartHeight + legendHeight - 10);

    var chart = svg.append("g")
        .attr("transform", "translate(" + width / 2 + "," + chartHeight / 2 + ")");

    var arcs = chart.selectAll(".arc")
        .data(pie(data));

    arcs.enter().append("g")
        .attr("class", "arc")
      .append("path")
        .attr("d", arc)
        .style("fill", function(d) { return color(d.data.hostname); })
        .style("stroke", "#eee");

    arcs.enter().append("g")
        .attr("class", "arc")
      .append("text")
        .attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
        .attr("dy", ".45em")
        .style("text-anchor", "middle")
        .text(function(d) { return d.data.hostname; });

    var legend = svg.append("g")
        .attr('transform', 'translate(10, ' + (chartHeight + legendTopMargin) + ')');

    var legendRows = legend.selectAll('.legendRow')
        .data(pie(data));

    legendRows.enter()
        .append('rect')
        .attr('width', legendBoxSize)
        .attr('height', legendBoxSize)
        .attr('y', function(d, i) { return i * legendRowHeight; })
        .style("fill", function(d) { return color(d.data.hostname); });

    legendRows.enter()
        .append('text')
        .attr('x', 30)
        .attr('y', function(d, i) { return i * legendRowHeight + legendBoxSize; })
        .attr('dy', '-0.2em')
        .text(function(d) { return d.data.hostname; });
  });
<|endoftext|>


<commit_before>#!/usr/bin/env node
/*
 * Parse an outline and dump the result to the console
 *
 * Usage: curl <outline url> | bin/dump.js
 *        cat <outline file> | bin/dump.js
 *
 */
var util = require('util')
  , OpmlParser = require('../');

process.stdin.pipe(new OpmlParser())
  .on('error', console.error)
  .on('readable', function() {
    var stream = this, item;
    while (item = stream.read()) {
      console.log(util.inspect(item, null, 10, true));
    }
  });
<commit_msg>Update dump script to also dump meta.<commit_after>#!/usr/bin/env node
/*
 * Parse an outline and dump the result to the console
 *
 * Usage: curl <outline url> | bin/dump.js
 *        cat <outline file> | bin/dump.js
 *
 */
var util = require('util')
  , OpmlParser = require('../');

process.stdin.pipe(new OpmlParser())
  .on('error', console.error)
  .on('readable', function() {
    var stream = this, outline;
    while (outline = stream.read()) {
      console.log(util.inspect(outline, null, 10, true));
    }
  })
  .on('end', function () {
    console.log(util.inspect(this.meta, null, 10, true));
  });
<|endoftext|>


<commit_before>define("recollect", [
  "lodash",
  "./errors",
  "./ixdb",
  "./utils"
], function (_, Errors, ixdb, utils) {

  var Datastore = function (options) {
    options = options || {};
    this.name = options.dsName;
    this.dbName = options.dbName;
  };

  Datastore.prototype.find = function (query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.name,
      query: query,
      findMany: true
    }).then(function (records) {
      return _.pluck(records, "value");
    });
  };

  Datastore.prototype.findOne = function (query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.name,
      query: query,
      findMany: false
    }).then(function (records) {
      return records.length && records[0].value || undefined;
    });
  };

  Datastore.prototype.findByIndex = function (indexedFieldName, indexedValue, query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.name,
      query: query,
      findMany: true,
      indexedFieldName: indexedFieldName,
      indexedValue: indexedValue
    }).then(function (records) {
      return _.pluck(records, "value");
    });
  };

  Datastore.prototype.findOneByIndex = function (indexedFieldName, indexedValue, query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.name,
      query: query,
      findMany: false,
      indexedFieldName: indexedFieldName,
      indexedValue: indexedValue
    }).then(function (records) {
      return records.length && records[0].value || undefined;
    });
  };

  Datastore.prototype.insertOne = function (newRecord) {
    if (!_.isObject(newRecord)) {
      throw new Error.InvalidArgumentError("insertOne requires newRecord as argument");
    }
    if (!_.isUndefined(newRecord._id)) {
      throw new Error.InvalidArgumentError("newRecord cannot contain `_id` property");
    }

    return ixdb.addMany({
      dbName: this.dbName,
      dsName: this.name,
      records: [newRecord]
    }).then(function (records) {
      return records[0].value;
    });
  };

  Datastore.prototype.insertMany = function (newRecords) {
    if (!_.isArray(newRecords)) {
      throw new Error.InvalidArgumentError("newRecords must be an array");
    }
    _.each(newRecords, function (record) {
      if (!_.isUndefined(record._id)) {
        throw new Error.InvalidArgumentError("new records cannot contain `_id` property");
      }
    });

    return ixdb.addMany({
      dbName: this.dbName,
      dsName: this.name,
      records: newRecords
    }).then(function (records) {
      return _.map(records, function (record) { return record.value; });
    });
  };

  Datastore.prototype.update = function (recordUpdates) {
    if (!_.isObject(recordUpdates)) {
      throw new Error.InvalidArgumentError("update requires recordUpdates as argument");
    }
    if (_.isUndefined(recordUpdates._id)) {
      throw new Error.InvalidArgumentError("update requires recordUpdates with `_id` property");
    }

    return ixdb.get({
      dbName: this.dbName,
      dsName: this.name,
      _id: recordUpdates._id
    }).then(_.partial(ixdb.update, recordUpdates));
  };

  Datastore.prototype.delete = function (query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.name,
      query: query,
      findMany: true
    }).then(ixdb.del);
  };

  Datastore.prototype.indexField = function (fieldName, isArray) {
    ixdb.indexField(this.name, fieldName, isArray);
  };

  /**
   * Creates instance of Datastore, and attaches instance as property on
   * parent Recollect instance.
   *
   * @param  {Object}    recollect  Parent recollect instance.
   * @param  {IDXCursor} dsRecord   Cursor for record in `_config`
   */
  var initDatastoreObject = function (recollect, dsRecord) {
    var dsName = dsRecord.key;
    // var dsConfig = dsRecord.value;

    if (dsName === "_config") {
      return;
    }
    if (!_.isUndefined(recollect[dsName])) {
      throw new Errors.InitializationError("Invalid datastore name or Recollect instance " +
        "already initialized.");
    }
    recollect[dsName] = new Datastore({
      dbName: recollect.name,
      dsName: dsName
    });
  };

  var Recollect = function (name) {
    if (!name) {
      throw new Errors.InvalidArgumentError("You must provide a database name.");
    }
    this.name = name;
    return this;
  };

  /**
   * Iterates through database's `_config` datastore.  For each record found
   * of name `n`, create `this.n` instance of Datastore prototype.
   *
   * @return {Promise}  Resolves to Recollect instance after Datastore instances
   *                    have successfully been attached.
   */
  Recollect.prototype.initialize = function () {
    var self = this;
    return ixdb.getMany({
      db: this.name,
      datastore: "_config",
      query: null,
      findMany: true
    }).then(function (datastoreRecords) {
      _.each(datastoreRecords, _.partial(initDatastoreObject, self));
      return self;
    });
  };

  /**
   * Creates and configures new datastore.
   *
   * @param  {String}  options.name              Name of new datastore.
   * @param  {Boolean} options.autoIncrement     If true, keys are automatically
   *                                             generated.
   * @param  {String}  options.keyField          Name of primary key field.
   * @param  {Object}  options.indexes           Object describing fields to index.
   *                     .fieldName.unique       If true, no two entries should share same
   *                                             value in fieldName.
   *                     .fieldName.multiEntry   If true, values in fieldName should be an
   *                                             array of values to be queried independently.
   *
   * @return {Promise}                        Resolves to new datastore object.
   */
  Recollect.prototype.createDatastore = function (options) {
    var self = this;

    options = utils.normalizeOptions(options, ["name"], {
      autoIncrement: true,
      keyField: "_id",
      indexes: {}
    });

    return ixdb.createDatastore(_.extend({}, options, { databaseName: this.name }))
      .then(function (/* datastore */) {
        return ixdb.addMany({
          dbName: self.name,
          dsName: "_config",
          records: [{
            name: options.name,
            autoIncrement: options.autoIncrement,
            keyField: options.keyField,
            indexes: options.indexes,
            created: (new Date()).getTime(),
            lastSynced: null
          }]
        });
      })
      .then(function () {
        initDatastoreObject(self, { key: options.name });
      });
  };

  Recollect.prototype.Datastore = Datastore;
  Recollect.prototype.Errors = Errors;

  return Recollect;
});
<commit_msg>use dsName and dbName everywhere<commit_after>define("recollect", [
  "lodash",
  "./errors",
  "./ixdb",
  "./utils"
], function (_, Errors, ixdb, utils) {

  var Datastore = function (options) {
    options = options || {};
    this.dsName = options.dsName;
    this.dbName = options.dbName;
  };

  Datastore.prototype.find = function (query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.dsName,
      query: query,
      findMany: true
    }).then(function (records) {
      return _.pluck(records, "value");
    });
  };

  Datastore.prototype.findOne = function (query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.dsName,
      query: query,
      findMany: false
    }).then(function (records) {
      return records.length && records[0].value || undefined;
    });
  };

  Datastore.prototype.findByIndex = function (indexedFieldName, indexedValue, query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.dsName,
      query: query,
      findMany: true,
      indexedFieldName: indexedFieldName,
      indexedValue: indexedValue
    }).then(function (records) {
      return _.pluck(records, "value");
    });
  };

  Datastore.prototype.findOneByIndex = function (indexedFieldName, indexedValue, query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.dsName,
      query: query,
      findMany: false,
      indexedFieldName: indexedFieldName,
      indexedValue: indexedValue
    }).then(function (records) {
      return records.length && records[0].value || undefined;
    });
  };

  Datastore.prototype.insertOne = function (newRecord) {
    if (!_.isObject(newRecord)) {
      throw new Error.InvalidArgumentError("insertOne requires newRecord as argument");
    }
    if (!_.isUndefined(newRecord._id)) {
      throw new Error.InvalidArgumentError("newRecord cannot contain `_id` property");
    }

    return ixdb.addMany({
      dbName: this.dbName,
      dsName: this.dsName,
      records: [newRecord]
    }).then(function (records) {
      return records[0].value;
    });
  };

  Datastore.prototype.insertMany = function (newRecords) {
    if (!_.isArray(newRecords)) {
      throw new Error.InvalidArgumentError("newRecords must be an array");
    }
    _.each(newRecords, function (record) {
      if (!_.isUndefined(record._id)) {
        throw new Error.InvalidArgumentError("new records cannot contain `_id` property");
      }
    });

    return ixdb.addMany({
      dbName: this.dbName,
      dsName: this.dsName,
      records: newRecords
    }).then(function (records) {
      return _.map(records, function (record) { return record.value; });
    });
  };

  Datastore.prototype.update = function (recordUpdates) {
    if (!_.isObject(recordUpdates)) {
      throw new Error.InvalidArgumentError("update requires recordUpdates as argument");
    }
    if (_.isUndefined(recordUpdates._id)) {
      throw new Error.InvalidArgumentError("update requires recordUpdates with `_id` property");
    }

    return ixdb.get({
      dbName: this.dbName,
      dsName: this.dsName,
      _id: recordUpdates._id
    }).then(_.partial(ixdb.update, recordUpdates));
  };

  Datastore.prototype.delete = function (query) {
    return ixdb.getMany({
      dbName: this.dbName,
      dsName: this.dsName,
      query: query,
      findMany: true
    }).then(ixdb.del);
  };

  Datastore.prototype.indexField = function (fieldName, isArray) {
    ixdb.indexField(this.dsName, fieldName, isArray);
  };

  /**
   * Creates instance of Datastore, and attaches instance as property on
   * parent Recollect instance.
   *
   * @param  {Object}    recollect  Parent recollect instance.
   * @param  {IDXCursor} dsRecord   Cursor for record in `_config`
   */
  var initDatastoreObject = function (recollect, dsRecord) {
    var dsName = dsRecord.key;
    // var dsConfig = dsRecord.value;

    if (dsName === "_config") {
      return;
    }
    if (!_.isUndefined(recollect[dsName])) {
      throw new Errors.InitializationError("Invalid datastore name or Recollect instance " +
        "already initialized.");
    }
    recollect[dsName] = new Datastore({
      dbName: recollect.dbName,
      dsName: dsName
    });
  };

  var Recollect = function (name) {
    if (!name) {
      throw new Errors.InvalidArgumentError("You must provide a database name.");
    }
    this.dbName = name;
    return this;
  };

  /**
   * Iterates through database's `_config` datastore.  For each record found
   * of name `n`, create `this.n` instance of Datastore prototype.
   *
   * @return {Promise}  Resolves to Recollect instance after Datastore instances
   *                    have successfully been attached.
   */
  Recollect.prototype.initialize = function () {
    var self = this;
    return ixdb.getMany({
      db: this.name,
      datastore: "_config",
      query: null,
      findMany: true
    }).then(function (datastoreRecords) {
      _.each(datastoreRecords, _.partial(initDatastoreObject, self));
      return self;
    });
  };

  /**
   * Creates and configures new datastore.
   *
   * @param  {String}  options.dsName            Name of new datastore.
   * @param  {Boolean} options.autoIncrement     If true, keys are automatically
   *                                             generated.
   * @param  {String}  options.keyPath           Name of primary key path.
   * @param  {Object}  options.indexes           Object describing fields to index.
   *                     .fieldName.unique       If true, no two entries should share same
   *                                             value in fieldName.
   *                     .fieldName.multiEntry   If true, values in fieldName should be an
   *                                             array of values to be queried independently.
   *
   * @return {Promise}                        Resolves to new datastore object.
   */
  Recollect.prototype.createDatastore = function (options) {
    var self = this;

    options = utils.normalizeOptions(options, ["dsName"], {
      autoIncrement: true,
      keyPath: "_id",
      indexes: {}
    });

    return ixdb.createDatastore(_.extend({}, options, { dbName: this.dbName }))
      .then(function (/* datastore */) {
        return ixdb.addMany({
          dbName: self.dbName,
          dsName: "_config",
          records: [{
            dsName: options.dsName,
            autoIncrement: options.autoIncrement,
            keyPath: options.keyPath,
            indexes: options.indexes,
            created: (new Date()).getTime(),
            lastSynced: null
          }]
        });
      })
      .then(function () {
        initDatastoreObject(self, { key: options.dsName });
      });
  };

  Recollect.prototype.Datastore = Datastore;
  Recollect.prototype.Errors = Errors;

  return Recollect;
});
<|endoftext|>