Link to home
Start Free TrialLog in
Avatar of Bruce Gust
Bruce GustFlag for United States of America

asked on

Is this explanation of Discriminators correct?

Here's my question:

If I attempt to add two notes without refreshing the page, I get an error that looks like this:

User generated image
Here's my Activity.js with the discriminator key:

const mongoose = require('mongoose');

const { Schema } = mongoose;

const ActivitySchema = new Schema(
  {
    type: {
      type: String,
      required: true,
      enum: [
        'call',
        'visit',
        'reminder',
        'event',
        'Company created',
        'Company updated',
        'Company deleted',
        'Broker info added',
        'Broker info updated',
        'Contact added',
        'Contact updated',
        'Contact deleted',
        'Address added',
        'Address updated',
        'Address deleted',
        'Proposal created',
        'Proposal updated',
        'Converted to company',
        'Target selected',
        'Target ignored',
        'Owner change',
        'Proposal assigned',
        'File attached',
        'File removed',
        'User added',
        'User deleted',
        'Service line added',
        'Service line deleted',
        'Swimlane added',
        'Swimlane removed',
        'Custom Fields Updated',
        'Proposal downloaded',
        'Proposal emailed'
      ]
    },
    company: {
      type: Schema.Types.ObjectId,
      required: true,
      ref: 'Company'
    },
    proposal: {
      type: Schema.Types.ObjectId,
      ref: 'Proposal'
    },
    pipeline: {
      type: Schema.Types.ObjectId, // for tracking activity on each pipeline stage.
      ref: 'PipelineColumn'
    },
    user: {
      type: Schema.Types.ObjectId,
      ref: 'User'
    },
    date: {
      type: Date,
      default: Date.now
    },
    meta: {
      Details: String,
      date: Date,
      notes: String
    },
    account: {
      type: Schema.Types.ObjectId,
      default: () => global._user.account._id
    },
    active: { type: Boolean, default: true }
  },
  { discriminatorKey: 'type' }
);
// removed strict:false and added discriminatorKey for meta.start, meta.end, meta.date fields for type=event/call/note/reminder

module.exports = ActivitySchema;

Open in new window


Based on what I understand about Discriminators, this:

// removed strict:false and added discriminatorKey for meta.start, meta.end, meta.date fields for type=event/call/note/reminder

...is bogus.

Tell me if I'm right...

A Discriminator is what connects two models together. For example, I have a base model where I define several fields as well as a "discriminator key." That "key" is actually another column that will be defined in a different model.

If I have a collection of books, a collection of movies and a collection of tv shows, they all have "titles" and "release dates." They're unique, however, in that each book has an "author," a TV Show has a "season" and a movie has  "director."

I can set up my database in a way where I've got a "base" model that has two columns set aside for "titles" and "release date" and also includes a "discriminator key" called, "itemtype." You can almost think of this as an abstract class in that you'll never directly called the "base" model, although it's present in every one of the other models that you will call.

My "books" model will have a single column called "author." But in reality, the "books" model will be processed as having, not just the "author" column, because it's an extension of the "base" model, it will include all of the other columns referenced in the "base" model as well.  

So, for all intents and purposes, when we go to retrieve the info from the "book" collection (which is based on the "book.js" model), it will look like this:

{
    "_id": {
        "$oid": "unique object ID"
    },
    "itemtype": "Book",
    "author": "Book Author 1",
    "title": "Book Title 1",
    "date_added": {
        "$date": "2018-02-01T00:00:00.000Z"
    },
    "redo": false,
}

What you see in bold is the "discriminator key" established in the "base" model.

Notice in the base.js file, the "discriminator key" is referred to as "itemtype." That's just a place holder. When it's actually rendered, that column will read "author" for books, "director" for movies and "season" for TV shows.

is that explanation correct?

Based on that explanation, when I do a search for "discriminator" in my schemas directory, it shows up one time and one time only and that's in the "Activity.js" schema that you see above.

That seems wrong for two reasons.

First of all, I don't see how the Activity.js schema is serving as a "base" for another model and...

...a Discriminator key is not going be named according to an incoming piece of data. Based on what I understand that key is a unique placeholder and to refer to it using the same name as an incoming value is wrong.

When I remove this:   { discriminatorKey: 'type' }

I can add as many notes as I want to and will not get that error.

But have I compromised something else based on the "// removed strict:false and added discriminatorKey for meta.start, meta.end, meta.date fields for type=event/call/note/reminder" comment?
ASKER CERTIFIED SOLUTION
Avatar of ste5an
ste5an
Flag of Germany image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of Bruce Gust

ASKER

stefan! I found the "beast..."

Here's my company.js service:

const { ObjectId } = require('mongodb');
const { Util } = require('node-utils');
const moment = require('moment-timezone');
const mongoose = require('mongoose');


const Service = require('./service');
const CompanySchema = require('../schemas/CompanySchema');
const ActivitySchema = require('../schemas/ActivitySchema');
const ProposalSchema = require('../schemas/ProposalSchema');
const UserSchema = require('../schemas/UserSchema');
const CompanyAddress = require('./companyaddress');
const GeoLocations = require('./GeoLocations');
const CompanyModel = require('../lib/model-company');


const ServiceSchema = require('../schemas/ServiceSchema');


const ProductsAndService = mongoose.model('Service', ServiceSchema);
const Proposal = mongoose.model('Proposal', ProposalSchema);


const userModel = mongoose.model('User', UserSchema);


class Company extends Service {
  constructor() {
    super();


    this.collection = 'companies';
    this.plural = 'companies';
    this.singular = 'company';


    this.model = mongoose.model('Company', CompanySchema);


    this.fields = CompanyModel;
    this.fields._id = {};
    this.fields._id.type = ObjectId;


    this.corePipeline = [
      {
        $lookup: {
          from: 'users',
          foreignField: '_id',
          localField: 'owner',
          as: 'owner'
        }
      },
      { $unwind: { path: '$owner', preserveNullAndEmptyArrays: true } }
    ];
  }


  datatable(input, baseFilters = {}) {
    return new Promise((resolve, reject) => {
      const config = this._getDataTablesFilters(input);
      let pipeline = [];
      let countPipeline = []; // the filters used for counts


      // add in the base filters
      config.filters = Util.merge(baseFilters, config.filters);


      // set the base pipeline where/match clause
      pipeline.push({ $match: this._enforceScope() });


      // add sorting
      if (Object.keys(config.sort).length > 0) {
        if (typeof config.sort[0] === 'undefined') {
          pipeline.push({ $sort: config.sort });
        }
      }
      // Owner
      pipeline.push(
        {
          $lookup: {
            from: 'users',
            let: { ownerId: '$owner' },
            pipeline: [
              { $match: { $expr: { $and: [{ $eq: ['$_id', '$$ownerId'] }] } } },
              {
                $project: {
                  name: { $concat: ['$firstName', ' ', '$lastName'] }
                }
              }
            ],
            as: 'owner'
          }
        },
        { $unwind: { path: '$owner', preserveNullAndEmptyArrays: true } }
      );
      // Broker
      pipeline.push(
        { $addFields: { brokers: { $slice: ['$brokers', 1] } } },
        { $unwind: { path: '$brokers', preserveNullAndEmptyArrays: true } }
      );
      pipeline.push(
        { $addFields: { contacts: { $slice: ['$contacts', 1] } } },
        { $unwind: { path: '$contacts', preserveNullAndEmptyArrays: true } }
      );
      pipeline.push({
        $match: {
          $or: [
            { 'addresses.postal': { $in: global._user.account.postalCodes } },
            { clientImport: global._user.account._id }
          ]
        }
      });
      // Search text
      if (typeof input.search !== 'undefined') {
        if (input.search.value) {
          const searchQuery = this._getDataTablesSearch(input.search.value);
          if (typeof searchQuery !== 'undefined') {
            pipeline.push({ $match: searchQuery });
          }
        }
      }


      // set the filters pipeline where/match clause
      if (Object.keys(config.filters).length > 0) {
        pipeline.push({ $match: config.filters });
      }


      // set the count pipeline here since all of the filters should be applied
      // by this point
      countPipeline = Array.from(pipeline);


      // add the limits
      pipeline.push({ $skip: config.limits[0] });
      pipeline.push({ $limit: config.limits[1] });


      // add in a datatable specific field
      pipeline.push({
        $addFields: {
          DT_RowId: '$_id',
          currentUserId: global._user._id // for 'Assign to me' link
        }
      });


      // call the lifecycle method to allow extending classes to customize just
      // the pipeline without having to overwrite the whole method
      pipeline = this._beforeDatatablesPipeline(pipeline);


      this.model
        .aggregate(pipeline)
        .collation({ locale: 'en' })
        .then(async data => {
          resolve({
            draw: parseInt(input.draw, 10),
            data,
            recordsFiltered: await this.__aggCount(countPipeline), // filter wrong? await this._count(this._enforceScope()),
            recordsTotal: await this._count(this._enforceScope())
          });
        })
        .catch(err => {
          reject(err);
        });
    });
  }


  async getOne(companyId) {
    return new Promise((resolve, reject) => {
      // create a clone of the pipeline so we don't alter the original
      const pipeline = this.corePipeline.slice(0);


      // add an initial match stage to the core pipeline
      pipeline.unshift({
        $match: this._enforceScope({
          _id: this._getId(companyId)
        })
      });


      this.model
        .aggregate(pipeline)
        .collation({ locale: 'en' })
        .then(data => {
          if (data.length === 0) {
            reject(new Error('Could not find company record.'));
          } else {
            resolve(data[0]);
          }
        })
        .catch(err => {
          reject(err);
        });
    });
  }


  getOneContact(contactId, companyId) {
    return new Promise((resolve, reject) => {
      const pipeline = [
        {
          $match: this._enforceScope({ _id: this._getId(companyId) })
        },
        {
          $project: {
            contacts: {
              $filter: {
                input: '$contacts',
                as: 'contacts',
                cond: { $eq: ['$$contacts._id', this._getId(contactId)] }
              }
            }
          }
        },
        { $addFields: { 'contacts.company': '$_id' } },
        { $unwind: { path: '$contacts', preserveNullAndEmptyArrays: true } },
        { $project: { contacts: 1, _id: 0 } }
      ];
      this.model
        .aggregate(pipeline)
        .collation({ locale: 'en' })
        .then(rows => {
          resolve(rows[0].contacts);
        })
        .catch(reject);
    });
  }


  saveContact(contactId, contact) {
    return new Promise((resolve, reject) => {
      const theContact = contact;
      const companyId = theContact.company;
      theContact.active = true;
      if (contactId === 'new') {
        delete theContact.company;
        this.model
          .updateOne(
            this._enforceScope({ _id: this._getId(companyId) }),
            { $push: { contacts: theContact } },
            { setDefaultsOnInsert: true }
          )
          .then(() => {
            theContact.company = companyId;
            resolve(theContact);
          })
          .catch(err => {
            reject(err);
          });
      } else {
        this.model
          .updateOne(
            { _id: companyId },
            {
              $set: {
                'contacts.$[i].email': theContact.email,
                'contacts.$[i].name': theContact.name,
                'contacts.$[i].phone': theContact.phone
              }
            },
            {
              arrayFilters: [
                {
                  'i._id': this._getId(contactId)
                }
              ],
              setDefaultsOnInsert: true
            }
          )
          .then(() => {
            theContact.company = companyId;
            resolve(theContact);
          })
          .catch(err => {
            reject(err);
          });
      }
    });
  }


  async getOneAddress(addressId, companyId) {
    return new Promise((resolve, reject) => {
      const pipeline = [
        {
          $match: this._enforceScope({ _id: this._getId(companyId) })
        },
        {
          $project: {
            addresses: {
              $filter: {
                input: '$addresses',
                as: 'addresses',
                cond: { $eq: ['$$addresses._id', this._getId(addressId)] }
              }
            }
          }
        },
        { $addFields: { 'addresses.company': '$_id' } },
        { $unwind: { path: '$addresses', preserveNullAndEmptyArrays: true } },
        { $project: { addresses: 1, _id: 0 } }
      ];
      this.model
        .aggregate(pipeline)
        .collation({ locale: 'en' })
        .then(rows => {
          resolve(rows[0].addresses);
        })
        .catch(reject);
    });
  }


  saveAddress(addressId, address) {
    return new Promise((resolve, reject) => {
      const theAddress = address;
      theAddress.active = true;
      if (addressId === 'new') {
        delete theAddress.company;
        this.model
          .updateOne(
            this._enforceScope({ _id: address.company }),
            { $push: { addresses: theAddress } },
            { setDefaultsOnInsert: true }
          )
          .then(() => {
            theAddress.company = address.company;
            resolve(theAddress);
          })
          .catch(err => {
            reject(err);
          });
      } else {
        this.model
          .updateOne(
            this._enforceScope({ _id: address.company }),
            {
              $set: {
                'addresses.$[i].street': theAddress.street,
                'addresses.$[i].street2': theAddress.street2,
                'addresses.$[i].city': theAddress.city,
                'addresses.$[i].state': theAddress.state,
                'addresses.$[i].postal': theAddress.postal,
                'addresses.$[i].hash': theAddress.hash,
                'addresses.$[i].nonGeo': theAddress.nonGeo
              }
            },
            {
              arrayFilters: [
                {
                  'i._id': this._getId(addressId)
                }
              ],
              setDefaultsOnInsert: true
            }
          )
          .then(() => {
            theAddress.company = address.company;
            resolve(theAddress);
          })
          .catch(err => {
            reject(err);
          });
      }
    });
  }


  async getOneBroker(index, companyId) {
    return new Promise((resolve, reject) => {
      const theCompanyId = this._getId(companyId);
      this.model.findOne(
        { _id: theCompanyId, account: global._user.account._id },
        (err, res) => {
          if (err) {
            reject(err);
          }
          resolve(res.brokers[index]);
        }
      );
    });
  }


  async saveBroker(index, broker) {
    return new Promise((resolve, reject) => {
      let theBroker = broker;
      const companyId = this._getId(theBroker.company);
      const accountId = this._getId(theBroker.account);
      delete theBroker.company;
      delete theBroker.account;
      this.model.findOne({ _id: companyId, account: accountId }, (err, res) => {
        if (err) {
          reject(err);
        }
        res.brokers = res.brokers ? res.brokers : [];
        if (index === 'new') {
          res.brokers.push(theBroker);
          theBroker = res.brokers;
        } else {
          res.brokers[index] = theBroker;
          theBroker = res.brokers;
        }
        this.model.updateOne(
          { _id: companyId, account: accountId },
          { $set: { brokers: theBroker } },
          (updateErr, raw) => {
            if (updateErr) {
              reject(updateErr);
            }
            resolve(raw);
          }
        );
      });
    });
  }


  deleteContact(id, companyId) {
    return new Promise((resolve, reject) => {
      this.model
        .findOneAndUpdate(this._enforceScope({ _id: this._getId(companyId) }), {
          $pull: { contacts: { _id: this._getId(id) } }
        })
        .then(() => {
          this.addHistory(
            'Contact deleted',
            companyId,
            '',
            global._user._id,
            ''
          );
          resolve();
        })
        .catch(err => {
          reject(err);
        });
    });
  }


  deleteAddress(id, companyId) {
    return new Promise((resolve, reject) => {
      this.model
        .findOneAndUpdate(this._enforceScope({ _id: this._getId(companyId) }), {
          $pull: { addresses: { _id: this._getId(id) } }
        })
        .then(() => {
          this.addHistory(
            'Address deleted',
            companyId,
            '',
            global._user._id,
            ''
          );
          resolve();
        })
        .catch(err => {
          reject(err);
        });
    });
  }


  // Fix: This function will check for dates field type array and convert it into object. Converts array field to Object
  resolveDatesArray() {
    return new Promise((resolve, reject) => {
      const condition = { dates: { $type: 'array' } };
      this.model.updateMany(
        condition,
        { dates: {} },
        { upsert: true },
        (err, raw) => {
          if (err) {
            reject(err);
          }
          resolve(raw);
        }
      );
    });
  }


  select2(query) {
    return new Promise((resolve, reject) => {
      this.model
        .aggregate([
          {
            $match: this._enforceScope({
              name: {
                $regex: query,
                $options: 'i'
              }
            })
          },
          {
            $sort: {
              name: 1
            }
          },
          { $limit: 25 },
          {
            $project: {
              id: '$_id',
              text: '$name'
            }
          }
        ])
        .collation({ locale: 'en' })
        .then(rows => {
          resolve(rows);
        })
        .catch(err => {
          reject(err);
        });
    });
  }


  assign(companies, user) {
    return new Promise((resolve, reject) => {
      const where = this._enforceScope({ _id: { $in: [] } });
      let update = { $set: {} };


      if (typeof companies === 'string') {
        // eslint-disable-next-line no-param-reassign
        companies = [companies];
      }


      if (typeof user === 'undefined' || typeof companies === 'undefined') {
        reject(new Error('Invalid or missing parameters.'));
      } else {
        const ids = [];
        Object.keys(companies).forEach(i => {
          where._id.$in.push(this._getId(companies[i]));
          ids.push(this._getId(companies[i]));
        });
        let pipeline = [
          { $match: { _id: { $in: ids } } },
          {
            $lookup: {
              from: 'users',
              localField: 'owner',
              foreignField: '_id',
              as: 'owner'
            }
          }
        ];
        this.model
          .aggregate(pipeline)
          .collation({ locale: 'en' })
          .then(beforeRows => {
            update = {
              $set: {
                owner: this._getId(user),
                account: global._user.account._id, // new added
                active: true, // new added
                'dates.assigned': moment().toISOString() // new added for reporting, pipeline target column display
              }
            };


            this.model.updateMany(where, update, err => {
              if (err) {
                reject(this.error('Invalid parameters provided.'));
              } else {
                beforeRows.forEach(oldDoc => {
                  pipeline = [
                    { $match: { _id: oldDoc._id } },
                    {
                      $lookup: {
                        from: 'users',
                        localField: 'owner',
                        foreignField: '_id',
                        as: 'owner'
                      }
                    },
                    { $limit: 1 }
                  ];
                  this.model
                    .aggregate(pipeline)
                    .collation({ locale: 'en' })
                    .then(async rows => {
                      const newDoc = rows[0];
                      const meta = this.jsonDiff(
                        {
                          owner:
                            oldDoc.owner.length > 0
                              ? `<a href="javascript:void(0);" class="user-info" data-user-id="${oldDoc.owner[0]._id}">${oldDoc.owner[0].firstName} ${oldDoc.owner[0].lastName}</a>`
                              : 'Unassigned'
                        },
                        {
                          owner:
                            newDoc.owner.length > 0
                              ? `<a href="javascript:void(0);" class="user-info" data-user-id="${newDoc.owner[0]._id}">${newDoc.owner[0].firstName} ${newDoc.owner[0].lastName}</a>`
                              : 'Unassigned'
                        }
                      );
                      await this.addHistory(
                        'Owner change',
                        newDoc._id,
                        '',
                        global._user._id,
                        meta
                      );
                      resolve(this.success());
                    });
                });
              }
            });
          });
      }
    });
  }


  unassign(companies) {
    return new Promise((resolve, reject) => {
      const update = { $set: { owner: null } };
      let id;


      if (typeof companies === 'string') {
        // eslint-disable-next-line no-param-reassign
        companies = [companies];
      }


      if (typeof companies === 'undefined') {
        reject(new Error('Invalid or missing parameters.'));
      }


      Object.keys(companies).forEach(i => {
        id = this._getId(companies[i]);
        if (id) {
          const pipeline = [
            { $match: this._enforceScope({ _id: id }) },
            {
              $lookup: {
                from: 'users',
                localField: 'owner',
                foreignField: '_id',
                as: 'owner'
              }
            },
            { $limit: 1 }
          ];
          this.model
            .aggregate(pipeline)
            .collation({ locale: 'en' })
            .then(res => {
              if (res.length > 0) {
                this.model.updateOne({ _id: id }, update, async err => {
                  if (err) {
                    reject(err);
                  }
                  if (typeof res[0].owner !== 'undefined') {
                    const meta = this.jsonDiff(
                      {
                        owner:
                          res[0].owner.length > 0
                            ? `<a href="javascript:void(0)" class="user-info" data-route="company" data-user-id="${res[0].owner[0]._id}">${res[0].owner[0].firstName} ${res[0].owner[0].lastName}</a>`
                            : 'Unassigned'
                      },
                      { owner: 'Unassigned' }
                    );
                    await this.addHistory(
                      'Owner change',
                      id,
                      '',
                      global._user._id,
                      meta
                    );
                  }
                  if (parseInt(i, 10) + 1 === companies.length) {
                    resolve(this.success());
                  }
                });
              }
            })
            .catch(reject);
        }
      });
    });
  }


  softDeleteItems(companies) {
    return new Promise((resolve, reject) => {
      if (typeof companies === 'string') {
        // eslint-disable-next-line no-param-reassign
        companies = [companies];
      }


      if (typeof companies === 'undefined' || companies.length === 0) {
        reject(Company.error('Please provide one or more Company IDs.'));
      } else {
        Object.keys(companies).forEach(i => {
          const id = this._getId(companies[i]);
          if (id) {
            const pipeline = [
              { $match: this._enforceScope({ _id: id }) },
              {
                $lookup: {
                  from: 'proposals',
                  localField: '_id',
                  foreignField: 'company',
                  as: 'proposals'
                }
              },
              { $limit: 1 }
            ];
            this.model
              .aggregate(pipeline)
              .collation({ locale: 'en' })
              .then(res => {
                if (res.length > 0) {
                  this.model.updateOne(
                    { _id: id },
                    { $set: { deleted: true, active: false } },
                    async err => {
                      if (err) {
                        reject(err);
                      }
                      // delete Proposals
                      if (res[0].proposals.length > 0) {
                        await Proposal.updateMany(
                          { company: id },
                          { $set: { active: false } },
                          async proposalErr => {
                            if (proposalErr) {
                              reject(proposalErr);
                            }
                            // To do nothing.
                          }
                        );
                      }
                      const meta = this.jsonDiff(
                        { active: true },
                        { active: false, deleted: true }
                      );
                      await this.addHistory(
                        'Company deleted',
                        id,
                        '',
                        global._user._id,
                        meta
                      );


                      if (parseInt(i, 10) + 1 === companies.length) {
                        resolve(this.success());
                      }
                    }
                  );
                }
              })
              .catch(reject);
          }
        });
      }
    });
  }


  getActivity(companyId) {
    return new Promise((resolve, reject) => {
      const Activity = mongoose.model('Activity', ActivitySchema);
      const pipeline = [
        {
          $match: {
            account: global._user.account._id,
            company: companyId
          }
        },
        {
          $lookup: {
            from: 'users',
            foreignField: '_id',
            localField: 'user',
            as: 'user'
          }
        },
        { $unwind: '$user' },
        {
          $group: {
            _id: {
              year: { $year: '$date' },
              month: { $month: '$date' },
              day: { $dayOfMonth: '$date' }
            },
            activities: {
              $push: {
                _id: '$_id',
                type: '$type',
                company: '$company',
                proposal: '$proposal',
                user: {
                  _id: '$user._id',
                  firstName: '$user.firstName',
                  lastName: '$user.lastName'
                },
                date: '$date',
                meta: '$meta'
              }
            }
          }
        },
        { $project: { date: '$_id', activities: 1, _id: 0 } },
        { $sort: { 'activities.date': -1 } }
      ];
      Activity.aggregate(pipeline)
        .collation({ locale: 'en' })
        .then(rows => {
          const theRows = rows;
          const activity = [];


          Object.keys(theRows).forEach(i => {
            Object.keys(theRows[i].activities).forEach(j => {
              // get the icon by type
              theRows[i].activities[j].icon = this.getIconByType(
                theRows[i].activities[j].type
              );
              theRows[i].activities[j].prettyDate = moment(
                theRows[i].activities[j].date
              ).fromNow();
              theRows[i].activities[j].readableDate = moment(
                theRows[i].activities[j].date
              ).format('MMM Do - LT');
            });
            // add it to the list
            theRows[i].prettyDate = moment(
              `${theRows[i].date.year}-${theRows[i].date.month}-${theRows[i].date.day}`,
              'YYYY-MM-DD'
            )
              .local()
              .fromNow();
            activity.push(theRows[i]);
          });


          resolve(activity);
        })
        .catch(err => {
          reject(err);
        });
    });
  }


  // eslint-disable-next-line class-methods-use-this
  getFiles(companyId) {
    return new Promise((resolve, reject) => {
      Proposal.aggregate([
        {
          $match: this._enforceScope({
            company: companyId,
            files: {
              $exists: true
            }
          })
        },
        { $unwind: '$files' },
        {
          $project: {
            proposal: '$_id',
            name: '$name',
            file: '$files'
          }
        }
      ])
        .then(resolve)
        .catch(reject);
    });
  }


  addActivity(companyId, type, data) {
    return new Promise((resolve, reject) => {
      if (
        typeof type !== 'string' ||
        ['call', 'visit', 'reminder', 'event'].indexOf(type.trim()) === -1
      ) {
        reject(new Error('Invalid type provided'));
      } else {
        const Base = mongoose.model('Activity', ActivitySchema);


        // to track activity on each pipeline stage
        if (data.proposal !== 'undefined' && data.proposal !== 'bulk') {
          Proposal.findById(this._getId(data.proposal), (err, res) => {
            if (err) {
              reject(err);
            }
            if (res) {
              // eslint-disable-next-line no-param-reassign
              data.pipeline = res.pipeline;
            }
          });
        }


        if (type === 'event') {
          // define dynamic Schema
          const Event = Base.discriminator(
            'event',
            new mongoose.Schema({
              meta: {
                start: Date,
                end: Date,
                subject: String,
                participants: String,
                location: String,
                details: String
              }
            })
          );
          const _data = {
            company: new ObjectId(companyId),
            type: type.trim(),
            meta: {
              start: data.start,
              end: data.end,
              subject: data.subject,
              participants: data.participants,
              location: data.location,
              details: data.details
            },
            user: global._user._id,
            account: global._user.account._id
          };
          if (data.proposal !== 'undefined' && data.proposal !== 'bulk') {
            _data.proposal = data.proposal;
            _data.pipeline = data.pipeline;
          }
          Event.create(_data, (err, record) => {
            if (err) {
              reject(
                new Error(
                  'An unexpected error was encountered. Please try again.'
                )
              );
            } else {
              resolve(record._id);
            }
          });
        } else {
          // define dynamic Schema
          // FIX: descriminator with name 'call' already exists.
          let ActivityModel = Base.discriminator(
            type.trim(),
            new mongoose.Schema({ meta: { date: Date, notes: String } })
          );
          if (
            Base.discriminators &&
            Object.keys(Base.discriminators).indexOf(type.trim()) >= 0
          ) {
            ActivityModel = Base.discriminators[type.trim()];
          }


          const _data = {
            company: new ObjectId(companyId),
            type: type.trim(),
            meta: {
              date: moment(data.date, 'MM/DD/YYYY hh:mm A'),
              notes: data.notes
            },
            user: global._user._id,
            account: global._user.account._id
          };
          if (data.proposal !== 'undefined' && data.proposal !== 'bulk') {
            _data.proposal = data.proposal;
            _data.pipeline = data.pipeline;
          }
          ActivityModel.create(_data, (err, record) => {
            if (err) {
              reject(
                new Error(
                  'An unexpected error was encountered. Please try again.'
                )
              );
            } else {
              resolve(record._id);
            }
          });
        }
      }
    });
  }


  deleteActivity(activityId) {
    return new Promise((resolve, reject) => {
      const match = this._enforceScope({
        _id: new ObjectId(activityId)
      });


      const Activity = mongoose.model('Activity', ActivitySchema);


      Activity.deleteOne(match, err => {
        if (err) {
          reject(err);
        } else {
          resolve();
        }
      });
    });
  }


  exportToCSV(input) {
    return new Promise((resolve, reject) => {
      const config = this._getDataTablesFilters(input);
      let pipeline = [];
      const baseFilters = {
        account: global._user.account._id,
        active: true,
        deleted: {
          $exists: false
        }
      };


      // add in the base filters
      config.filters = Util.merge(baseFilters, config.filters);


      // set the base pipeline where/match clause
      pipeline.push({ $match: this._enforceScope() });


      // lifecylce method to give the user the ability to inject setup stages to
      // the pipeline before we start adding in user-defined filters, limits and
      // sorting options
      pipeline = this._startPipeline(pipeline);


      // Search text
      if (typeof input.search !== 'undefined') {
        if (input.search.value) {
          const searchQuery = this._getDataTablesSearch(input.search.value);
          if (typeof searchQuery !== 'undefined') {
            pipeline.push({ $match: searchQuery });
          }
        }
      }


      // set the filters pipeline where/match clause
      if (Object.keys(config.filters).length > 0) {
        pipeline.push({ $match: config.filters });
      }


      // add sorting
      if (Object.keys(config.sort).length > 0) {
        pipeline.push({ $sort: config.sort });
      }


      // we need to format the arrays - not sure what the heck is going on with
      // using $addresses.0.street in the projection stage below, but I had some
      // issues getting it to work
      pipeline.push({
        $addFields: {
          _contact: {
            $slice: ['$contacts', 1]
          },
          _address: {
            $slice: ['$addresses', 1]
          }
        }
      });


      pipeline.push({
        $unwind: {
          path: '$owner',
          preserveNullAndEmptyArrays: true
        }
      });


      pipeline.push({
        $unwind: {
          path: '$_contact',
          preserveNullAndEmptyArrays: true
        }
      });


      pipeline.push({
        $unwind: {
          path: '$_address',
          preserveNullAndEmptyArrays: true
        }
      });


      // add the project stage
      pipeline.push({
        $project: {
          name: '$name',
          owner: '$owner.name',
          contact_name: '$_contact.name',
          contact_email: '$_contact.email',
          contact_phone: '$_contact.phone',
          address_street: '$_address.street',
          address_street2: '$_address.street2',
          address_city: '$_address.city',
          address_state: '$_address.state',
          address_postal: '$_address.postal',
          address_county: '$_address.county',
          industry: '$meta.industry',
          ftes: '$meta.ftes',
          fteGrouping: '$meta.fteGrouping',
          premium: '$meta.premium',
          premiumPerFte: '$meta.premiumPerFte',
          brokerCommissionTotal: '$meta.brokerCommissionTotal',
          brokerFeesTotal: '$mata.brokerFeesTotal',
          fundingType: '$meta.fundingType',
          ein: '$meta.ein'
        }
      });


      // get the data
      // TODO: replace with Brad's CSV library for consistency
      this.model
        .aggregate(pipeline)
        .collation({ locale: 'en' })
        .then(rows => {
          const papaparse = require('papaparse');
          const csv = papaparse.unparse(rows);


          resolve(csv);
        })
        .catch(err => {
          reject(err);
        });
    });
  }


  _startPipeline(pipeline) {
    const coreClone = this.corePipeline.slice(0);


    // remove the first stage of the core to add in the below projection
    coreClone.shift();


    // add in the owner's info
    coreClone.unshift({
      $lookup: {
        from: 'users',
        let: { ownerId: '$owner' },
        pipeline: [
          {
            $match: {
              $expr: {
                $and: [{ $eq: ['$_id', '$$ownerId'] }]
              }
            }
          },
          {
            $project: {
              name: {
                $concat: ['$firstName', ' ', '$lastName']
              }
            }
          }
        ],
        as: 'owner'
      }
    });


    return pipeline.concat(coreClone);
  }


  async targetSelected(companies = [], user) {
    const Targetpipelines = await this.getPipelineColumns();


    return new Promise((resolve, reject) => {
      try {
        if (companies.length > 0 && user.length > 0) {
          const index = Targetpipelines.map(o => o.key).indexOf('target');
          const Targetpipeline = Targetpipelines[index];
          if (typeof Targetpipeline !== 'undefined') {
            let count = 0;
            companies.forEach(company => {
              const targetProposal = {
                name: '_target',
                owner: this._getId(user),
                company: this._getId(company),
                pipeline: Targetpipeline._id,
                active: true,
                services: [],
                files: [],
                _range: [moment().toISOString(), moment().toISOString()],
                _pipelineDate: moment().toISOString(),
                pipelineIndex: 0,
                account: global._user.account._id,
                pipelineDuration: [
                  {
                    pipelineFrom: Targetpipeline._id,
                    fDate: moment().toISOString(),
                    pipelineTo: Targetpipeline._id,
                    tDate: moment().toISOString()
                  }
                ]
              };


              targetProposal.dates =
                typeof targetProposal.dates === 'undefined'
                  ? {}
                  : targetProposal.dates;


              targetProposal.dates[Targetpipeline._id] = moment().toISOString();


              Proposal.updateOne(
                {
                  owner: this._getId(user),
                  company: this._getId(company),
                  name: '_target'
                },
                targetProposal,
                { runValidators: true, upsert: true },
                async error => {
                  if (error) {
                    reject(error);
                  }
                  const targetUser = await this.getUserbyID(user);
                  if (targetUser) {
                    const meta = {
                      'Target Selected to': `<a href='javascript:void(0);' class='user-info' data-user-id='${targetUser._id}' data-route='market'>${targetUser.firstName} ${targetUser.lastName}</a>`
                    };
                    this.addHistory(
                      'Target selected',
                      this._getId(company),
                      '',
                      global._user._id,
                      meta
                    );
                  }
                  count += 1;
                  if (count === companies.length) {
                    resolve(true);
                  }
                }
              );
            });
          } else {
            reject(new Error('Pipeline Column is undefined.'));
          }
        } else {
          reject(
            new Error('Invalid or undefined company id or user id provided.')
          );
        }
      } catch (e) {
        reject(e);
      }
    });
  }


  getUserbyID(id) {
    return new Promise((resolve, reject) => {
      if (id) {
        userModel.findById(this._getId(id), (err, res) => {
          if (err) {
            reject(err);
          }
          resolve(res);
        });
      } else {
        reject(new Error('No user id supplied.'));
      }
    });
  }


  async ignoreTargetSelected(companies = []) {
    const Targetpipelines = await this.getPipelineColumns();
    return new Promise((resolve, reject) => {
      try {
        if (companies.length > 0) {
          const index = Targetpipelines.map(o => o.key).indexOf('target');
          const Targetpipeline = Targetpipelines[index];
          if (typeof Targetpipeline !== 'undefined') {
            const companyIds = [];
            companies.forEach(async company => {
              companyIds.push(this._getId(company));
              await this.addHistory(
                'Target ignored',
                this._getId(company),
                '',
                global._user._id,
                {}
              );
            });
            Proposal.deleteMany(
              { company: { $in: companyIds }, name: '_target' },
              error => {
                if (error) {
                  reject(error);
                }
                resolve(true);
              }
            );
          } else {
            reject(new Error('Pipeline Column is undefined.'));
          }
        } else {
          reject(new Error('Invalid or undefined company id provided.'));
        }
      } catch (e) {
        reject(e);
      }
    });
  }


  getOneForHistory(id) {
    return new Promise((resolve, reject) => {
      const filters = this._enforceScope({
        _id: this._getId(id)
      });


      const pipeline = [
        {
          $match: filters
        },
        {
          $lookup: {
            from: 'users',
            let: { ownerId: '$owner' },
            pipeline: [
              { $match: { $expr: { $eq: ['$_id', '$$ownerId'] } } },
              {
                $project: {
                  _id: 0,
                  assigned: {
                    $concat: [
                      "<a href='javascript:void(0)' class='user-info' data-user-id='",
                      { $toString: '$_id' },
                      "'>",
                      '$firstName',
                      ' ',
                      '$lastName',
                      '</a>'
                    ]
                  }
                }
              }
            ],
            as: 'owner'
          }
        },
        { $unwind: { path: '$owner', preserveNullAndEmptyArrays: true } },
        { $addFields: { owner: '$owner.assigned' } }
      ];


      this.model
        .aggregate(pipeline)
        .then(data => {
          if (data.length === 0) {
            reject(new Error('Could not find record.'));
          } else {
            resolve(data[0]);
          }
        })
        .catch(err => {
          reject(err);
        });
    });
  }


  getProposals(companyId) {
    return new Promise((resolve, reject) => {
      const pipeline = [
        {
          $match: this._enforceScope({
            company: this._getId(companyId),
            name: { $ne: '_target' }, // should not show _target proposals they are just 'Target Selected' pipeline
            $or: [{ active: { $exists: false } }, { active: true }]
          })
        },
        {
          $lookup: {
            from: 'pipelinecolumns',
            localField: 'pipeline',
            foreignField: '_id',
            as: 'pipeline'
          }
        },
        { $unwind: '$pipeline' },
        {
          $lookup: {
            from: 'services',
            localField: 'services.service',
            foreignField: '_id',
            as: 'products'
          }
        },
        {
          $lookup: {
            from: 'users',
            localField: 'owner',
            foreignField: '_id',
            as: 'owner'
          }
        },
        {
          $project: {
            'products.model': 0,
            'products.account': 0,
            'products.amount': 0,
            'owner.settings': 0,
            'owner.password': 0,
            'owner.email': 0,
            'owner.mobile': 0,
            'owner.account': 0
          }
        },
        {
          $unwind: '$owner'
        }
      ];


      Proposal.aggregate(pipeline, (err, rows) => {
        if (err) {
          reject(err);
        } else {
          resolve(rows);
        }
      });
    });
  }


  // eslint-disable-next-line class-methods-use-this
  getService(serviceId) {
    return new Promise((resolve, reject) => {
      const pipeline = [{ $match: this._enforceScope({ _id: serviceId }) }];
      ProductsAndService.aggregate(pipeline, (err, rows) => {
        if (err) {
          reject(err);
        } else {
          resolve(rows[0]);
        }
      });
    });
  }


  heatmap(ids) {
    return new Promise((resolve, reject) => {
      const pipeline = [];


      const _ids = [];
      if (ids && ids.length > 0) {
        ids.forEach(id => {
          _ids.push(this._getId(id));
        });


        const match = { $match: this._enforceScope() };
        delete match.$match.account;
        match.$match._id = { $in: _ids };
        pipeline.push(match);
        pipeline.push(
          { $unwind: { path: '$addresses', preserveNullAndEmptyArrays: true } },
          { $addFields: { company: { _id: '$_id', name: '$name' } } },
          {
            $lookup: {
              from: 'geo_locations',
              let: { addressId: '$addresses._id' },
              pipeline: [
                {
                  $match: {
                    $and: [
                      { $expr: { $eq: ['$address', '$$addressId'] } },
                      { $expr: { $eq: ['$active', true] } }
                    ]
                  }
                }
              ],
              as: 'geo_locations'
            }
          },
          {
            $unwind: {
              path: '$geo_locations',
              preserveNullAndEmptyArrays: true
            }
          },
          {
            $project: {
              _id: 0,
              type: 'Feature',
              properties: {
                name: '$name',
                popupContent: '$name',
                _id: '$_id',
                street: '$addresses.street',
                street2: '$addresses.street2',
                city: '$addresses.city',
                state: '$addresses.state',
                postal: '$addresses.postal',
                country: '$addresses.country',
                county: '$addresses.county',
                addresses: '$addresses',
                company: '$company',
                nonGeo: '$addresses.nonGeo'
              },
              geometry: '$geo_locations.geo'
            }
          }
        );
        this.model
          .aggregate(pipeline)
          .then(async rows => {
            let count = 0;
            const theRows = rows;


            rows.forEach(async (row, index) => {
              const theRow = row;
              if (theRow.geometry === undefined) {
                theRow.properties.addresses = theRow.properties.addresses
                  ? theRow.properties.addresses
                  : {};
                theRow.properties.addresses.company =
                  theRow.properties.company._id;
                const geoAddress = await this.getGeoAddress(
                  theRow.properties.addresses
                );
                theRows[index].geometry = geoAddress.geo;
                count += 1;
              } else {
                count += 1;
              }
              if (rows.length === count) {
                resolve({ type: 'FeatureCollection', features: theRows });
              }
            });
          })
          .catch(err => {
            reject(err);
          });
      } else {
        resolve({ type: 'FeatureCollection', features: [] });
      }
    });
  }


  getGeoAddress(address = {}) {
    return new Promise(resolve => {
      const theAddress = address;
      try {
        if (typeof theAddress.geo !== 'undefined') {
          resolve(theAddress);
        } else {
          let isValidAddress = false;
          theAddress.street = theAddress.street.trim();
          if (
            !theAddress ||
            theAddress.street === '' ||
            theAddress.city === '' ||
            theAddress.state === '' ||
            theAddress.postal === '' ||
            theAddress.street === undefined ||
            theAddress.city === undefined ||
            theAddress.state === undefined ||
            theAddress.postal === undefined
          ) {
            isValidAddress = false;
            theAddress.error = 'invalid';
            resolve(theAddress);
          } else {
            isValidAddress = true;
          }
          if (isValidAddress) {
            CompanyAddress.getGeocode(theAddress).then(geoaddress => {
              if (geoaddress) {
                if (geoaddress.error_message) {
                  theAddress.error = geoaddress.error_message;
                  resolve(theAddress);
                } else {
                  GeoLocations.model.updateOne(
                    { address: theAddress._id },
                    {
                      $set: {
                        address: theAddress._id,
                        company: theAddress.company,
                        geo: geoaddress.geo
                      }
                    },
                    { upsert: true, setDefaultsOnInsert: true },
                    err => {
                      if (err) {
                        resolve(theAddress);
                      } else {
                        this.model
                          .updateOne(
                            { _id: theAddress.company },
                            {
                              $set: {
                                'addresses.$[i].street': geoaddress.street,
                                'addresses.$[i].street2':
                                  geoaddress.street2 || '',
                                'addresses.$[i].city': geoaddress.city,
                                'addresses.$[i].state': geoaddress.state,
                                'addresses.$[i].postal': geoaddress.postal,
                                'addresses.$[i].hash': geoaddress.hash,
                                'addresses.$[i].nonGeo': geoaddress.nonGeo
                              }
                            },
                            {
                              arrayFilters: [
                                {
                                  'i._id': theAddress._id
                                }
                              ]
                            }
                          )
                          .then(() => {
                            resolve(geoaddress);
                          })
                          .catch(updateErr => {
                            theAddress.error = updateErr.msg;
                            resolve(theAddress);
                          });
                      }
                    }
                  );
                }
              } else {
                theAddress.error = 'invalid';
                resolve(theAddress);
              }
            });
          }
        }
      } catch (e) {
        resolve(theAddress);
      }
    });
  }


  updateMarketId(marketId, companyId) {
    return new Promise((resolve, reject) => {
      this.model
        .updateOne(this._enforceScope({ _id: this._getId(companyId) }), {
          $set: { market: marketId }
        })
        .then(company => {
          resolve(company);
        })
        .catch(updateErr => {
          reject(updateErr.msg);
        });
    });
  }
}


module.exports = new Company();



Open in new window

Look at line #912. What's happening there and why is it throwing an error?

I can make my problem go away completely simply be eliminating line #78 on my Activity.js schema: { discriminatorKey: 'type' }

...but why does the problem go away? I don't want to just kill some code without understanding what's happening and what I can do differently.

Thanks!