import { Meteor } from 'meteor/meteor'; import { Mongo } from 'meteor/mongo'; import { check } from 'meteor/check'; import {SimpleSchema} from 'meteor/aldeed:simple-schema'; /** * Notes: * The Batch object has a date field which stores the date as a number in the format YYYYMMDD. Converting this number into a local date is done with moment(batch.date.toString(), "YYYYMMDD").toDate(), and converting it to a number from a date can be accomplished with ~~(moment(date).format("YYYYMMDD")), where the ~~ is a bitwise not and converts a string to a number quickly and reliably. * A Batch in this system refers to one or more instances of cooking or preparing a product on a given date. It does NOT refer to each instance of cooking the product on that date (what might be called a batch in a kitchen). This might be more effectively called a Run, but that is a confusing word to use in a software system, so I chose to reuse the word Batch since we will not be tracking kitchen batches, just kitchen runs. */ let Batches = new Mongo.Collection('Batches'); let BatchesSchema = new SimpleSchema({ date: { type: Number, // A number in the format of YYYYMMDD to allow for searching using greater and less than, and to prevent timezones from messing everything up. label: "Date", optional: false, index: 1 }, timestamp: { //This is based off the date with zero for the time and set to GMT (Zulu time). type: Date, label: "Timestamp", optional: true }, weekOfYear: { type: Number, label: "Week Of Year", optional: true }, amount: { type: Number, label: "Amount", optional: false, decimal: true }, measureId: { type: String, label: "Measure Id", trim: false, regEx: SimpleSchema.RegEx.Id, index: 1 }, productId: { type: String, label: "Product Id", trim: false, regEx: SimpleSchema.RegEx.Id, index: 1, optional: false }, cookId: { type: String, label: "Cook Worker Id", trim: false, regEx: SimpleSchema.RegEx.Id, index: 1 }, cannerId: { type: String, label: "Canner Worker Id", trim: false, regEx: SimpleSchema.RegEx.Id, index: 1, optional: false }, hasLabels: { type: Boolean, label: "Has Labels", optional: false, defaultValue: false }, comment: { type: String, trim: false, optional: true }, createdAt: { type: Date, label: "Created On", optional: false }, deletedAt: { type: Date, label: "Deleted On", optional: true } }); Batches.attachSchema(BatchesSchema); //Ensure that the product ID, measure ID, and date combination are unique. // Note: I took this out because while it provides for cleaner views, it is overly complicated and could be easily done with a cleanup routine after the fact, or by aggregating the data in the queries. // What makes this complicated is the notes, cook, and canner references which may not be the same. //Batches.createIndex({productId: 1, measureId: 1, date: 1}, {unique: true, name: "ProductMeasureDateIndex"}); if(Meteor.isServer) { Meteor.publish('batches', function(query, sort, limit = 100, skipCount) { let dbQuery = []; if(query) { _.each(_.keys(query), function(key) { //if(_.isObject(query[key])) dbQuery.push({[key]: query[key]}); if(_.isObject(query[key])) { if(query[key].type === 'dateRange') { if(query[key].start && query[key].end) dbQuery.push({[key]: {$gte: query[key].start, $lte: query[key].end}}); else if(query[key].start) dbQuery.push({[key]: {$gte: query[key].start}}); else if(query[key].end) dbQuery.push({[key]: {$lte: query[key].end}}); // Do nothing if a start and/or end are not provided. } else { dbQuery.push({[key]: query[key]}); } } else if(_.isNumber(query[key])) dbQuery.push({[key]: query[key]}); else { let searchValue = query[key]; let searches = searchValue && searchValue.length > 0 ? searchValue.split(/\s+/) : undefined; for(let search of searches) { dbQuery.push({[key]: {$regex: '\\b' + search, $options: 'i'}}); } } }); } if(!_.isNumber(limit)) limit = 100; if(!_.isNumber(skipCount) || skipCount < 0) skipCount = 0; dbQuery = dbQuery.length > 0 ? {$and: dbQuery} : {}; //console.log("dbQuery=" + JSON.stringify(dbQuery)); //console.log("Result Count: " + Batches.find(query).count()); return Batches.find(dbQuery, {limit: limit, sort, skip: skipCount}); }); Meteor.methods({ getBatchCount: function(query) { //TODO: Validate the query? return Batches.find(query).count(); }, insertBatches: function(batches) { //Insert one or more batches (if one, you can pass just the batch). if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { //Force it to be an array if it isn't. if(!Array.isArray(batches)) batches = [batches]; //Validate them all. for(let batch of batches) { check(batch, { date: Number, // TODO: Check that the format is YYYYMMDD amount: Match.Where(function(x) { check(x, Number); return x > 0; }), measureId: String, productId: String, cookId: String, cannerId: String, comment: Match.Optional(String) }); } for(let batch of batches) { let dateString = batch.date.toString(); batch.createdAt = new Date(); batch.timestamp = new Date(dateString.substring(0, 4) + "-" + dateString.substring(4, 6) + "-" + dateString.substring(6, 8) + "T00:00:00Z"); batch.weekOfYear = batch.timestamp.getWeek().toString(); if(batch.hasLabels === undefined) batch.hasLabels = false; } for(let batch of batches) { Batches.insert(batch, function(err, id) { if(err) console.log(err); }, {bypassCollection2: true}); } } else throw new Meteor.Error(403, "Not authorized."); }, deleteBatch: function(id) { //Does not actually delete the batch, but rather just marks it for deleting by applying a deletion date. check(id, String); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { let deletedAt = new Date(); //Batches.remove(id); Batches.update(id, {$set: {deletedAt}}, function(err, id) { if(err) console.log(err); }); } else throw new Meteor.Error(403, "Not authorized."); }, undeleteBatch: function(id) { //Revokes the previous deletion. check(id, String); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { Batches.update(id, {$unset: {deletedAt:""}}, function(err, id) { if(err) console.log(err); }); } else throw new Meteor.Error(403, "Not authorized."); }, //editBatchComment: function(id, comment) { // check(id, String); // check(comment, String); // //Trim and convert empty comment to undefined. // comment = comment ? comment.trim() : undefined; // comment = comment && comment.length > 0 ? comment : undefined; // // if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { // console.log("Changed comment of " + id + " to: " + comment); // // if(comment) { // Batches.update(id, {$set: {comment}}, function(error, count) { // if(error) throw new Meteor.Error(400, "Unexpected database error: " + error); // }); // } // else { // Batches.update(id, {$unset: {comment: ""}}, function(error, count) { // if(error) throw new Meteor.Error(400, "Unexpected database error: " + error); // }); // } // } // else throw new Meteor.Error(403, "Not authorized."); //}, updateBatch: function(id, amount, comment) { check(id, String); check(amount, Number); check(comment, Match.OneOf(String, undefined)); //Trim and convert empty comment to undefined. comment = comment ? comment.trim() : undefined; comment = comment && comment.length > 0 ? comment : undefined; //let dateString = date.toString(); //let timestamp = new Date(dateString.substring(0, 4) + "-" + dateString.substring(4, 6) + "-" + dateString.substring(6, 8) + "T00:00:00Z"); //let weekOfYear = timestamp.getWeek().toString(); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { Batches.update(id, {$set: {comment, amount}}, function(err, id) { if(err) console.log(err); }, {bypassCollection2: true}); } else throw new Meteor.Error(403, "Not authorized."); }, setBatchHasLabels: function(id, hasLabels) { //console.log(id); //console.log(hasLabels); //check(id, Meteor.validators.ObjectID); check(id, String); check(hasLabels, Boolean); if(Roles.userIsInRole(this.userId, [Meteor.UserRoles.ROLE_UPDATE])) { Batches.update(id, {$set: {hasLabels}}, function(err, id) { if(err) console.log(err); }, {bypassCollection2: true}); } else throw new Meteor.Error(403, "Not authorized."); } }); } //Allows the client to do DB interaction without calling server side methods, while still retaining control over whether the user can make changes. Batches.allow({ insert: function() {return false;}, update: function() {return false;}, remove: function() {return false;} }); export default Batches;