<commit_before>'use strict';

/**
 * ButtressJS - Realtime datastore for software
 *
 * @file schemaModel.js
 * @description A default model for schemas
 * @module Model
 * @exports model, schema, constants
 * @author Chris Bates-Keegan
 *
 */

const ObjectId = require('mongodb').ObjectId;
const Logging = require('../logging');
const Shared = require('./shared');
// const Helpers = require('../helpers');
const shortId = require('../helpers').ShortId;

/* ********************************************************************************
 *
 * LOCALS
 *
 **********************************************************************************/

class SchemaModel {
	constructor(MongoDb, schemaData, app) {
		this.schemaData = schemaData;
		this.app = app || null;

		this.appShortId = (app) ? shortId(app._id) : null;
		this.collectionName = `${schemaData.collection}`;

		if (this.appShortId) {
			this.collectionName = `${this.appShortId}-${this.collectionName}`;
		}

		this.collection = MongoDb.collection(this.collectionName);
	}

	__doValidation(body) {
		const res = {
			isValid: true,
			missing: [],
			invalid: [],
		};

		const app = Shared.validateAppProperties(this.schemaData, body);
		if (app.isValid === false) {
			res.isValid = false;
			res.invalid = res.invalid.concat(app.invalid);
			res.missing = res.missing.concat(app.missing);
		}

		return res;
	}
	validate(body) {
		if (body instanceof Array === false) {
			body = [body];
		}
		const validation = body.map((b) => this.__doValidation(b)).filter((v) => v.isValid === false);

		return validation.length >= 1 ? validation[0] : {isValid: true};
	}

	static parseQuery(query, envFlat = {}, output = {}) {
		for (const property in query) {
			if (!query.hasOwnProperty(property)) continue;
			const command = query[property];

			if (property === '$or' && Array.isArray(command)) {
				output['$or'] = command.map((q) => SchemaModel.parseQuery(q, envFlat, {}));
			} else if (property === '$and' && Array.isArray(command)) {
				output['$and'] = command.map((q) => SchemaModel.parseQuery(q, envFlat, {}));
			} else {
				for (const operator in command) {
					if (!command.hasOwnProperty(operator)) continue;
					let operand = command[operator];

					// Check to see if operand is a path and fetch value
					if (operand.indexOf && operand.indexOf('.') !== -1) {
						let path = operand.split('.');
						const key = path.shift();

						path = path.join('.');

						if (key === 'env' && envFlat[path]) {
							operand = envFlat[path];
						} else {
							throw new Error(`Unable to find ${path} in schema.authFilter.env`);
						}
					}

					if (!output[property]) {
						output[property] = {};
					}
					output[property][`${operator}`] = operand;
				}
			}
		}

		return output;
	}

	generateRoleFilterQuery(token, roles, Model) {
		if (!roles.schema || !roles.schema.authFilter) {
			return Promise.resolve({});
		}

		// Parse schema authFilter.env object, gather information that's needed.
		const buildEnv = (authFilter) => {
			const env = {
				authUserId: token._user,
			};

			const tasks = [];

			if (authFilter.env) {
				for (const property in roles.schema.authFilter.env) {
					if (!roles.schema.authFilter.env.hasOwnProperty(property)) continue;
					const query = roles.schema.authFilter.env[property];

					let propertyMap = '_id';
					if (query.map) {
						propertyMap = query.map;
					}
					for (const command in query) {
						if (!query.hasOwnProperty(command)) continue;

						if (command.includes('schema.')) {
							const commandPath = command.split('.');
							commandPath.shift(); // Remove "schema"
							const collectionName = `${this.appShortId}-${commandPath.shift()}`;
							const propertyPath = commandPath.join('.');

							let propertyQuery = {};
							propertyQuery[propertyPath] = query[command];
							propertyQuery = SchemaModel.parseQuery(propertyQuery, env, {});

							const fields = {};
							fields[propertyPath] = true;

							tasks.push(() => {
								return Model[collectionName].find(propertyQuery, fields)
									.then((res) => {
										// Map fetched properties into a array.
										env[property] = res.map((i) => i[propertyMap]);
										// Hack - Flattern any sub arrays down to the single level.
										env[property] = [].concat.apply([], env[property]);
									});
							});
						} else {
							// Unknown operation
						}
					}
				}
			}

			// The env process may need to query other collections so group them into tasks.
			return tasks.reduce((prev, task) => prev.then(() => task()), Promise.resolve())
				.then(() => env);
		};

		// Build the query structure with the built env object applyed to it.
		const buildQuery = (schemaQuery, env) => {
			return new Promise((resolve) => {
				const query = (schemaQuery) ? schemaQuery : {};

				resolve(SchemaModel.parseQuery(query, env, {}));
			});
		};

		// Engage.
		return buildEnv(roles.schema.authFilter)
			.then((env) => buildQuery(roles.schema.authFilter.query, env));
	}

	/*
	* @param {Object} body - body passed through from a POST request
	* @return {Promise} - returns a promise that is fulfilled when the database request is completed
	*/
	__add(body, internals) {
		return (prev) => {
			const entity = Object.assign({}, internals);

			if (body.id) {
				entity._id = new ObjectId(body.id);
			}

			if (this.schemaData.extends && this.schemaData.extends.includes('timestamps')) {
				entity.createdAt = new Date();
				entity.updatedAt = null;
			}

			const validated = Shared.applyAppProperties(this.schemaData, body);
			return prev.concat([Object.assign(validated, entity)]);
		};
	}
	add(body, internals) {
		const sharedAddFn = Shared.add(this.collection, (item) => this.__add(item, internals));
		return sharedAddFn(body);
	}

	update(query, id) {
		Logging.logSilly(`update: ${this.collectionName} ${id} ${query}`);

		return new Promise((resolve, reject) => {
			this.collection.updateOne({_id: id}, {
				$set: query,
			}, (err, object) => {
				if (err) throw new Error(err);

				resolve(object);
			});
		});
	}
	validateUpdate(body) {
		const sharedFn = Shared.validateUpdate({}, this.schemaData);
		return sharedFn(body);
	}
	updateByPath(body, id) {
		const sharedFn = Shared.updateByPath({}, this.schemaData, this.collection);

		if (body instanceof Array === false) {
			body = [body];
		}

		if (this.schemaData.extends && this.schemaData.extends.includes('timestamps')) {
			body.push({
				path: 'updatedAt',
				value: new Date(),
				contextPath: '^updatedAt$',
			});
		}

		return sharedFn(body, id);
	}

	exists(id) {
		Logging.logSilly(`exists: ${this.collectionName} ${id}`);

		return this.collection.find({_id: new ObjectId(id)})
			.limit(1)
			.count()
			.then((count) => count > 0);
	}

	/*
	* @return {Promise} - returns a promise that is fulfilled when the database request is completed
	*/
	isDuplicate(details) {
		return Promise.resolve(false);
	}

	/**
	 * @param {App} entity - entity object to be deleted
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rm(entity) {
		Logging.log(`DELETING: ${entity._id}`, Logging.Constants.LogLevel.DEBUG);
		return new Promise((resolve) => {
			this.collection.deleteOne({_id: new ObjectId(entity._id)}, (err, cursor) => {
				if (err) throw err;
				resolve(cursor);
			});
		});
	}

	/**
	 * @param {Array} ids - Array of entity ids to delete
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rmBulk(ids) {
		Logging.log(`rmBulk: ${this.collectionName} ${ids}`, Logging.Constants.LogLevel.SILLY);
		return this.rmAll({_id: {$in: ids}});
	}

	/*
	 * @param {Object} query - mongoDB query
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rmAll(query) {
		if (!query) query = {};
		Logging.logSilly(`rmAll: ${this.collectionName} ${query}`);

		return new Promise((resolve) => {
			this.collection.deleteMany(query, (err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {String} id - entity id to get
	 * @return {Promise} - resolves to an array of Companies
	 */
	findById(id) {
		Logging.logSilly(`findById: ${this.collectionName} ${id}`);

		return new Promise((resolve) => {
			this.collection.findOne({_id: new ObjectId(id)}, {}, (err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @param {Object} excludes - mongoDB query excludes
	 * @param {Boolean} stream - should return a stream
	 * @return {Promise} - resolves to an array of docs
	 */
	find(query, excludes = {}, stream = false) {
		Logging.logSilly(`find: ${this.collectionName} ${query}`);

		if (stream) {
			return this.collection.find(query, excludes);
		}

		return new Promise((resolve) => {
			this.collection.find(query, excludes).toArray((err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @param {Object} excludes - mongoDB query excludes
	 * @return {Promise} - resolves to an array of docs
	 */
	findOne(query, excludes = {}) {
		Logging.logSilly(`findOne: ${this.collectionName} ${query}`);

		return new Promise((resolve) => {
			this.collection.find(query, excludes).toArray((err, doc) => {
				if (err) throw err;
				resolve(doc[0]);
			});
		});
	}

	/**
	 * @return {Promise} - resolves to an array of Companies
	 */
	findAll() {
		Logging.logSilly(`findAll: ${this.collectionName}`);

		return this.collection.find({});
	}

	/**
	 * @param {Array} ids - Array of entities ids to get
	 * @return {Promise} - resolves to an array of Companies
	 */
	findAllById(ids) {
		Logging.logSilly(`update: ${this.collectionName} ${ids}`);

		return this.collection.find({_id: {$in: ids.map((id) => new ObjectId(id))}}, {});
	}
}

module.exports = SchemaModel;
<commit_msg>TWEAK: Allow user to pass through updatedAt when creating a object<commit_after>'use strict';

/**
 * ButtressJS - Realtime datastore for software
 *
 * @file schemaModel.js
 * @description A default model for schemas
 * @module Model
 * @exports model, schema, constants
 * @author Chris Bates-Keegan
 *
 */

const ObjectId = require('mongodb').ObjectId;
const Logging = require('../logging');
const Shared = require('./shared');
const Sugar = require('sugar');
// const Helpers = require('../helpers');
const shortId = require('../helpers').ShortId;

/* ********************************************************************************
 *
 * LOCALS
 *
 **********************************************************************************/

class SchemaModel {
	constructor(MongoDb, schemaData, app) {
		this.schemaData = schemaData;
		this.app = app || null;

		this.appShortId = (app) ? shortId(app._id) : null;
		this.collectionName = `${schemaData.collection}`;

		if (this.appShortId) {
			this.collectionName = `${this.appShortId}-${this.collectionName}`;
		}

		this.collection = MongoDb.collection(this.collectionName);
	}

	__doValidation(body) {
		const res = {
			isValid: true,
			missing: [],
			invalid: [],
		};

		const app = Shared.validateAppProperties(this.schemaData, body);
		if (app.isValid === false) {
			res.isValid = false;
			res.invalid = res.invalid.concat(app.invalid);
			res.missing = res.missing.concat(app.missing);
		}

		return res;
	}
	validate(body) {
		if (body instanceof Array === false) {
			body = [body];
		}
		const validation = body.map((b) => this.__doValidation(b)).filter((v) => v.isValid === false);

		return validation.length >= 1 ? validation[0] : {isValid: true};
	}

	static parseQuery(query, envFlat = {}, output = {}) {
		for (const property in query) {
			if (!query.hasOwnProperty(property)) continue;
			const command = query[property];

			if (property === '$or' && Array.isArray(command)) {
				output['$or'] = command.map((q) => SchemaModel.parseQuery(q, envFlat, {}));
			} else if (property === '$and' && Array.isArray(command)) {
				output['$and'] = command.map((q) => SchemaModel.parseQuery(q, envFlat, {}));
			} else {
				for (const operator in command) {
					if (!command.hasOwnProperty(operator)) continue;
					let operand = command[operator];

					// Check to see if operand is a path and fetch value
					if (operand.indexOf && operand.indexOf('.') !== -1) {
						let path = operand.split('.');
						const key = path.shift();

						path = path.join('.');

						if (key === 'env' && envFlat[path]) {
							operand = envFlat[path];
						} else {
							throw new Error(`Unable to find ${path} in schema.authFilter.env`);
						}
					}

					if (!output[property]) {
						output[property] = {};
					}
					output[property][`${operator}`] = operand;
				}
			}
		}

		return output;
	}

	generateRoleFilterQuery(token, roles, Model) {
		if (!roles.schema || !roles.schema.authFilter) {
			return Promise.resolve({});
		}

		// Parse schema authFilter.env object, gather information that's needed.
		const buildEnv = (authFilter) => {
			const env = {
				authUserId: token._user,
			};

			const tasks = [];

			if (authFilter.env) {
				for (const property in roles.schema.authFilter.env) {
					if (!roles.schema.authFilter.env.hasOwnProperty(property)) continue;
					const query = roles.schema.authFilter.env[property];

					let propertyMap = '_id';
					if (query.map) {
						propertyMap = query.map;
					}
					for (const command in query) {
						if (!query.hasOwnProperty(command)) continue;

						if (command.includes('schema.')) {
							const commandPath = command.split('.');
							commandPath.shift(); // Remove "schema"
							const collectionName = `${this.appShortId}-${commandPath.shift()}`;
							const propertyPath = commandPath.join('.');

							let propertyQuery = {};
							propertyQuery[propertyPath] = query[command];
							propertyQuery = SchemaModel.parseQuery(propertyQuery, env, {});

							const fields = {};
							fields[propertyPath] = true;

							tasks.push(() => {
								return Model[collectionName].find(propertyQuery, fields)
									.then((res) => {
										// Map fetched properties into a array.
										env[property] = res.map((i) => i[propertyMap]);
										// Hack - Flattern any sub arrays down to the single level.
										env[property] = [].concat.apply([], env[property]);
									});
							});
						} else {
							// Unknown operation
						}
					}
				}
			}

			// The env process may need to query other collections so group them into tasks.
			return tasks.reduce((prev, task) => prev.then(() => task()), Promise.resolve())
				.then(() => env);
		};

		// Build the query structure with the built env object applyed to it.
		const buildQuery = (schemaQuery, env) => {
			return new Promise((resolve) => {
				const query = (schemaQuery) ? schemaQuery : {};

				resolve(SchemaModel.parseQuery(query, env, {}));
			});
		};

		// Engage.
		return buildEnv(roles.schema.authFilter)
			.then((env) => buildQuery(roles.schema.authFilter.query, env));
	}

	/*
	* @param {Object} body - body passed through from a POST request
	* @return {Promise} - returns a promise that is fulfilled when the database request is completed
	*/
	__add(body, internals) {
		return (prev) => {
			const entity = Object.assign({}, internals);

			if (body.id) {
				entity._id = new ObjectId(body.id);
			}

			if (this.schemaData.extends && this.schemaData.extends.includes('timestamps')) {
				entity.createdAt = Sugar.Date.create();
				entity.updatedAt = (body.updatedAt) ? Sugar.Date.create(body.updatedAt) : null;
			}

			const validated = Shared.applyAppProperties(this.schemaData, body);
			return prev.concat([Object.assign(validated, entity)]);
		};
	}
	add(body, internals) {
		const sharedAddFn = Shared.add(this.collection, (item) => this.__add(item, internals));
		return sharedAddFn(body);
	}

	update(query, id) {
		Logging.logSilly(`update: ${this.collectionName} ${id} ${query}`);

		return new Promise((resolve, reject) => {
			this.collection.updateOne({_id: id}, {
				$set: query,
			}, (err, object) => {
				if (err) throw new Error(err);

				resolve(object);
			});
		});
	}
	validateUpdate(body) {
		const sharedFn = Shared.validateUpdate({}, this.schemaData);
		return sharedFn(body);
	}
	updateByPath(body, id) {
		const sharedFn = Shared.updateByPath({}, this.schemaData, this.collection);

		if (body instanceof Array === false) {
			body = [body];
		}

		if (this.schemaData.extends && this.schemaData.extends.includes('timestamps')) {
			body.push({
				path: 'updatedAt',
				value: new Date(),
				contextPath: '^updatedAt$',
			});
		}

		return sharedFn(body, id);
	}

	exists(id) {
		Logging.logSilly(`exists: ${this.collectionName} ${id}`);

		return this.collection.find({_id: new ObjectId(id)})
			.limit(1)
			.count()
			.then((count) => count > 0);
	}

	/*
	* @return {Promise} - returns a promise that is fulfilled when the database request is completed
	*/
	isDuplicate(details) {
		return Promise.resolve(false);
	}

	/**
	 * @param {App} entity - entity object to be deleted
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rm(entity) {
		Logging.log(`DELETING: ${entity._id}`, Logging.Constants.LogLevel.DEBUG);
		return new Promise((resolve) => {
			this.collection.deleteOne({_id: new ObjectId(entity._id)}, (err, cursor) => {
				if (err) throw err;
				resolve(cursor);
			});
		});
	}

	/**
	 * @param {Array} ids - Array of entity ids to delete
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rmBulk(ids) {
		Logging.log(`rmBulk: ${this.collectionName} ${ids}`, Logging.Constants.LogLevel.SILLY);
		return this.rmAll({_id: {$in: ids}});
	}

	/*
	 * @param {Object} query - mongoDB query
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rmAll(query) {
		if (!query) query = {};
		Logging.logSilly(`rmAll: ${this.collectionName} ${query}`);

		return new Promise((resolve) => {
			this.collection.deleteMany(query, (err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {String} id - entity id to get
	 * @return {Promise} - resolves to an array of Companies
	 */
	findById(id) {
		Logging.logSilly(`findById: ${this.collectionName} ${id}`);

		return new Promise((resolve) => {
			this.collection.findOne({_id: new ObjectId(id)}, {}, (err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @param {Object} excludes - mongoDB query excludes
	 * @param {Boolean} stream - should return a stream
	 * @return {Promise} - resolves to an array of docs
	 */
	find(query, excludes = {}, stream = false) {
		Logging.logSilly(`find: ${this.collectionName} ${query}`);

		if (stream) {
			return this.collection.find(query, excludes);
		}

		return new Promise((resolve) => {
			this.collection.find(query, excludes).toArray((err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @param {Object} excludes - mongoDB query excludes
	 * @return {Promise} - resolves to an array of docs
	 */
	findOne(query, excludes = {}) {
		Logging.logSilly(`findOne: ${this.collectionName} ${query}`);

		return new Promise((resolve) => {
			this.collection.find(query, excludes).toArray((err, doc) => {
				if (err) throw err;
				resolve(doc[0]);
			});
		});
	}

	/**
	 * @return {Promise} - resolves to an array of Companies
	 */
	findAll() {
		Logging.logSilly(`findAll: ${this.collectionName}`);

		return this.collection.find({});
	}

	/**
	 * @param {Array} ids - Array of entities ids to get
	 * @return {Promise} - resolves to an array of Companies
	 */
	findAllById(ids) {
		Logging.logSilly(`update: ${this.collectionName} ${ids}`);

		return this.collection.find({_id: {$in: ids.map((id) => new ObjectId(id))}}, {});
	}
}

module.exports = SchemaModel;
<|endoftext|>


<commit_before>var express = require('express')
var publicRouter = express.Router()
var privateRouter = express.Router()
var adminRouter = express.Router()
var competitiondb = require('../../models/competition')
var mapdb = require('../../models/map')
var query = require('../../helper/query-helper')
var validator = require('validator')
var async = require('async')
var ObjectId = require('mongoose').Types.ObjectId
var logger = require('../../config/logger').mainLogger
var fs = require('fs')
var pathFinder = require('../../helper/pathFinder')
var scoreCalculator = require('../../helper/scoreCalculator')

var socketIo

module.exports.connectSocketIo = function(io) {
  socketIo = io
}

publicRouter.get('/', function (req, res) {
  query.doFindResultSortQuery(req, res, null, null, competitiondb.run)
})

publicRouter.get('/:runid', function (req, res, next) {
  var id = req.params.runid

  if (!ObjectId.isValid(id)) {
    return next()
  }

  var populate
  if (req.query['populate'] !== undefined && req.query['populate']) {
    populate = ["round", "team", "field", "competition", {path: 'tiles', populate: {path: 'tileType'}}]
  }

  var query = competitiondb.run.findById(id)
  if (populate !== undefined) {
    query.populate(populate)
  }
  query.exec(function (err, data) {
    if (err) {
      logger.error(err)
      res.status(400).send({msg: "Could not get run"})
    } else {
      res.status(200).send(data)
    }
  })
})

privateRouter.post('/:runid/update', function (req, res, next) {
  var id = req.params.runid

  if (!ObjectId.isValid(id)) {
    return next()
  }

  var run = req.body

  competitiondb.run.findById(id, function (err, dbrun) {
    if (err) {
      logger.error(err)
      res.status(400).send({msg: "Could not get run"})
    } else {
      if (run.showedUp !== undefined) {
        dbrun.showedUp = run.showedUp
      }
      if (run.LoPs !== undefined) {
        dbrun.LoPs = run.LoPs
      }
      if (run.rescuedVictims !== undefined) {
        dbrun.rescuedVictims = run.rescuedVictims
      }
      if (run.time !== undefined && run.time.minutes !== undefined && run.time.seconds !== undefined) {
        dbrun.time.minutes = run.time.minutes
        dbrun.time.seconds = run.time.seconds
      }
      if (run.tiles !== undefined) {

        for (var i in run.tiles) {
          var tile = run.tiles[i]

          for (var j in dbrun.tiles) {
            var dbtile = dbrun.tiles[j]
            if (tile.x == dbtile.x &&
                tile.y == dbtile.y &&
                tile.z == dbtile.z) {

              if (tile.items !== undefined) {
                if (tile.items.dropTiles !== undefined) {
                  dbtile.items.dropTiles = tile.items.dropTiles
                }
              }

              if (tile.scoredItems !== undefined) {
                if (tile.scoredItems.obstacles !== undefined) {
                  dbtile.scoredItems.obstacles = tile.scoredItems.obstacles
                }
                if (tile.scoredItems.speedbumps !== undefined) {
                  dbtile.scoredItems.speedbumps = tile.scoredItems.speedbumps
                }
                if (tile.scoredItems.intersections !== undefined) {
                  dbtile.scoredItems.intersections = tile.scoredItems.intersections
                }
                if (tile.scoredItems.gaps !== undefined) {
                  dbtile.scoredItems.gaps = tile.scoredItems.gaps
                }
                if (tile.scoredItems.dropTiles !== undefined) {
                  dbtile.scoredItems.dropTiles = tile.scoredItems.dropTiles
                }
              }
              break
            }
          }
        }
      }

      dbrun.score = scoreCalculator.calculateScore(dbrun)

      dbrun.save(function (err) {
        if (err) {
          logger.error(err)
          res.status(400).send({msg: "Could not save run"})
        } else {
          if (socketIo !== undefined) {
            socketIo.sockets.in('runs/').emit('changed')
            socketIo.sockets.in('runs/' + dbrun._id).emit('data', dbrun)
            socketIo.sockets.in('fields/' + dbrun.field).emit('data', {newRun : dbrun._id})
          }
          res.status(200).send({msg: "Saved run", score : dbrun.score})
        }
      })
    }
  })
})

adminRouter.get('/:runid/delete', function (req, res, next) {
  var id = req.params.runid

  if (!ObjectId.isValid(id)) {
    return next()
  }

  competitiondb.run.remove({_id: id}, function (err) {
    if (err) {
      logger.error(err)
      res.status(400).send({msg: "Could not remove run"})
    } else {
      res.status(200).send({msg: "Run has been removed!"})
    }
  })
})

adminRouter.post('/createrun', function (req, res) {
  var run = req.body

  var mapId = run.map
  var roundId = run.round
  var teamId = run.team
  var fieldId = run.field
  var competitionId = run.competition

  if (!ObjectId.isValid(mapId)) {
    return res.status(400).send({ msg: "mapId is not a valid ObjectId" });

  } else if (!ObjectId.isValid(roundId)) {
    return res.status(400).send({ msg: "roundId is not a valid ObjectId" });

  } else if (!ObjectId.isValid(teamId)) {
    return res.status(400).send({ msg: "teamId is not a valid ObjectId" });
  }

  } else if (!ObjectId.isValid(fieldId)) {
    return res.status(400).send({ msg: "fieldId is not a valid ObjectId" });

  } else if (!ObjectId.isValid(competitionId)) {
    return res.status(400).send({ msg: "competitionId is not a valid ObjectId" });
  }

  var map
  var round
  var team
  var field
  var competition

  async.parallel([
    function (cb) {
      mapdb.map.findById(mapId).populate({
        path    : 'tiles',
        populate: {path: 'tileType'}
      }).exec(function (err, dbmap) {
        map = dbmap
        return cb(err)
      })
    },
    function (cb) {
      competitiondb.round.findById(roundId, function (err, dbround) {
        round = dbround
        return cb(err)
      })
    },
    function (cb) {
      competitiondb.team.findById(teamId, function (err, dbteam) {
        team = dbteam
        return cb(err)
      })
    },
    function (cb) {
      competitiondb.field.findById(fieldId, function (err, dbfield) {
        field = dbfield
        return cb(err)
      })
    },
    function (cb) {
      competitiondb.competition.findById(competitionId, function (err, dbcompetition) {
        competition = dbcompetition
        return cb(err)
      })
    }
  ], function (err) {
    if (err) {
      logger.error(err)
      return res.status(400).send({msg: "Error saving run", err: err})
    } else {
      if (map === undefined ||
          round === undefined ||
          team === undefined ||
          field === undefined ||
          competition === undefined) {
        return res.status(400).send({msg: "Error saving run, could't find references in database"})
      }

      if (team.competition != competition._id.toString() ||
          round.competition != competition._id.toString() ||
          field.competition != competition._id.toString()) {
        return res.status(400).send({msg: "Error saving run, mismatch with competition id"})
      }

      var foundPath = pathFinder.findPath(map)

      var tiles = []
      for (var i in foundPath) {
        var tile = foundPath[i]

        var newTile = {
          x          : tile.x,
          y          : tile.y,
          z          : tile.z,
          tileType   : tile.tileType._id,
          rot        : tile.rot,
          items      : {
            obstacles    : tile.scoreItems.obstacles,
            speedbumps   : tile.scoreItems.speedbumps,
            intersections: tile.scoreItems.intersections,
            gaps         : tile.scoreItems.gaps,
            dropTiles : 0
          },
          scoredItems: {
            obstacles    : filledArray(tile.scoreItems.obstacles, false),
            speedbumps   : filledArray(tile.scoreItems.speedbumps, false),
            intersections: filledArray(tile.scoreItems.intersections, false),
            gaps         : filledArray(tile.scoreItems.gaps, false),
            dropTiles    : []
          },
          index      : tile.index,
          levelUp    : tile.levelUp,
          levelDown  : tile.levelDown
        }
        tiles.push(newTile)
      }

      var newRun = new competitiondb.run({
        round      : round._id,
        team       : team._id,
        field      : field._id,
        competition: competition._id,

        height           : map.height,
        width            : map.width,
        length           : map.length,
        tiles            : tiles,
        startTile        : map.startTile,
        numberOfDropTiles: map.numberOfDropTiles,
        LoPs             : filledArray(map.numberOfDropTiles, 0),
        rescuedVictims   : 0,
        score            : 0,
        showedUp         : false,
        time             : {
          minutes: 0,
          seconds: 0
        }
      })

      newRun.save(function (err, data) {
        if (err) {
          logger.error(err)
          return res.status(400).send({msg: "Error saving run in db"})
        } else {
          return res.status(201).send({
            msg: "New run has been saved",
            id : data._id
          })
        }
      })
    }
  })
})

function filledArray(len, val) {
  var arr = []
  for (var i = 0; i < len; i++) {
    arr.push(val)
  }
  return arr
}

publicRouter.all('*', function (req, res, next) {
  next()
})
privateRouter.all('*', function (req, res, next) {
  next()
})

module.exports.public = publicRouter
module.exports.private = privateRouter
module.exports.admin = adminRouter<commit_msg>syntax error<commit_after>var express = require('express')
var publicRouter = express.Router()
var privateRouter = express.Router()
var adminRouter = express.Router()
var competitiondb = require('../../models/competition')
var mapdb = require('../../models/map')
var query = require('../../helper/query-helper')
var validator = require('validator')
var async = require('async')
var ObjectId = require('mongoose').Types.ObjectId
var logger = require('../../config/logger').mainLogger
var fs = require('fs')
var pathFinder = require('../../helper/pathFinder')
var scoreCalculator = require('../../helper/scoreCalculator')

var socketIo

module.exports.connectSocketIo = function(io) {
  socketIo = io
}

publicRouter.get('/', function (req, res) {
  query.doFindResultSortQuery(req, res, null, null, competitiondb.run)
})

publicRouter.get('/:runid', function (req, res, next) {
  var id = req.params.runid

  if (!ObjectId.isValid(id)) {
    return next()
  }

  var populate
  if (req.query['populate'] !== undefined && req.query['populate']) {
    populate = ["round", "team", "field", "competition", {path: 'tiles', populate: {path: 'tileType'}}]
  }

  var query = competitiondb.run.findById(id)
  if (populate !== undefined) {
    query.populate(populate)
  }
  query.exec(function (err, data) {
    if (err) {
      logger.error(err)
      res.status(400).send({msg: "Could not get run"})
    } else {
      res.status(200).send(data)
    }
  })
})

privateRouter.post('/:runid/update', function (req, res, next) {
  var id = req.params.runid

  if (!ObjectId.isValid(id)) {
    return next()
  }

  var run = req.body

  competitiondb.run.findById(id, function (err, dbrun) {
    if (err) {
      logger.error(err)
      res.status(400).send({msg: "Could not get run"})
    } else {
      if (run.showedUp !== undefined) {
        dbrun.showedUp = run.showedUp
      }
      if (run.LoPs !== undefined) {
        dbrun.LoPs = run.LoPs
      }
      if (run.rescuedVictims !== undefined) {
        dbrun.rescuedVictims = run.rescuedVictims
      }
      if (run.time !== undefined && run.time.minutes !== undefined && run.time.seconds !== undefined) {
        dbrun.time.minutes = run.time.minutes
        dbrun.time.seconds = run.time.seconds
      }
      if (run.tiles !== undefined) {

        for (var i in run.tiles) {
          var tile = run.tiles[i]

          for (var j in dbrun.tiles) {
            var dbtile = dbrun.tiles[j]
            if (tile.x == dbtile.x &&
                tile.y == dbtile.y &&
                tile.z == dbtile.z) {

              if (tile.items !== undefined) {
                if (tile.items.dropTiles !== undefined) {
                  dbtile.items.dropTiles = tile.items.dropTiles
                }
              }

              if (tile.scoredItems !== undefined) {
                if (tile.scoredItems.obstacles !== undefined) {
                  dbtile.scoredItems.obstacles = tile.scoredItems.obstacles
                }
                if (tile.scoredItems.speedbumps !== undefined) {
                  dbtile.scoredItems.speedbumps = tile.scoredItems.speedbumps
                }
                if (tile.scoredItems.intersections !== undefined) {
                  dbtile.scoredItems.intersections = tile.scoredItems.intersections
                }
                if (tile.scoredItems.gaps !== undefined) {
                  dbtile.scoredItems.gaps = tile.scoredItems.gaps
                }
                if (tile.scoredItems.dropTiles !== undefined) {
                  dbtile.scoredItems.dropTiles = tile.scoredItems.dropTiles
                }
              }
              break
            }
          }
        }
      }

      dbrun.score = scoreCalculator.calculateScore(dbrun)

      dbrun.save(function (err) {
        if (err) {
          logger.error(err)
          res.status(400).send({msg: "Could not save run"})
        } else {
          if (socketIo !== undefined) {
            socketIo.sockets.in('runs/').emit('changed')
            socketIo.sockets.in('runs/' + dbrun._id).emit('data', dbrun)
            socketIo.sockets.in('fields/' + dbrun.field).emit('data', {newRun : dbrun._id})
          }
          res.status(200).send({msg: "Saved run", score : dbrun.score})
        }
      })
    }
  })
})

adminRouter.get('/:runid/delete', function (req, res, next) {
  var id = req.params.runid

  if (!ObjectId.isValid(id)) {
    return next()
  }

  competitiondb.run.remove({_id: id}, function (err) {
    if (err) {
      logger.error(err)
      res.status(400).send({msg: "Could not remove run"})
    } else {
      res.status(200).send({msg: "Run has been removed!"})
    }
  })
})

adminRouter.post('/createrun', function (req, res) {
  var run = req.body

  var mapId = run.map
  var roundId = run.round
  var teamId = run.team
  var fieldId = run.field
  var competitionId = run.competition

  if (!ObjectId.isValid(mapId)) {
    return res.status(400).send({ msg: "mapId is not a valid ObjectId" });

  } else if (!ObjectId.isValid(roundId)) {
    return res.status(400).send({ msg: "roundId is not a valid ObjectId" });

  } else if (!ObjectId.isValid(teamId)) {
    return res.status(400).send({ msg: "teamId is not a valid ObjectId" });

  } else if (!ObjectId.isValid(fieldId)) {
    return res.status(400).send({ msg: "fieldId is not a valid ObjectId" });

  } else if (!ObjectId.isValid(competitionId)) {
    return res.status(400).send({ msg: "competitionId is not a valid ObjectId" });
  }

  var map
  var round
  var team
  var field
  var competition

  async.parallel([
    function (cb) {
      mapdb.map.findById(mapId).populate({
        path    : 'tiles',
        populate: {path: 'tileType'}
      }).exec(function (err, dbmap) {
        map = dbmap
        return cb(err)
      })
    },
    function (cb) {
      competitiondb.round.findById(roundId, function (err, dbround) {
        round = dbround
        return cb(err)
      })
    },
    function (cb) {
      competitiondb.team.findById(teamId, function (err, dbteam) {
        team = dbteam
        return cb(err)
      })
    },
    function (cb) {
      competitiondb.field.findById(fieldId, function (err, dbfield) {
        field = dbfield
        return cb(err)
      })
    },
    function (cb) {
      competitiondb.competition.findById(competitionId, function (err, dbcompetition) {
        competition = dbcompetition
        return cb(err)
      })
    }
  ], function (err) {
    if (err) {
      logger.error(err)
      return res.status(400).send({msg: "Error saving run", err: err})
    } else {
      if (map === undefined ||
          round === undefined ||
          team === undefined ||
          field === undefined ||
          competition === undefined) {
        return res.status(400).send({msg: "Error saving run, could't find references in database"})
      }

      if (team.competition != competition._id.toString() ||
          round.competition != competition._id.toString() ||
          field.competition != competition._id.toString()) {
        return res.status(400).send({msg: "Error saving run, mismatch with competition id"})
      }

      var foundPath = pathFinder.findPath(map)

      var tiles = []
      for (var i in foundPath) {
        var tile = foundPath[i]

        var newTile = {
          x          : tile.x,
          y          : tile.y,
          z          : tile.z,
          tileType   : tile.tileType._id,
          rot        : tile.rot,
          items      : {
            obstacles    : tile.scoreItems.obstacles,
            speedbumps   : tile.scoreItems.speedbumps,
            intersections: tile.scoreItems.intersections,
            gaps         : tile.scoreItems.gaps,
            dropTiles : 0
          },
          scoredItems: {
            obstacles    : filledArray(tile.scoreItems.obstacles, false),
            speedbumps   : filledArray(tile.scoreItems.speedbumps, false),
            intersections: filledArray(tile.scoreItems.intersections, false),
            gaps         : filledArray(tile.scoreItems.gaps, false),
            dropTiles    : []
          },
          index      : tile.index,
          levelUp    : tile.levelUp,
          levelDown  : tile.levelDown
        }
        tiles.push(newTile)
      }

      var newRun = new competitiondb.run({
        round      : round._id,
        team       : team._id,
        field      : field._id,
        competition: competition._id,

        height           : map.height,
        width            : map.width,
        length           : map.length,
        tiles            : tiles,
        startTile        : map.startTile,
        numberOfDropTiles: map.numberOfDropTiles,
        LoPs             : filledArray(map.numberOfDropTiles, 0),
        rescuedVictims   : 0,
        score            : 0,
        showedUp         : false,
        time             : {
          minutes: 0,
          seconds: 0
        }
      })

      newRun.save(function (err, data) {
        if (err) {
          logger.error(err)
          return res.status(400).send({msg: "Error saving run in db"})
        } else {
          return res.status(201).send({
            msg: "New run has been saved",
            id : data._id
          })
        }
      })
    }
  })
})

function filledArray(len, val) {
  var arr = []
  for (var i = 0; i < len; i++) {
    arr.push(val)
  }
  return arr
}

publicRouter.all('*', function (req, res, next) {
  next()
})
privateRouter.all('*', function (req, res, next) {
  next()
})

module.exports.public = publicRouter
module.exports.private = privateRouter
module.exports.admin = adminRouter<|endoftext|>


<commit_before>"use strict";

const expect = require("chai").expect;
const LightnessFinder = require("../lib/threshold-finder").LightnessFinder;
const BrightnessFinder = require("../lib/threshold-finder").BrightnessFinder;
const Checker = require("../lib/contrast-checker").ContrastChecker;
const Calc = require("../lib/color-contrast-calc").ColorContrastCalc;

const higherLuminanceThan = (mainRgb, otherRgb) => {
  const mainLum = Checker.relativeLuminance(mainRgb);
  const otherLum = Checker.relativeLuminance(otherRgb);
  return mainLum > otherLum;
};

describe("LightnessFinder", function() {
  const orange = Calc.colorFrom("orange").rgb;
  const blueviolet = Calc.colorFrom("blueviolet").rgb;
  const blue = Calc.colorFrom("blue").rgb;
  const brown = Calc.colorFrom("brown").rgb;
  const black = Calc.colorFrom("black").rgb;
  const white = Calc.colorFrom("white").rgb;
  const darkgreen = Calc.colorFrom("darkgreen").rgb;
  const green = Calc.colorFrom("green").rgb;
  const springgreen = Calc.colorFrom("springgreen").rgb;
  const mintcream = Calc.colorFrom("mintcream").rgb;
  const yellow = Calc.colorFrom("yellow").rgb;
  const fuchsia = Calc.colorFrom("fuchsia").rgb;
  const azure = Calc.colorFrom("azure").rgb;

  describe("binarySearchWidth", function() {
    it("expects to return a smaller value for each iteration", function() {
      let ds = [];
      for (let d of LightnessFinder.binarySearchWidth(100, 1)) {
        ds.push(d);
      }

      expect(ds).to.deep.equal([50, 25, 12.5, 6.25, 3.125, 1.5625]);
    });
  });

  describe("find", function() {
    context("when the required level is A", function() {
      it("expects to return a darker color when azure is passed to fuchsia", function() {
        const newRgb = LightnessFinder.find(fuchsia, azure, "A");
        const ratio = Checker.contrastRatio(fuchsia, newRgb);

        expect(higherLuminanceThan(azure, fuchsia)).to.be.true;
        expect(higherLuminanceThan(azure, newRgb)).to.be.true;
        expect(ratio).to.be.above(3.0);
        expect(ratio).to.be.closeTo(3.0, 0.5);
        expect(newRgb).to.deep.equal([233, 255, 255]);
      });

      it("expects to return a lighter green when both colors are darkgreen", function() {
        const contrast_with_white = Checker.contrastRatio(darkgreen, white);
        const contrast_with_black = Checker.contrastRatio(darkgreen, black);
        const newRgb = LightnessFinder.find(darkgreen, darkgreen, "A");
        const ratio = Checker.contrastRatio(darkgreen, newRgb);

        expect(Checker.isLightColor(darkgreen)).to.be.false;
        expect(contrast_with_white).to.be.greaterThan(contrast_with_black);
        expect(higherLuminanceThan(newRgb, darkgreen)).to.be.true;
        expect(ratio).to.be.above(3.0);
        expect(ratio).to.be.closeTo(3.0, 0.1);
        expect(newRgb).to.deep.equal([0, 192, 0]);
      });
    });

    context("when the required level is AA", function() {
      it("expects to return a darker orange when orange is passed to white", function() {
        const newRgb = LightnessFinder.find(white, orange, "AA");
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(4.5);
        expect(ratio).to.be.closeTo(4.5, 0.1);
        expect(newRgb).to.deep.equal([165, 106, 0]);
      });

      it("expects to return a darker green when green is passed to white", function() {
        const newRgb = LightnessFinder.find(white, green, "AA");
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(4.5);
        expect(ratio).to.be.closeTo(4.5, 0.1);
        expect(newRgb).to.deep.equal([0, 138, 0]);
      });

      it("expects to return a lighter orange when orange is passed to blueviolet", function() {
        const newRgb = LightnessFinder.find(blueviolet, orange, "AA");
        const ratio = Checker.contrastRatio(blueviolet, newRgb);

        expect(ratio).to.be.above(4.5);
        expect(ratio).to.be.closeTo(4.5, 0.1);
        expect(newRgb).to.deep.equal([255, 220, 154]);
      });

      it("expects to return a darker green when both colors are springgreen", function() {
        const contrast_with_white = Checker.contrastRatio(springgreen, white);
        const contrast_with_black = Checker.contrastRatio(springgreen, black);
        const newRgb = LightnessFinder.find(springgreen, springgreen, "AA");
        const ratio = Checker.contrastRatio(springgreen, newRgb);

        expect(Checker.isLightColor(springgreen)).to.be.true;
        expect(contrast_with_white).to.be.lessThan(contrast_with_black);
        expect(higherLuminanceThan(newRgb, springgreen)).to.be.false;
        expect(ratio).to.be.above(4.5);
        expect(ratio).to.be.closeTo(4.5, 0.1);
        expect(newRgb).to.deep.equal([0, 114, 57]);
      });

      it("expects to return white when yellow is passed to orange", function() {
        const newRgb = LightnessFinder.find(orange, yellow, "AA");
        const ratio = Checker.contrastRatio(orange, newRgb);

        expect(ratio).to.be.lessThan(4.5);
        expect(newRgb).to.deep.equal(white);
      });

      it("expects to return white when mintcream is passed to yellow", function() {
        const newRgb = LightnessFinder.find(yellow, mintcream, "AA");
        const ratio = Checker.contrastRatio(yellow, newRgb);

        expect(ratio).to.be.lessThan(4.5);
        expect(newRgb).to.deep.equal(white);
      });
    });

    context("when the required level is AAA", function() {
      it("expects to return a darker orange when orange is passed to white", function() {
        const newRgb = LightnessFinder.find(white, green, "AAA");
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(7.0);
        expect(ratio).to.be.closeTo(7.0, 0.1);
        expect(newRgb).to.deep.equal([0, 104, 0]);
      });

      it("expects to return a darker green when green is passed to white", function() {
        const newRgb = LightnessFinder.find(white, orange, "AAA");
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(7.0);
        expect(ratio).to.be.closeTo(7.0, 0.1);
        expect(newRgb).to.deep.equal([123, 80, 0]);
      });

      it("expects to return black when blue is passed to green", function() {
        const newRgb = LightnessFinder.find(green, blue, "AA");
        const ratio = Checker.contrastRatio(green, newRgb);

        expect(ratio).to.be.lessThan(7.0);
        expect(newRgb).to.deep.equal(black);
      });
    });

    context("when the required level is specified by a ratio", function() {
      it("expects to return a darker orange when orange is passed to white", function() {
        const newRgb = LightnessFinder.find(white, orange, 6.5);
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(6.5);
        expect(ratio).to.be.closeTo(6.5, 0.1);
        expect(newRgb).to.deep.equal([130, 84, 0]);
      });

      it("expects to return a darker green when green is passed to white", function() {
        const newRgb = LightnessFinder.find(white, green, 6.5);
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(6.5);
        expect(ratio).to.be.closeTo(6.5, 0.1);
        expect(newRgb).to.deep.equal([0, 110, 0]);
      });

      it("expects to return black when blue is passed to green", function() {
        const newRgb = LightnessFinder.find(green, blue, 6.5);
        const ratio = Checker.contrastRatio(green, newRgb);

        expect(ratio).to.be.lessThan(6.5);
        expect(newRgb).to.deep.equal(black);
      });
    });
  });
});

describe("BrightnessFinder", function() {
  const orange = Calc.colorFrom("orange").rgb;
  const blueviolet = Calc.colorFrom("blueviolet").rgb;
  const blue = Calc.colorFrom("blue").rgb;
  const brown = Calc.colorFrom("brown").rgb;
  const black = Calc.colorFrom("black").rgb;
  const white = Calc.colorFrom("white").rgb;
  const darkgreen = Calc.colorFrom("darkgreen").rgb;
  const green = Calc.colorFrom("green").rgb;
  const mintcream = Calc.colorFrom("mintcream").rgb;
  const yellow = Calc.colorFrom("yellow").rgb;
  const springgreen = Calc.colorFrom("springgreen").rgb;
  const fuchsia = Calc.colorFrom("fuchsia").rgb;
  const azure = Calc.colorFrom("azure").rgb;

  describe("find", function() {
    it("expects to return a darker orange if orange is passed", function() {
      const threshold = BrightnessFinder.find(orange, orange);
      const ratio = Checker.contrastRatio(orange, threshold);

      expect(Checker.contrastRatio(orange, orange)).to.be.below(4.5);
      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
      expect(threshold).to.deep.equal([103, 66, 0]);
    });

    it("expects to return a more darker color when a darker color is passed", function() {
      const threshold = BrightnessFinder.find(orange, blueviolet);
      const ratio = Checker.contrastRatio(orange, threshold);

      expect(Checker.contrastRatio(orange, blueviolet)).to.be.below(4.5);
      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
      expect(threshold).to.deep.equal([103, 32, 169]);
    });

    it("expects to return a brighter orange if blueviolet is combined with orange", function() {
      const threshold = BrightnessFinder.find(blueviolet, orange);
      const ratio = Checker.contrastRatio(blueviolet, threshold);

      expect(Checker.contrastRatio(blueviolet, orange)).to.be.below(4.5);
      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
      expect(threshold).to.deep.equal([255, 224, 0]);
    });

    it("expects to return a brigher color if brown is passed to brown", function() {
      const threshold = BrightnessFinder.find(brown, brown);
      const ratio = Checker.contrastRatio(brown, threshold);

      expect(Checker.contrastRatio(brown, black)).to.be.below(4.5);
      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
      expect(threshold).to.deep.equal([255, 190, 190]);
    });

    it("expects to return a darker green if darkgreen is passed to white - AA", function() {
      const threshold = BrightnessFinder.find(white, darkgreen);
      const ratio = Checker.contrastRatio(white, threshold);

      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
    });

    it("expects to return a darker green if darkgreen is passed to white - AA", function() {
      const threshold = BrightnessFinder.find(white, darkgreen, "AAA");
      const ratio = Checker.contrastRatio(white, threshold);

      expect(ratio).to.be.above(7.0);
      expect(ratio).to.be.closeTo(7.0, 0.5);
    });

    it("expects to return black for AAA if blue is passed to green", function() {
      const newColor = BrightnessFinder.find(green, blue, "AAA");

      expect(Checker.contrastRatio(newColor, black)).to.be.below(7.0);
      expect(newColor).to.deep.equal(black);
    });

    it("expects to return white when mintcream is passed to yellow", function() {
      const a = BrightnessFinder.find(yellow, mintcream, "A");
      const aa = BrightnessFinder.find(yellow, mintcream, "AA");
      const aaa = BrightnessFinder.find(yellow, mintcream, "AAA");

      expect(BrightnessFinder.calcUpperRatioLimit(mintcream)).to.equal(105);
      expect(a).to.deep.equal(white);
      expect(aa).to.deep.equal(white);
      expect(aaa).to.deep.equal(white);
    });

    it("expects to return darker green when springgreen is passed to green", function() {
      const newColor = BrightnessFinder.find(green, springgreen, "A");
      const springgreenLum = Checker.relativeLuminance(springgreen);
      const newLum = Checker.relativeLuminance(newColor);
      const ratio = Checker.contrastRatio(green, newColor);

      expect(springgreenLum).to.be.greaterThan(newLum);
      expect(ratio).to.be.above(3.0);
      expect(ratio).to.be.closeTo(3.0, 0.5);
    });

    it("expects to return a darker color when azure is passed to fuchsia", function() {
      const threshold = BrightnessFinder.find(fuchsia, azure, "A");
      const azureLum = Checker.relativeLuminance(azure);
      const newLum = Checker.relativeLuminance(threshold);
      const ratio = Checker.contrastRatio(fuchsia, threshold);

      expect(azureLum).to.be.greaterThan(newLum);
      expect(ratio).to.be.above(3.0);
      expect(ratio).to.be.closeTo(3.0, 0.5);
    });
  });

  describe("calcUpperRatioLimit", function() {
    it("expects to return 155 for orange", function() {
      expect(BrightnessFinder.calcUpperRatioLimit(orange)).to.equal(155);
    });

    it("expects to return 594 for blueviolet", function() {
      expect(BrightnessFinder.calcUpperRatioLimit(blueviolet)).to.equal(594);
    });

    it("expects to return 100 for black", function() {
      expect(BrightnessFinder.calcUpperRatioLimit(black)).to.equal(100);
    });
  });
});
<commit_msg>remove an unused variable in threshold-finder_test.js<commit_after>"use strict";

const expect = require("chai").expect;
const LightnessFinder = require("../lib/threshold-finder").LightnessFinder;
const BrightnessFinder = require("../lib/threshold-finder").BrightnessFinder;
const Checker = require("../lib/contrast-checker").ContrastChecker;
const Calc = require("../lib/color-contrast-calc").ColorContrastCalc;

const higherLuminanceThan = (mainRgb, otherRgb) => {
  const mainLum = Checker.relativeLuminance(mainRgb);
  const otherLum = Checker.relativeLuminance(otherRgb);
  return mainLum > otherLum;
};

describe("LightnessFinder", function() {
  const orange = Calc.colorFrom("orange").rgb;
  const blueviolet = Calc.colorFrom("blueviolet").rgb;
  const blue = Calc.colorFrom("blue").rgb;
  const black = Calc.colorFrom("black").rgb;
  const white = Calc.colorFrom("white").rgb;
  const darkgreen = Calc.colorFrom("darkgreen").rgb;
  const green = Calc.colorFrom("green").rgb;
  const springgreen = Calc.colorFrom("springgreen").rgb;
  const mintcream = Calc.colorFrom("mintcream").rgb;
  const yellow = Calc.colorFrom("yellow").rgb;
  const fuchsia = Calc.colorFrom("fuchsia").rgb;
  const azure = Calc.colorFrom("azure").rgb;

  describe("binarySearchWidth", function() {
    it("expects to return a smaller value for each iteration", function() {
      let ds = [];
      for (let d of LightnessFinder.binarySearchWidth(100, 1)) {
        ds.push(d);
      }

      expect(ds).to.deep.equal([50, 25, 12.5, 6.25, 3.125, 1.5625]);
    });
  });

  describe("find", function() {
    context("when the required level is A", function() {
      it("expects to return a darker color when azure is passed to fuchsia", function() {
        const newRgb = LightnessFinder.find(fuchsia, azure, "A");
        const ratio = Checker.contrastRatio(fuchsia, newRgb);

        expect(higherLuminanceThan(azure, fuchsia)).to.be.true;
        expect(higherLuminanceThan(azure, newRgb)).to.be.true;
        expect(ratio).to.be.above(3.0);
        expect(ratio).to.be.closeTo(3.0, 0.5);
        expect(newRgb).to.deep.equal([233, 255, 255]);
      });

      it("expects to return a lighter green when both colors are darkgreen", function() {
        const contrast_with_white = Checker.contrastRatio(darkgreen, white);
        const contrast_with_black = Checker.contrastRatio(darkgreen, black);
        const newRgb = LightnessFinder.find(darkgreen, darkgreen, "A");
        const ratio = Checker.contrastRatio(darkgreen, newRgb);

        expect(Checker.isLightColor(darkgreen)).to.be.false;
        expect(contrast_with_white).to.be.greaterThan(contrast_with_black);
        expect(higherLuminanceThan(newRgb, darkgreen)).to.be.true;
        expect(ratio).to.be.above(3.0);
        expect(ratio).to.be.closeTo(3.0, 0.1);
        expect(newRgb).to.deep.equal([0, 192, 0]);
      });
    });

    context("when the required level is AA", function() {
      it("expects to return a darker orange when orange is passed to white", function() {
        const newRgb = LightnessFinder.find(white, orange, "AA");
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(4.5);
        expect(ratio).to.be.closeTo(4.5, 0.1);
        expect(newRgb).to.deep.equal([165, 106, 0]);
      });

      it("expects to return a darker green when green is passed to white", function() {
        const newRgb = LightnessFinder.find(white, green, "AA");
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(4.5);
        expect(ratio).to.be.closeTo(4.5, 0.1);
        expect(newRgb).to.deep.equal([0, 138, 0]);
      });

      it("expects to return a lighter orange when orange is passed to blueviolet", function() {
        const newRgb = LightnessFinder.find(blueviolet, orange, "AA");
        const ratio = Checker.contrastRatio(blueviolet, newRgb);

        expect(ratio).to.be.above(4.5);
        expect(ratio).to.be.closeTo(4.5, 0.1);
        expect(newRgb).to.deep.equal([255, 220, 154]);
      });

      it("expects to return a darker green when both colors are springgreen", function() {
        const contrast_with_white = Checker.contrastRatio(springgreen, white);
        const contrast_with_black = Checker.contrastRatio(springgreen, black);
        const newRgb = LightnessFinder.find(springgreen, springgreen, "AA");
        const ratio = Checker.contrastRatio(springgreen, newRgb);

        expect(Checker.isLightColor(springgreen)).to.be.true;
        expect(contrast_with_white).to.be.lessThan(contrast_with_black);
        expect(higherLuminanceThan(newRgb, springgreen)).to.be.false;
        expect(ratio).to.be.above(4.5);
        expect(ratio).to.be.closeTo(4.5, 0.1);
        expect(newRgb).to.deep.equal([0, 114, 57]);
      });

      it("expects to return white when yellow is passed to orange", function() {
        const newRgb = LightnessFinder.find(orange, yellow, "AA");
        const ratio = Checker.contrastRatio(orange, newRgb);

        expect(ratio).to.be.lessThan(4.5);
        expect(newRgb).to.deep.equal(white);
      });

      it("expects to return white when mintcream is passed to yellow", function() {
        const newRgb = LightnessFinder.find(yellow, mintcream, "AA");
        const ratio = Checker.contrastRatio(yellow, newRgb);

        expect(ratio).to.be.lessThan(4.5);
        expect(newRgb).to.deep.equal(white);
      });
    });

    context("when the required level is AAA", function() {
      it("expects to return a darker orange when orange is passed to white", function() {
        const newRgb = LightnessFinder.find(white, green, "AAA");
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(7.0);
        expect(ratio).to.be.closeTo(7.0, 0.1);
        expect(newRgb).to.deep.equal([0, 104, 0]);
      });

      it("expects to return a darker green when green is passed to white", function() {
        const newRgb = LightnessFinder.find(white, orange, "AAA");
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(7.0);
        expect(ratio).to.be.closeTo(7.0, 0.1);
        expect(newRgb).to.deep.equal([123, 80, 0]);
      });

      it("expects to return black when blue is passed to green", function() {
        const newRgb = LightnessFinder.find(green, blue, "AA");
        const ratio = Checker.contrastRatio(green, newRgb);

        expect(ratio).to.be.lessThan(7.0);
        expect(newRgb).to.deep.equal(black);
      });
    });

    context("when the required level is specified by a ratio", function() {
      it("expects to return a darker orange when orange is passed to white", function() {
        const newRgb = LightnessFinder.find(white, orange, 6.5);
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(6.5);
        expect(ratio).to.be.closeTo(6.5, 0.1);
        expect(newRgb).to.deep.equal([130, 84, 0]);
      });

      it("expects to return a darker green when green is passed to white", function() {
        const newRgb = LightnessFinder.find(white, green, 6.5);
        const ratio = Checker.contrastRatio(white, newRgb);

        expect(ratio).to.be.above(6.5);
        expect(ratio).to.be.closeTo(6.5, 0.1);
        expect(newRgb).to.deep.equal([0, 110, 0]);
      });

      it("expects to return black when blue is passed to green", function() {
        const newRgb = LightnessFinder.find(green, blue, 6.5);
        const ratio = Checker.contrastRatio(green, newRgb);

        expect(ratio).to.be.lessThan(6.5);
        expect(newRgb).to.deep.equal(black);
      });
    });
  });
});

describe("BrightnessFinder", function() {
  const orange = Calc.colorFrom("orange").rgb;
  const blueviolet = Calc.colorFrom("blueviolet").rgb;
  const blue = Calc.colorFrom("blue").rgb;
  const brown = Calc.colorFrom("brown").rgb;
  const black = Calc.colorFrom("black").rgb;
  const white = Calc.colorFrom("white").rgb;
  const darkgreen = Calc.colorFrom("darkgreen").rgb;
  const green = Calc.colorFrom("green").rgb;
  const mintcream = Calc.colorFrom("mintcream").rgb;
  const yellow = Calc.colorFrom("yellow").rgb;
  const springgreen = Calc.colorFrom("springgreen").rgb;
  const fuchsia = Calc.colorFrom("fuchsia").rgb;
  const azure = Calc.colorFrom("azure").rgb;

  describe("find", function() {
    it("expects to return a darker orange if orange is passed", function() {
      const threshold = BrightnessFinder.find(orange, orange);
      const ratio = Checker.contrastRatio(orange, threshold);

      expect(Checker.contrastRatio(orange, orange)).to.be.below(4.5);
      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
      expect(threshold).to.deep.equal([103, 66, 0]);
    });

    it("expects to return a more darker color when a darker color is passed", function() {
      const threshold = BrightnessFinder.find(orange, blueviolet);
      const ratio = Checker.contrastRatio(orange, threshold);

      expect(Checker.contrastRatio(orange, blueviolet)).to.be.below(4.5);
      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
      expect(threshold).to.deep.equal([103, 32, 169]);
    });

    it("expects to return a brighter orange if blueviolet is combined with orange", function() {
      const threshold = BrightnessFinder.find(blueviolet, orange);
      const ratio = Checker.contrastRatio(blueviolet, threshold);

      expect(Checker.contrastRatio(blueviolet, orange)).to.be.below(4.5);
      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
      expect(threshold).to.deep.equal([255, 224, 0]);
    });

    it("expects to return a brigher color if brown is passed to brown", function() {
      const threshold = BrightnessFinder.find(brown, brown);
      const ratio = Checker.contrastRatio(brown, threshold);

      expect(Checker.contrastRatio(brown, black)).to.be.below(4.5);
      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
      expect(threshold).to.deep.equal([255, 190, 190]);
    });

    it("expects to return a darker green if darkgreen is passed to white - AA", function() {
      const threshold = BrightnessFinder.find(white, darkgreen);
      const ratio = Checker.contrastRatio(white, threshold);

      expect(ratio).to.be.above(4.5);
      expect(ratio).to.be.closeTo(4.5, 0.5);
    });

    it("expects to return a darker green if darkgreen is passed to white - AA", function() {
      const threshold = BrightnessFinder.find(white, darkgreen, "AAA");
      const ratio = Checker.contrastRatio(white, threshold);

      expect(ratio).to.be.above(7.0);
      expect(ratio).to.be.closeTo(7.0, 0.5);
    });

    it("expects to return black for AAA if blue is passed to green", function() {
      const newColor = BrightnessFinder.find(green, blue, "AAA");

      expect(Checker.contrastRatio(newColor, black)).to.be.below(7.0);
      expect(newColor).to.deep.equal(black);
    });

    it("expects to return white when mintcream is passed to yellow", function() {
      const a = BrightnessFinder.find(yellow, mintcream, "A");
      const aa = BrightnessFinder.find(yellow, mintcream, "AA");
      const aaa = BrightnessFinder.find(yellow, mintcream, "AAA");

      expect(BrightnessFinder.calcUpperRatioLimit(mintcream)).to.equal(105);
      expect(a).to.deep.equal(white);
      expect(aa).to.deep.equal(white);
      expect(aaa).to.deep.equal(white);
    });

    it("expects to return darker green when springgreen is passed to green", function() {
      const newColor = BrightnessFinder.find(green, springgreen, "A");
      const springgreenLum = Checker.relativeLuminance(springgreen);
      const newLum = Checker.relativeLuminance(newColor);
      const ratio = Checker.contrastRatio(green, newColor);

      expect(springgreenLum).to.be.greaterThan(newLum);
      expect(ratio).to.be.above(3.0);
      expect(ratio).to.be.closeTo(3.0, 0.5);
    });

    it("expects to return a darker color when azure is passed to fuchsia", function() {
      const threshold = BrightnessFinder.find(fuchsia, azure, "A");
      const azureLum = Checker.relativeLuminance(azure);
      const newLum = Checker.relativeLuminance(threshold);
      const ratio = Checker.contrastRatio(fuchsia, threshold);

      expect(azureLum).to.be.greaterThan(newLum);
      expect(ratio).to.be.above(3.0);
      expect(ratio).to.be.closeTo(3.0, 0.5);
    });
  });

  describe("calcUpperRatioLimit", function() {
    it("expects to return 155 for orange", function() {
      expect(BrightnessFinder.calcUpperRatioLimit(orange)).to.equal(155);
    });

    it("expects to return 594 for blueviolet", function() {
      expect(BrightnessFinder.calcUpperRatioLimit(blueviolet)).to.equal(594);
    });

    it("expects to return 100 for black", function() {
      expect(BrightnessFinder.calcUpperRatioLimit(black)).to.equal(100);
    });
  });
});
<|endoftext|>


<commit_before>'use strict';

/**
 * ButtressJS - Realtime datastore for software
 *
 * @file schemaModel.js
 * @description A default model for schemas
 * @module Model
 * @exports model, schema, constants
 * @author Chris Bates-Keegan
 *
 */

const ObjectId = require('mongodb').ObjectId;
// const Logging = require('../logging');
const Shared = require('./shared');
const Sugar = require('sugar');
const Helpers = require('../helpers');
const shortId = require('../helpers').shortId;

/* ********************************************************************************
 *
 * LOCALS
 *
 **********************************************************************************/

class SchemaModel {
	constructor(MongoDb, schemaData, app) {
		this.schemaData = schemaData;
		this.flatSchemaData = Helpers.getFlattenedSchema(this.schemaData);

		this.app = app || null;

		this.appShortId = (app) ? shortId(app._id) : null;
		this.collectionName = `${schemaData.collection}`;

		if (this.appShortId) {
			this.collectionName = `${this.appShortId}-${this.collectionName}`;
		}

		this.collection = MongoDb.collection(this.collectionName);
	}

	__doValidation(body) {
		const res = {
			isValid: true,
			missing: [],
			invalid: [],
		};

		const app = Shared.validateAppProperties(this.schemaData, body);
		if (app.isValid === false) {
			res.isValid = false;
			res.invalid = res.invalid.concat(app.invalid);
			res.missing = res.missing.concat(app.missing);
		}

		return res;
	}
	validate(body) {
		if (body instanceof Array === false) {
			body = [body];
		}
		const validation = body.map((b) => this.__doValidation(b)).filter((v) => v.isValid === false);

		return validation.length >= 1 ? validation[0] : {isValid: true};
	}

	static parseQuery(query, envFlat = {}, schemaFlat = {}) {
		const output = {};

		for (let property in query) {
			if (!{}.hasOwnProperty.call(query, property)) continue;
			if (property === '__crPath') continue;
			const command = query[property];

			if (property === '$or' && Array.isArray(command) && command.length > 0) {
				output['$or'] = command.map((q) => SchemaModel.parseQuery(q, envFlat, schemaFlat));
			} else if (property === '$and' && Array.isArray(command) && command.length > 0) {
				output['$and'] = command.map((q) => SchemaModel.parseQuery(q, envFlat, schemaFlat));
			} else {
				for (let operator in command) {
					if (!{}.hasOwnProperty.call(command, operator)) continue;
					let operand = command[operator];
					let operandOptions = null;
					switch (operator) {
					case '$not':
						operator = '$ne';
						break;

					case '$elMatch':
						operator = '$elemMatch';
						break;
					case '$gtDate':
						operator = '$gt';
						break;
					case '$ltDate':
						operator = '$lt';
						break;
					case '$gteDate':
						operator = '$gte';
						break;
					case '$lteDate':
						operator = '$lte';
						break;

					case '$rex':
					case '$rexi':
						operator = '$regex';
						operandOptions = 'i';
						break;
					case '$inProp':
						operator = '$regex';
						break;

					default:
						// TODO: Throw an error if operator isn't supported
					}

					// Check to see if operand is a path and fetch value
					if (operand && operand.indexOf && operand.indexOf('.') !== -1) {
						let path = operand.split('.');
						const key = path.shift();

						path = path.join('.');

						if (key === 'env' && envFlat[path]) {
							operand = envFlat[path];
						} else {
							// throw new Error(`Unable to find ${path} in schema.authFilter.env`);
						}
					}

					// Convert id
					let propSchema = null;
					if (!schemaFlat[property] && property === 'id') {
						// Convert id -> _id to handle querying of document root index without having to pass _id
						property = '_id';
						propSchema = {
							__type: 'id',
						};
					} else if (schemaFlat[property]) {
						propSchema = schemaFlat[property];
					} else {
						// TODO: Should maybe reject query
					}

					if (operator === '$elemMatch' && propSchema && propSchema.__schema) {
						operand = SchemaModel.parseQuery(operand, envFlat, propSchema.__schema);
					} else if (propSchema) {
						if (propSchema.__type === 'array' && propSchema.__schema) {
							Object.keys(operand).forEach((op) => {
								if (propSchema.__schema[op].__type === 'id') {
									Object.keys(operand[op]).forEach((key) => {
										operand[op][key] = new ObjectId(operand[op][key]);
									});
								}
							});
						}

						if (propSchema.__type === 'date' && typeof operand === 'string') {
							operand = new Date(operand);
						}

						if ((propSchema.__type === 'id' || propSchema.__itemtype === 'id') && typeof operand === 'string') {
							operand = new ObjectId(operand);
						}
						if (propSchema.__type === 'id' && Array.isArray(operand)) {
							operand = operand.map((o) => new ObjectId(o));
						}
					}

					if (!output[property]) {
						output[property] = {};
					}

					if (operandOptions) {
						output[property][`$options`] = operandOptions;
					}

					if (operator.indexOf('$') !== 0) {
						output[property][`$${operator}`] = operand;
					} else {
						output[property][`${operator}`] = operand;
					}
				}
			}
		}

		return output;
	}

	generateRoleFilterQuery(token, roles, Model) {
		if (!roles.schema || !roles.schema.authFilter) {
			return Promise.resolve({});
		}

		const env = {
			authUserId: token._user,
		};

		const tasks = [];

		if (roles.schema.authFilter.env) {
			for (const property in roles.schema.authFilter.env) {
				if (!{}.hasOwnProperty.call(roles.schema.authFilter.env, property)) continue;
				const query = roles.schema.authFilter.env[property];

				let propertyMap = '_id';
				if (query.map) {
					propertyMap = query.map;
				}
				for (const command in query) {
					if (!{}.hasOwnProperty.call(query, command)) continue;

					if (command.includes('schema.')) {
						const commandPath = command.split('.');
						commandPath.shift(); // Remove "schema"
						const collectionName = commandPath.shift();
						const collectionPath = `${this.appShortId}-${collectionName}`;
						const collection = Model[collectionPath];

						if (!collection) {
							throw new Error(`Unable to find a collection named ${collectionName} while building authFilter.env`);
						}

						const propertyPath = commandPath.join('.');

						let propertyQuery = {};
						propertyQuery[propertyPath] = query[command];
						propertyQuery = SchemaModel.parseQuery(propertyQuery, env);

						const fields = {};
						fields[propertyPath] = true;

						tasks.push(() => {
							return collection.find(propertyQuery, fields)
								.then((res) => {
									// Map fetched properties into a array.
									env[property] = res.map((i) => i[propertyMap]);
									// Hack - Flattern any sub arrays down to the single level.
									env[property] = [].concat(...env[property]);
								});
						});
					} else {
						// Unknown operation
					}
				}
			}
		}

		// Engage.
		return tasks.reduce((prev, task) => prev.then(() => task()), Promise.resolve())
			.then(() => SchemaModel.parseQuery(roles.schema.authFilter.query, env, this.flatSchemaData));
	}

	/*
	* @param {Object} body - body passed through from a POST request
	* @return {Promise} - returns a promise that is fulfilled when the database request is completed
	*/
	__add(body, internals) {
		return (prev) => {
			const entity = Object.assign({}, internals);

			if (body.id) {
				entity._id = new ObjectId(body.id);
			}

			if (this.schemaData.extends && this.schemaData.extends.includes('timestamps')) {
				entity.createdAt = Sugar.Date.create();
				entity.updatedAt = (body.updatedAt) ? Sugar.Date.create(body.updatedAt) : null;
			}

			const validated = Shared.applyAppProperties(this.schemaData, body);
			return prev.concat([Object.assign(validated, entity)]);
		};
	}
	add(body, internals) {
		const sharedAddFn = Shared.add(this.collection, (item) => this.__add(item, internals));
		return sharedAddFn(body);
	}

	update(query, id) {
		// Logging.logSilly(`update: ${this.collectionName} ${id} ${query}`);

		return new Promise((resolve, reject) => {
			this.collection.updateOne({_id: id}, {
				$set: query,
			}, (err, object) => {
				if (err) throw new Error(err);

				resolve(object);
			});
		});
	}
	validateUpdate(body) {
		const sharedFn = Shared.validateUpdate({}, this.schemaData);
		return sharedFn(body);
	}
	updateByPath(body, id) {
		const sharedFn = Shared.updateByPath({}, this.schemaData, this.collection);

		if (body instanceof Array === false) {
			body = [body];
		}

		if (this.schemaData.extends && this.schemaData.extends.includes('timestamps')) {
			body.push({
				path: 'updatedAt',
				value: new Date(),
				contextPath: '^updatedAt$',
			});
		}

		return sharedFn(body, id);
	}

	exists(id) {
		// Logging.logSilly(`exists: ${this.collectionName} ${id}`);

		return this.collection.find({_id: new ObjectId(id)})
			.limit(1)
			.count()
			.then((count) => count > 0);
	}

	/*
	* @return {Promise} - returns a promise that is fulfilled when the database request is completed
	*/
	isDuplicate(details) {
		return Promise.resolve(false);
	}

	/**
	 * @param {App} entity - entity object to be deleted
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rm(entity) {
		// Logging.log(`DELETING: ${entity._id}`, Logging.Constants.LogLevel.DEBUG);
		return new Promise((resolve) => {
			this.collection.deleteOne({_id: new ObjectId(entity._id)}, (err, cursor) => {
				if (err) throw err;
				resolve(cursor);
			});
		});
	}

	/**
	 * @param {Array} ids - Array of entity ids to delete
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rmBulk(ids) {
		// Logging.log(`rmBulk: ${this.collectionName} ${ids}`, Logging.Constants.LogLevel.SILLY);
		return this.rmAll({_id: {$in: ids}});
	}

	/*
	 * @param {Object} query - mongoDB query
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rmAll(query) {
		if (!query) query = {};
		// Logging.logSilly(`rmAll: ${this.collectionName} ${query}`);

		return new Promise((resolve) => {
			this.collection.deleteMany(query, (err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {String} id - entity id to get
	 * @return {Promise} - resolves to an array of Companies
	 */
	findById(id) {
		// Logging.logSilly(`Schema:findById: ${this.collectionName} ${id}`);

		return new Promise((resolve) => {
			this.collection.findOne({_id: id}, {}, (err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @param {Object} excludes - mongoDB query excludes
	 * @param {Boolean} stream - should return a stream
	 * @param {Int} limit - should return a stream
	 * @param {Int} skip - should return a stream
	 * @param {Object} sort - mongoDB sort object
	 * @param {Boolean} project - mongoDB project ids
	 * @return {Promise} - resolves to an array of docs
	 */
	find(query, excludes = {}, stream = false, limit = 0, skip = 0, sort, project = null) {
		// Logging.logSilly(`find: ${this.collectionName} ${query}`);
		if (stream) {
			let results = this.collection.find(query, excludes).skip(skip).limit(limit).sort(sort);

			if (project) {
				results = results.project(project);
			}

			return results;
		}

		return new Promise((resolve) => {
			this.collection.find(query, excludes)
				.skip(skip)
				.limit(limit)
				.sort(sort)
				.toArray((err, doc) => {
					if (err) throw err;
					resolve(doc);
				});
		});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @param {Object} excludes - mongoDB query excludes
	 * @return {Promise} - resolves to an array of docs
	 */
	findOne(query, excludes = {}) {
		// Logging.logSilly(`findOne: ${this.collectionName} ${query}`);

		return new Promise((resolve) => {
			this.collection.find(query, excludes).toArray((err, doc) => {
				if (err) throw err;
				resolve(doc[0]);
			});
		});
	}

	/**
	 * @return {Promise} - resolves to an array of Companies
	 */
	findAll() {
		// Logging.logSilly(`findAll: ${this.collectionName}`);

		return this.collection.find({});
	}

	/**
	 * @param {Array} ids - Array of entities ids to get
	 * @return {Promise} - resolves to an array of Companies
	 */
	findAllById(ids) {
		// Logging.logSilly(`update: ${this.collectionName} ${ids}`);

		return this.collection.find({_id: {$in: ids.map((id) => new ObjectId(id))}}, {});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @return {Promise} - resolves to an array of Companies
	 */
	count(query) {
		return this.collection.countDocuments(query);
	}
}

module.exports = SchemaModel;
<commit_msg>FIX: convert $in query ids to ObjectId (#15)<commit_after>'use strict';

/**
 * ButtressJS - Realtime datastore for software
 *
 * @file schemaModel.js
 * @description A default model for schemas
 * @module Model
 * @exports model, schema, constants
 * @author Chris Bates-Keegan
 *
 */

const ObjectId = require('mongodb').ObjectId;
// const Logging = require('../logging');
const Shared = require('./shared');
const Sugar = require('sugar');
const Helpers = require('../helpers');
const shortId = require('../helpers').shortId;

/* ********************************************************************************
 *
 * LOCALS
 *
 **********************************************************************************/

class SchemaModel {
	constructor(MongoDb, schemaData, app) {
		this.schemaData = schemaData;
		this.flatSchemaData = Helpers.getFlattenedSchema(this.schemaData);

		this.app = app || null;

		this.appShortId = (app) ? shortId(app._id) : null;
		this.collectionName = `${schemaData.collection}`;

		if (this.appShortId) {
			this.collectionName = `${this.appShortId}-${this.collectionName}`;
		}

		this.collection = MongoDb.collection(this.collectionName);
	}

	__doValidation(body) {
		const res = {
			isValid: true,
			missing: [],
			invalid: [],
		};

		const app = Shared.validateAppProperties(this.schemaData, body);
		if (app.isValid === false) {
			res.isValid = false;
			res.invalid = res.invalid.concat(app.invalid);
			res.missing = res.missing.concat(app.missing);
		}

		return res;
	}
	validate(body) {
		if (body instanceof Array === false) {
			body = [body];
		}
		const validation = body.map((b) => this.__doValidation(b)).filter((v) => v.isValid === false);

		return validation.length >= 1 ? validation[0] : {isValid: true};
	}

	static parseQuery(query, envFlat = {}, schemaFlat = {}) {
		const output = {};

		for (let property in query) {
			if (!{}.hasOwnProperty.call(query, property)) continue;
			if (property === '__crPath') continue;
			const command = query[property];

			if (property === '$or' && Array.isArray(command) && command.length > 0) {
				output['$or'] = command.map((q) => SchemaModel.parseQuery(q, envFlat, schemaFlat));
			} else if (property === '$and' && Array.isArray(command) && command.length > 0) {
				output['$and'] = command.map((q) => SchemaModel.parseQuery(q, envFlat, schemaFlat));
			} else {
				for (let operator in command) {
					if (!{}.hasOwnProperty.call(command, operator)) continue;
					let operand = command[operator];
					let operandOptions = null;
					switch (operator) {
					case '$not':
						operator = '$ne';
						break;

					case '$elMatch':
						operator = '$elemMatch';
						break;
					case '$gtDate':
						operator = '$gt';
						break;
					case '$ltDate':
						operator = '$lt';
						break;
					case '$gteDate':
						operator = '$gte';
						break;
					case '$lteDate':
						operator = '$lte';
						break;

					case '$rex':
					case '$rexi':
						operator = '$regex';
						operandOptions = 'i';
						break;
					case '$inProp':
						operator = '$regex';
						break;

					default:
						// TODO: Throw an error if operator isn't supported
					}

					// Check to see if operand is a path and fetch value
					if (operand && operand.indexOf && operand.indexOf('.') !== -1) {
						let path = operand.split('.');
						const key = path.shift();

						path = path.join('.');

						if (key === 'env' && envFlat[path]) {
							operand = envFlat[path];
						} else {
							// throw new Error(`Unable to find ${path} in schema.authFilter.env`);
						}
					}

					// Convert id
					let propSchema = null;
					if (!schemaFlat[property] && property === 'id') {
						// Convert id -> _id to handle querying of document root index without having to pass _id
						property = '_id';
						propSchema = {
							__type: 'id',
						};
					} else if (schemaFlat[property]) {
						propSchema = schemaFlat[property];
					} else {
						// TODO: Should maybe reject query
					}

					if (operator === '$elemMatch' && propSchema && propSchema.__schema) {
						operand = SchemaModel.parseQuery(operand, envFlat, propSchema.__schema);
					} else if (propSchema) {
						if (propSchema.__type === 'array' && propSchema.__schema) {
							Object.keys(operand).forEach((op) => {
								if (propSchema.__schema[op].__type === 'id') {
									Object.keys(operand[op]).forEach((key) => {
										operand[op][key] = new ObjectId(operand[op][key]);
									});
								}
							});
						}

						if (propSchema.__type === 'date' && typeof operand === 'string') {
							operand = new Date(operand);
						}

						if ((propSchema.__type === 'id' || propSchema.__itemtype === 'id') && typeof operand === 'string') {
							operand = new ObjectId(operand);
						}
						if ((propSchema.__type === 'id' || propSchema.__itemtype === 'id') && Array.isArray(operand)) {
							operand = operand.map((o) => new ObjectId(o));
						}
					}

					if (!output[property]) {
						output[property] = {};
					}

					if (operandOptions) {
						output[property][`$options`] = operandOptions;
					}

					if (operator.indexOf('$') !== 0) {
						output[property][`$${operator}`] = operand;
					} else {
						output[property][`${operator}`] = operand;
					}
				}
			}
		}

		return output;
	}

	generateRoleFilterQuery(token, roles, Model) {
		if (!roles.schema || !roles.schema.authFilter) {
			return Promise.resolve({});
		}

		const env = {
			authUserId: token._user,
		};

		const tasks = [];

		if (roles.schema.authFilter.env) {
			for (const property in roles.schema.authFilter.env) {
				if (!{}.hasOwnProperty.call(roles.schema.authFilter.env, property)) continue;
				const query = roles.schema.authFilter.env[property];

				let propertyMap = '_id';
				if (query.map) {
					propertyMap = query.map;
				}
				for (const command in query) {
					if (!{}.hasOwnProperty.call(query, command)) continue;

					if (command.includes('schema.')) {
						const commandPath = command.split('.');
						commandPath.shift(); // Remove "schema"
						const collectionName = commandPath.shift();
						const collectionPath = `${this.appShortId}-${collectionName}`;
						const collection = Model[collectionPath];

						if (!collection) {
							throw new Error(`Unable to find a collection named ${collectionName} while building authFilter.env`);
						}

						const propertyPath = commandPath.join('.');

						let propertyQuery = {};
						propertyQuery[propertyPath] = query[command];
						propertyQuery = SchemaModel.parseQuery(propertyQuery, env);

						const fields = {};
						fields[propertyPath] = true;

						tasks.push(() => {
							return collection.find(propertyQuery, fields)
								.then((res) => {
									// Map fetched properties into a array.
									env[property] = res.map((i) => i[propertyMap]);
									// Hack - Flattern any sub arrays down to the single level.
									env[property] = [].concat(...env[property]);
								});
						});
					} else {
						// Unknown operation
					}
				}
			}
		}

		// Engage.
		return tasks.reduce((prev, task) => prev.then(() => task()), Promise.resolve())
			.then(() => SchemaModel.parseQuery(roles.schema.authFilter.query, env, this.flatSchemaData));
	}

	/*
	* @param {Object} body - body passed through from a POST request
	* @return {Promise} - returns a promise that is fulfilled when the database request is completed
	*/
	__add(body, internals) {
		return (prev) => {
			const entity = Object.assign({}, internals);

			if (body.id) {
				entity._id = new ObjectId(body.id);
			}

			if (this.schemaData.extends && this.schemaData.extends.includes('timestamps')) {
				entity.createdAt = Sugar.Date.create();
				entity.updatedAt = (body.updatedAt) ? Sugar.Date.create(body.updatedAt) : null;
			}

			const validated = Shared.applyAppProperties(this.schemaData, body);
			return prev.concat([Object.assign(validated, entity)]);
		};
	}
	add(body, internals) {
		const sharedAddFn = Shared.add(this.collection, (item) => this.__add(item, internals));
		return sharedAddFn(body);
	}

	update(query, id) {
		// Logging.logSilly(`update: ${this.collectionName} ${id} ${query}`);

		return new Promise((resolve, reject) => {
			this.collection.updateOne({_id: id}, {
				$set: query,
			}, (err, object) => {
				if (err) throw new Error(err);

				resolve(object);
			});
		});
	}
	validateUpdate(body) {
		const sharedFn = Shared.validateUpdate({}, this.schemaData);
		return sharedFn(body);
	}
	updateByPath(body, id) {
		const sharedFn = Shared.updateByPath({}, this.schemaData, this.collection);

		if (body instanceof Array === false) {
			body = [body];
		}

		if (this.schemaData.extends && this.schemaData.extends.includes('timestamps')) {
			body.push({
				path: 'updatedAt',
				value: new Date(),
				contextPath: '^updatedAt$',
			});
		}

		return sharedFn(body, id);
	}

	exists(id) {
		// Logging.logSilly(`exists: ${this.collectionName} ${id}`);

		return this.collection.find({_id: new ObjectId(id)})
			.limit(1)
			.count()
			.then((count) => count > 0);
	}

	/*
	* @return {Promise} - returns a promise that is fulfilled when the database request is completed
	*/
	isDuplicate(details) {
		return Promise.resolve(false);
	}

	/**
	 * @param {App} entity - entity object to be deleted
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rm(entity) {
		// Logging.log(`DELETING: ${entity._id}`, Logging.Constants.LogLevel.DEBUG);
		return new Promise((resolve) => {
			this.collection.deleteOne({_id: new ObjectId(entity._id)}, (err, cursor) => {
				if (err) throw err;
				resolve(cursor);
			});
		});
	}

	/**
	 * @param {Array} ids - Array of entity ids to delete
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rmBulk(ids) {
		// Logging.log(`rmBulk: ${this.collectionName} ${ids}`, Logging.Constants.LogLevel.SILLY);
		return this.rmAll({_id: {$in: ids}});
	}

	/*
	 * @param {Object} query - mongoDB query
	 * @return {Promise} - returns a promise that is fulfilled when the database request is completed
	 */
	rmAll(query) {
		if (!query) query = {};
		// Logging.logSilly(`rmAll: ${this.collectionName} ${query}`);

		return new Promise((resolve) => {
			this.collection.deleteMany(query, (err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {String} id - entity id to get
	 * @return {Promise} - resolves to an array of Companies
	 */
	findById(id) {
		// Logging.logSilly(`Schema:findById: ${this.collectionName} ${id}`);

		return new Promise((resolve) => {
			this.collection.findOne({_id: id}, {}, (err, doc) => {
				if (err) throw err;
				resolve(doc);
			});
		});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @param {Object} excludes - mongoDB query excludes
	 * @param {Boolean} stream - should return a stream
	 * @param {Int} limit - should return a stream
	 * @param {Int} skip - should return a stream
	 * @param {Object} sort - mongoDB sort object
	 * @param {Boolean} project - mongoDB project ids
	 * @return {Promise} - resolves to an array of docs
	 */
	find(query, excludes = {}, stream = false, limit = 0, skip = 0, sort, project = null) {
		// Logging.logSilly(`find: ${this.collectionName} ${query}`);
		if (stream) {
			let results = this.collection.find(query, excludes).skip(skip).limit(limit).sort(sort);

			if (project) {
				results = results.project(project);
			}

			return results;
		}

		return new Promise((resolve) => {
			this.collection.find(query, excludes)
				.skip(skip)
				.limit(limit)
				.sort(sort)
				.toArray((err, doc) => {
					if (err) throw err;
					resolve(doc);
				});
		});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @param {Object} excludes - mongoDB query excludes
	 * @return {Promise} - resolves to an array of docs
	 */
	findOne(query, excludes = {}) {
		// Logging.logSilly(`findOne: ${this.collectionName} ${query}`);

		return new Promise((resolve) => {
			this.collection.find(query, excludes).toArray((err, doc) => {
				if (err) throw err;
				resolve(doc[0]);
			});
		});
	}

	/**
	 * @return {Promise} - resolves to an array of Companies
	 */
	findAll() {
		// Logging.logSilly(`findAll: ${this.collectionName}`);

		return this.collection.find({});
	}

	/**
	 * @param {Array} ids - Array of entities ids to get
	 * @return {Promise} - resolves to an array of Companies
	 */
	findAllById(ids) {
		// Logging.logSilly(`update: ${this.collectionName} ${ids}`);

		return this.collection.find({_id: {$in: ids.map((id) => new ObjectId(id))}}, {});
	}

	/**
	 * @param {Object} query - mongoDB query
	 * @return {Promise} - resolves to an array of Companies
	 */
	count(query) {
		return this.collection.countDocuments(query);
	}
}

module.exports = SchemaModel;
<|endoftext|>