1
\$\begingroup\$

I have the following code in a file called database.js which will handle all the database operations for my small application. For some background, this application fetches stock data from an API and stores it in a local database so that future requests can pull the data from the database instead of making a network request (basically a rudimentary cache).

const DATABASE = 'mongodb://localhost/stockcli'

const _ = require('lodash')
const Promise = require('bluebird')
const moment = require('moment')
const mongoose = require('mongoose')
const Schema = mongoose.Schema
mongoose.Promise = Promise
mongoose.connect(DATABASE, { useMongoClient: true })

/**
 * This schema will be part of a mongoose model that contains each entry
 * in the stock data collections.
 * @type {Schema}
 */
const stockDataSchema = new Schema({
  timestamp: Date,
  ticker: String,
  open: Number,
  high: Number,
  low: Number,
  close: Number,
  volume: Number
})
stockDataSchema.index({ ticker: 1 })
stockDataSchema.index({ timestamp: 1 }, { unique: true })
const dataModels = {
  '1min': mongoose.model('1min', stockDataSchema),
  '5min': mongoose.model('5min', stockDataSchema),
  '15min': mongoose.model('15min', stockDataSchema),
  '30min': mongoose.model('30min', stockDataSchema),
  '60min': mongoose.model('60min', stockDataSchema),
  day: mongoose.model('day', stockDataSchema),
  week: mongoose.model('week', stockDataSchema),
  month: mongoose.model('month', stockDataSchema)
}

/**
 * This schema will be part of a set of models that contain the last time
 * an interval type was updated.
 * @type {Schema}
 */
const stockUpdateSchema = new Schema({
  ticker: String,
  lastUpdateTime: Date
})
stockUpdateSchema.index({ ticker: 1 }, { unique: true })
const lastUpdateModels = {
  '1min': mongoose.model('1min_update', stockUpdateSchema),
  '5min': mongoose.model('5min_update', stockUpdateSchema),
  '15min': mongoose.model('15min_update', stockUpdateSchema),
  '30min': mongoose.model('30min_update', stockUpdateSchema),
  '60min': mongoose.model('60min_update', stockUpdateSchema),
  day: mongoose.model('day_update', stockUpdateSchema),
  week: mongoose.model('week_update', stockUpdateSchema),
  month: mongoose.model('month_update', stockUpdateSchema)
}

/**
 * For a given data interval and company ticker, this function determines
 * the last time that we updated that company's stock data in our database.
 * It wraps the timestamp, regardless of validity, with a moment.js object.
 * @param {string} interval The data point interval
 * @param {string} ticker The company stock ticker
 * @return {Promise}
 */
const getLastUpdateTime = (interval, ticker) => {
  return lastUpdateModels[interval].findOne({ ticker }).exec().then(data => {
    return moment(data === null ? null : data.timestamp)
  })
}

/**
 * This function updates the entry in the database containing the last time that
 * a company's stock data was updated.
 * @param {string} interval The data point interval
 * @param {string} ticker The company stock ticker
 * @return {Promise}
 */
const updateLastUpdateTime = (interval, ticker) => {
  return lastUpdateModels[interval].findOneAndUpdate({ ticker }, {
    lastUpdateTime: new Date()
  }, {
    upsert: true
  }).exec()
}

/**
 * Given a data interval, a company ticker, a start date, and an end date,
 * this function will return all pertinent stock data stored in the database.
 * @param {string} interval The time interval between the data points
 * @param {string} ticker The company stock ticker
 * @param {Object} start The moment.js object representing the start date
 * @param {Object} end The moment.js object representing the end date
 * @return {Promise}
 */
const getStockData = (interval, ticker, start, end) => {
  return dataModels[interval].find({
    ticker: ticker,
    timestamp: { $gt: start, $lt: end }
  }).exec()
}

/**
 * Given a data interval and a company ticker, this function returns the oldest
 * stock data point that we have stored in our database. This is used to
 * determine if we should fetch new data for a query.
 * @param {string} interval The time interval between the data points
 * @param {string} ticker The company stock ticker
 * @return {Promise}
 */
const getLatestStockEntry = (interval, ticker) => {
  return dataModels[interval].findOne({ ticker }).sort({
    timestamp: 'descending'
  }).limit(1).exec()
}

/**
 * Given data points fetched from the AlphaVantage API, this function
 * determines which of the data points we are missing in our database and
 * adds them to our database.
 * @param {string} interval The data point interval
 * @param {string} ticker The company stock ticker
 * @param {Array<Object>} data The data points to insert into the database
 * @return {Promise}
 */
const insertData = (interval, ticker, data) => {
  return getLatestStockEntry(interval, ticker).then(latest => {
    let filteredData = data
    if (latest !== null) {
      const latestTimestamp = moment(latest.timestamp)
      filteredData = data.filter(element => {
        return moment(element.timestamp).isAfter(latestTimestamp)
      })
    }
    return Promise.all(filteredData.map(element => {
      return dataModels[interval].create(_.merge({ ticker }, element))
    })).finally(() => {
      return updateLastUpdateTime(interval, ticker)
    })
  })
}

module.exports = exports = { getStockData, insertData, getLastUpdateTime }

Comments about the code are appreciated and welcome, but I have a few specific questions about best practices with regards to using Mongoose.

  1. Previously, I implemented several of these methods as statics on the schema definition, that is:

    stockDataSchema.statics.someFunction = function(...) {...}
    

    To invoke these methods, I would then have to export the models to the external calling file, which seemed like bad practice to me because I would then be exposing unnecessary internal parts of the module. My reasoning for designing the module the way I did was so that I could abstract away the complexity and only expose necessary methods. Is this good practice or am I approaching this paradigm completely wrong?

  2. This code works and accomplishes what I want it to do, so it falls under working code as per the site guidelines. If I invoke the exported functions in a one-off script however, the function will execute and then the script will hang because mongoose.disconnect() was never called.

    As far as I can tell from the mongoose tutorials, database code is supposed to be wrapped within a mongoose.connect() call, though I am not entirely sure how that works since the code that I have above works fine for database access and update. Should I wrap the functionality here with mongoose.connect(), and if so, what is best practice for doing so, or should I wrap the calls with mongoose.connect() in the external file invoking these methods?

  3. Is there a better way to do the bulk insert in the insertData() function?

\$\endgroup\$
  • \$\begingroup\$ I don't know much about mongoose, but I just a small tip: if you're going to write one liners, you can prevent unnecesary brackets like this: changing this: .finally(() => { return updateLastUpdateTime(interval, ticker) }) To this: .finally(() => updateLastUpdateTime(interval, ticker)) \$\endgroup\$ – Javier García Manzano Sep 13 '17 at 8:20
  • \$\begingroup\$ I'm aware of this. The only reason I don't is because I have an .eslintrc rule to disallow that. \$\endgroup\$ – omgimanerd Sep 13 '17 at 14:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.