diff --git a/.gitignore b/.gitignore index e43b0f9..7595163 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ .DS_Store +node_modules +npm-debug.log diff --git a/app.js b/app.js new file mode 100644 index 0000000..188a288 --- /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('/movies', movies); +app.use('/customers', customers); +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..a8c2d36 --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('app: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..4093559 --- /dev/null +++ b/controllers/customers.js @@ -0,0 +1,39 @@ +"use strict"; +var Customer = require('../models/customer'); //class needs to be instantiated + +exports.customersController = { + index: function(req, res) { + var db = new Customer(); + db.find_all(function(err, result) { + return res.status(200).json(result); + }); + }, + + by_column: function(req, res) { + var db = new Customer(); + db.by_column(req.params.column, req.params.n, req.params.p, function(err, result) { + return res.status(200).json(result); + }); + }, + + movies_by_customer_current: function(req, res) { + var db = new Customer(); + db.movies_by_customer_current(req.params.customer_id, function(err, result) { + return res.status(200).json(result); + }); + }, + + movies_by_customer_history: function(req, res) { + var db = new Customer(); + db.movies_by_customer_history(req.params.customer_id, function(err, result) { + return res.status(200).json(result); + }); + }, + + customers_overdue: function(req, res) { + var db = new Customer(); + db.customers_overdue(function(err, result) { + return res.status(200).json(result); + }); + } +}; diff --git a/controllers/index.js b/controllers/index.js new file mode 100644 index 0000000..3a9cbfe --- /dev/null +++ b/controllers/index.js @@ -0,0 +1,12 @@ +"use strict"; + +exports.indexController = { + zomg: function zomg(req,res) { + var results = { + zomg: "zomg it WORKED!", + moar: "zomg moar" + }; + + return res.status(200).json(results); + }, +} diff --git a/controllers/movies.js b/controllers/movies.js new file mode 100644 index 0000000..7d6d741 --- /dev/null +++ b/controllers/movies.js @@ -0,0 +1,47 @@ +"use strict"; +var Movie = require('../models/movie'); //class needs to be instantiated + +exports.moviesController = { + index: function(req, res) { + var db = new Movie(); + db.find_all(function(err, result) { + + return res.status(200).json(result); + }); + }, + + by_column: function(req, res) { + var db = new Movie(); + db.by_column(req.params.column, req.params.n, req.params.p, function(err, result) { + return res.status(200).json(result); + }); + }, + + customers_by_movie_current: function(req, res) { + var db = new Movie(); + db.customers_by_movie_current(req.params.title, function(err, result) { + return res.status(200).json(result); + }); + }, + + movie_info: function(req, res) { + var db = new Movie(); + db.movie_info(req.params.title, function(err, result) { + return res.status(200).json(result); + }); + }, + + customers_by_movie_history: function(req, res) { + var db = new Movie(); + db.customers_by_movie_history(req.params.title, function(err, result) { + return res.status(200).json(result); + }); + }, + + customers_by_movie_history_sorted: function(req, res) { + var db = new Movie(); + db.customers_by_movie_history_sorted(req.params.title, req.params.table, req.params.column, function(err, result) { + return res.status(200).json(result); + }); + } +}; diff --git a/controllers/rentals.js b/controllers/rentals.js new file mode 100644 index 0000000..e16dde6 --- /dev/null +++ b/controllers/rentals.js @@ -0,0 +1,27 @@ +"use strict"; +var Rental = require('../models/rental'); //class needs to be instantiated + +var rentalsController = { + create: function(request, response, next) { + var customer_id = request.body.id, + movie_title = request.body.title, + db = new Rental(); + + db.check_out(customer_id, movie_title, function(err, result) { + return response.status(200).json(result); + }); + }, + + update: function(request, response, next) { + var customer_id = request.body.id, + movie_title = request.body.title, + db = new Rental(); + + db.check_in(customer_id, movie_title, function(err, result) { + return response.status(200).json(result); + }); + }, + +}; + +module.exports = rentalsController; diff --git a/database.js b/database.js new file mode 100644 index 0000000..dd24848 --- /dev/null +++ b/database.js @@ -0,0 +1,28 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(); +var db_env = process.env.DB || 'development'; + +var Database = { + find_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(); + }); + }, + + by_column: function(column, number, page, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var statement = "SELECT * FROM " + this.table_name + " ORDER BY " + column + " LIMIT " + number + " OFFSET " + page + ";"; + + db.all(statement, function(err, res) { + if (callback) callback(err, res); + db.close(); + }); + }, +}; + +module.exports = Database; diff --git a/db/development.db b/db/development.db new file mode 100644 index 0000000..fc5746d Binary files /dev/null and b/db/development.db differ diff --git a/db/test.db b/db/test.db new file mode 100644 index 0000000..203303b Binary files /dev/null and b/db/test.db differ diff --git a/helpers/date_helper.js b/helpers/date_helper.js new file mode 100644 index 0000000..2a74f51 --- /dev/null +++ b/helpers/date_helper.js @@ -0,0 +1,15 @@ +"use strict"; + +function format_date(days_to_add) { + var today = new Date(Date.now() + (days_to_add * 24 * 60 * 60 * 1000)), + today_dd = today.getDate(), + today_mm = today.getMonth() + 1, + today_yyyy = today.getFullYear(), + target_date; + if(today_dd < 10) { today_dd ='0'+ today_dd; } + if(today_mm < 10) { today_mm ='0'+ today_mm; } + target_date = today_yyyy + "-" + today_mm + "-" + today_dd; + return target_date; +} + +module.exports = format_date; diff --git a/models/customer.js b/models/customer.js new file mode 100644 index 0000000..dec7a7b --- /dev/null +++ b/models/customer.js @@ -0,0 +1,46 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(); +var db_env = process.env.DB || 'development'; + +function Customer() { + this.table_name = "customers"; +} + +Customer.prototype = require('../database'); + + // Called by Customers controller: +Customer.prototype.movies_by_customer_current = function(customer_id, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + // all movies currently checked out by that customer + var statement = "SELECT 'movies'.* FROM movies INNER JOIN rentals ON movies.id = rentals.movie_id WHERE rentals.customer_id = (SELECT customers.id FROM customers WHERE customers.id = ? COLLATE NOCASE LIMIT 1) AND rentals.checked_out = 'true'; "; + + db.all(statement, customer_id, function(err, rows) { + callback(err, rows); + db.close(); + }); +}; + +Customer.prototype.movies_by_customer_history = function(customer_id, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + // all movies checked out by that customer in the past + var statement = "SELECT 'movies'.*, rentals.checkout_date, rentals.return_date FROM movies INNER JOIN rentals ON movies.id = rentals.movie_id WHERE rentals.customer_id = (SELECT customers.id FROM customers WHERE customers.id = ? COLLATE NOCASE LIMIT 1) AND rentals.checked_out = 'false' ORDER BY rentals.checkout_date; "; + + db.all(statement, customer_id, function(err, rows) { + callback(err, rows); + db.close(); + }); +}; + +Customer.prototype.customers_overdue = function(callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + // all customers who currently have the movie checked out and past return_date + var statement = "SELECT * FROM customers INNER JOIN rentals ON customers.id = rentals.customer_id WHERE date(rentals.return_date) < date('now') AND rentals.checked_out = 'true' ORDER BY rentals.return_date; "; + + db.all(statement, function(err, rows) { + callback(err, rows); + db.close(); + }); +}; + +module.exports = Customer; diff --git a/models/movie.js b/models/movie.js new file mode 100644 index 0000000..2906540 --- /dev/null +++ b/models/movie.js @@ -0,0 +1,60 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(); +var db_env = process.env.DB || 'development'; + +function Movie() { + this.table_name = "movies"; +} + +Movie.prototype = require('../database'); + +// Movie.prototype - extend to validation functions (e.g., is_a_movie) + +Movie.prototype.movie_info = function(title, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + // information about specified movie + var statement = "SELECT 'movies'.* FROM movies where movies.title = ? COLLATE NOCASE LIMIT 1; "; + + db.all(statement, title, function(err, rows) { + callback(err, rows); + db.close(); + }); +}; + +Movie.prototype.customers_by_movie_current = function(title, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + // all customers who currently have the movie checked out + var statement = "SELECT 'customers'.* FROM customers INNER JOIN rentals ON customers.id = rentals.customer_id WHERE rentals.movie_id = (SELECT movies.id FROM movies WHERE movies.title = ? COLLATE NOCASE LIMIT 1) AND rentals.checked_out = 'true'; "; + + db.all(statement, title, function(err, rows) { + callback(err, rows); + db.close(); + }); +}; + +Movie.prototype.customers_by_movie_history= function(title, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + // all customers who have checked out this movie in the past + var statement = "SELECT 'customers'.* FROM customers INNER JOIN rentals ON customers.id = rentals.customer_id WHERE rentals.movie_id = (SELECT movies.id FROM movies WHERE movies.title = ? COLLATE NOCASE LIMIT 1) AND rentals.checked_out = 'false'; "; + + db.all(statement, title, function(err, rows) { + callback(err, rows); + db.close(); + }); + }, + + +Movie.prototype.customers_by_movie_history_sorted = function(title, table, column, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + // all customers who have checked out specified movie in the past sorted by specified column + + var statement = "SELECT * FROM customers INNER JOIN rentals ON customers.id = rentals.customer_id WHERE rentals.movie_id = (SELECT movies.id FROM movies WHERE movies.title = ? COLLATE NOCASE LIMIT 1) AND rentals.checked_out = 'false' ORDER BY " + table + "." + column + ";"; + + db.all(statement, title, function(err, rows) { + callback(err, rows); + db.close(); + }); +}; + +module.exports = Movie; diff --git a/models/rental.js b/models/rental.js new file mode 100644 index 0000000..68f2743 --- /dev/null +++ b/models/rental.js @@ -0,0 +1,74 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(); +var db_env = process.env.DB || 'development'; +var date_format = require('../helpers/date_helper'); + +function Rental() { + this.table_name = "rentals"; +} + +Rental.prototype = { + check_out: function(customer_id, movie_title, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'), + rental_duration_days = 7, + checkout_date = date_format(0), + return_date = date_format(rental_duration_days), + rental_cost = 1.0; + + var begin_statement = "BEGIN IMMEDIATE;" + + var create_statement = "INSERT INTO " + this.table_name + " (checkout_date, return_date, movie_id, customer_id, checked_out) " + "VALUES ('" + checkout_date + "', '" + return_date + "', (SELECT id FROM movies WHERE title = '" + movie_title + "' AND num_available >= 1), " + customer_id + ", 'true');"; + + var availability_statement = "UPDATE movies SET num_available = (num_available - 1) WHERE id = (SELECT id FROM movies WHERE title = '" + movie_title + "' AND num_available >= 1);"; + + var charge_statement = "UPDATE customers SET account_credit = (account_credit - " + rental_cost + ") WHERE ID = " + customer_id + " AND account_credit >= 1.0;"; + + var zero_statement = "UPDATE customers SET account_credit = 0.0 WHERE id = " + customer_id + " AND account_credit < 1.0;"; + + db.serialize(function(err) { + + db.exec(begin_statement + create_statement + availability_statement + charge_statement + zero_statement + "COMMIT;", function(error) { + + if (callback) { + if (error) { + callback(error, { error: error, result: "Unsuccessful request. There may not be any copies of that movie available." }); + } else { + callback(error, { result: "Successful check out", movie: movie_title, customer_id: customer_id, checked_out_on: checkout_date }); + } + } + db.close(); + }); + + }); + }, + + check_in: function(customer_id, movie_title, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'), + return_date = date_format(0), + changes = 0; + + var update_statement = "UPDATE " + this.table_name + " SET return_date = '" + return_date + "', checked_out = 'false' WHERE movie_id = (SELECT id FROM movies WHERE title = '" + movie_title + "') AND customer_id = " + customer_id + " AND checked_out = 'true';"; + + var availability_statement = "UPDATE movies SET num_available = (num_available + 1) WHERE id = (SELECT id FROM movies WHERE title = '" + movie_title + "');"; + + db.serialize(function(err) { + db.exec("BEGIN IMMEDIATE"); + db.run(update_statement, function(err) { + changes = this.changes; + }); + + db.run(availability_statement); + + db.exec("COMMIT;", function(error) { + if (callback) { + callback(error, { movie: movie_title, customer_id: customer_id, checked_in_on: return_date, number_of_records_changed: changes }); + } + }); + + db.close(); + }); + } +}; + +module.exports = Rental; diff --git a/package.json b/package.json new file mode 100644 index 0000000..b230f0a --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "VideoStoreAPI", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "nodemon ./bin/www", + "test": "clear; DB=test mocha --recursive -R nyan", + "schema": "node ./utils/schema", + "seed": "node ./utils/seed", + "setup": "npm run schema; npm run seed" + }, + "dependencies": { + "body-parser": "~1.13.2", + "cookie-parser": "~1.3.5", + "debug": "~2.2.0", + "express": "~4.13.1", + "jade": "~1.11.0", + "mocha": "^2.3.2", + "morgan": "~1.6.1", + "serve-favicon": "~2.3.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..a0c5bbc --- /dev/null +++ b/routes/customers.js @@ -0,0 +1,30 @@ +var express = require('express'); +var router = express.Router(); +var customers_exports = require('../controllers/customers'); + +/* GET /customers */ +router.get('/', function(req, res, next) { + return customers_exports.customersController.index(req, res); +}); + +/* GET /customers/:column/:n/:p */ +router.get('/:column/:n/:p', function(req, res, next) { + return customers_exports.customersController.by_column(req, res); +}); + +/* GET /customers/:customer_id/movies */ +router.get('/:customer_id/movies', function(req, res, next) { + return customers_exports.customersController.movies_by_customer_current(req, res); +}); + +/* GET /customers/:customer_id/history */ +router.get('/:customer_id/history', function(req, res, next) { + return customers_exports.customersController.movies_by_customer_history(req, res); +}); + +/* GET /customers/overdue */ +router.get('/overdue', function(req, res, next) { + return customers_exports.customersController.customers_overdue(req, res); +}); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..1231b24 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,10 @@ +var express = require('express'); +var router = express.Router(); +var index_exports = require('../controllers/index'); + +/* GET zomg for practice. */ +router.get('/', function(req, res, next) { + return index_exports.indexController.zomg(req,res); +}); + +module.exports = router; diff --git a/routes/movies.js b/routes/movies.js new file mode 100644 index 0000000..1459c1a --- /dev/null +++ b/routes/movies.js @@ -0,0 +1,35 @@ +var express = require('express'); +var router = express.Router(); +var movies_exports = require('../controllers/movies'); + +/* GET /movies */ +router.get('/', function(req, res, next) { + return movies_exports.moviesController.index(req, res); +}); + +/* GET /movies/:column/:n/:p */ +router.get('/:column/:n/:p', function(req, res, next) { + return movies_exports.moviesController.by_column(req, res); +}); + +/* GET /movies/:title */ +router.get('/:title', function(req, res, next) { + return movies_exports.moviesController.movie_info(req, res); +}); + +/* GET /movies/:title/customers */ +router.get('/:title/customers', function(req, res, next) { + return movies_exports.moviesController.customers_by_movie_current(req, res); +}); + +/* GET /movies/:title/history */ +router.get('/:title/history', function(req, res, next) { + return movies_exports.moviesController.customers_by_movie_history(req, res); +}); + +/* GET /movies/:title/:column */ +router.get('/:title/history/:table/:column', function(req, res, next) { + return movies_exports.moviesController.customers_by_movie_history_sorted(req, res); +}); + +module.exports = router; diff --git a/routes/rentals.js b/routes/rentals.js new file mode 100644 index 0000000..cd81490 --- /dev/null +++ b/routes/rentals.js @@ -0,0 +1,9 @@ +var express = require('express'); +var router = express.Router(); +var rentals_exports = require('../controllers/rentals'); + +router.post('/checkout', rentals_exports.create); +router.patch('/checkin', rentals_exports.update); +router.put('/checkin', rentals_exports.update); + +module.exports = router; diff --git a/test/controllers/customers.js b/test/controllers/customers.js new file mode 100644 index 0000000..3f43b6e --- /dev/null +++ b/test/controllers/customers.js @@ -0,0 +1,259 @@ +var assert = require('assert'), + sqlite3 = require('sqlite3').verbose(), + request = require('supertest'), + app = require('../../app'), + customers_controller = require('../../controllers/customers'), + agent = request.agent(app); + KEYS = ['id', 'name', 'registered_at', 'address', 'city', 'state', 'postal_code', 'phone', 'account_credit']; + +describe("Endpoints under /customers", function() { + var db_cleaner; + + beforeEach(function(done) { + db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.exec( + "BEGIN; \ + DELETE FROM customers; \ + INSERT INTO customers(name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES('Beetle Juice', '2015-01-01', '123 street', 'Burlington', 'WA', '98101', '3604216650', 5.25), \ + ('Juicy Beetle', '2014-01-01', '123 street', 'Burlington', 'WA', '98211', '3604216650', 5.55), \ + ('Aaron Aaronson', '2015-02-01', '123 street', 'Burlington', 'WA', '98195', '3604216650', 5.55); \ + DELETE FROM movies; \ + INSERT INTO movies(title, overview, release_date, inventory, num_available) \ + VALUES('Jaws', 'Shark!', 'Yesterday', 10, 10), \ + ('Maws', 'Worm!', 'Yesterday', 11, 11); \ + DELETE FROM rentals; \ + INSERT INTO rentals(checkout_date, return_date, movie_id, customer_id, checked_out) \ + VALUES('2015-02-14', '2015-02-21', '1', '1', 'false'), \ + ('2015-09-22', '2015-09-29', '1', '2', 'true'), \ + ('2015-09-22', '2015-09-29', '2', '2', 'true'), \ + ('2015-07-22', '2015-09-01', '2', '1', 'true'); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + describe("customer instance methods", function() { + context("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, done); + }); + + it('knows about the route', function(done) { + customer_request + .expect(200, function(err,res) { + assert.equal(err, undefined); + done(); + }); + }); + + it("returns an array of customer objects", function(done) { + customer_request + .expect(200, function(error, result) { + assert(result.body instanceof Array); + assert.equal(result.body.length, 3); + assert.equal(result.body[0].name, 'Beetle Juice'); + assert.equal(result.body[1].name, 'Juicy Beetle'); + assert.deepEqual(Object.keys(result.body[0]), KEYS); + done(); + }); + }); + }); + + context("GET /customers/:column/:n/:p", function() { + var customer_request; + + beforeEach(function(done) { + customer_request = agent.get('/customers/name/2/1').set('Accept', 'application/json'); + done(); + }); + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('knows about the route', function(done) { + customer_request + .expect(200, function(err,res) { + assert.equal(err, undefined); + done(); + }); + }); + + it("returns an array of customer objects", function(done) { + customer_request + .expect(200, function(error, result) { + assert(result.body instanceof Array); + assert.equal(result.body[0].name, 'Beetle Juice'); + assert.equal(result.body[1].name, 'Juicy Beetle'); + assert.deepEqual(Object.keys(result.body[0]), KEYS); + done(); + }); + }); + + it("returns specified number of customers", function(done) { + customer_request + .expect(200, function(error, result) { + assert.equal(result.body.length, 2); + done(); + }); + }); + + it("returns customers with specified offset", function(done) { + customer_request + .expect(200, function(error, result) { + assert.notEqual(result.body[0].name, "Aaron Aaronson"); + done(); + }); + }); + }); + + context("GET /customers/:customer_id/movies", function() { + var customer_request; + var movie_keys = ['id', 'title', 'overview', 'release_date', 'inventory', 'num_available']; + + beforeEach(function(done) { + customer_request = agent.get('/customers/2/movies').set('Accept', 'application/json'); + done(); + }); + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('knows about the route', function(done) { + customer_request + .expect(200, function(err,res) { + assert.equal(err, undefined); + done(); + }); + }); + + it('returns an array of movie objects', function(done) { + customer_request + .expect(200, function(error, result) { + assert(result.body instanceof Array); + assert.equal(result.body.length, 2); + assert.deepEqual(Object.keys(result.body[0]), movie_keys); + done(); + }); + }); + + it('contains movies the customer currently has checked out', function(done) { + customer_request + .expect(200, function(error, result) { + assert.equal(result.body[0].title, 'Jaws'); + assert.equal(result.body[1].title, 'Maws'); + done(); + }); + }); + + }); + + context("GET /customers/:customer_id/history", function() { + var customer_request; + var movie_and_rental_keys = ['id', 'title', 'overview', 'release_date', 'inventory', 'num_available', 'checkout_date', 'return_date']; + + 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('knows about the route', function(done) { + customer_request + .expect(200, function(err,res) { + assert.equal(err, undefined); + done(); + }); + }); + + it('returns an array of movie objects', function(done) { + customer_request + .expect(200, function(error, result) { + assert(result.body instanceof Array); + assert.deepEqual(Object.keys(result.body[0]), movie_and_rental_keys); + done(); + }); + }); + + it('contains movies the customer previously checked_out', function(done) { + customer_request + .expect(200, function(error, result) { + assert.equal(result.body.length, 1); + assert.equal(result.body[0].title, 'Jaws'); + done(); + }); + }); + + }); + + context("GET /customers/overdue", function() { + var customer_request; + var customer_and_rental_keys = KEYS.concat(['checkout_date', 'return_date', 'movie_id', 'customer_id', 'checked_out']); + + beforeEach(function(done) { + customer_request = agent.get('/customers/overdue').set('Accept', 'application/json'); + done(); + }); + + it("responds with json", function(done) { + customer_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('knows about the route', function(done) { + customer_request + .expect(200, function(err,res) { + assert.equal(err, undefined); + done(); + }); + }); + + it('returns an array of customer objects', function(done) { + customer_request + .expect(200, function(error, result) { + assert(result.body instanceof Array); + assert.deepEqual(Object.keys(result.body[0]), customer_and_rental_keys); + done(); + }); + }); + + it('contains customers with overdue movies', function(done) { + customer_request + .expect(200, function(error, result) { + assert.equal(result.body.length, 1); + assert.equal(result.body[0].name, 'Beetle Juice'); + assert.equal(result.body[0].movie_id, 2); + + done(); + }); + }); + }); + + }); +}); diff --git a/test/controllers/movies.js b/test/controllers/movies.js new file mode 100644 index 0000000..4ceaa6c --- /dev/null +++ b/test/controllers/movies.js @@ -0,0 +1,141 @@ +var assert = require('assert'), + sqlite3 = require('sqlite3').verbose(), + request = require('supertest'), + app = require('../../app'), + movie_controller = require('../../controllers/movies'), + agent = request.agent(app); + +describe("Endpoints under /movies", function() { + var db_cleaner; + + beforeEach(function(done) { + + db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.exec( + "BEGIN; \ + DELETE FROM movies; \ + INSERT INTO movies(title, overview, release_date, inventory, num_available) \ + VALUES('Jaws', 'Shark!', '2015-01-01', 10, 8), \ + ('Maws', 'Worm!', '2015-01-01', 11, 4), \ + ('Claws', 'Cat!', '2015-01-01', 12, 5), \ + ('Paws', 'Bear!', '2015-01-01', 13, 10), \ + ('Gauze', 'Ouch!', '2015-01-01', 14, 10); \ + DELETE FROM customers; \ + INSERT INTO customers(name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES('BeetleJaws', '2015-01-01', '123 street', 'Burlington', \ + 'WA', '98233', '(908) 949-6758', 5.25), \ + ('JuiceMaws', '2010-10-10', '123 Lane', 'Mt. Vernon', \ + 'WA', '11111', '(908) 949-6758', 10.00); \ + DELETE FROM rentals; \ + INSERT INTO rentals(checkout_date, return_date, movie_id, customer_id, checked_out) \ + VALUES('2015-09-23', '2015-09-30', 1, 1, 'true'), \ + ('2015-09-16', '2015-09-23', 2, 2, 'false'); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + describe("movie instance methods", function() { + var movie_keys; + var customer_keys; + + beforeEach(function(done) { + movie_keys = ['id', 'title', 'overview', 'release_date', 'inventory', 'num_available']; + customer_keys = ['id', 'name', 'registered_at', 'address', 'city', 'state', 'postal_code', 'phone', 'account_credit']; + done(); + }); + + context("GET /movies", function() { + it("responds with json", function(done) { + var movie_request = agent.get('/movies').set('Accept', 'application/json'); + movie_request + .expect('Content-Type', /application\/json/) + .expect(200, done); + }); + + it('knows about the route', function(done) { + var movie_request = agent.get('/movies').set('Accept', 'application/json'); + movie_request + .expect('Content-Type', /application\/json/) + .expect(200, function(err,res) { + assert.equal(err, undefined); + done(); + }); + }); + + it("returns an array of movie objects", function(done) { + var movie_request = agent.get('/movies').set('Accept', 'application/json'); + movie_request + .expect(200, function(error, result) { + assert.equal(result.body.length, 5); + assert.deepEqual(Object.keys(result.body[0]), movie_keys); + done(); + }); + }); + }); + + context("GET /movies/:column/:p ", function() { + it("sorted movie column", function(done) { + var movie_request = agent.get('/movies/title/1/1').set('Accept', 'application/json'); + movie_request + .expect('Content-Type', /application\/json/) + .expect(200, function(error, result) { + assert.equal(result.body.length, 1); + assert.deepEqual(Object.keys(result.body[0]), movie_keys); + assert.equal(result.body[0].title, 'Gauze'); + done(); + }); + }); + }); + + context("GET /movies/:title ", function() { + it("can find Jaws", function(done) { + movie_keys; + var movie_request = agent.get('/movies/Jaws').set('Accept', 'application/json'); + movie_request + .expect('Content-Type', /application\/json/) + .expect(200, function(error, result) { + assert.equal(result.body.length, 1); + assert.deepEqual(Object.keys(result.body[0]), movie_keys); + assert.equal(result.body[0].title, 'Jaws'); + done(); + }); + }); + }); + + context("GET /movies/:title/history ", function() { + it("can see Jaws customer history", function(done) { + customer_keys; + var movie_request = agent.get('/movies/Maws/history').set('Accept', 'application/json'); + movie_request + .expect('Content-Type', /application\/json/) + .expect(200, function(error, result) { + assert.equal(result.body.length, 1); + assert.deepEqual(Object.keys(result.body[0]), customer_keys); + assert.equal(result.body[0].name, 'JuiceMaws'); + done(); + }); + }); + }); + + context("GET /movies/:title/customers", function() { + it("can see current customers for a movie", function(done) { + customer_keys; + var movie_request = agent.get('/movies/Jaws/customers').set('Accept', 'application/json'); + movie_request + .expect('Content-Type', /application\/json/) + .expect(200, function(error, result) { + assert.equal(result.body.length, 1); + assert.deepEqual(Object.keys(result.body[0]), customer_keys); + assert.equal(result.body[0].name, 'BeetleJaws'); + done(); + }); + }); + }); + }); +}); diff --git a/test/controllers/rentals.js b/test/controllers/rentals.js new file mode 100644 index 0000000..c2a14a0 --- /dev/null +++ b/test/controllers/rentals.js @@ -0,0 +1,81 @@ +var assert = require('assert'), + sqlite3 = require('sqlite3').verbose(), + request = require('supertest'), + app = require('../../app'), + movie_controller = require('../../controllers/rentals'), + agent = request.agent(app); + +describe("Endpoints under /rentals", function() { + var db_cleaner; + var date_format = require('../../helpers/date_helper'); + + beforeEach(function(done) { + + db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.exec( + "BEGIN; \ + DELETE FROM movies; \ + INSERT INTO movies(title, overview, release_date, inventory, num_available) \ + VALUES('Jaws', 'Shark!', '2015-01-01', 10, 8), \ + ('Maws', 'Worm!', '2015-01-01', 11, 4), \ + ('Claws', 'Cat!', '2015-01-01', 12, 5), \ + ('Paws', 'Bear!', '2015-01-01', 13, 10), \ + ('Gauze', 'Ouch!', '2015-01-01', 14, 10); \ + DELETE FROM customers; \ + INSERT INTO customers(name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES('BeetleJaws', '2015-01-01', '123 street', 'Burlington', \ + 'WA', '98233', '(908) 949-6758', 5.25), \ + ('JuiceMaws', '2010-10-10', '123 Lane', 'Mt. Vernon', \ + 'WA', '11111', '(908) 949-6758', 10.00); \ + DELETE FROM rentals; \ + INSERT INTO rentals(checkout_date, return_date, movie_id, customer_id, checked_out) \ + VALUES('2015-09-23', '2015-09-30', 1, 1, 'true'), \ + ('2015-09-16', '2015-09-23', 2, 2, 'false'), \ + ('2015-09-16', '2015-09-23', 5, 1, 'true'); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + describe("rentals instance methods", function() { + context("POST /checkout", function() { + it('posts to the rentals table', function(done) { + request(app) + .post('/rentals/checkout') + .send({ id: '1', title: 'Gauze'}) + .expect(200, function(err, res) { + assert.equal(res.body.customer_id, 1); + assert.equal(res.body.checked_out_on, date_format(0)); + if(err) { + done(err); + } else { + done(); + } + }); + }); + }); + + context("PATCH /checkin", function() { + it('should update a rental record', function(done){ + request(app) + .patch('/rentals/checkin') + .send({ id: '1', title: 'Gauze'}) + .expect(200, function(err, res) { + assert.equal(res.body.customer_id, 1); + assert.equal(res.body.number_of_records_changed, 1); + assert.equal(res.body.checked_in_on, date_format(0)); + if(err) { + done(err); + } else { + done(); + } + }); + }); + }); + }); +}); diff --git a/test/models/customers.js b/test/models/customers.js new file mode 100644 index 0000000..0bd8994 --- /dev/null +++ b/test/models/customers.js @@ -0,0 +1,153 @@ +var assert = require('assert'), + Customer = require('../../models/customer'), + sqlite3 = require('sqlite3').verbose(); + +describe("Customer", function() { + var customer, db_cleaner; + + beforeEach(function(done) { + customer = new Customer(); + + db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.exec( + "BEGIN; \ + DELETE FROM customers; \ + INSERT INTO customers(name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES('Beetle Juice', '2015-01-01', '123 street', 'Burlington', 'WA', '98101', '3604216650', 5.25), \ + ('Juicy Beetle', '2014-01-01', '123 street', 'Burlington', 'WA', '98211', '3604216650', 5.55), \ + ('Aaron Aaronson', '2015-02-01', '123 street', 'Burlington', 'WA', '98195', '3604216650', 5.55); \ + DELETE FROM movies; \ + INSERT INTO movies(title, overview, release_date, inventory, num_available) \ + VALUES('Jaws', 'Shark!', 'Yesterday', 10, 8), \ + ('Maws', 'Worm!', 'Yesterday', 11, 8); \ + DELETE FROM rentals; \ + INSERT INTO rentals(checkout_date, return_date, movie_id, customer_id, checked_out) \ + VALUES('2015-02-14', '2015-02-21', '1', '1', 'false'), \ + ('2015-09-22', '2015-09-29', '1', '2', 'true'), \ + ('2015-09-22', '2015-09-29', '2', '2', 'true'), \ + ('2015-09-14', '2015-09-20', '1', '1', 'true'), \ + ('2015-07-22', '2015-09-01', '1', '2', 'true'); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + it("can be instantiated", function() { + assert(customer instanceof Customer); + }); + + describe("instance methods", function() { + context("GET #find_all", function() { + it("retrieves all customer records", function(done) { + customer.find_all(function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 3); + assert.equal(res[0].name, 'Beetle Juice'); + done(); + }); + }); + }); + + context("GET #by_column", function() { + it("successfully retrieves an Array object", function (done) { + customer.by_column("name", 3, 0, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + done(); + }); + }); + + it("retrieves records sorted by name column", function(done) { + customer.by_column("name", 3, 0, function(err, res) { + assert.equal(res.length, 3); + assert.equal(res[0].name, 'Aaron Aaronson'); + assert.equal(res[1].name, 'Beetle Juice'); + done(); + }); + }); + + it("retrieves records sorted by registered_at column", function(done) { + customer.by_column("registered_at", 3, 0, function(err, res) { + assert.equal(res.length, 3); + assert.equal(res[0].name, 'Juicy Beetle'); + assert.equal(res[1].name, 'Beetle Juice'); + done(); + }); + }); + + it("retrieves records sorted by postal_code column", function(done) { + customer.by_column("postal_code", 3, 0, function(err, res) { + assert.equal(res.length, 3); + assert.equal(res[0].name, 'Beetle Juice'); + assert.equal(res[1].name, 'Aaron Aaronson'); + done(); + }); + }); + + it("retrieves correct number of records", function(done) { + customer.by_column("name", 2, 0, function(err, res) { + assert.equal(res.length, 2); + done(); + }); + }); + + it("retrieves records offset by correct number of records", function(done) { + customer.by_column("name", 2, 1, function(err, res) { + assert.equal(res.length, 2); + assert.equal(res[0].name, 'Beetle Juice'); + done(); + }); + }); + }); + + context("GET #movies_by_customer_current", function() { + it("retrieves customer's currently checked out movies", function(done) { + customer.movies_by_customer_current(2, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 3); + assert.equal(res[0].title, 'Jaws'); + done(); + }); + }); + }); + + context("GET #movies_by_customer_history", function() { + it("retrieves customer's previously checked out movies", function(done) { + customer.movies_by_customer_history(1, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 1); + assert.equal(res[0].title, 'Jaws'); + done(); + }); + }); + }); + + context("GET #customers_overdue", function() { + it("retrieves list of customers with overdue movies", function(done) { + customer.customers_overdue(function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 2); + done(); + }); + }); + + it("customers list is sorted by return_date", function(done) { + customer.customers_overdue(function(err, res) { + assert.equal(res[0].name, 'Juicy Beetle'); + done(); + }); + }); + + }); + + }); +}); diff --git a/test/models/movies.js b/test/models/movies.js new file mode 100644 index 0000000..127eba5 --- /dev/null +++ b/test/models/movies.js @@ -0,0 +1,137 @@ +var assert = require('assert'), + Movie = require('../../models/movie'), + app = require('../../app'), + sqlite3 = require('sqlite3').verbose(); + +describe("Movie", function() { + var movie, db_cleaner; + + beforeEach(function(done) { + movie = new Movie(); + + db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.exec( + "BEGIN; \ + DELETE FROM movies; \ + INSERT INTO movies(title, overview, release_date, inventory, num_available) \ + VALUES('Jaws', 'Shark!', '2015-01-01', 10, 8), \ + ('Maws', 'Worm!', '2015-01-01', 11, 4), \ + ('Claws', 'Cat!', '2015-01-01', 12, 5), \ + ('Paws', 'Bear!', '2015-01-01', 13, 10), \ + ('Gauze', 'Ouch!', '2015-01-01', 14, 10); \ + DELETE FROM customers; \ + INSERT INTO customers(name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES('BeetleJaws', '2015-01-01', '123 street', 'Burlington', \ + 'WA', '98233', '(908) 949-6758', 5.25), \ + ('JuiceMaws', '2010-10-10', '123 Lane', 'Mt. Vernon', \ + 'WA', '11111', '(908) 949-6758', 10.00), \ + ('SecondMaws', '2010-10-10', '123 Lane', 'Mt. Vernon', \ + 'WA', '11111', '(908) 949-6758', 10.00); \ + DELETE FROM rentals; \ + INSERT INTO rentals(checkout_date, return_date, movie_id, customer_id, checked_out) \ + VALUES('2015-09-23', '2015-09-30', 1, 1, 'true'), \ + ('2015-09-16', '2015-09-01', 2, 2, 'false'), \ + ('2015-09-14', '2015-09-05', 2, 3, 'false'); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + it("can be instantiated", function() { + assert(movie instanceof Movie); + }); + + describe("instance methods", function() { + context("GET #find_all", function() { + it("retrieves all movie records", function(done) { + movie.find_all(function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 5); + assert.equal(res[0].title, 'Jaws'); + assert.equal(res[1].title, 'Maws'); + done(); + }); + }); + }); + + context("GET #by_column", function() { + it("successfully retrieves an array object", function(done) { + movie.by_column("title", 1, 0, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + done(); + }); + }); + + it("retrieves records sorted by title column", function(done) { + movie.by_column("title", 2, 0, function(err, res) { + assert.equal(res.length, 2); + assert.equal(res[0].title, 'Claws'); + assert.equal(res[1].title, 'Gauze'); + done(); + }); + }); + + it("retrieves records sorted by release date", function(done) { + movie.by_column("release_date", 2, 0, function(err, res) { + assert.equal(res.length, 2); + assert.equal(res[0].title, 'Jaws'); + assert.equal(res[1].title, 'Maws'); + done(); + }); + }); + + it("retrieves records offset by correct number of records", function(done) { + movie.by_column("title", 2, 1, function(err, res) { + assert.equal(res.length, 2); + assert.equal(res[0].title, 'Gauze'); + done(); + }); + }); + }); + + context("GET #customers_by_movie_current", function() { + it("gets customers that have currently checked out a copy of the film", function(done) { + movie.customers_by_movie_current("Jaws", function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 1); + assert.equal(res[0].name, 'BeetleJaws'); + done(); + }); + }); + }); + + context("GET customers_by_movie_history", function() { + it("gets customers that have checked out a copy of the film in the past", function(done) { + movie.customers_by_movie_history("Maws", function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 2); + assert.equal(res[0].name, 'JuiceMaws'); + assert.equal(res[1].name, 'SecondMaws'); + done(); + }); + }); + }); + + context("GET customers_by_movie_history_sorted", function() { + it("gets customers that have checked out a copy of the film in the past", function(done) { + movie.customers_by_movie_history_sorted("Maws", "rentals", "checkout_date", function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 2); + assert.equal(res[0].name, 'SecondMaws'); + assert.equal(res[1].name, 'JuiceMaws'); + done(); + }); + }); + }); + }); +}); diff --git a/test/models/rentals.js b/test/models/rentals.js new file mode 100644 index 0000000..e180d3f --- /dev/null +++ b/test/models/rentals.js @@ -0,0 +1,94 @@ +var assert = require('assert'), + Rental = require('../../models/rental'), + app = require('../../app'), + request = require('supertest'), + sqlite3 = require('sqlite3').verbose(); + +describe("Rental", function() { + var rental, db_cleaner; + + beforeEach(function(done) { + rental = new Rental(); + + db_cleaner = new sqlite3.Database('db/test.db'); + db_cleaner.serialize(function() { + db_cleaner.exec( + "BEGIN; \ + DELETE FROM customers; \ + INSERT INTO customers(name, registered_at, address, city, state, postal_code, phone, account_credit) \ + VALUES('Beetle Juice', '2015-01-01', '123 street', 'Burlington', 'WA', '98101', '3604216650', 5.25), \ + ('Juicy Beetle', '2014-01-01', '123 street', 'Burlington', 'WA', '98211', '3604216650', 5.55), \ + ('Aaron Aaronson', '2015-02-01', '123 street', 'Burlington', 'WA', '98195', '3604216650', 5.55); \ + DELETE FROM movies; \ + INSERT INTO movies(title, overview, release_date, inventory, num_available) \ + VALUES('Jaws', 'Shark!', 'Yesterday', 10, 8), \ + ('Maws', 'Worm!', 'Yesterday', 11, 8); \ + DELETE FROM rentals; \ + INSERT INTO rentals(checkout_date, return_date, movie_id, customer_id, checked_out) \ + VALUES('2015-02-14', '2015-02-21', '1', '1', 'false'), \ + ('2015-09-22', '2015-09-29', '1', '2', 'true'), \ + ('2015-09-22', '2015-09-29', '2', '2', 'true'), \ + ('2015-09-14', '2015-09-20', '1', '1', 'true'), \ + ('2015-07-22', '2015-09-01', '1', '2', 'true'); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + it("can be instantiated", function() { + assert(rental instanceof Rental); + }); + + describe("instance methods", function() { + context("POST #check_out", function() { + + it("is successful", function(done) { + rental.check_out(1, "Jaws", function(err, res) { + assert.equal(err, undefined); + done(); + }); + }); + + it("returns a 'success' object", function(done) { + var keys = [ "result", "movie", "customer_id", "checked_out_on" ]; + rental.check_out(1, "Jaws", function(err, res) { + assert.deepEqual(Object.keys(res), keys); + assert.equal(res.movie, "Jaws"); + assert.equal(res.customer_id, 1); + done(); + }); + }); + }); + + context("POST #check_in", function() { + + it("is successful", function(done) { + rental.check_in(1, "Jaws", function(err, res) { + assert.equal(err, undefined); + done(); + }); + }); + + it("returns an object with the expected keys", function(done) { + var keys = [ "movie", + "customer_id", + "checked_in_on", + "number_of_records_changed" ]; + + rental.check_in(2, "Jaws", function(err, res) { + assert.deepEqual(Object.keys(res), keys); + assert.equal(res.movie, "Jaws"); + assert.equal(res.customer_id, "2"); + // below is 2 because we have two rentals for same movie and customer. + assert.equal(res.number_of_records_changed, 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..b1a648d --- /dev/null +++ b/utils/rentals.json @@ -0,0 +1,72 @@ +[ + { + "checkout_date": "2015-09-23", + "return_date": "2015-09-30", + "movie_id": 1, + "customer_id": 1, + "checked_out": "true" + }, + { + "checkout_date": "2015-09-20", + "return_date": "2015-09-22", + "movie_id": 1, + "customer_id": 2, + "checked_out": "false" + }, + { + "checkout_date": "2015-09-23", + "return_date": "2015-09-30", + "movie_id": 2, + "customer_id": 1, + "checked_out": "true" + }, + { + "checkout_date": "2015-09-23", + "return_date": "2015-09-30", + "movie_id": 1, + "customer_id": 2, + "checked_out": "true" + }, + { + "checkout_date": "2015-07-25", + "return_date": "2015-08-01", + "movie_id": 3, + "customer_id": 1, + "checked_out": "false" + }, + { + "checkout_date": "2015-07-30", + "return_date": "2015-08-01", + "movie_id": 3, + "customer_id": 2, + "checked_out": "false" + }, + { + "checkout_date": "2015-06-20", + "return_date": "2015-07-01", + "movie_id": 4, + "customer_id": 1, + "checked_out": "true" + }, + { + "checkout_date": "2015-07-20", + "return_date": "2015-08-01", + "movie_id": 18, + "customer_id": 2, + "checked_out": "false" + }, + { + "checkout_date": "2015-08-20", + "return_date": "2015-09-01", + "movie_id": 34, + "customer_id": 2, + "checked_out": "true" + }, + { + "checkout_date": "2015-09-12", + "return_date": "2015-09-18", + "movie_id": 38, + "customer_id": 3, + "checked_out": "true" + } +] diff --git a/utils/schema.js b/utils/schema.js new file mode 100644 index 0000000..ddee5a5 --- /dev/null +++ b/utils/schema.js @@ -0,0 +1,75 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(), + db_env = process.env.DB || 'development', + db = new sqlite3.Database('db/' + db_env + '.db'); // creates db connection + +var movie_table = "movies"; + +var customer_table = "customers"; +var customer_fields = [ + ['name', 'text'], + ['registered_at', 'text'], + ['address', 'text'], + ['city', 'text'], + ['state', 'text'], + ['postal_code', 'text'], + ['phone', 'text'], + ['account_credit', 'float'], +]; + +var rental_table = "rentals"; + +function create_table(table_name, table_fields) { + db.serialize(function() { + // drop existing tables + db.run("DROP TABLE IF EXISTS " + table_name + ";"); + + // create fresh version of table with id as primary key + db.run("CREATE TABLE " + table_name + + " (id INTEGER PRIMARY KEY);"); + + // add columns to those tables + for(var i = 0; i < table_fields.length; i++) { + var name = table_fields[i][0], + type = table_fields[i][1]; + + db.run("ALTER TABLE " + table_name + + " ADD COLUMN " + name + " " + type + + ";"); + } + }); +} + +function create_movies_table(table_name) { + db.serialize(function() { + // drop existing tables + db.run("DROP TABLE IF EXISTS " + table_name + ";"); + + // create fresh version of table with id as primary key + db.run("CREATE TABLE " + table_name + + " (id INTEGER PRIMARY KEY, \ + title TEXT, \ + overview TEXT, \ + release_date TEXT, \ + inventory INTEGER NOT NULL CHECK(inventory > 0), \ + num_available INTEGER NOT NULL CHECK(num_available <= inventory \ + AND num_available >= 0));"); + }); +} + +function create_rentals_table(table_name) { + db.serialize(function() { + // drop existing tables + db.run("DROP TABLE IF EXISTS " + table_name + ";"); + + // create fresh version of table with id as primary key + db.run("CREATE TABLE rentals (id INTEGER PRIMARY KEY, checkout_date text NOT NULL, return_date text NOT NULL, movie_id integer NOT NULL, customer_id integer NOT NULL, checked_out boolean NOT NULL, FOREIGN KEY(movie_id) REFERENCES movie(id), FOREIGN KEY(customer_id) REFERENCES customer(id));"); + }); +} + +create_movies_table(movie_table); +create_table(customer_table, customer_fields); +create_rentals_table(rental_table); + +db.close(); diff --git a/utils/seed.js b/utils/seed.js new file mode 100644 index 0000000..ff5a090 --- /dev/null +++ b/utils/seed.js @@ -0,0 +1,114 @@ +"use strict"; + +// creates db connection +var sqlite3 = require('sqlite3').verbose(), + db_env = process.env.DB || 'development', + db = new sqlite3.Database('db/' + db_env + '.db'); + +// node will parse json files for you if you require them! +var movies = require('./movies'); +var movie_statement = db.prepare( + "INSERT INTO movies(title, overview, release_date, inventory, num_available) \ + VALUES (?, ?, ?, ?, ?);" +); + +db.serialize(function() { + // loop through 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.release_date, + movie.inventory, + movie.inventory + ); + } + 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(checkout_date, return_date, movie_id, customer_id, checked_out) \ + VALUES (?, ?, ?, ?, ?);" +); +// var adjust_num_available_statement = db.prepare( +// "CASE WHEN rentals.checked_out = 'true' AND rentals.movie_id = 1 THEN UPDATE movies SET num_available = (num_available - 1) WHERE id = 1; " +// +// "UPDATE movies SET num_available = CASE WHEN (SELECT * FROM movies INNER JOIN rentals ON movies.id = rentals.movie_id WHERE checked_out = 'true' AND movie_id = 1 LIMIT 1) THEN (num_available - 1) ELSE num_available END WHERE id = 1; " + + // SELECT * FROM movies INNER JOIN rentals ON movies.id = rentals.movie_id WHERE checked_out = 'true' + + // UPDATE movies SET num_available = (num_available - 1) WHERE id = 1 AND (SELECT checked_out FROM rentals WHERE rentals.movie_id = 1 AND rentals.checked_out = 'true'); +// ); + +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.checkout_date, + rental.return_date, + rental.movie_id, + rental.customer_id, + rental.checked_out + ); + // decrease number of copies available in movies table if checked out. + // adjust_num_available_statement.run(rental.movie_id); + } + rental_statement.finalize(); + // adjust_num_available_statement.finalize(); +}); + +// close db +db.close(); + + +// TRIED TO REDUCE REDUNDENCY BY WRITING A MORE GENERAL FUNCTION +// BUT IT DIDN'T WORK; MAYBE TRY LATER. +// var movie_values = [ title, overview, release_date, +// inventory ]; +// +// function seed(seed_file, statement, values) { +// db.serialize(function() { +// // loop through movies +// for(var i = 0; i < seed_file.length; i++) { +// var record = seed_file[i]; +// // insert each one into the db +// statement.run( +// for(var i = 0; i < values.length; i++) { +// record.values[i]; +// } +// ); +// } +// }); +// } +// +// seed(movies, movie_statement, movie_values); diff --git a/videostore_scaffolding.md b/videostore_scaffolding.md new file mode 100644 index 0000000..6677c74 --- /dev/null +++ b/videostore_scaffolding.md @@ -0,0 +1,51 @@ +### Customers +- Retrieve a list of all customers +GET /customers + +-Retrive 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) +GET /customers/:column/:p + +-Given a customer's id... +GET /customers/:id + +-List the movies they currently have checked out +GET /customers/:id/movies + +-List the movies a customer has checked out in the past +Get /customers/:id/history + +### Movies +- Retrieve a list of all movies +GET /movies + +- 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) +GET /movies/:column/:p + +- Get a list of customers that have _currently_ checked out a copy of the film +GET /movies/:title/customers + + - Get a list of customers that have checked out a copy _in the past_ +GET /movies/:title/history + +### Rental +- Look a movie up by title to see +GET /movies/:title + +- Know if a movie has any inventory available to rent +GET /movies/:title/:checked_out + +- See a list of customers that have _currently_ checked out any of the movie's inventory +`This is identical to the requirements under movie` +GET /movies/:title + +- Given a customer's `id` and a movie's `title` ... + - "check out" one of the movie's inventory to the customer + POST /customers/:id/:title + + - "check in" one of customer's rentals + PATCH /customers/:id/:title + +- See a list of customers with overdue movies +GET /customers/overdue 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