diff --git a/.gitignore b/.gitignore index e43b0f9..776bb1a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .DS_Store +/node_modules +/db +npm-debug.log diff --git a/README.md b/README.md index 28b57b9..90392af 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,8 @@ The API you build should have the following capabilities. The schema of your dat - There is not an authentication requirement for this project; assume all users interacting with the API are video store employees. ### Customers -- Retrive a list of all customers -- Retrive a subset of customers +- Retrieve a list of all customers +- Retrieve a subset of customers - Given a sort column, return _n_ customer records, offset by _p_ records (this will be used to create "pages" of customers) - Sort columns are - `name` @@ -57,6 +57,10 @@ The API you build should have the following capabilities. The schema of your dat ### Movies - Retrieve a list of all movies +- Look a movie up by title to see + - it's synopsis + - release date + - and inventory total - Retrieve a subset of movies - Given a sort column, return _n_ movie records, offset by _p_ records (this will be used to create "pages" of movies) - Sort columns are @@ -70,11 +74,7 @@ The API you build should have the following capabilities. The schema of your dat - ordered by check out date ### Rental -- Look a movie up by title to see - - it's synopsis - - release date - - and inventory total -- Know if a movie has any inventory available to rent +- Know if a movie has available inventory to rent (diff than total inventory) - See a list of customers that have _currently_ checked out any of the movie's inventory - Given a customer's `id` and a movie's `title` ... - "check out" one of the movie's inventory to the customer diff --git a/app.js b/app.js new file mode 100644 index 0000000..05f9202 --- /dev/null +++ b/app.js @@ -0,0 +1,64 @@ +var express = require('express'); +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 movies = require('./routes/movies'); +var customers = require('./routes/customers'); +var rentals = require('./routes/rentals'); + +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(path.join(__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('/customers', customers); +app.use('/movies', movies); +app.use('/rentals', rentals); + +// 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..cc43085 --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('C3Projects--VideoStoreAPI: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/controllers/customers.js b/controllers/customers.js new file mode 100644 index 0000000..dd8026f --- /dev/null +++ b/controllers/customers.js @@ -0,0 +1,53 @@ +"use strict"; + +var customer_instance = require('../models/customers'); +var Customer = new customer_instance; + +function sortBy(the_function, sort_by, req, res) { + var number = req["query"]["n"]; + var pages = req["query"]["p"]; + the_function(sort_by, number, pages, function(err, result) { + return res.status(200).json(result); + }); +} + +function showRentals(the_function, req, res) { + var id = req["params"]["id"]; + the_function(id, function(err, result) { + return res.status(200).json(result); + }); +} + +exports.customersController = { + // GET /customers + index: function(req, res) { + Customer.all(function(err, result) { + return res.status(200).json(result); + }); + }, + + // GET /customers/by_name?n=XXX&p=XXX + showByName: function(req, res) { + sortBy(Customer.find_by_sorted, "name", req, res); + }, + + // GET /customers/by_registered_at?n=XXX&p=XXX + showByRegisteredAt: function(req, res) { + sortBy(Customer.find_by_sorted_date, "registered_at", req, res) + }, + + // GET /customers/by_postal_code?n=XXX&p=XXX + showByPostalCode: function(req, res) { + sortBy(Customer.find_by_sorted, "postal_code", req, res); + }, + + // GET /customers/:id/current + showCustomerCurrent: function(req, res) { + showRentals(Customer.find_current, req, res); + }, + + // GET /customers/:id/history + showCustomerHistory: function(req, res) { + showRentals(Customer.find_history, req, res); + } +}; diff --git a/controllers/movies.js b/controllers/movies.js new file mode 100644 index 0000000..4aa072c --- /dev/null +++ b/controllers/movies.js @@ -0,0 +1,134 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(); +var db_env = process.env.DB || 'development'; +var db = new sqlite3.Database('db/' + db_env + '.db'); +var Movie = require('../models/movies'); + +exports.moviesController = { + + // GET /movies + getAllMovies: function(res) { + db.all("SELECT title, overview, release_date, inventory FROM movies", function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // Get /movies/id/:id(synopsis, inventory, release_date) + getMovieById: function(id, res) { + db.all("SELECT title, overview, release_date, inventory FROM movies WHERE id=?", id, function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /movies/title/:title + getMovieByTitle: function(title, res) { + db.all("SELECT title, overview, release_date, inventory FROM movies WHERE title LIKE ?", title, function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /movies/title/:title/:inventory + getMovieByTitleInventory: function(title, res) { + db.all("SELECT title, inventory FROM movies WHERE title LIKE ?", title, function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /movies/release_date?n=XXX&p=XXX + getMoviesByReleaseDate: function(num, page, res) { + var callback = function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }; + + // This is the case when no query parameters are given + if (num === undefined && page === undefined) { + db.all("SELECT title, overview, release_date, inventory FROM movies ORDER BY release_date DESC", callback); + } + // This is the case when only n is given + else if (page === undefined) { + db.all("SELECT title, overview, release_date, inventory FROM movies ORDER BY release_date DESC LIMIT ?", num, callback); + } + else { + // Assume both num and page are specified + db.all("SELECT title, release_date FROM movies WHERE id NOT IN ( SELECT id FROM movies ORDER BY release_date DESC LIMIT ?) ORDER BY release_date DESC LIMIT ?", (page - 1) * num, num, callback); + } + }, + + // GET /movies/title?n=XXX&p=XXX + getMoviesByTitle: function(num, page, res) { + var callback = function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }; + + // This is the case when no query parameters are given + if (num === undefined && page === undefined) { + db.all("SELECT title, overview, release_date, inventory FROM movies ORDER BY title ASC", callback); + } + // This is the case when only n is given + else if (page === undefined) { + db.all("SELECT title, overview, release_date, inventory FROM movies ORDER BY title ASC LIMIT ?", num, callback); + } + else { + // Assume both num and page are specified + db.all("SELECT title, overview, release_date, inventory FROM movies WHERE id NOT IN ( SELECT id FROM movies ORDER BY title ASC LIMIT ?) ORDER BY title ASC LIMIT ?", (page - 1) * num, num, callback); + } + }, + + // GET /movies/title/:title/checked_out_current + getCheckedOutCustomersByTitle: function(title, res) { + db.all("SELECT customers.id, customers.name, customers.phone, rentals.check_out_date FROM customers INNER JOIN rentals ON customers.id = rentals.customer_id INNER JOIN movies ON movies.id = rentals.movie_id WHERE movies.title LIKE ? AND rentals.check_in_date IS NULL", title, function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /movies/title/:title/checked_out_history?ordered_by=XXX + // ordered_by id, name, check out date + getCheckedOutHistoryByTitle: function(title, ordered_by, res) { + + var order = "customers.id"; + if (ordered_by == "id") { + order = "customers.id"; + } + else if (ordered_by == "name") + order = "customers.name"; + else if (ordered_by == "check_out_date" || ordered_by == "checkout_date") + order = "rentals.check_out_date"; + else if (ordered_by !== undefined) + order = ordered_by; + + var statement = + "SELECT customers.id, customers.name, customers.phone, rentals.check_out_date \ + FROM customers INNER JOIN rentals ON customers.id = rentals.customer_id INNER JOIN movies ON movies.id = rentals.movie_id \ + WHERE movies.title LIKE ? AND rentals.check_in_date IS NOT NULL ORDER BY " + order; + +// SELECT customers.id, customers.name, customers.phone, rentals.check_out_date FROM customers INNER JOIN rentals ON customers.id = rentals.customer_id INNER JOIN movies ON movies.id = rentals.movie_id WHERE movies.title LIKE 'psycho' AND rentals.check_in_date IS NOT NULL ORDER BY customers.id; + db.all(statement, title, function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + } +} diff --git a/controllers/rentals.js b/controllers/rentals.js new file mode 100644 index 0000000..dd6acc3 --- /dev/null +++ b/controllers/rentals.js @@ -0,0 +1,123 @@ +"use strict"; +var sqlite3 = require('sqlite3').verbose(); +var db_env = process.env.DB || 'development'; +var db = new sqlite3.Database('db/' + db_env + '.db'); +var Movie = require('../models/movies'); +var Customer = require('../models/customers'); +var Rental = require('../models/rentals'); + +// Convert a JavaScript Date to a string in yyyymmdd format +function yyyymmdd(date) { + var yyyy = date.getFullYear().toString(); + var mm = (date.getMonth()+1).toString(); // getMonth() is zero-based + var dd = date.getDate().toString(); + return yyyy + '-' + (mm[1]?mm:"0"+mm[0]) + '-' + (dd[1]?dd:"0"+dd[0]); +} + +exports.rentalsController = { + // GET /rentals + getAllRentals: function(res) { + db.all("SELECT * FROM rentals ", function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /rentals/overdue + // movies and the customer associated with it + getAllOverdue:function(res) { + db.all("SELECT customers.name, movies.title FROM rentals INNER JOIN customers on customers.id = rentals.customer_id INNER JOIN movies on movies.id = rentals.movie_id WHERE check_in_date IS NULL AND date(expected_return_date) < date('now')", function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /rentals/currently_out + getAllCurrentlyOut:function(res) { + db.all("SELECT customers.name, movies.title FROM rentals INNER JOIN customers on customers.id = rentals.customer_id INNER JOIN movies on movies.id = rentals.movie_id WHERE check_in_date IS NULL", function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /rentals/available_inventory + getAvailableInventory:function(res) { + db.all("SELECT movies.title, movies.inventory-(SELECT COUNT(*) from rentals WHERE rentals.movie_id=movies.id AND check_in_date IS NULL) AS available from movies", function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /rentals/:title/available_inventory + getMovieAvailableInventory:function(title, res) { + db.all("SELECT movies.title, movies.inventory-(SELECT COUNT(*) from rentals WHERE rentals.movie_id=movies.id AND check_in_date IS NULL) AS available from movies WHERE title LIKE ?", title, function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // GET /rentals/current_renters/:title + getAllCurrentRenters:function(title, res) { + db.all("SELECT customers.name from rentals INNER JOIN movies ON movies.id = rentals.movie_id INNER JOIN customers on customers.id = rentals.customer_id WHERE movies.title LIKE ? AND check_in_date IS NULL", title, function(err, rows) { + if (err !== null) { + console.log(err); + } + res.status(200).json(rows); + }); + }, + + // PATCH rentals/check_in?id=XXX&title=XXX + checkIn:function(id, title, res) { + var now = new Date(); + var late_fee = 3; + var statement = "SELECT id FROM rentals WHERE customer_id=? \ + AND movie_id=(SELECT id FROM movies where title LIKE ?) \ + AND check_in_date IS NULL;" + + db.all(statement, id, title, function(err, result1) { + var statement_oldest = "UPDATE rentals SET check_in_date=? WHERE id=" + result1[0].id; + db.all(statement_oldest, yyyymmdd(now), function(err, result2) { + if (err !== null) { + console.log(err); + } else { + var statement_late_fee = "UPDATE customers SET account_credit=(account_credit-" + late_fee + ") WHERE id=? AND date((SELECT expected_return_date FROM rentals WHERE id=?)) < date('now');"; + + db.run(statement_late_fee, id, result1[0].id, function(err, result3) { + if (err !== null) { + console.log(err); + } + else { + res.status(200).json([]); + } + }); + } + }) + }) + }, + + // POST /rentals/check_out(cust id, movie title) (math for checkout cost) + // creating a new rental with no returned date + checkOut:function(req, res) { + var data = req["body"]; + var db = new Customer(); + + db.check_out(data, function(err, result) { + if (err !== null) { + console.log(err); + return res.status(500).json(result); + } else { + return res.status(200).json(result); + } + }); + } +}; diff --git a/database.js b/database.js new file mode 100644 index 0000000..716b47a --- /dev/null +++ b/database.js @@ -0,0 +1,68 @@ +"use strict"; + +var sqlite3 = require("sqlite3").verbose(), + db_env = process.env.DB || 'development'; + +module.exports = { + all: function(callback){ + var db = new sqlite3.Database('db/' + db_env + '.db'); + var statement = "SELECT * FROM " + this.table_name + ";"; + + db.all(statement, function(err, res){ + if (callback) callback(err, res); + db.close(); + }); + }, + + find_by: function(column, value, callback){ + var db = new sqlite3.Database('db/' + db_env + '.db'); + var statement = "SELECT * FROM " + this.table_name + " WHERE " + column + " = ?;"; + + db.all(statement, value, function(err, res){ + if (callback) callback(err, res); + db.close(); + }); + }, + + create: function(data, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var keys = Object.keys(data); + var key_pairs = []; + var values = []; + + for (var i = 0; i < keys.length; i++) { + values.push(data[keys[i]]); + key_pairs.push('?'); + } + + var statement = "INSERT INTO " + this.table_name + " (" + keys.join(',') + ") " + "VALUES(" + key_pairs.join(',') + ");"; + + db.run(statement, values, function(err) { + callback(err, { inserted_id: this.lastID, changed: this.changes }); + db.close(); + }); + }, + + save: function(data, callback) { + if (data.id) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var keys = Object.keys(data); + var key_pairs = []; + var values = []; + + for (var i = 0; i < keys.length; i++) { + values.push(data[keys[i]]); + key_pairs.push(keys[i] + "=? "); + } + + var statement = "UPDATE " + this.table_name + " SET " + key_pairs.join(',') + " WHERE id=" + data.id; + + db.run(statement, values, function(err) { + callback(err, { inserted_id: this.lastID, changed: this.changed }); + db.close(); + }); + } else { + callback({ err: "Missing key", message: "Can't save without an id; use `create`" }); + } + } +} diff --git a/models/customers.js b/models/customers.js new file mode 100644 index 0000000..39605bf --- /dev/null +++ b/models/customers.js @@ -0,0 +1,85 @@ +"use strict"; + +var sqlite3 = require("sqlite3").verbose(), + db_env = process.env.DB || 'development'; + +function Customer() { + this.table_name = "customers"; +} + +Customer.prototype = require('../database'); + +Customer.prototype.find_current = function(id, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var statement = + "SELECT movies.id AS movie_id, movies.title, rentals.check_out_date, rentals.expected_return_date \ + FROM rentals INNER JOIN movies ON movies.id = rentals.movie_id \ + WHERE customer_id=" + id + " AND check_in_date IS NULL \ + ORDER BY date(check_out_date);"; + + db.all(statement, function(err, res) { + if (callback) callback(err, res); + db.close(); + }); +}; + +Customer.prototype.find_history = function(id, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var statement = + "SELECT movies.id AS movie_id, movies.title, rentals.check_out_date, rentals.check_in_date, rentals.expected_return_date \ + FROM rentals INNER JOIN movies ON movies.id = rentals.movie_id \ + WHERE customer_id=" + id + " AND check_in_date IS NOT NULL \ + ORDER BY date(check_out_date);"; + + db.all(statement, function(err, res) { + if (callback) callback(err, res); + db.close(); + }); +}; + +Customer.prototype.find_by_sorted = function(sort_by, number, pages, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var statement; + if (number && pages) { + var offset = (pages - 1) * number; + statement = "SELECT * FROM customers ORDER BY " + sort_by + " ASC LIMIT " + number + " OFFSET " + offset + ";"; + } + else { + statement = "SELECT * FROM customers ORDER BY " + sort_by + " ASC;"; + } + + db.all(statement, function(err, res) { + if (callback) callback(err, res); + db.close(); + }); +}; + +Customer.prototype.find_by_sorted_date = function(sort_by, number, pages, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var statement = "SELECT * FROM customers;"; + + db.all(statement, function(err, res) { + var sorted = res.sort(function(a, b) { + return new Date(a.registered_at) - new Date(b.registered_at); + }); + + var select_sorted; + + if (number && pages) { + select_sorted = []; + var offset = (pages - 1) * number; + var start = parseInt(offset); + var end = parseInt(offset) + parseInt(number); + + for (var i = start; i < end; i++) { + select_sorted.push(sorted[i]); + } + } + + var result = select_sorted || sorted; + if (callback) callback(err, result); + db.close(); + }); +}; + +module.exports = Customer; diff --git a/models/movies.js b/models/movies.js new file mode 100644 index 0000000..d12ec50 --- /dev/null +++ b/models/movies.js @@ -0,0 +1,9 @@ +"use strict"; + +function Movie() { + this.table_name = "movies"; +} + +Movie.prototype = require('../database'); + +module.exports = Movie; diff --git a/models/rentals.js b/models/rentals.js new file mode 100644 index 0000000..2f2383b --- /dev/null +++ b/models/rentals.js @@ -0,0 +1,60 @@ +"use strict"; +var sqlite3 = require("sqlite3").verbose(), + db_env = process.env.DB || 'development'; + +function Rental() { + this.table_name = "rentals"; +} + +function yyyymmdd(date) { + var yyyy = date.getFullYear().toString(); + var mm = (date.getMonth()+1).toString(); // getMonth() is zero-based + var dd = date.getDate().toString(); + return yyyy + '-' + (mm[1]?mm:"0"+mm[0]) + '-' + (dd[1]?dd:"0"+dd[0]); +} + +Rental.prototype = require('../database'); + +// Data should be an object with keys of customer_id and movie_title +Rental.prototype.check_out = function(data, callback){ + var db = new sqlite3.Database('db/'+ db_env + '.db'); + var checkoutLengthDays = 4; + var now = new Date(); + var due = new Date(now); + var rental_cost = 5 + var late_fee = 3 + var id = data.customer_id; + var title = data.movie_title + due.setDate(now.getDate()+checkoutLengthDays); + + var insert_statement = "INSERT INTO rentals (check_out_date, expected_return_date, customer_id, movie_id) VALUES (?,?,?,(SELECT id FROM movies where title LIKE ?))"; + + var charge_statement = "UPDATE customers SET account_credit=(account_credit-" +rental_cost+") WHERE id=?"; + + var check_inventory_statement = "SELECT movies.inventory-(SELECT COUNT(*) from rentals WHERE rentals.movie_id=movies.id AND check_in_date IS NULL) AS available from movies WHERE title LIKE ?"; + + db.all(check_inventory_statement, title, function(err, result){ + if (result[0].available > 0) { + db.run(insert_statement, yyyymmdd(now), yyyymmdd(due), id, title, function(err, rows) { + if (err !== null) { + console.log(err); + } + }); + db.run(charge_statement, id, function(err, res) { + if (err !== null) { + console.log(err); + } + result = { "result": "Successful", "message": "Rental created" }; + if (callback) callback(err, result); + db.close(); + }); + } + else { + var res = { "result": "Unsuccessful", "message": "Not enough inventory to complete rental" }; + if (callback) callback(err, res); + db.close(); + } + }); +}; + +module.exports = Rental; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0bb1217 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "C3Projects--VideoStoreAPI", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "nodemon ./bin/www", + "test": "clear; DB=test mocha --recursive;", + "nyan": "clear; DB=test mocha --recursive -R nyan;", + "reset": "node ./utils/schema", + "seed": "node ./utils/seed", + "setup": "npm run reset; npm run seed; DB=test npm run reset" + }, + "dependencies": { + "body-parser": "~1.13.2", + "cookie-parser": "~1.3.5", + "debug": "~2.2.0", + "express": "~4.13.1", + "jade": "~1.11.0", + "morgan": "~1.6.1", + "serve-favicon": "~2.3.0", + "sqlite3": "^3.1.0", + "supertest": "^1.1.0" + } +} diff --git a/public/stylesheets/style.css b/public/stylesheets/style.css new file mode 100644 index 0000000..9453385 --- /dev/null +++ b/public/stylesheets/style.css @@ -0,0 +1,8 @@ +body { + padding: 50px; + font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; +} + +a { + color: #00B7FF; +} diff --git a/routes/customers.js b/routes/customers.js new file mode 100644 index 0000000..d5c76d2 --- /dev/null +++ b/routes/customers.js @@ -0,0 +1,35 @@ +var express = require('express'); +var router = express.Router(); +var customer_exports = require('../controllers/customers'); + +// all customers +router.get('/', function(req, res, next) { + return customer_exports.customersController.index(req, res); +}); + +// all customers by name (optional query for number (n=#) & page (p=#)) +router.get('/by_name', function(req, res, next) { + return customer_exports.customersController.showByName(req, res); +}); + +// all customers by registered at (optional query for number (n=#) & page (p=#)) +router.get('/by_registered_at', function(req, res, next) { + return customer_exports.customersController.showByRegisteredAt(req, res); +}); + +// all customers by postal code (optional query for number (n=#) & page (p=#)) +router.get('/by_postal_code', function(req, res, next) { + return customer_exports.customersController.showByPostalCode(req, res); +}); + +// a single customer's currently checked out movies +router.get('/:id/current', function(req, res, next) { + return customer_exports.customersController.showCustomerCurrent(req, res); +}); + +// a single customer's previously checked out (& returned) movies +router.get('/:id/history', function(req, res, next) { + return customer_exports.customersController.showCustomerHistory(req, res); +}); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..43f03e6 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,12 @@ +var express = require('express'); +var router = express.Router(); +var movie_exports = require('../controllers/movies') + +router.get('/zomg', function(req, res, next){ + var results = { + zomg: "it works!" + }; + res.status(200).json(results); +}); + +module.exports = router; diff --git a/routes/movies.js b/routes/movies.js new file mode 100644 index 0000000..0841f43 --- /dev/null +++ b/routes/movies.js @@ -0,0 +1,46 @@ +var express = require('express'); +var router = express.Router(); +var movie_exports = require('../controllers/movies'); + +// GET /movies +router.get('/', function(req, res, next) { + movie_exports.moviesController.getAllMovies(res); +}); + +// GET /movies/:id(synopsis, inventory, release_date) +router.get('/id/:id', function(req, res, next) { + movie_exports.moviesController.getMovieById(req.params.id, res); +}); + +// GET /movies/:title/inventory +router.get('/title/:title/inventory', function(req, res, next){ + movie_exports.moviesController.getMovieByTitleInventory(req.params.title, res); +}); + +// GET /movies/title/:title +router.get('/title/:title', function(req, res, next) { + movie_exports.moviesController.getMovieByTitle(req.params.title, res); +}); + +// GET /movies/release_date?n=XXX&p=XXX +router.get('/release_date', function(req, res, next){ + movie_exports.moviesController.getMoviesByReleaseDate(req.query.n, req.query.p, res); +}); + +// GET /movies/title?n=XXX&p=XXX +router.get('/title', function(req, res, next){ + movie_exports.moviesController.getMoviesByTitle(req.query.n, req.query.p, res); +}); + +// GET /movies/title/:title/checked_out_current +router.get('/title/:title/checked_out_current', function(req, res, next){ + movie_exports.moviesController.getCheckedOutCustomersByTitle(req.params.title, res); +}); + +// GET /movies/:title/checked_out_history?ordered_by=XXX +// ordered_by id, name, check out date +router.get('/title/:title/checked_out_history', function(req, res, next){ + movie_exports.moviesController.getCheckedOutHistoryByTitle(req.params.title, req.query.ordered_by, res); +}); + +module.exports = router; diff --git a/routes/rentals.js b/routes/rentals.js new file mode 100644 index 0000000..639f12b --- /dev/null +++ b/routes/rentals.js @@ -0,0 +1,45 @@ +var express = require('express'); +var router = express.Router(); +var rental_exports = require('../controllers/rentals'); + +// GET /rentals +router.get('/', function(req, res, next) { + rental_exports.rentalsController.getAllRentals(res); +}); + +// GET /rentals/overdue +router.get('/overdue', function(req, res, next) { + rental_exports.rentalsController.getAllOverdue(res); +}); + +// GET /rentals/currently_out +router.get('/currently_out', function(req, res, next){ + rental_exports.rentalsController.getAllCurrentlyOut(res); +}); + +// GET rentals/available_inventory +router.get('/available_inventory', function(req, res, next){ + rental_exports.rentalsController.getAvailableInventory(res); +}); + +// GET rentals/:title/available_inventory +router.get('/:title/available_inventory', function(req, res, next){ + rental_exports.rentalsController.getMovieAvailableInventory(req.params.title, res); +}); + +// GET rentals/current_renters/:title +router.get('/current_renters/:title', function(req, res, next) { + rental_exports.rentalsController.getAllCurrentRenters(req.params.title, res); +}); + +// POST rentals/check_in?id=XXX&title=XXX +router.patch('/check_in', function(req, res, next) { + rental_exports.rentalsController.checkIn(req.query.id, req.query.title, res); +}); + +// POST rentals/check_out?id=XXX&title=XXX +router.post('/check_out', function(req, res, next) { + rental_exports.rentalsController.checkOut(req, res); +}); + +module.exports = router; diff --git a/test/controllers/customers.js b/test/controllers/customers.js new file mode 100644 index 0000000..0810388 --- /dev/null +++ b/test/controllers/customers.js @@ -0,0 +1,350 @@ +var request = require('supertest'); +var assert = require('assert'); +var app = require('../../app'); +var sqlite3 = require('sqlite3').verbose(); +var agent = request.agent(app); + +describe("Customers Controller", function() { + beforeEach(function(done) { + db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.parallelize(function() { + db_cleaner.exec( + "BEGIN TRANSACTION; \ + DELETE FROM rentals; \ + INSERT INTO rentals(check_out_date, check_in_date, expected_return_date, customer_id, movie_id) \ + VALUES('2015-01-01', '2015-01-10', '2015-02-01', 1, 1), \ + ('2015-01-01', null, '2015-02-01', 1, 2); \ + COMMIT TRANSACTION;" + ); // rentals + + db_cleaner.exec( + "BEGIN TRANSACTION; \ + DELETE FROM movies; \ + INSERT INTO movies(title, overview, release_date, inventory) \ + VALUES('The Movie', 'See title.', '2000-01-01', 10), \ + ('The Movie: Sequal', 'So amazing.', '2001-01-01', 5); \ + COMMIT TRANSACTION;" + ); // movies + + db_cleaner.exec( + "BEGIN TRANSACTION; \ + DELETE FROM customers; \ + INSERT INTO customers(name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES('B', '2015-01-01', '111 St', 'Seattle', 'WA', '55555', '555-5555', 19.50), \ + ('A', '2015-01-02', '222 St', 'Seattle', 'WA', '11111', '666-6666', 18.50), \ + ('C', '2014-01-01', '333 St', 'Seattle', 'WA', '99999', '444-4444', 21.00); \ + COMMIT TRANSACTION;" + ); // customers + }) + + db_cleaner.close(function() { + done(); + }); + }); + }) + + describe("GET /customers", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200) + .end(function(err, res) { + if (err) return done(err); + done(); + }); + }); + + it("returns an array of all customer objects", function(done) { + customer_request + .expect(200, function(error, result) { + assert.equal(result.body.length, 3); + + var keys = ['id', 'name', 'registered_at', 'address', 'city', 'state', 'postal_code', 'phone', 'account_credit']; + assert.deepEqual(Object.keys(result.body[0]), keys); + done(); + }) + }); + }); + + describe("GET /customers/:id/current", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers/1/current') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it("can find customer with id 1", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + var keys = ['movie_id', 'title', "check_out_date", "expected_return_date"]; + assert.deepEqual(Object.keys(res.body[0]), keys); + + assert.equal(res.body.length, 1); + assert.equal(res.body[0].movie_id, 2); + + done(); + }) + }); + }); + + describe("GET /customers/:id/history", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers/1/history') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it("can find customer with id 1", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + var keys = ['movie_id', 'title', "check_out_date", "check_in_date", "expected_return_date"]; + assert.deepEqual(Object.keys(res.body[0]), keys); + + assert.equal(res.body.length, 1); + assert.equal(res.body[0].movie_id, 1); + + done(); + }) + }); + }); + + describe("GET /customers/by_name", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers/by_name') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it("returns all customers sorted by name", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + assert.equal(res.body.length, 3); + + assert.equal(res.body[0].name, 'A'); + assert.equal(res.body[1].name, 'B'); + assert.equal(res.body[2].name, 'C'); + + done(); + }) + }); + + it("returns a customer with the correct keys", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + var keys = ['id', 'name', 'registered_at', 'address', 'city', 'state', 'postal_code', 'phone', 'account_credit']; + assert.deepEqual(Object.keys(res.body[0]), keys); + + done(); + }) + }); + }); + + describe("GET /customers/by_name?n=1&p=2", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers/by_name?n=1&p=2') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it("returns a customer sorted by name", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + assert.equal(res.body.length, 1); + assert.equal(res.body[0].name, 'B'); + + done(); + }) + }); + }); + + describe("GET /customers/by_registered_at", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers/by_registered_at') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it("returns all customers sorted by registered at", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + assert.equal(res.body.length, 3); + + assert.equal(res.body[0].name, 'C'); + assert.equal(res.body[1].name, 'B'); + assert.equal(res.body[2].name, 'A'); + + done(); + }) + }); + + it("returns a customer with the correct keys", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + var keys = ['id', 'name', 'registered_at', 'address', 'city', 'state', 'postal_code', 'phone', 'account_credit']; + assert.deepEqual(Object.keys(res.body[0]), keys); + + done(); + }) + }); + }); + + describe("GET /customers/by_registered_at?n=1&p=3", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers/by_registered_at?n=1&p=3') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it("returns a customer sorted by registered_at", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + assert.equal(res.body.length, 1); + assert.equal(res.body[0].name, 'A'); + + done(); + }) + }); + }); + + describe("GET /customers/by_postal_code", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers/by_postal_code') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it("returns all customers sorted by postal code", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + assert.equal(res.body.length, 3); + + assert.equal(res.body[0].name, 'A'); + assert.equal(res.body[1].name, 'B'); + assert.equal(res.body[2].name, 'C'); + + done(); + }) + }); + + it("returns a customer with the correct keys", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + var keys = ['id', 'name', 'registered_at', 'address', 'city', 'state', 'postal_code', 'phone', 'account_credit']; + assert.deepEqual(Object.keys(res.body[0]), keys); + + done(); + }) + }); + }); + + describe("GET /customers/by_postal_code?n=1&p=3", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent + .get('/customers/by_postal_code?n=1&p=3') + .set('Accept', 'application/json'); + done(); + }) + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it("returns a customer sorted by postal code", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err, res) { + assert.equal(res.body.length, 1); + assert.equal(res.body[0].name, 'C'); + + done(); + }) + }); + }); +}); diff --git a/test/controllers/movies.js b/test/controllers/movies.js new file mode 100644 index 0000000..f1e8b31 --- /dev/null +++ b/test/controllers/movies.js @@ -0,0 +1,262 @@ +var request = require('supertest'), + assert = require('assert'), + sqlite3 = require('sqlite3').verbose(), + app = require('../../app'), + agent = request.agent(app); + +describe("Endpoints under /movies", function() { + + // Reset test.db with test data before each test + beforeEach(function(done) { + var db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.exec( + "BEGIN; \ + DELETE FROM movies; \ + INSERT INTO movies(id, title, overview, release_date, inventory) \ + VALUES(1, 'Jaws', 'Shark!', 'Yesterday', 10), \ + (2, 'Maws', 'Worm!', 'Yesterday', 11), \ + (3, 'Claws', 'Cat!', 'Yesterday', 12), \ + (4, 'Paws', 'Bear!', 'Yesterday', 13), \ + (5, 'Gauze', 'Ouch!', 'Yesterday', 14); \ + DELETE FROM customers; \ + INSERT INTO customers(id, name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES(1, 'Shelly', '2013-09-16', '123 somewhere st', 'Kirkland', 'WA', '98033', '(123)-456-7890', '40'), \ + (2, 'Michelle', '2012-09-19', '234 blvd e', 'Tacoma', 'WA', '98047', '(123)-456-7810', '34'), \ + (3, 'Adam', 2014-09-22, '5 privet dr', 'Little Winging', 'HP', '12345', '(425)-456-7890', '30'), \ + (4, 'Tyler', '2015-09-22', '1600 Pennsylvania ave', 'Yakima', 'WA', '98908', '(360)-123-3345', '24'), \ + (5, 'Brandi', 2011-09-22, '567 your town', 'Seattle', 'WA', '98102', '(206)-456-7890', '10'); \ + DELETE FROM rentals; \ + INSERT INTO rentals(id, check_out_date, check_in_date, expected_return_date, movie_id, customer_id) \ + VALUES(1, '2015-09-15', '2015-09-16', '2015-09-19', 1, 1), \ + (2, '2015-09-16', '2015-09-19', '2015-09-20', 2, 2), \ + (3, '2015-09-17', null, '2015-09-21', 1, 3), \ + (4, '2015-09-18', '2015-09-22', '2015-09-22', 3, 4), \ + (5, '2015-09-19', null, '2015-09-23', 4, 5); \ + COMMIT;", + function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + describe('GET /movies', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/movies') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"title":"Jaws","overview":"Shark!","release_date":"Yesterday","inventory":10},{"title":"Maws","overview":"Worm!","release_date":"Yesterday","inventory":11},{"title":"Claws","overview":"Cat!","release_date":"Yesterday","inventory":12},{"title":"Paws","overview":"Bear!","release_date":"Yesterday","inventory":13},{"title":"Gauze","overview":"Ouch!","release_date":"Yesterday","inventory":14}]', done); + }); + }); + + describe('GET /movies/id/:id', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/movies/id/1') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"title":"Jaws","overview":"Shark!","release_date":"Yesterday","inventory":10}]', done); + }); + }); + + describe('GET /movies/title/:title/inventory', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/movies/title/Jaws/inventory') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"title":"Jaws","inventory":10}]', done); + }); + }); + + describe('GET /movies/title/:title', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/movies/title/Paws') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with json (different casing of title)', function(done){ + agent + .get('/movies/title/pAwS') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"title":"Paws","overview":"Bear!","release_date":"Yesterday","inventory":13}]', done); + }); + + it('responds with correct data (different casing of title)', function(done){ + agent + .get('/movies/title/pAwS') + .set('Accept', 'application/json') + .expect('[{"title":"Paws","overview":"Bear!","release_date":"Yesterday","inventory":13}]', done); + }); + }); + + describe('GET /movies/release_date?n=XXX&p=XXX', function(){ + + it('handles no n and no p', function(done){ + agent + .get('/movies/release_date') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200) + .expect('[{"title":"Jaws","overview":"Shark!","release_date":"Yesterday","inventory":10},{"title":"Maws","overview":"Worm!","release_date":"Yesterday","inventory":11},{"title":"Claws","overview":"Cat!","release_date":"Yesterday","inventory":12},{"title":"Paws","overview":"Bear!","release_date":"Yesterday","inventory":13},{"title":"Gauze","overview":"Ouch!","release_date":"Yesterday","inventory":14}]', done); + }); + + it('handles n=0', function(done){ + agent + .get('/movies/release_date?n=0') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200) + .expect('[]', done); + }); + + it('handles n=1', function(done){ + agent + .get('/movies/release_date?n=1') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200) + .expect('[{"title":"Jaws","overview":"Shark!","release_date":"Yesterday","inventory":10}]', done); + }); + + it('handles n=2', function(done){ + agent + .get('/movies/release_date?n=2') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200) + .expect('[{"title":"Jaws","overview":"Shark!","release_date":"Yesterday","inventory":10},{"title":"Maws","overview":"Worm!","release_date":"Yesterday","inventory":11}]', done); + }); + + it('handles n=0, p=1', function(done){ + agent + .get('/movies/release_date?n=0&p=1') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200) + .expect('[]', done); + }); + + it('handles n=1, p=3', function(done){ + agent + .get('/movies/release_date?n=1&p=3') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200) + .expect('[{"title":"Claws","release_date":"Yesterday"}]', done); + }); + + it('handles n=2, p=2', function(done){ + agent + .get('/movies/release_date?n=2&p=2') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200) + .expect('[{"title":"Claws","release_date":"Yesterday"},{"title":"Paws","release_date":"Yesterday"}]', done); + }); + + it('handles n=2, p=3', function(done){ + agent + .get('/movies/release_date?n=2&p=3') + .set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200) + .expect('[{"title":"Gauze","release_date":"Yesterday"}]', done); + }); + }); + + describe('GET /movies/title/:title/checked_out_current', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/movies/title/Jaws/checked_out_current') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"id":3,"name":"Adam","phone":"(425)-456-7890","check_out_date":"2015-09-17"}]', function(err, res){ + done(err); + }); + }); + }); + + describe('GET /movies/title/:title/checked_out_history', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/movies/title/Jaws/checked_out_history') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"id":1,"name":"Shelly","phone":"(123)-456-7890","check_out_date":"2015-09-15"}]', done); + }); + }); +}); diff --git a/test/controllers/rentals.js b/test/controllers/rentals.js new file mode 100644 index 0000000..42989dc --- /dev/null +++ b/test/controllers/rentals.js @@ -0,0 +1,253 @@ +var request = require('supertest'), +assert = require('assert'), +app = require('../../app'), +sqlite3 = require('sqlite3').verbose(), +agent = request.agent(app); + +describe("Endpoints under /rentals", function() { + + // Reset test.db with test data before each test + beforeEach(function(done) { + var db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.exec( + "BEGIN; \ + DELETE FROM movies; \ + INSERT INTO movies(id, title, overview, release_date, inventory) \ + VALUES(1, 'Jaws', 'Shark!', 'Yesterday', 10), \ + (2, 'Maws', 'Worm!', 'Yesterday', 11), \ + (3, 'Claws', 'Cat!', 'Yesterday', 12), \ + (4, 'Paws', 'Bear!', 'Yesterday', 13), \ + (5, 'Gauze', 'Ouch!', 'Yesterday', 14); \ + DELETE FROM customers; \ + INSERT INTO customers(id, name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES(1, 'Shelly', '2013-09-16', '123 somewhere st', 'Kirkland', 'WA', '98033', '(123)-456-7890', '40'), \ + (2, 'Michelle', '2012-09-19', '234 blvd e', 'Tacoma', 'WA', '98047', '(123)-456-7810', '34'), \ + (3, 'Adam', 2014-09-22, '5 privet dr', 'Little Winging', 'HP', '12345', '(425)-456-7890', '30'), \ + (4, 'Tyler', '2015-09-22', '1600 Pennsylvania ave', 'Yakima', 'WA', '98908', '(360)-123-3345', '24'), \ + (5, 'Brandi', 2011-09-22, '567 your town', 'Seattle', 'WA', '98102', '(206)-456-7890', '10'); \ + DELETE FROM rentals; \ + INSERT INTO rentals(id, check_out_date, check_in_date, expected_return_date, movie_id, customer_id) \ + VALUES(1, '2015-09-15', '2015-09-16', '2015-09-19', 1, 1), \ + (2, '2015-09-16', '2015-09-19', '2015-09-20', 2, 2), \ + (3, '2015-09-17', null, '2015-09-21', 1, 3), \ + (4, '2015-09-18', '2015-09-22', '2015-09-22', 3, 4), \ + (5, '2015-09-19', null, '2015-09-23', 4, 5); \ + COMMIT;", + function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + describe('GET /rentals', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/rentals') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"id":1,"check_out_date":"2015-09-15","check_in_date":"2015-09-16","expected_return_date":"2015-09-19","customer_id":1,"movie_id":1},{"id":2,"check_out_date":"2015-09-16","check_in_date":"2015-09-19","expected_return_date":"2015-09-20","customer_id":2,"movie_id":2},{"id":3,"check_out_date":"2015-09-17","check_in_date":null,"expected_return_date":"2015-09-21","customer_id":3,"movie_id":1},{"id":4,"check_out_date":"2015-09-18","check_in_date":"2015-09-22","expected_return_date":"2015-09-22","customer_id":4,"movie_id":3},{"id":5,"check_out_date":"2015-09-19","check_in_date":null,"expected_return_date":"2015-09-23","customer_id":5,"movie_id":4}]', done); + }); + }); + + describe('GET /rentals/overdue', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/rentals/overdue') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"name":"Adam","title":"Jaws"},{"name":"Brandi","title":"Paws"}]', done); + }); + }); + + + describe('GET /rentals/currently_out', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/rentals/currently_out') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"name":"Adam","title":"Jaws"},{"name":"Brandi","title":"Paws"}]', done); + }); + }); + + describe('GET /rentals/available_inventory', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/rentals/available_inventory') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"title":"Jaws","available":9},{"title":"Maws","available":11},{"title":"Claws","available":12},{"title":"Paws","available":12},{"title":"Gauze","available":14}]', done); + }); + }); + + describe('GET /rentals/:title/available_inventory', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/rentals/jaws/available_inventory') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"title":"Jaws","available":9}]', done); + }); + }); + + + describe('GET /rentals/current_renters/jaws', function(){ + var request; + beforeEach(function(done) { + request = agent + .get('/rentals/current_renters/jaws') + .set('Accept', 'application/json'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[{"name":"Adam"}]', done); + }); + }); + + describe('POST /rentals/check_out', function(){ + var request; + beforeEach(function(done) { + request = agent + .post('/rentals/check_out') + .send({ 'customer_id': '1', 'movie_title': 'Jaws' }); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('{"result":"Successful","message":"Rental created"}', done); + }); + + it('creates a new rental in the database', function(done) { + var db = new sqlite3.Database('db/test.db'); + + request + .end(function(err, res) { + db.all("SELECT COUNT(*) AS 'num_of_rentals' FROM rentals", function(error, result) { + assert.deepEqual([{'num_of_rentals': 6}], result); + done(); + }); + }); + }); + }); + + + describe('PATCH /rentals/check_in', function(){ + var request; + beforeEach(function(done) { + request = agent + .patch('/rentals/check_in?id=3&title=Jaws'); + done(); + }); + + it('responds with json', function(done){ + request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('responds with correct data', function(done){ + request + .expect('[]', done); + }); + + it('updates the rental in the database', function(done) { + var db = new sqlite3.Database('db/test.db'); + + request + .end(function(err, res) { + db.all("SELECT COUNT(*) AS num_of_checked_out_movies FROM rentals WHERE check_in_date IS NULL", function(error, result) { + assert.deepEqual([{'num_of_checked_out_movies': 1}], result); + done(); + }); + }); + }); + + it('charges customer if returned movie is overdue', function(done) { + var db = new sqlite3.Database('db/test.db'); + + db.all("SELECT account_credit FROM customers WHERE id=3;", function(err1, money_before) { + request + .end(function(err, res) { + db.all("SELECT account_credit FROM customers WHERE id=3;", function(err2, money_after) { + assert.notEqual(money_before[0].account_credit, money_after[0].account_credit); + // HOW TO MAKE THIS LESS THAN OR GREATER THAN CONDITIONAL? + done(); + }); + }); + }); + }); + }); +}); diff --git a/test/models/customers.js b/test/models/customers.js new file mode 100644 index 0000000..a3c3274 --- /dev/null +++ b/test/models/customers.js @@ -0,0 +1,88 @@ +"use strict"; + +var assert = require('assert'), + sqlite3 = require('sqlite3').verbose(), + customer = require('../../models/customers'); + +describe("Customer Model", function() { + var a_customer; + var db_cleaner; + + beforeEach(function(done) { + a_customer = new customer(); + db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.parallelize(function() { + db_cleaner.exec( + "BEGIN TRANSACTION; \ + DELETE FROM rentals; \ + INSERT INTO rentals(check_out_date, check_in_date, expected_return_date, customer_id, movie_id) \ + VALUES('2015-01-01', '2015-01-10', '2015-02-01', 1, 1), \ + ('2015-01-01', null, '2015-02-01', 1, 2); \ + COMMIT TRANSACTION;" + ); // rentals + + db_cleaner.exec( + "BEGIN TRANSACTION; \ + DELETE FROM movies; \ + INSERT INTO movies(title, overview, release_date, inventory) \ + VALUES('The Movie', 'See title.', '2000-01-01', 10), \ + ('The Movie: Sequal', 'So amazing.', '2001-01-01', 5); \ + COMMIT TRANSACTION;" + ); // movies + + db_cleaner.exec( + "BEGIN TRANSACTION; \ + DELETE FROM customers; \ + INSERT INTO customers(name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES('B', '2015-01-01', '111 St', 'Seattle', 'WA', '55555', '555-5555', 19.50), \ + ('A', '2015-01-02', '222 St', 'Seattle', 'WA', '11111', '666-6666', 18.50), \ + ('C', '2014-01-01', '333 St', 'Seattle', 'WA', '99999', '444-4444', 21.00); \ + COMMIT TRANSACTION;" + ); // customers + }) + + db_cleaner.close(function() { + done(); + }); + }); + }) + + it("can be instantiated", function(done) { + assert(a_customer instanceof customer); + done(); + }); + + describe("instance methods: all", function() { + it("can find all customers", function(done) { + a_customer.all(function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 3); + + assert.equal(res[0].name, 'B'); + assert.equal(res[1].name, 'A'); + assert.equal(res[2].name, 'C'); + + done(); + }); + }); + }); + + describe("instance methods: find_current", function() { + it("finds the checked out movies for the customer", function(done) { + a_customer.find_current(1, function(err, res) { + var keys = ['movie_id', 'title', "check_out_date", "expected_return_date"]; + assert.equal(err, undefined); + assert(res instanceof Object); + + assert.deepEqual(Object.keys(res[0]), keys); + + assert.equal(res.length, 1); + assert.equal(res[0].movie_id, 2); + + done(); + }); + }); + }); +}) diff --git a/customers.json b/utils/customers.json similarity index 100% rename from customers.json rename to utils/customers.json diff --git a/movies.json b/utils/movies.json similarity index 100% rename from movies.json rename to utils/movies.json diff --git a/utils/rentals.json b/utils/rentals.json new file mode 100644 index 0000000..0a93e17 --- /dev/null +++ b/utils/rentals.json @@ -0,0 +1,93 @@ +[ + { + "check_out_date": "2015-09-19", + "check_in_date": null, + "expected_return_date": "2015-10-01", + "customer_id": 1, + "movie_id": 20 + }, + { + "check_out_date": "2015-09-19", + "check_in_date": "2015-10-01", + "expected_return_date": "2015-10-01", + "customer_id": 1, + "movie_id": 21 + }, + { + "check_out_date": "1988-07-07", + "check_in_date": "1988-07-17", + "expected_return_date": "1988-08-07", + "customer_id": 1, + "movie_id": 54 + }, + { + "check_out_date": "1989-10-01", + "check_in_date": "1989-10-02", + "expected_return_date": "1989-11-01", + "customer_id": 3, + "movie_id": 33 + }, + { + "check_out_date": "1999-10-01", + "check_in_date": "1999-10-02", + "expected_return_date": "1999-11-01", + "customer_id": 5, + "movie_id": 1 + }, + { + "check_out_date": "1989-10-01", + "check_in_date": "1989-10-02", + "expected_return_date": "1989-11-01", + "customer_id": 1, + "movie_id": 1 + }, + { + "check_out_date": "2000-10-01", + "check_in_date": "2000-10-02", + "expected_return_date": "2000-11-01", + "customer_id": 10, + "movie_id": 1 + }, + { + "check_out_date": "1989-10-01", + "check_in_date": "1989-10-02", + "expected_return_date": "1989-11-01", + "customer_id": 90, + "movie_id": 1 + }, + { + "check_out_date": "1990-01-01", + "check_in_date": null, + "expected_return_date": "1990-02-01", + "customer_id": 5, + "movie_id": 15 + }, + { + "check_out_date": "2015-01-01", + "check_in_date": "2015-01-16", + "expected_return_date": "2015-01-16", + "customer_id": 10, + "movie_id": 2 + }, + { + "check_out_date": "1990-01-01", + "check_in_date": "1990-01-16", + "expected_return_date": "1990-02-01", + "customer_id": 10, + "movie_id": 1 + }, + { + "check_out_date": "1990-01-01", + "check_in_date": null, + "expected_return_date": "1990-02-01", + "customer_id": 5, + "movie_id": 16 + }, + { + "check_out_date": "2015-09-19", + "check_in_date": null, + "expected_return_date": "2015-10-01", + "customer_id": 5, + "movie_id": 17 + } +] diff --git a/utils/schema.js b/utils/schema.js new file mode 100644 index 0000000..67d0ff5 --- /dev/null +++ b/utils/schema.js @@ -0,0 +1,84 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(), + db_env = process.env.DB || 'development', + db = new sqlite3.Database('db/' + db_env + '.db'); + +var movie_fields = [ + ['title', 'text'], + ['overview', 'text'], + ['release_date', 'text'], + ['inventory', 'integer'] +] + +db.serialize(function() { + // drop existing tables + db.run("DROP TABLE IF EXISTS movies;"); + + // create fresh versions of those tables + db.run("CREATE TABLE movies (id INTEGER PRIMARY KEY);"); + + // add columns that I need to those tables + for(var i = 0; i < movie_fields.length; i++) { + var name = movie_fields[i][0], + type = movie_fields[i][1]; + + // ALTER TABLE movies ADD COLUMN title text; + db.run("ALTER TABLE movies ADD COLUMN " + name + " " + type + ";"); + } +}); + +var customer_fields = [ + ['name', 'text'], + ['registered_at', 'text'], + ['address', 'text'], + ['city', 'text'], + ['state', 'text'], + ['postal_code', 'text'], + ['phone', 'text'], + ['account_credit', 'real'] +] + +db.serialize(function() { + // drop existing tables + db.run("DROP TABLE IF EXISTS customers;"); + + // create fresh versions of those tables + db.run("CREATE TABLE customers (id INTEGER PRIMARY KEY);"); + + // add columns that I need to those tables + for(var i = 0; i < customer_fields.length; i++) { + var name = customer_fields[i][0], + type = customer_fields[i][1]; + + // ALTER TABLE movies ADD COLUMN title text; + db.run("ALTER TABLE customers ADD COLUMN " + name + " " + type + ";"); + } +}); + +var rental_fields = [ + ['check_out_date', 'text'], + ['check_in_date', 'text'], + ['expected_return_date', 'text'], + ['customer_id', 'integer'], + ['movie_id', 'integer'] +]; + +db.serialize(function() { + // drop existing tables + db.run("DROP TABLE IF EXISTS rentals;"); + + // create fresh versions of those tables + db.run("CREATE TABLE rentals (id INTEGER PRIMARY KEY);"); + + // add columns that I need to those tables + for(var i = 0; i < rental_fields.length; i++) { + var name = rental_fields[i][0], + type = rental_fields[i][1]; + + // ALTER TABLE movies ADD COLUMN title text; + db.run("ALTER TABLE rentals ADD COLUMN " + name + " " + type + ";"); + } +}); + +db.close(); diff --git a/utils/seed.js b/utils/seed.js new file mode 100644 index 0000000..e839745 --- /dev/null +++ b/utils/seed.js @@ -0,0 +1,82 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(), + db_env = process.env.DB || 'development', + db = new sqlite3.Database('db/' + db_env + '.db'); + +var movies = require('./movies'); +//insert into movies(title, inventory) values("Jaws", 10); +var movie_statement = db.prepare( + "INSERT INTO movies(title, overview, inventory, release_date) \ + VALUES (?, ?, ?, ?);" +); + +db.serialize(function() { + // loop them movies + for(var i = 0; i < movies.length; i++) { + var movie = movies[i]; + + // insert each one into the db + movie_statement.run( + movie.title, + movie.overview, + movie.inventory, + movie.release_date + ); + } + + movie_statement.finalize(); +}); + +var customers = require('./customers'); +var customer_statement = db.prepare( + "INSERT INTO customers(name, registered_at, address, city, state, postal_code, \ + phone, account_credit) VALUES (?, ?, ?, ?, ?, ?, ?, ?);" +); + +db.serialize(function() { + // loop through customers + for(var i = 0; i < customers.length; i++) { + var customer = customers[i]; + + // insert each one into the db + customer_statement.run( + customer.name, + customer.registered_at, + customer.address, + customer.city, + customer.state, + customer.postal_code, + customer.phone, + customer.account_credit + ); + } + + customer_statement.finalize(); +}); + +var rentals = require('./rentals'); +var rental_statement = db.prepare( + "INSERT INTO rentals(check_out_date, check_in_date, expected_return_date, \ + customer_id, movie_id) VALUES (?, ?, ?, ?, ?);" +); + +db.serialize(function() { + // loop through rentals + for(var i = 0; i < rentals.length; i++) { + var rental = rentals[i]; + + // insert each one into the db + rental_statement.run( + rental.check_out_date, + rental.check_in_date, + rental.expected_return_date, + rental.customer_id, + rental.movie_id + ); + } + + rental_statement.finalize(); +}); + +db.close(); 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..3d63b9a --- /dev/null +++ b/views/index.jade @@ -0,0 +1,5 @@ +extends layout + +block content + h1= title + p Welcome to #{title} diff --git a/views/layout.jade b/views/layout.jade new file mode 100644 index 0000000..15af079 --- /dev/null +++ b/views/layout.jade @@ -0,0 +1,7 @@ +doctype html +html + head + title= title + link(rel='stylesheet', href='/stylesheets/style.css') + body + block content