Source

api/page-tracking/PageInterestsDailySnapshotCollection.ts

import SimpleSchema from 'simpl-schema';
import moment from 'moment';
import BaseCollection from '../base/BaseCollection';
import { PageInterestsDailySnapshot, PageInterestsDailySnapshotDefine } from '../../typings/radgrad';
import { Slugs } from '../slug/SlugCollection';

/**
 * Represents a snapshot of the aggregated student interest views for the different categories for a particular day
 *  * The different topic categories that are tracked an counted towards student interest views are Career Goal, Course, Interest, and Opportunity
 * See PageInteressCollection to see the definition of a page interest view.
 * This collection is populated automatically on the server-side.
 * @extends api/base.BaseCollection
 * @memberOf @api/page-tracking
 */
class PageInterestsDailySnapshotCollection extends BaseCollection {

  /**
   * Creates the PageInterestsDailySnapshot collection.
   */
  constructor() {
    super('PageInterestsDailySnapshot', new SimpleSchema({
      careerGoals: Array,
      'careerGoals.$': Object,
      'careerGoals.$.name': String,
      'careerGoals.$.views': { type: SimpleSchema.Integer, min: 0 },
      courses: Array,
      'courses.$': Object,
      'courses.$.name': String,
      'courses.$.views': { type: SimpleSchema.Integer, min: 0 },
      interests: Array,
      'interests.$': Object,
      'interests.$.name': String,
      'interests.$.views': { type: SimpleSchema.Integer, min: 0 },
      opportunities: Array,
      'opportunities.$': Object,
      'opportunities.$.name': String,
      'opportunities.$.views': { type: SimpleSchema.Integer, min: 0 },
      timestamp: Date,
      retired: { type: Boolean, optional: true },
    }));
  }

  /**
   * Gets the correct timestamp date that the daily snapshot represents
   * The cron job that autopopulates this collection runs at 00:00:00 (morning of next day)
   * Since a daily snapshot represents the page interest views for the day BEFORE, we need to subtract one day from
   * .toDate() when the define() method is called.
   * i.e.,
   *  A cron job runs at June 18,2020 00:00:00 AM
   *  Therefore, the timestamp for the daily snapshot that it creates should represent the page interest views that
   *  were defined between June 17,2020 00:00:00 AM and June 17,2020 23:59:59 PM
   *  Thus, the timestamp of the daily snapshot created on June 18,2020 00:00:00 AM should be June 17, 2020 23:59:59 PM
   * @returns {Date}
   */
  private getDateOffset = moment().subtract(1, 'day').endOf('day').toDate();

  /**
   * Defines a snapshot of the aggregated student interest views for the different categories for the particular timestamp.
   * This should never be called manually as it is handled by a cron job.
   * @param careerGoals Array of objects that describe the name and views for all career goals
   * @param courses Array of objects that describe the name and views for all courses
   * @param interests Array of objects that describe the name and views for all interests
   * @param opportunities Array of objects that describe the name and views for all opportunities
   * @param timestamp the timestamp that represents the page interests it aggregated
   *          by default, this is the day before .toDate(). This is because the cron job that autopopulates
   *          this collection runs on 00:00:00 time (the day after), so we need to subtract one day
   * @param retired boolean optional defaults to false.
   * @returns {String} the id of the document created
   */
  public define({ careerGoals, courses, interests, opportunities, timestamp = this.getDateOffset, retired = false }: PageInterestsDailySnapshotDefine): string {
    // Duplicates are not allowed
    // 1) Documents with the same values for all its fields
    let doc: PageInterestsDailySnapshot;
    doc = this.collection.findOne({ careerGoals, courses, interests, opportunities, timestamp, retired });
    if (doc) {
      return doc._id;
    }
    // 2) Documents with the same values for all its fields within the same day that it represents
    doc = this.collection.findOne({
      careerGoals,
      courses,
      interests,
      opportunities,
      timestamp: {
        $gte: moment(timestamp).startOf('day').toDate(),
        $lte: moment(timestamp).endOf('day').toDate(),
      },
      retired,
    });
    if (doc) {
      return doc._id;
    }
    return this.collection.insert({ careerGoals, courses, interests, opportunities, timestamp, retired });
  }

  /**
   * Remove the Page Interests Daily Snapshot
   * @param docID The docID of the Page Interests Daily Snapshot
   */
  public removeIt(docID: string): boolean {
    this.assertDefined(docID);
    return super.removeIt(docID);
  }

  /**
   * Returns an array of strings, each one representing an integrity problem with this collection.
   * Returns an empty array if no problems were found.
   * Checks each object in the careerGoals, courses, interests, and opportunities arrays.
   * For each object, check the name if it is a valid slug and validate the views to be a positive integer.
   * @returns {Array} A (possibly empty) array of strings indicating integrity issues.
   */
  public checkIntegrity(): string[] {
    const problems = [];
    this.find({}, {}).forEach((doc: PageInterestsDailySnapshot) => {
      doc.careerGoals.forEach((careerGoal) => {
        if (!Slugs.isDefined(careerGoal.name)) {
          problems.push(`Bad careerGoal slug: ${careerGoal.name}`);
        }
        if (careerGoal.views < 0) {
          problems.push(`Bad careerGoal views: ${careerGoal.views}`);
        }
      });
      doc.courses.forEach((course) => {
        if (!Slugs.isDefined(course.name)) {
          problems.push(`Bad course slug: ${course.name}`);
        }
        if (course.views < 0) {
          problems.push(`Bad course views: ${course.views}`);
        }
      });
      doc.interests.forEach((interest) => {
        if (!Slugs.isDefined(interest.name)) {
          problems.push(`Bad interest slug: ${interest.name}`);
        }
        if (interest.views < 0) {
          problems.push(`Bad interest views: ${interest.views}`);
        }
      });
      doc.opportunities.forEach((opportunity) => {
        if (!Slugs.isDefined(opportunity.name)) {
          problems.push(`Bad opportunity slug: ${opportunity.name}`);
        }
        if (opportunity.views < 0) {
          problems.push(`Bad opportunity views: ${opportunity.views}`);
        }
      });
    });
    return problems;
  }

  /**
   * Returns an object representing the PageInterestsDailySnapshot docID in a format acceptable to define().
   * @param docID The docID of a PageInterestsDailySnapshot
   * @returns { Object } An object representing the definition of docID.
   */
  public dumpOne(docID: string): PageInterestsDailySnapshotDefine {
    const doc: PageInterestsDailySnapshot = this.findDoc(docID);
    const careerGoals = doc.careerGoals;
    const courses = doc.courses;
    const interests = doc.interests;
    const opportunities = doc.opportunities;
    const timestamp = doc.timestamp;
    const retired = doc.retired;
    return { careerGoals, courses, interests, opportunities, timestamp, retired };
  }
}

export const PageInterestsDailySnapshots = new PageInterestsDailySnapshotCollection();