Mongoose Middleware

This was a quick exercise in DX improvement for my team while at Griffin. We were spending a lot of time on rework finding broken connections between related entity types, adding double or triple ODM operations in the affected microservices, and fixing existing occurrences manually in the database.

This middleware set was added to a library used across our distributed system to check certain relationship parameters and make any appropriate changes to the related entity any time an operation affecting relational fields was executed.


addSelfToParentsChildren.js:
import _ from 'lodash';
import GPConstants from '@griffingroupglobal/constants';

import { getResourceUri } from '../utilities';

// Disabling these b/c Mongoose middleware needs context binding and next() chain (using
// fallthrough does not wait for asynchronous side effects which can break subsequent middlewares)
/* eslint-disable func-names, no-else-return, no-underscore-dangle */

/**
 * Add self to parent's children
 * @param next {function} The built-in Mongoose function to invoke the next middleware in the chain
 * @returns {Promise} Subsequent middleware after local side effects
 */
const addSelfToParentsChildren = async function (next) {
  const fieldPath = process.env.RELATIONSHIPS_FIELD_PATH || 'relationships';

  // If my parent is being changed...
  /* istanbul ignore next */
  if (_.includes(this.modifiedPaths(), `${fieldPath}.parent`)) {
    const selfUri = getResourceUri(this);
    const db = this.constructor.db.client.db();

    return Promise.all(_.map(GPConstants.API.resource.name, resourceType => (
      db
        .collection(resourceType)
        .updateMany(
          {
            // ...find the parent I'm now referencing...
            reference: _.get(this, `${fieldPath}.parent`),
          },
          {
            // ...and add me to its list of children
            $push: { [`${fieldPath}.children`]: selfUri },
            // ...update the metadata.lastUpdated field with current datetime
            $set: { 'metadata.lastUpdated': Date.now() },
          },
        )
        .then(/* istanbul ignore next */ () => next())
        .catch(/* istanbul ignore next */ err => next(err))
    )));
  } else {
    return next();
  }
};

export default addSelfToParentsChildren;

removeDuplicateChildren.js:
import _ from 'lodash';

// Disabling these b/c Mongoose middleware needs context binding and next() chain (using
// fallthrough does not wait for asynchronous side effects which can break subsequent middlewares)
/* eslint-disable func-names, no-else-return, no-underscore-dangle */

/**
 * Detect & remove any duplicate children
 * @param next {function} The built-in Mongoose function to invoke the next middleware in the chain
 * @returns {Promise} Subsequent middleware after local side effects
 */
const removeDuplicateChildren = function (next) {
  const fieldPath = process.env.RELATIONSHIPS_FIELD_PATH || 'relationships';

  // If we're PATCHing
  if (!this.isNew) {
    // ...and children are one of the things being changed
    /* istanbul ignore next */
    if (_.includes(this.modifiedPaths(), `${fieldPath}.children`)) {
      return (
        // Get the existing thing from the db
        this.constructor
          .findOne({ id: this.id })
          .then((existing) => {
            // Check op type flag
            if (this._isRemoveOp) {
              // If the remove op flag is set, perform a (destructive) filter on the existing object
              _.set(this, `${fieldPath}.children`, _.filter(
                _.get(existing, `${fieldPath}.children`),
                item => _.includes(_.get(this, `${fieldPath}.children`), item),
              ));

              delete this._isRemoveOp;
            } else {
              // Or if it's an $add op or regular save i.e. from the create or update route handler,
              // do a non-duping, non-destructive merge of its children w/ our patch's
              _.set(this, `${fieldPath}.children`, _.uniq([
                ..._.get(this, `${fieldPath}.children`),
                ..._.get(existing, `${fieldPath}.children`),
              ]));
            }
          })
          .catch(/* istanbul ignore next */ err => next(err))
      );
    } else {
      /* istanbul ignore next */
      return next();
    }
  } else {
    // If we're POSTing, remove dupes directly from the input array
    _.set(this, `${fieldPath}.children`, _.uniq(_.get(this, `${fieldPath}.children`)));

    /* istanbul ignore next */
    return next();
  }
};

export default removeDuplicateChildren;

removeOrphanRelationships.js:
import _ from 'lodash';
import GPConstants from '@griffingroupglobal/constants';
import { getResourceUri } from '../utilities';

// Disabling these b/c Mongoose middleware needs context binding and next() chain (using
// fallthrough does not wait for asynchronous side effects which can break subsequent middlewares)
/* eslint-disable func-names, no-else-return, no-underscore-dangle */

// This lib also uses asynchronous functionality across multiple database collections that it
// would not be practical/useful to mock in unit tests.
/* istanbul ignore file */

/**
 * Break any one-way relationships w/ orphans
 * @param next {function} The built-in Mongoose function to invoke the next middleware in the chain
 * @returns {Promise} Subsequent middleware after local side effects
 */
const removeOrphanRelationships = async function (next) {
  const fieldPath = process.env.RELATIONSHIPS_FIELD_PATH || 'relationships';
  const selfUri = getResourceUri(this);
  const db = this.constructor.db.client.db();

  return Promise.all(_.map(GPConstants.API.resource.name, resourceType => (
    db
      .collection(resourceType)
      .updateMany({
        $and: [
          // If it thinks I'm its parent...
          { [`${fieldPath}.parent`]: selfUri },

          // ...and it's not in my list of children...
          { reference: { $nin: _.get(this, `${fieldPath}.children`) } },
        ],
      }, {
        // ...remove its parent reference
        $unset: { [`${fieldPath}.parent`]: '' },
      })
      .then(/* istanbul ignore next */ () => next())
      .catch(/* istanbul ignore next */ err => next(err))
  )));
};

export default removeOrphanRelationships;

setChildrensParentToSelf.js:
import _ from 'lodash';
import GPConstants from '@griffingroupglobal/constants';
import { getResourceUri } from '../utilities';

// Disabling these b/c Mongoose middleware needs context binding and next() chain (using
// fallthrough does not wait for asynchronous side effects which can break subsequent middlewares)
/* eslint-disable func-names, no-else-return, no-underscore-dangle */

// This lib also uses asynchronous functionality across multiple database collections that it
// would not be practical/useful to mock in unit tests.
/* istanbul ignore file */

/**
 * Set children's parent to self
 * @param next {function} The built-in Mongoose function to invoke the next middleware in the chain
 * @returns {Promise} Subsequent middleware after local side effects
 */

const setChildrensParentToSelf = async function (next) {
  const fieldPath = process.env.RELATIONSHIPS_FIELD_PATH || 'relationships';
  const selfUri = getResourceUri(this);
  const db = this.constructor.db.client.db();

  await Promise.all(_.map(GPConstants.API.resource.name, resourceType => (
    db
      .collection(resourceType)
      .updateMany(
        {
          // If it's in my list of children...
          reference: { $in: _.get(this, `${fieldPath}.children`) },
        },
        {
          $set: {
            // ...set its parent reference to point at me
            [fieldPath]: { parent: selfUri },
            // ...update the metadata.lastUpdated field with current datetime
            'metadata.lastUpdated': Date.now(),
          },
        },
      )
      .then(/* istanbul ignore next */ () => next())
      .catch(/* istanbul ignore next */ err => next(err))
  )));

  next();
};

export default setChildrensParentToSelf;