diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..9d8d516 --- /dev/null +++ b/.babelrc @@ -0,0 +1 @@ +{ "presets": ["es2015"] } diff --git a/.eslintrc b/.eslintrc index 76970cd..a9f54d3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,5 +1,5 @@ { - "extends": "airbnb/base", + "extends": "", "globals": {}, "env" : { "node" : true diff --git a/.gitignore b/.gitignore index 38cc84f..b95f1ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +node_modules +.env + ### API Keys ### *.auth.js @@ -179,3 +182,4 @@ node_modules # compiled files dist +>>>>>>> 567fb62210242b715e97f93bf0d9fbfb4ea50dcb diff --git a/app.js b/app.js new file mode 100644 index 0000000..70634c3 --- /dev/null +++ b/app.js @@ -0,0 +1,64 @@ +var express = require('express'); +var babel = require('babel-core'); +require('babel-register'); +require('dotenv').load(); + +var path = require('path'); +var favicon = require('serve-favicon'); +var logger = require('morgan'); +var cookieParser = require('cookie-parser'); +var bodyParser = require('body-parser'); + +var routes = require('./routes/index'); +var sites = require('./routes/sites'); + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +// uncomment after placing your favicon in /public +//app.use(favicon(__dirname + '/public/favicon.ico')); +app.use(logger('dev')); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', routes); +app.use('/site', sites); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handlers + +// development error handler +// will print stacktrace +if (app.get('env') === 'development') { + app.use(function(err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: err + }); + }); +} + +// production error handler +// no stacktraces leaked to user +app.use(function(err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); +}); + + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..ce2abb9 --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('gstv:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/README.md b/instructions.md similarity index 100% rename from README.md rename to instructions.md diff --git a/lib/errors.js b/lib/errors.js new file mode 100644 index 0000000..94bdfd0 --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,26 @@ +/*=== Custom error messages ===*/ + +export const ERROR = { + + // Unable to Create/Update: {itemName} is required. + ITEM_REQUIRED: function(item){ return `Unable to CREATE/UPDATE. ${item} value is Missing and Required`;}, + + // Unable to Create/Update: {itemName} {itemValue} already exists. + DUPLICATE_ENTRY: function(item){ return `Unable to CREATE/UPDATE, ${item} already exits.`;}, + + // Unable to Create/Update: The start time must be before the end time + END_BEFORE_START: function(day,item){ return `${day, item}: The end time must be earlier than the start.`;}, + + // Unable to Create/Update: The start time may not be the same date as the end time + TIMES_EQUAL: function(day, item){ return `${day, item}: The start and end times cannot be the same.`;}, + + // NULL TIMESLOT + NULL_TIME: function(day, item) {return `${day, item}: The times cannot be null.`;}, + + // TIME_OVERLAP + TIMES_OVERLAP: function(day) {return `${day}: Adjust Timeslots, HOURS OVERLAP.`;}, + + // INCORRECT FORMAT: + INCORRECT_FORMAT: function(item){ return `${item} is formatted incorrectly.`;} + +}; diff --git a/lib/logic.js b/lib/logic.js new file mode 100644 index 0000000..4bb4c67 --- /dev/null +++ b/lib/logic.js @@ -0,0 +1,508 @@ +import { Sites } from '../models/index.js'; +import moment from 'moment'; +import moment_timezone from 'moment-timezone'; +import { ERROR } from './errors.js'; + +/*=============== MAIN LOGIC ===============*/ + +// Validates and formats Request Body (Jump table style === fast) +/*=== Input: create/update verb, request body, Returns: result object ===*/ +export function validateBody(verb, data){ + var schedule = data.schedule, + sunday = schedule.sunday, + monday = schedule.monday, + tuesday = schedule.tuesday, + wednesday = schedule.wednesday, + thursday = schedule.thursday, + friday = schedule.friday, + saturday = schedule.saturday, + isfinished = false; + + var response = {isValid: true, msg: 'SUCCESS'}; + + // model for construction + var result = { + schedule: { + sunday: {}, + monday: {}, + tuesday: {}, + wednesday: {}, + thursday: {}, + friday: {}, + saturday: {} + } + }; + + while(response.isValid && !isfinished){ + for(var key in data){ + switch(key){ + case 'name': + VALIDATE.siteName(data.name, result, response); + break; + case 'street': + VALIDATE.street(data.street, result, response); + break; + case 'city': + VALIDATE.city(data.city, result, response); + break; + case 'state': + VALIDATE.state(data.state, result, response); + break; + case 'timezone': + VALIDATE.timezone(data.timezone, result, response); + break; + case 'phone': + VALIDATE.phone(data.phone, result, response); + break; + case 'email': + VALIDATE.email(data.email, result, response); + break; + case 'primaryContactName': + VALIDATE.primaryContact(data.primaryContactName, result, response); + break; + case 'otherContacts': + VALIDATE.otherContacts(data.otherContacts, result, response); + break; + case 'lastUpdated': + VALIDATE.lastUpdated(result); + break; + case 'observedHolidays': + VALIDATE.observedHolidays(data.observedHolidays, result, response); + break; + case 'schedule': + for(var day in schedule){ + switch(day){ + case 'sunday': + switch(verb.toUpperCase()){ + case 'CREATE': + VALIDATE.isOpenAllDay('sunday', data.schedule.sunday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'sunday', + result.schedule.sunday.hours || sunday.hours, + result, + response + ); + break; + case 'UPDATE': + VALIDATE.isOpenAllDay('sunday', data.schedule.sunday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'sunday', + result.schedule.sunday.hours || sunday.hours, + result, + response + ); + break; + } + break; + case 'monday': + switch(verb.toUpperCase()){ + case 'CREATE': + VALIDATE.isOpenAllDay('monday', monday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'monday', + result.schedule.monday.hours || monday.hours, + result, + response + ); + break; + case 'UPDATE': + VALIDATE.isOpenAllDay('monday', monday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'monday', + result.schedule.monday.hours || monday.hours, + result, + response + ); + break; + } + break; + case 'tuesday': + switch(verb.toUpperCase()){ + case 'CREATE': + VALIDATE.isOpenAllDay('tuesday', tuesday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'tuesday', + result.schedule.tuesday.hours || tuesday.hours, + result, + response + ); + break; + case 'UPDATE': + VALIDATE.isOpenAllDay('tuesday', tuesday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'tuesday', + result.schedule.tuesday.hours || tuesday.hours, + result, + response + ); + break; + } + break; + case 'wednesday': + switch(verb.toUpperCase()){ + case 'CREATE': + VALIDATE.isOpenAllDay('wednesday', wednesday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'wednesday', + result.schedule.wednesday.hours || wednesday.hours, + result, + response + ); + break; + case 'UPDATE': + VALIDATE.isOpenAllDay('wednesday', wednesday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'wednesday', + result.schedule.wednesday.hours || wednesday.hours, + result, + response + ); + break; + } + break; + case 'thursday': + switch(verb.toUpperCase()){ + case 'CREATE': + VALIDATE.isOpenAllDay('thursday', thursday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'thursday', + result.schedule.thursday.hours || thursday.hours, + result, + response + ); + break; + case 'UPDATE': + VALIDATE.isOpenAllDay('thursday', thursday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'thursday', + result.schedule.thursday.hours || thursday.hours, + result, + response + ); + break; + } + break; + case 'friday': + switch(verb.toUpperCase()){ + case 'CREATE': + VALIDATE.isOpenAllDay('friday', friday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'friday', + result.schedule.friday.hours || friday.hours, + result, + response + ); + break; + case 'UPDATE': + VALIDATE.isOpenAllDay('friday', friday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'friday', + result.schedule.friday.hours || friday.hours, + result, + response + ); + break; + } + break; + case 'saturday': + switch(verb.toUpperCase()){ + case 'CREATE': + VALIDATE.isOpenAllDay('saturday', saturday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'saturday', + result.schedule.saturday.hours || saturday.hours, + result, + response + ); + break; + case 'UPDATE': + VALIDATE.isOpenAllDay('saturday', saturday.isOpenAllDay, result, response); + VALIDATE.createHours( + 'saturday', + result.schedule.saturday.hours || saturday.hours, + result, + response + ); + break; + } + isfinished = true; + break; + } + } + break; + } + } + } + return {isValid: response.isValid, body: result, msg: response.msg}; +} + + +/*========== Validation functions library, easily extendable ==========*/ + +export var VALIDATE = { + + siteName: function(data, result, response){ + isValueNull(data) ? (response.isValid=false, response.msg=ERROR.ITEM_REQUIRED('NAME')) : + result.name=data.toUpperCase(); + }, + + street: function (data, result, response) { + // possibly re-arrange address-format in the future + isValueNull(data) ? (response.isValid=false, response.msg=ERROR.ITEM_REQUIRED('STREET')) : + result.street=data.toUpperCase(); + }, + + city: function(data, result, response) { + // possibly cross reference again spelling, citities, location, or zip? + isValueNull(data) ? (response.isValid=false, response.msg=ERROR.ITEM_REQUIRED('CITY')) : + result.city=data.toUpperCase(); + }, + + state: function(data,result, response) { + // possibly cross reference array of availble states/timezones + isValueNull(data) ? (response.isValid=false, response.msg=ERROR.ITEM_REQUIRED('STATE')) : + result.state=data.toUpperCase(); + }, + + timezone: function(data, result, response){ + if(isValueNull(data) || !isTzFormatted(data)){ + response.isValid=false; + response.msg = ERROR.ITEM_REQUIRED('TIMEZONE'); + } + else if(isTzFormatted(data)){ + result.timezone=data.toUpperCase(); + } + }, + + phone: function(data, result, response){ + isValueNull(data) ? (response.isValid=false, response.msg=ERROR.ITEM_REQUIRED('PHONE')) : + result.phone=data.toUpperCase(); + }, + + email: function(data, result, response){ + isValueNull(data) ? (response.isValid=false, response.msg=ERROR.ITEM_REQUIRED('EMAIL')) : + result.email=data.toUpperCase(); + }, + + primaryContact: function(data, result, response){ + isValueNull(data) ? (response.isValid=false, response.msg=ERROR.ITEM_REQUIRED('Primary Contact')) : + result.primaryContactName=data.toUpperCase(); + }, + + otherContacts: function(data, result){ + if(Array.isArray(data)){ + isValueNull(data) ? result.otherContacts=[] : result.otherContacts=stringArrayBuilder(data); + } + }, + + lastUpdated: function(result){ + result.lastUpdated = Date.now(); + }, + + observedHolidays: function(data, result) { + if(Array.isArray(data)){ + isValueNull(data) ? result.observedHolidays=[] : result.observedHolidays=stringArrayBuilder(data); + } + }, + + isOpenAllDay: function(day, data, result, response) { + if(typeof data === 'boolean'){ + const defaultTime = [{open: '0000', close: '2400'}]; + switch(day){ + case 'sunday': + result.schedule.sunday.isOpenAllDay = data; + if(data){ result.schedule.sunday.hours=defaultTime; } + break; + case 'monday': + result.schedule.monday.isOpenAllDay = data; + if(data){ result.schedule.monday.hours=defaultTime; } + break; + case 'tuesday': + result.schedule.tuesday.isOpenAllDay = data; + if(data){ result.schedule.tuesday.hours=defaultTime; } + break; + case 'wednesday': + result.schedule.wednesday.isOpenAllDay = data; + if(data){ result.schedule.wednesday.hours=defaultTime; } + break; + case 'thursday': + result.schedule.thursday.isOpenAllDay = data; + if(data){ result.schedule.thursday.hours=defaultTime; } + break; + case 'friday': + result.schedule.friday.isOpenAllDay = data; + if(data){ result.schedule.friday.hours=defaultTime; } + break; + case 'saturday': + result.schedule.saturday.isOpenAllDay = data; + if(data){ result.schedule.saturday.hours=defaultTime; } + break; + } + } else { + response.isValid = false; + response.msg = ERROR.INCORRECT_FORMAT('isOpenAllDay'); + } + }, + createHours: function(day, input, result, response){ + let valid = true, + loopfinished = false; + var holder = []; + if(input != null){ + while(valid && !loopfinished){ + input.forEach(timeSlot => { + if (timeSlot.open > timeSlot.close){ + response.isValid = false; + response.msg = ERROR.END_BEFORE_START(day, timeSlot); + valid = false; + } + if (timeSlot.open == timeSlot.close){ + response.isValid = false; + response.msg = ERROR.TIMES_EQUAL(day, timeSlot); + valid = false; + } + if ((timeSlot.open || timeSlot.close) == null){ + response.isValid = false; + response.msg = ERROR.NULL_TIME(day, timeSlot); + valid = false; + } + if (timeSlot.open < timeSlot.close){ + holder.push({open: timeSlot.open, close: timeSlot.close}); + } + }); + if(holder.length === input.length){ + loopfinished = true; + if(!checkOverlap(holder)){ + switch(day){ + case 'sunday': + result.schedule.sunday.hours=holder; + break; + case 'monday': + result.schedule.monday.hours=holder; + break; + case 'tuesday': + result.schedule.tuesday.hours=holder; + break; + case 'wednesday': + result.schedule.wednesday.hours=holder; + break; + case 'thursday': + result.schedule.thursday.hours=holder; + break; + case 'friday': + result.schedule.friday.hours=holder; + break; + case 'saturday': + result.schedule.saturday.hours=holder; + break; + } + } else { + response.isValid = false; + response.msg = ERROR.TIMES_OVERLAP(day); + } + } + } + } else { + response.isValid = false; + response.msg = ERROR.ITEM_REQUIRED(day); + } + } +}; + + +// Checks for Missing values in Strings or Arrays/ unassigned variables +/*=== Input: String, Returns: bool ===*/ +export function isValueNull(str) { + if(str == null || undefined || (str.length === 0)){ + return true; + } else { + return false; + } +} + +// Cleans array of strings +/*=== Input: Array of Strings, Returns: Array (trimmed and uppercase) ===*/ +export function stringArrayBuilder(input){ + var output = input.map(function(val){ + return val.trim().toUpperCase(); + }); + return output; +} + +// Sorts Times +/*=== Input: Array of timeslots, Returns: sorted array by open time ===*/ +export function sortItems(array){ + array.sort(function (a, b) { + if (a.open > b.open) { + return 1; + } + if (a.open < b.open) { + return -1; + } + return 0; + }); + return array; +} + +// Checks overlap +/*== Input: Array of timeslots, Return: bool based on overlapping times ===*/ +export function checkOverlap(array){ + var sorted = sortItems(array); + var overlap = false; + for(var i = 1; i= cur.open){ + overlap = true; + } + } + return overlap; +} + +// Checks and formats Timezone +/*=== Input: String, Returns: Bool ===*/ +export function isTzFormatted(data){ + const timezones =['AST','EST','CST','MST','PST','AKST','HAST']; + let match = false; + timezones.forEach(tz => { + data.trim().toUpperCase() === tz ? match=true : ''; + }); + return match; +} + + +/*=============== Data Access Methods ===============*/ + +// Retrieve all sites +export function getAllSites(){ + return Sites.find({}); +} + +//Retrive one site +export function getSite(id){ + return Sites.findOne({_id: id}); +} + +// Patch with $set +export function patchSite(id, body){ + return Sites.update({_id: id}, {$set: body}); +} + +// Update the entire object body +export function updateSite(id,body) { + return Sites.update({_id: id}, {body}); +} + +// Delete Site +export function deleteSite(id) { + return Sites.remove({_id: id}); +} + +// Create Site & Schedule Object +export function createSiteAndSchedule(obj){ + return Sites.create(obj); +} + +// Default update method. Pass new valid object into the doc +export function updateDocument(id, obj){ + return Sites.update({_id: id}, obj); +} diff --git a/models/index.js b/models/index.js new file mode 100644 index 0000000..caa8779 --- /dev/null +++ b/models/index.js @@ -0,0 +1,5 @@ +import mongoose from 'mongoose'; + +mongoose.connect("mongodb://" + process.env.MONGOLAB_URI); + +module.exports.Sites = require('./site.js'); diff --git a/models/site.js b/models/site.js new file mode 100644 index 0000000..dc87def --- /dev/null +++ b/models/site.js @@ -0,0 +1,50 @@ +import mongoose from 'mongoose'; +var Schema = mongoose.Schema; + +var siteSchema = new Schema({ + name: String, + street: String, + city: String, + state: String, + timezone: String, + phone: String, + email: String, + primaryContactName: String, + otherContacts: Array, + lastUpdated: {type: Date, default: Date.now}, + observedHolidays: Array, + schedule: { + sunday: { + isOpenAllDay: Boolean, + hours: Array + }, + monday: { + isOpenAllDay: Boolean, + hours: Array + }, + tuesday: { + isOpenAllDay: Boolean, + hours: Array + }, + wednesday: { + isOpenAllDay: Boolean, + hours: Array + }, + thursday: { + isOpenAllDay: Boolean, + hours: Array + }, + friday: { + isOpenAllDay: Boolean, + hours: Array + }, + saturday: { + isOpenAllDay: Boolean, + hours: Array + }, + } +}); + + +var Sites = mongoose.model('Sites', siteSchema); +module.exports = Sites; diff --git a/notes.md b/notes.md new file mode 100644 index 0000000..dc53951 --- /dev/null +++ b/notes.md @@ -0,0 +1,47 @@ +## Notes: + + +#### Timezones and Open 24Hr: +* I deliberately kept the time formatting simple. It takes one moment method on the FE to display properly. Also, based on the validations information, the FE is responsible for a dropdown containing 30 min increments. Once passed to BE, everything is handled. +* Because time is a Number held in a string, it is easily comparable, and can easily be cross referenced for Timezones. If(timezone === 'MST'){timeslot - 2} Moment can also handle it. + +#### The Validate Object Library +* The validate object can take any number of specific validation functions + Here, it is contrived, so many of them need very similar validations. +However, in the future, it can easily be extended to be as robust as needed. + I'm using very similar functions for now, but each can be extended to +parse or format the data properly..email,phone,etc + +#### Update +* Currently I am using the .update() method as the default for several reasons. In order for all or any of the fields to validate, they are processed through the validateBody() function. +It would seem redundant and unnecessary to create another version just for update. +* The other patch endpoint, with $set, is there, but only so that it can be extended if the team deems it worthy. There are no unique fields which could, after validating, be duplicated. +* The only async call that COULD be needed, would be ADDRESS. It is the only unique identifier, as of now, that could determine duplicate documents. However, that's another issue for the team to discuss, client side form standardization would need to be in place. *IMPORTANT*: duplicated code for now in "UPDATE" case of validateBody(). Depending on how the team decides to move forward, async can be added. + + +#### My reasoning behind the `validateBody()` method: + A. It is safe to use JSON Format + + B. I have formed the structure of the cases around my Data Model. + However, with the switch case, it can be + extended (indefinitely) or modified (once) + easily to suit the needs of another data model. I am trying to + maintain flexibility and openness to futures needs. If this Model + were to be used, for example, it would take only a couple of minutes + to add new fields or validations. Open to extension, inherently closed + to modification + + C. This is a dispatcher function which routes the request.body fields + and is thus not reliant on anything but a request body and request verb. + It return a clean object ready for operation + + D. Data types can be changed as need because the actual parsing, cleaning, + restructuring is extracted to smaller external methods. + + E. The return value is an object containing {isValid: body: msg:}... + so the route can determine easily whether to do IO work, render, , redirect, or throw. + + F. Update is tricky, and will be determined by business needs and the team + preference. Each site is its own document, so duplicates, I would think, + would only apply to timeslots and addresses (formatting problems + there). However, if you are sending the whole object back for validation in order to update, timeslots are run through a validation function which disallows duplicates. No need for async because update over-writes. diff --git a/package.json b/package.json index 74d77ee..52218f0 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,35 @@ { "name": "node-coding-exercise", + "devDependencies": { + "babel": "^6.3.26", + "babel-core": "^6.4.5", + "babel-preset-es2015": "^6.3.13", + "babel-register": "^6.4.3", + "chai": "^3.4.1", + "eslint": "^1.10.3", + "eslint-config-airbnb": "^4.0.0", + "mocha": "^2.4.2" + }, + "dependencies": { + "babel": "^6.3.26", + "babel-polyfill": "^6.3.14", + "body-parser": "~1.12.4", + "chai": "^3.4.1", + "chai-http": "^1.0.0", + "cookie-parser": "~1.3.5", + "debug": "~2.2.0", + "express": "~4.12.4", + "jade": "~1.9.2", + "jquery": "^2.2.0", + "morgan": "~1.5.3", + "serve-favicon": "~2.2.1", + "supertest": "^1.1.0" + }, "version": "1.0.0", "description": "GSTV coding exercise for BE candidates", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": " mocha --compilers js:babel-core/register" }, "repository": { "type": "git", diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..6b30291 --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,16 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; + width: 80%; + margin: 0 auto; +} + +a { + color: #00B7FF; +} + + +.siteInfo{ + border: 2px solid black; + height: 5em; +} diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..c8f6ab2 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,10 @@ +// var express = require('express'); +import express from 'express'; + +var router = express.Router(); + +router.get('/', function(req, res, next) { + res.render('index', {title: 'Gas Station TV!'}); +}); + +module.exports = router; diff --git a/routes/sites.js b/routes/sites.js new file mode 100644 index 0000000..491fd96 --- /dev/null +++ b/routes/sites.js @@ -0,0 +1,68 @@ +import moment from 'moment'; +import moment_timezone from 'moment-timezone'; +import express from 'express'; +import * as db from '../lib/logic'; +var router = express.Router(); + + +/*=== SITES INDEX ===*/ +router.get('/', function(req,res,next) { + db.getAllSites().then(function (sites) { + res.json(sites); + }); +}); + + +/*=== CREATE A SITE AND SCHEDULE ===*/ +router.post('/', function (req,res,next) { + var result = db.validateBody('CREATE', req.body); + if(result.isValid){ + db.createSiteAndSchedule(result.body).then(function (site) { + res.json({MSG: 'WRITE SUCCESSFUL', BODY: site}); + }); + } + if(!result.isValid){ + res.json({ERROR: result.msg, BODY: result.body}); + } +}); + + +/*=== Get One Site ===*/ +router.get('/:id', function(req,res,next){ + db.getSite(req.params.id).then(function (site) { + res.json(site); + }); +}); + + +/*=== PATCH and update partial data ===*/ +router.patch('/:id', function(req,res,next){ + db.patchSite(req.params.id, req.body).then(function (site) { + res.json(site); + }); +}); + + +/*=== Update Document -- Default ===*/ +router.put('/:id/', function(req,res,next){ + var result = db.validateBody('UPDATE', req.body); + if(result.isValid){ + db.updateDocument(req.params.id, result.body).then(function (site) { + res.json({MSG: 'UPDATE SUCCESSFUL', BODY: site}); + }); + } + if(!result.isValid){ + res.json({ERROR: result.msg, BODY: result.body}); + } +}); + + +/*=== Delete Site ===*/ +router.delete('/:id', function(req,res,next){ + db.deleteSite(req.params.id).then(function () { + res.json({OK: "SITE DELETED"}); + }); +}); + + +module.exports = router; diff --git a/test/seed.js b/test/seed.js new file mode 100644 index 0000000..a2ece93 --- /dev/null +++ b/test/seed.js @@ -0,0 +1,378 @@ +var demoGet = { + "_id": "56a7fe30c8e9e00c20ff7d6f", + "name": "LOAF N JUG", + "street": "CHURCH ROAD", + "city": "SLC", + "state": "UT", + "timezone": "MST", + "phone": "9034647682", + "email": "MILKE@GTL.COM", + "primaryContactName": "TOPHER THE LOAFER", + "__v": 0, + "schedule": { + "saturday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "friday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "thursday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "wednesday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "tuesday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "monday": { + "isOpenAllDay": false, + "hours": [ + { + "open": "0000", + "close": "0700" + }, + { + "open": "0800", + "close": "1800" + } + ] + }, + "sunday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + } + }, + "observedHolidays": [ + "CHRISTMAS", + "NEW YEARS EVE", + "KWANZAMAKKAH", + "APRIL FOOLS" + ], + "lastUpdated": "2016-01-26T23:16:00.523Z", + "otherContacts": [ + "SCOTT", + "HARRY", + "AARON", + "KEITH", + "CHRIS" + ] +}; + +export var demoWrite = { + "name": "DEMO WRITE", + "street": "Rt 22", + "city": "Plainfield", + "state": "NJ", + "timezone": "est", + "phone": "9084547682", + "email": "guidos@gtl.com", + "primaryContactName": "Tony Soprano", + "otherContacts": ["Scott", "Harry", "Aaron", "Keith"], + "lastUpdated": "", + "observedHolidays": ["Christmas", "New Years Eve", "Kwanzamakkah"], + "schedule": { + "sunday": { + "isOpenAllDay": false, + "hours": [{"open": "0000","close":"0700"}, {"open": "0800","close":"1800"}] + }, + "monday": { + "isOpenAllDay": false, + "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] + }, + "tuesday": { + "isOpenAllDay": true, + "hours": [] + }, + "wednesday": { + "isOpenAllDay": true, + "hours": [{"open": "0800", "close": "1700"}] + }, + "thursday": { + "isOpenAllDay": true, + "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] + }, + "friday": { + "isOpenAllDay": true, + "hours": [{"open": "0000","close":"0200"}, {"open": "0730","close":"1430"}] + }, + "saturday": { + "isOpenAllDay": true, + "hours": [] + } + } +}; + + + +// var demoWrite = { +// "name": "DEMO WRITE", +// "street": "Rt 22", +// "city": "Plainfield", +// "state": "NJ", +// "timezone": "est", +// "phone": "9084547682", +// "email": "guidos@gtl.com", +// "primaryContactName": "Tony Soprano", +// "otherContacts": ["Scott", "Harry", "Aaron", "Keith"], +// "lastUpdated": "", +// "observedHolidays": ["Christmas", "New Years Eve", "Kwanzamakkah"], +// "schedule": { +// "sunday": { +// "isOpenAllDay": false, +// "hours": [{"open": "0000","close":"0900"}, {"open": "1000","close":"1800"}] +// }, +// "monday": { +// "isOpenAllDay": false, +// "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] +// }, +// "tuesday": { +// "isOpenAllDay": true, +// "hours": [] +// }, +// "wednesday": { +// "isOpenAllDay": true, +// "hours": [{"open": "0800", "close": "1700"}] +// }, +// "thursday": { +// "isOpenAllDay": true, +// "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] +// }, +// "friday": { +// "isOpenAllDay": true, +// "hours": [{"open": "0000","close":"0200"}, {"open": "0730","close":"1430"}] +// }, +// "saturday": { +// "isOpenAllDay": true, +// "hours": [] +// } +// } +// }; + +var demoInvalid = { + "name": "PLAIN OLD INVALID", + "street": "519", + "city": "Plainfield", + "state": "NJ", + "timezone": "est", + "phone": "9084547682", + "email": "guidos@gtl.com", + "primaryContactName": "Tony Soprano", + "otherContacts": ["Scott", "Harry", "Aaron", "Keith"], + "lastUpdated": "", + "observedHolidays": ["Christmas", "New Years Eve", "Kwanzamakkah"], + "schedule": { + "sunday": { + "isOpenAllDay": false, + "hours": [{"open": "0000","close":"1000"}, {"open": "0800","close":"1800"}] + }, + "monday": { + "isOpenAllDay": false, + "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] + }, + "tuesday": { + "isOpenAllDay": true, + "hours": [] + }, + "wednesday": { + "isOpenAllDay": true, + "hours": [{"open": "0800", "close": "1700"}] + }, + "thursday": { + "isOpenAllDay": true, + "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] + }, + "friday": { + "isOpenAllDay": true, + "hours": [{"open": "0000","close":"0200"}, {"open": "0730","close":"1430"}] + }, + "saturday": { + "isOpenAllDay": true, + "hours": [] + } + } +}; + +//Change Data +var demoPut = { + "_id": "56a83229514185094aa72c77", + "name": "LOAF N JUG", + "street": "CHURCH ROAD", + "city": "Salt Lake City", + "state": "UT", + "timezone": "MST", + "phone": "9034647682", + "email": "MILKE@GTL.COM", + "primaryContactName": "TOPHER THE LOAFER", + "__v": 0, + "schedule": { + "saturday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "friday": { + "isOpenAllDay": false, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "thursday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "wednesday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "tuesday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + }, + "monday": { + "isOpenAllDay": false, + "hours": [ + { + "open": "0000", + "close": "0700" + }, + { + "open": "0800", + "close": "1800" + } + ] + }, + "sunday": { + "isOpenAllDay": true, + "hours": [ + { + "open": "0000", + "close": "2400" + } + ] + } + }, + "observedHolidays": [ + "CHRISTMAS", + "NEW YEARS EVE", + "KWANZAMAKKAH", + "APRIL FOOLS" + ], + "lastUpdated": "2016-01-27T02:57:45.034Z", + "otherContacts": [ + "SCOTT", + "HARRY", + "AARON", + "KEITH", + "CHRIS" + ] +}; + +var demoTest = { + name: 'Nick Mattei', + street: '1452 24th', + city: 'Denver', + state: 'Co', + timezone: 'MST', + phone: '909827272', + email: 'nmattei@gmailcom', + primaryContactName: 'capn crunch', + otherContacts: ['guy fieri', 'donny j trump'], + lastUpdated: '1453756219855', + observedHolidays: [], + schedule: { + sunday: { + isOpenAllDay: true, + hours: Array + }, + monday: { + isOpenAllDay: true, + hours: Array + }, + tuesday: { + isOpenAllDay: true, + hours: Array + }, + wednesday: { + isOpenAllDay: true, + hours: Array + }, + thursday: { + isOpenAllDay: true, + hours: Array + }, + friday: { + isOpenAllDay: true, + hours: Array + }, + saturday: { + isOpenAllDay: true, + hours: Array + }, + } +}; + + + +module.exports = demoPut; +module.exports = demoTest; +module.exports = demoInvalid; +module.exports = demoWrite; +module.exports = demoGet; diff --git a/test/test_sites_logic.js b/test/test_sites_logic.js new file mode 100644 index 0000000..3142602 --- /dev/null +++ b/test/test_sites_logic.js @@ -0,0 +1,155 @@ +var chai = require('chai'); +var expect = require('chai').expect; +var should = chai.should(); +import * as logic from '../lib/logic.js'; + + +describe('logic', function() { + describe('isValueNull', function() { + it('should return true if value is null, else false', function() { + expect(logic.isValueNull('')).to.equal(true); + expect(logic.isValueNull([])).to.equal(true); + expect(logic.isValueNull('hello')).to.equal(false); + expect(logic.isValueNull([1,2,3])).to.equal(false); + }); + }); + describe('stringArrayBuilder', function() { + it('should clean/format an array of strings', function() { + let foo = logic.stringArrayBuilder([" nick ", " mattEi"]); + let bar = ["NICK","MATTEI"]; + JSON.stringify(foo).should.equal(JSON.stringify(bar)); + }); + }); + describe('isTzFormatted', function() { + it('should return true if the timezone is correct', function() { + let bar = logic.isTzFormatted('ast'), + foo = logic.isTzFormatted('zsc'); + foo.should.equal(false); + bar.should.equal(true); + }); + }); + describe('checkOverlap', function() { + it('check for overlapping times', function() { + let first = {open: '0000', close: '0800'}; + let second = {open: '0500', close: '1700'}; + let third = {open: '0900', close: '2400'}; + logic.checkOverlap([first, second]).should.equal(true); + logic.checkOverlap([first, third]).should.equal(false); + }); + }); + describe('sortItems', function() { + it('sort timeslots from earliest to latest', function() { + var one = {open: '0000', close: '0800'}; + var two = {open: '0900', close: '1200'}; + var three = {open: '1300', close: '1800'}; + var output = JSON.stringify([one, two, three]); + var input = JSON.stringify(logic.sortItems([three, two, one])); + input.should.equal(output); + }); + }); + describe('validateBody', function() { + it('should validate request body, and return the object', function() { + var goodInput = logic.validateBody('CREATE', demoWrite); + var badInput = logic.validateBody('CREATE', demoInvalid); + expect(goodInput).to.be.a('object'); + expect(goodInput.msg).to.equal('SUCCESS'); + expect(goodInput.isValid).to.equal(true); + expect(badInput).to.be.a('object'); + expect(badInput.isValid).to.equal(false); + }); + }); +}); + + + + + + +// Sample Data: +var demoWrite = { + "name": "DEMO WRITE", + "street": "Rt 22", + "city": "Plainfield", + "state": "NJ", + "timezone": "est", + "phone": "9084547682", + "email": "guidos@gtl.com", + "primaryContactName": "Tony Soprano", + "otherContacts": ["Scott", "Harry", "Aaron", "Keith"], + "lastUpdated": "", + "observedHolidays": ["Christmas", "New Years Eve", "Kwanzamakkah"], + "schedule": { + "sunday": { + "isOpenAllDay": false, + "hours": [{"open": "0000","close":"0700"}, {"open": "0800","close":"1800"}] + }, + "monday": { + "isOpenAllDay": false, + "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] + }, + "tuesday": { + "isOpenAllDay": true, + "hours": [] + }, + "wednesday": { + "isOpenAllDay": true, + "hours": [{"open": "0800", "close": "1700"}] + }, + "thursday": { + "isOpenAllDay": true, + "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] + }, + "friday": { + "isOpenAllDay": true, + "hours": [{"open": "0000","close":"0200"}, {"open": "0730","close":"1430"}] + }, + "saturday": { + "isOpenAllDay": true, + "hours": [] + } + } +}; + +var demoInvalid = { + "name": "PLAIN OLD INVALID", + "street": "519", + "city": "Plainfield", + "state": "NJ", + "timezone": "est", + "phone": "9084547682", + "email": "guidos@gtl.com", + "primaryContactName": "Tony Soprano", + "otherContacts": ["Scott", "Harry", "Aaron", "Keith"], + "lastUpdated": "", + "observedHolidays": ["Christmas", "New Years Eve", "Kwanzamakkah"], + "schedule": { + "sunday": { + "isOpenAllDay": false, + "hours": [{"open": "0000","close":"1000"}, {"open": "0800","close":"1800"}] + }, + "monday": { + "isOpenAllDay": false, + "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] + }, + "tuesday": { + "isOpenAllDay": true, + "hours": [] + }, + "wednesday": { + "isOpenAllDay": true, + "hours": [{"open": "0800", "close": "1700"}] + }, + "thursday": { + "isOpenAllDay": true, + "hours": [{"open": "0000","close":"0200"}, {"open": "0800","close":"1800"}] + }, + "friday": { + "isOpenAllDay": true, + "hours": [{"open": "0000","close":"0200"}, {"open": "0730","close":"1430"}] + }, + "saturday": { + "isOpenAllDay": true, + "hours": [] + } + } +}; diff --git a/test/test_sites_routes.js b/test/test_sites_routes.js new file mode 100644 index 0000000..05b79d5 --- /dev/null +++ b/test/test_sites_routes.js @@ -0,0 +1,89 @@ +var chai = require('chai'); +var chaiHttp = require('chai-http'); +var expect = require('chai').expect; +var should = chai.should(); +var demoGet = require('./seed.js'); +var demoWrite = require('./seed.js'); +var demoInvalid = require('./seed.js'); +var demoPut = require('./seed.js'); +var server = require('../app'); +chai.use(chaiHttp); + +// var request = require('supertest'); +// var mongoose = require('mongoose'); +// var mockgoose = require('mockgoose'); + + +describe('Site Routes', function() { + xit('should GET all sites: /site', function (done) { + chai.request(server) + .get('/site/') + .end(function (err, res) { + res.should.have.status(200); + res.should.be.a('object'); + }); + done(); + }); + xit('should GET a site: /site/:id', function (done) { + chai.request(server) + .get('/site/56a7fe30c8e9e00c20ff7d6f') + .end(function (err, res) { + res.should.have.status(200); + res.should.be.a('object'); + expect(res.body._id).to.equal(demoGet._id); + expect(res.body.city).to.equal(demoGet.city); + }); + done(); + }); + xit('should POST a new site to: /site', function (done) { + chai.request(server) + .post('/site') + .send(demoWrite) + .end(function (err, res) { + res.should.have.status(200); + expect(res.body.MSG).to.equal('WRITE SUCCESSFUL'); + }); + done(); + }); + // Fix This + // it('should POST an invalid site to /site', function (done) { + // chai.request(server) + // .post('/site') + // .send(demoInvalid) + // .end(function (err, res) { + // res.should.have.status(200); + // console.log(res.body); + // expect(res.body.ERROR).to.equal("sunday: Adjust Timeslots, HOURS OVERLAP."); + // done(); + // }); + // }); + xit('should PATCH a site: /site/:id', function (done) { + chai.request(server) + .patch('/site/56a83229514185094aa72c77') + .send({name: "NOT THE LOAF N JUG"}) + .end(function (err, res) { + res.should.have.status(200); + }); + done(); + }); + xit('should PUT update to /site/:id', function (done) { + chai.request(server) + .put('/site/56a83229514185094aa72c77') + .send(demoPut) + .end(function (err, res) { + res.should.have.status(200); + expect(res.body.MSG).to.equal('UPDATE SUCCESSFUL'); + }); + done(); + }); + xit('should DELETE a site /site/:id/', function (done) { + //for now change ids + chai.request(server) + .delete('/site/56a827cd18c6d7e03beea520') + .end(function (err, res) { + res.should.have.status(200); + expect(res.body.OK).to.equal("SITE DELETED"); + }); + done(); + }); +}); diff --git a/views/error.jade b/views/error.jade new file mode 100644 index 0000000..51ec12c --- /dev/null +++ b/views/error.jade @@ -0,0 +1,6 @@ +extends layout + +block content + h1= message + h2= error.status + pre #{error.stack} diff --git a/views/index.jade b/views/index.jade new file mode 100644 index 0000000..84a76ec --- /dev/null +++ b/views/index.jade @@ -0,0 +1,14 @@ +extends layout + +block content + h1= title + br + h4 GSTV Site Management Suite + br + a(href='/site') Sites + br + + + + + diff --git a/views/layout.jade b/views/layout.jade new file mode 100644 index 0000000..826d3f6 --- /dev/null +++ b/views/layout.jade @@ -0,0 +1,10 @@ +doctype html +html + head + title= title + script(src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-beta1/jquery.min.js') + script(src='../lib/test.js') + + link(rel='stylesheet', href='/stylesheets/style.css') + body + block content diff --git a/views/sites.jade b/views/sites.jade new file mode 100644 index 0000000..5e3189a --- /dev/null +++ b/views/sites.jade @@ -0,0 +1,20 @@ +extends layout + +block content + h1= GSTV + br + br + a(href='/site/new') Add New Site + br + h2 Current List of Sites: + if sites + each site in sites + a(href='/site/'+site._id) + h4 #{site.name} + p #{site.address} + + + + + +