senecajs/seneca-mongo-store

View on GitHub
lib/intern.js

Summary

Maintainability
C
7 hrs
Test Coverage
const Mongo = require('mongodb')
const ObjectID = Mongo.ObjectID


function ensure_id(ent, options) {
  let id = undefined

  if (undefined !== ent.id$) {
    id = ent.id$
  } else if (options.generate_id) {
    id = options.generate_id(ent)
  }

  return makeid(id)
}


function makeid(hexstr) {
  if ('string' === typeof hexstr && 24 === hexstr.length) {
    try {
      return ObjectID.createFromHexString(hexstr)
    } catch (e) {
      return hexstr
    }
  }

  return hexstr
}


function idstr(obj) {
  return obj && obj.toHexString ? obj.toHexString() : '' + obj
}


function fixquery(q, seneca, options) {
  if (q.native$) {
    return  Array.isArray(q.native$) ? q.native$[0] : q.native$
  }

  if ('string' === typeof q) {
    return { _id: makeid(q) }
  }
  
  if (Array.isArray(q)) {
    return {
      _id: { $in: q.map((id) => makeid(id)) }
    }
  }

  if (q.id) {
    if (Array.isArray(q.id)) {
      return {
        _id: { $in: q.id.map((id) => makeid(id)) }
      }
    }

    return { _id: makeid(q.id) }
  }


  const qq = {}

  for (const qp in q) {
    if (is_seneca_directive(qp)) {
      continue
    }


    if (is_mongo_operator(qp)) {
      if (should_strip_mongo_qualifiers(options)) {
        continue
      }

      seneca.log.warn('Passing MongoDB operators directly via the query'  +
        ' may be unsafe and is being deprecated. In the future releases,' +
        ' support for this may be removed.')
    }


    if (Array.isArray(q[qp]) && !is_mongo_operator(qp)) {
      qq[qp] = { $in: q[qp] }
    } else {
      qq[qp] = q[qp]
    }
  }

  return qq
}


function is_seneca_directive(p) {
  return p.match(/\$$/)
}


function is_mongo_operator(p) {
  return p.startsWith('$')
}


function should_strip_mongo_qualifiers(options) {
  return false == options.mongo_operator_shortcut
}


function metaquery(q) {
  let mq = {}

  if (!q.native$) {
    if (q.sort$) {
      let sf
      for (sf in q.sort$) break
      const sd = q.sort$[sf] < 0 ? 'descending' : 'ascending'
      mq.sort = [[sf, sd]]
    }

    if (q.limit$) {
      mq.limit = q.limit$ >= 0 ? q.limit$ : 0
    }

    if (q.skip$) {
      mq.skip = q.skip$ >= 0 ? q.skip$ : 0
    }

    if (q.fields$) {
      mq.fields = q.fields$
    }
  } else {
    mq = Array.isArray(q.native$) ? q.native$[1] : mq
  }

  return mq
}


function makeent(base_doc, ent, seneca) {
  if (null == base_doc) {
    return null
  }

  const doc = seneca.util.deep(base_doc)

  doc.id = idstr(doc._id)
  delete doc._id

  return ent.make$(doc)
}


function should_merge(ent, options) {
  return !(false === options.merge || false === ent.merge$)
}


function is_mongo_duplicate_key_error(err) {
  return err &&
    11000 === err.code &&
    'DuplicateKey' === err.codeName
}


function attempt_upsert(coll, filter_by, replacement, done) {
  function attempt(attempt_no) {
    // NOTE:
    // Sources:
    // - https://stackoverflow.com/questions/29305405/mongodb-impossible-e11000-duplicate-key-error-dup-key-when-upserting
    // - https://jira.mongodb.org/browse/SERVER-14322
    //
    // > > It is possible that two updates come in with upsert:true,
    // > > resulting in neither finding a document and both inserting
    // > > new documents which conflict on unique index violations of
    // > > the query predicate.
    // >
    // > The "solution" here is to add a retry code into the client.
    //
    return coll.findOneAndUpdate(
      filter_by,
      replacement,
      { upsert: true, returnOriginal: false },

      function (err, update) {
        if (err) {
          if (is_mongo_duplicate_key_error(err) && attempt_no < 3) {
            return attempt(attempt_no + 1)
          }

          return done(err)
        }

        return done(null, update)
      }
    )
  }

  return attempt(1)
}


module.exports = {
  intern: {
    ensure_id, makeid, idstr, fixquery, metaquery, makeent,
    should_merge, is_seneca_directive, is_mongo_operator,
    should_strip_mongo_qualifiers, is_mongo_duplicate_key_error,
    attempt_upsert
  }
}