diff --git a/.gitignore b/.gitignore index e43b0f9..20124a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ .DS_Store + +# Let package.json handle dependencies +/node_modules + +db/test.db diff --git a/README.md b/README.md index e3012ac..3200f8a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,14 @@ Alice & Anita # Project: VideoStoreAPI +###How to run +1. Clone the repo. +2. Install all packages by running `npm install`. +3. See tests by running `npm test`. +4. See instructions for using our endpoints [here](./endpoint-readme.md). + +------------- + The overall goal of this project is to create a system that a video store (remember those?) could use to track their inventory of rental videos and their collection of customers. We will use [NodeJS](https://nodejs.org/en/) to construct a RESTful API. The goal of this API is to quickly serve information about the store's video collection, customer information, and to update rental status. This repository provides two JSON datafiles to serve as the initial seeds for this system. diff --git a/app.js b/app.js new file mode 100644 index 0000000..d50b4f4 --- /dev/null +++ b/app.js @@ -0,0 +1,71 @@ +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 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'))); + +// Are we going to use this?? +// register root routes +var routes = require('./routes/index'); +app.use('/', routes); + +// register movies routes +var movie_routes = require('./routes/movies'); +app.use('/movies', movie_routes); + +// register customer routes +var customer_routes = require('./routes/customers'); +app.use('/customers', customer_routes); + +// register rental routes +var rental_routes = require('./routes/rentals'); +app.use('/rentals', rental_routes); + +// 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/database_adapter.js b/database_adapter.js new file mode 100644 index 0000000..4af12c7 --- /dev/null +++ b/database_adapter.js @@ -0,0 +1,107 @@ +"use strict"; + +var sqlite3 = require('sqlite3').verbose(), + db_env = process.env.DB || 'development'; + +module.exports = { + 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(); + }); + }, + + // get returns one record, making this emulate Active Record's find_by + find_by: function(column, value, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var statement = "SELECT * FROM " + this.table_name + " WHERE " + column + " LIKE ?"; + + db.get(statement, value, function(err, res) { + if (callback) { callback(err, res); } + db.close(); + }); + }, + + // returns array of records + where: function(columns, values, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var where_statements = []; + + // where_statements => ["city = ?", "state = ?"] + for (var i = 0; i < columns.length; i++) { + where_statements.push(columns[i] + " = ?"); + } + // where_statement => "city = ? AND state = ?" + var where_statement = where_statements.join(" AND "); + + var statement = "SELECT * FROM " + this.table_name + " WHERE " + where_statement; + + db.all(statement, values, function(err, res) { + if (callback) { callback(err, res); } + db.close(); + }); + }, + + where_in: function(column, values, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var questionMarks = Array(values.length + 1).join('?').split('').join(', '); + + var statement = "SELECT * FROM " + this.table_name + " WHERE " + column + " IN (" + questionMarks + ");"; + + db.all(statement, values, function(error, result) { + if (callback) { callback(error, result); } + db.close(); + }); + }, + + subset: function(column, queries, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + + var statement = "SELECT * FROM " + this.table_name + " ORDER BY " + column + " LIMIT ? OFFSET ?"; + + db.all(statement, queries, function(err, res) { + if (callback) { callback(err, res); } + db.close(); + }); + }, + + // Example route: + // customers/create/:name/:registered_at/:address/:city/:state/:postal_code/:phone + create: function(columns, values, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + var column_names = columns.join(', '); + var question_marks = Array(columns.length + 1).join('?').split('').join(', '); + + var statement = "INSERT INTO " + this.table_name + " (" + column_names + ") \ + VALUES (" + question_marks + ");"; + + db.run(statement, values, function(err, res) { + if (callback) { callback(err, { inserted_id: this.lastID }); } + db.close(); + }); + }, + + // Example route: + // customers/update/:id?name=name&city=city&state=state + update: function(id, columns, values, callback) { + var db = new sqlite3.Database('db/' + db_env + '.db'); + // eg. "column1 = ?, column2 = ?, column3 = ?" + var columnsQueries = []; + + for (var i = 0; i < columns.length; i++) { + columnsQueries.push(columns[i] + " = ?"); + }; + + var update_statement = columnsQueries.join(', '); + + var statement = "UPDATE " + this.table_name + " SET " + update_statement + "WHERE id = " + id + ";"; + + db.run(statement, values, function(err, res) { + if (callback) { callback(err, { changes: this.changes }); } + db.close(); + }); + } +} diff --git a/db/development.db b/db/development.db new file mode 100644 index 0000000..e48209e 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..647fe98 Binary files /dev/null and b/db/test.db differ diff --git a/endpoint-readme.md b/endpoint-readme.md new file mode 100644 index 0000000..66a615a --- /dev/null +++ b/endpoint-readme.md @@ -0,0 +1,497 @@ +#Anita & Alice's Video Store + +See below for the available endpoints for this API. + +1. [All Customers](#all-customers) +2. [Single Customer](#single-customer) +3. [Subset of Customers](#subset-of-customers) +4. [All Movies](#all-movies) +5. [Single Movie](#single-movie) +6. [Subset of Movies](#subset-of-movies) +7. [Overdue Rentals](#overdue-rentals) +8. [Rental History of Single Movie](#rental-history-of-single-movie) +9. [Rent Movie](#rent-movie) +10. [Return Movie](#return-movie) + +##Customers + +###All Customers +- **Endpoint:** GET `/customers` +- Retrieves a list of all customers. +- Returns an object with a `customers` property containing an array of customer objects. +- Each customer object contains the following properties: `id`, `name`, `registered_at` (date of registration), `address`, `city`, `state`, `postal_code`, `phone`, and `account_credit` (in cents). +- **Sample:** GET `/customers` +```json +{ + "customers": [{ + "id": 1, + "name": "Shelley Rocha", + "registered_at": "Wed, 29 Apr 2015 07:54:14 -0700", + "address": "Ap #292-5216 Ipsum Rd.", + "city": "Hillsboro", + "state": "OR", + "postal_code": "24309", + "phone": "(322) 510-8695", + "account_credit": 1315 + }, { + "id": 2, + "name": "XCurran Stout", + "registered_at": "Wed, 16 Apr 2014 21:40:20 -0700", + "address": "Ap #658-1540 Erat Rd.", + "city": "San Francisco", + "state": "California", + "postal_code": "94267", + "phone": "(908) 949-6758", + "account_credit": 3565.9999999999995 + }, { + "id": 3, + "name": "Roanna Robinson", + "registered_at": "Fri, 28 Nov 2014 13:14:08 -0800", + "address": "Ap #561-4214 Eget St.", + "city": "Harrisburg", + "state": "PA", + "postal_code": "15867", + "phone": "(323) 336-1841", + "account_credit": 5039 + }, { + "id": 4, + "name": "Carolyn Chandler", + "registered_at": "Fri, 04 Jul 2014 11:05:11 -0700", + "address": "133-8707 Arcu. Avenue", + "city": "Fort Wayne", + "state": "IN", + "postal_code": "73684", + "phone": "(234) 837-2886", + "account_credit": 2079 + }, { + "id": 5, + "name": "Aquila Riddle", + "registered_at": "Thu, 27 Aug 2015 08:17:24 -0700", + "address": "Ap #187-9582 Primis St.", + "city": "Tacoma", + "state": "WA", + "postal_code": "73251", + "phone": "(925) 161-2223", + "account_credit": 1782 + }] +} +``` + +###Single Customer + +- **Endpoint:** GET `/customers/:id` +- Retrieves data about the customer identified by the `id` passed in the URL. +- Returns an object with `customer_data` and `movies` properties. + - `customer_data` contains the following properties: `id`, `name`, `registered_at` (date of registration), `address`, `city`, `state`, `postal_code`, `phone`, and `account_credit` (in cents). + - `movies` contains: `current_rentals` and `past_rentals`. + - `current_rentals` is a list of movies rented by the customer. + - Each movie object contains the following propeties: `id`, `title`, `overview`, `release_date`, and `inventory`. + - `past_rentals` is a list of movie objects. + - Each movie object contains `movie_data` and `dates`. + - `dates` contains `checkout_date` and `returned_date`. + - `movie_data` contains: `id`, `title`, `overview`, `release_date`, and `inventory`. +- **Sample:** GET `/customers/1` +```json +{ + "customer_data": { + "id": 1, + "name": "Shelley Rocha", + "registered_at": "Wed, 29 Apr 2015 07:54:14 -0700", + "address": "Ap #292-5216 Ipsum Rd.", + "city": "Hillsboro", + "state": "OR", + "postal_code": "24309", + "phone": "(322) 510-8695", + "account_credit": 1315 + }, + "movies": { + "current_rentals": [{ + "id": 2, + "title": "Jaws", + "overview": "An insatiable great white shark terrorizes the townspeople of Amity Island, The police chief, an oceanographer and a grizzled shark hunter seek to destroy the bloodthirsty beast.", + "release_date": "1975-06-19", + "inventory": 6 + }, { + "id": 3, + "title": "The Exorcist", + "overview": "12-year-old Regan MacNeil begins to adapt an explicit new personality as strange events befall the local area of Georgetown. Her mother becomes torn between science and superstition in a desperate bid to save her daughter, and ultimately turns to her last hope: Father Damien Karras, a troubled priest who is struggling with his own faith.", + "release_date": "1973-12-26", + "inventory": 7 + }, { + "id": 5, + "title": "The Silence of the Lambs", + "overview": "FBI trainee Clarice Starling ventures into a maximum-security asylum to pick the diseased brain of Hannibal Lecter, a psychiatrist turned homicidal cannibal. Starling needs clues to help her capture a serial killer. Unfortunately, her Faustian relationship with Lecter soon leads to his escape, and now two deranged killers are on the loose.", + "release_date": "1991-02-14", + "inventory": 3 + }, { + "id": 49, + "title": "Ben-Hur", + "overview": "Ben-Hur is a 1959 epic film directed by William Wyler, the third film version of Lew Wallace's 1880 novel Ben-Hur: A Tale of the Christ. It premiered at Loew's State Theatre in New York City on November 18, 1959. The film went on to win a record of eleven Academy Awards, including Best Picture, a feat equaled only by Titanic in 1998 and The Lord of the Rings: The Return of the King in 2004. It was also the last film to win the Oscar for both Best Actor and Best Supporting Actor, until nearly 44 years later when Mystic River achieved the same feat.The movie revolves around a Jewish prince who is betrayed and sent into slavery by a Roman friend and how he regains his freedom and comes back for revenge.", + "release_date": "1959-11-18", + "inventory": 5 + }, { + "id": 84, + "title": "Poltergeist", + "overview": "Craig T. Nelson stars as Steve Freeling, the main protagonist, who lives with his wife, Diane, (JoBeth Williams) and their three children, Dana (Dominique Dunne), Robbie (Oliver Robins), and Carol Anne (Heather O'Rourke), in Southern California where he sells houses for the company that built the neighborhood. It starts with just a few odd occurrences, such as broken dishes and furniture moving around by itself. However, a tree comes alive and takes Robbie through his bedroom window, and Carol Anne is abducted by ghosts. Realizing that something evil haunts his home, Steve calls in a team of parapsychologists led by Dr. Lesh (Beatrice Straight) to investigate, hoping to get Carol Anne back, so he can remove his family from the house before it's too late.", + "release_date": "1982-06-04", + "inventory": 4 + }, { + "id": 99, + "title": "Speed", + "overview": "Los Angeles SWAT cop Jack Traven is up against bomb expert Howard Payne, who's after major ransom money. First it's a rigged elevator in a very tall building. Then it's a rigged bus--if it slows, it will blow, bad enough any day, but a nightmare in LA traffic. And that's still not the end.", + "release_date": "1994-06-09", + "inventory": 10 + }], + "past_rentals": [{ + "dates": { + "returned_date": "07 Dec 2008 06:19:02", + "checkout_date": "03 Dec 2008 02:58:43" + }, + "movie_data": { + "id": 39, + "title": "Die Hard", + "overview": "NYPD cop John McClane's plan to reconcile with his estranged wife, Holly, is thrown for a serious loop when minutes after he arrives at her office, the entire building is overtaken by a group of pitiless terrorists. With little help from the LAPD, wisecracking McClane sets out to single-handedly rescue the hostages and bring the bad guys down.", + "release_date": "1988-07-14", + "inventory": 4 + } + }, { + "dates": { + "returned_date": "07 Dec 2009 06:19:02", + "checkout_date": "03 Dec 2009 02:58:43" + }, + "movie_data": { + "id": 72, + "title": "Platoon", + "overview": "Chris Taylor, a young, naive recruit in Vietnam, faces a moral crisis when confronted with the horrors of war and the duality of man.", + "release_date": "1986-12-18", + "inventory": 1 + } + }, { + "dates": { + "returned_date": "14 May 2011 02:58:43", + "checkout_date": "10 May 2011 02:58:43" + }, + "movie_data": { + "id": 68, + "title": "Halloween", + "overview": "A psychotic murderer institutionalized since childhood for the murder of his sister, escapes and stalks a bookish teenage girl and her friends while his doctor chases him through the streets.", + "release_date": "1978-10-25", + "inventory": 4 + } + }, { + "dates": { + "returned_date": "14 May 2011 02:58:43", + "checkout_date": "10 May 2011 02:58:43" + }, + "movie_data": { + "id": 100, + "title": "The Adventures of Robin Hood", + "overview": "Robin Hood (Errol Flynn) fights nobly for justice against the evil Sir Guy of Gisbourne (Basil Rathbone) while striving to win the hand of the beautiful Maid Marian (Olivia de Havilland).", + "release_date": "1938-05-14", + "inventory": 3 + } + }] + } +} +``` + +###Subset of Customers + +- **Endpoint:** GET `/customers/:sort_by/:limit/:offset` +- Sorts the entire set of customers by a certain property (`sort_by`), then retrieves a number (`limit`) of customers, starting at a certain index (`offset`). + - `sort_by` accepts `name` (customer name), `registered_at` (registration date), or `postal_code`. + - `limit` must be an integer >= 0. + - `offset` must be an integer >= 0. +- Returns an object with a `customers` property containing an array of customer objects. +- Each customer object contains the following properties: `id`, `name`, `registered_at` (date of registration), `address`, `city`, `state`, `postal_code`, `phone`, and `account_credit` (in cents). +- **Sample:** GET `/customers/name/2/2` +```json +{ + "customers": [{ + "id": 155, + "name": "Abigail Lara", + "registered_at": "Wed, 12 Aug 2015 03:21:43 -0700", + "address": "P.O. Box 388, 1190 Donec St.", + "city": "Shreveport", + "state": "Louisiana", + "postal_code": "41243", + "phone": "(235) 178-3417", + "account_credit": 8856 + }, { + "id": 46, + "name": "Acton Gilliam", + "registered_at": "Thu, 26 Feb 2015 20:00:53 -0800", + "address": "Ap #508-8214 Senectus Av.", + "city": "Portland", + "state": "Oregon", + "postal_code": "62594", + "phone": "(903) 973-1984", + "account_credit": 4864 + }] +} +``` + +##Movies + +###All Movies + +- **Endpoint:** GET `/movies` +- Retrieves a list of all movies. +- Returns an object with a `movies` property containing an array of movie objects. +- Each movie object contains the following properties: `id`, `title`, `overview`, `release_date`, and `inventory`. +- **Sample:** GET `/movies` +```json +{ + "movies": [{ + "id": 1, + "title": "Psycho", + "overview": "When larcenous real estate clerk Marion Crane goes on the lam with a wad of cash and hopes of starting a new life, she ends up at the notorious Bates Motel, where manager Norman Bates cares for his housebound mother. The place seems quirky, but fine… until Marion decides to take a shower.", + "release_date": "1960-06-16", + "inventory": 8 + }, { + "id": 2, + "title": "Jaws", + "overview": "An insatiable great white shark terrorizes the townspeople of Amity Island, The police chief, an oceanographer and a grizzled shark hunter seek to destroy the bloodthirsty beast.", + "release_date": "1975-06-19", + "inventory": 6 + }, { + "id": 3, + "title": "The Exorcist", + "overview": "12-year-old Regan MacNeil begins to adapt an explicit new personality as strange events befall the local area of Georgetown. Her mother becomes torn between science and superstition in a desperate bid to save her daughter, and ultimately turns to her last hope: Father Damien Karras, a troubled priest who is struggling with his own faith.", + "release_date": "1973-12-26", + "inventory": 7 + }, { + "id": 4, + "title": "North by Northwest", + "overview": "Madison Avenue advertising man Roger Thornhill finds himself thrust into the world of spies when he is mistaken for a man by the name of George Kaplan. Foreign spy Philip Vandamm and his henchman Leonard try to eliminate him but when Thornhill tries to make sense of the case, he is framed for murder. Now on the run from the police, he manages to board the 20th Century Limited bound for Chicago where he meets a beautiful blond, Eve Kendall, who helps him to evade the authorities. His world is turned upside down yet again when he learns that Eve isn't the innocent bystander he thought she was. Not all is as it seems however, leading to a dramatic rescue and escape at the top of Mt. Rushmore.", + "release_date": "1959-07-17", + "inventory": 10 + }, { + "id": 5, + "title": "The Silence of the Lambs", + "overview": "FBI trainee Clarice Starling ventures into a maximum-security asylum to pick the diseased brain of Hannibal Lecter, a psychiatrist turned homicidal cannibal. Starling needs clues to help her capture a serial killer. Unfortunately, her Faustian relationship with Lecter soon leads to his escape, and now two deranged killers are on the loose.", + "release_date": "1991-02-14", + "inventory": 3 + }] +} +``` + +###Single Movie + +- **Endpoint:** GET `/movies/:title/:order` +- `order` accepts `name` (customer name), `id` (customer id), and `checkout_date`. +- Retrieves data about the movie identified by the `title` passed in the URL, with past customers sorted in the `order` passed in the URL. +- Returns an object with `movie_data` and `customers` properties. + - `movie_data` contains the following properties: `id`, `title`, `overview`, `release_date`, and `inventory`. + - `customers` contains: `current_renters` and `past_renters`. + - `current_renters` is a list of customers who have an unreturned rental of the movie. + - Each customer object contains the following propeties: `id`, `name`, `registered_at` (date of registration), `address`, `city`, `state`, `postal_code`, `phone`, and `account_credit` (in cents). + - `past_renters` is a list of customer objects. + - Each customer object contains `customer_data` and `dates`. + - `dates` contains `checkout_date`. + - `customer_data` contains: `id`, `name`, `registered_at` (date of registration), `address`, `city`, `state`, `postal_code`, `phone`, and `account_credit` (in cents). +- **Sample:** GET `/movies/The Guns of Navarone/name` +```json +{ + "movie_data": { + "id": 89, + "title": "The Guns of Navarone", + "overview": "A team of allied saboteurs are assigned an impossible mission: infiltrate an impregnable Nazi-held island and destroy the two enormous long-range field guns that prevent the rescue of 2,000 trapped British soldiers.", + "release_date": "1961-06-22", + "inventory": 3 + }, + "customers": { + "current_renters": [{ + "id": 4, + "name": "Carolyn Chandler", + "registered_at": "Fri, 04 Jul 2014 11:05:11 -0700", + "address": "133-8707 Arcu. Avenue", + "city": "Fort Wayne", + "state": "IN", + "postal_code": "73684", + "phone": "(234) 837-2886", + "account_credit": 2079 + }], + "past_renters": [{ + "dates": { + "checkout_date": "2009-12-03T10:58:43.000Z" + }, + "customer_data": { + "id": 3, + "name": "Roanna Robinson", + "registered_at": "Fri, 28 Nov 2014 13:14:08 -0800", + "address": "Ap #561-4214 Eget St.", + "city": "Harrisburg", + "state": "PA", + "postal_code": "15867", + "phone": "(323) 336-1841", + "account_credit": 5039 + } + }, { + "dates": { + "checkout_date": "1999-10-03T07:25:08.000Z" + }, + "customer_data": { + "id": 2, + "name": "XCurran Stout", + "registered_at": "Wed, 16 Apr 2014 21:40:20 -0700", + "address": "Ap #658-1540 Erat Rd.", + "city": "San Francisco", + "state": "California", + "postal_code": "94267", + "phone": "(908) 949-6758", + "account_credit": 3565.9999999999995 + } + }] + } +} +``` + +### Subset of Movies +- **Endpoint:** GET `/movies/:sort_by/:limit/:offset` +- Sorts the entire set of movies by a certain property (`sort_by`), then retrieves a number (`limit`) of movies, starting at a certain index (`offset`). + - `sort_by` accepts `title` and `release_date`. + - `limit` must be an integer >= 0. + - `offset` must be an integer >= 0. +- Returns an object with a `movies` property containing an array of movie objects. +- Each movie object contains the following properties: `id`, `title`, `overview`, `release_date`, and `inventory`. +- **Sample:** GET `/movies/title/2/2` +```json +{ + "movies": [{ + "id": 21, + "title": "A Clockwork Orange", + "overview": "The head of a gang of toughs, in an insensitive futuristic society, is conditioned to become physically ill at sex and violence during a prison sentence. When he is released, he's brutally beaten by all of his old adversaries.", + "release_date": "1971-12-18", + "inventory": 4 + }, { + "id": 6, + "title": "Alien", + "overview": "During its return to the earth, commercial spaceship Nostromo intercepts a distress signal from a distant planet. When a three-member team of the crew discovers a chamber containing thousands of eggs on the planet, a creature inside one of the eggs attacks an explorer. The entire crew is unaware of the impending nightmare set to descend upon them when the alien parasite planted inside its unfortunate host is birthed.", + "release_date": "1979-05-25", + "inventory": 4 + }] +} +``` + +##Rentals + +###Overdue Rentals +- **Endpoint:** GET `/rentals/overdue` +- Retrieves a list of all customers with overdue rentals. +- Returns an object with an `overdue_customers` property that contains a list of customer objects. + - Each customer object contains the following properties: `id`, `name`, `registered_at` (date of registration), `address`, `city`, `state`, `postal_code`, `phone`, and `account_credit` (in cents). +- **Sample:** GET `/rentals/overdue` +```json + +{ + "overdue_customers": [{ + "id": 1, + "name": "Shelley Rocha", + "registered_at": "Wed, 29 Apr 2015 07:54:14 -0700", + "address": "Ap #292-5216 Ipsum Rd.", + "city": "Hillsboro", + "state": "OR", + "postal_code": "24309", + "phone": "(322) 510-8695", + "account_credit": 1315 + }, { + "id": 2, + "name": "XCurran Stout", + "registered_at": "Wed, 16 Apr 2014 21:40:20 -0700", + "address": "Ap #658-1540 Erat Rd.", + "city": "San Francisco", + "state": "California", + "postal_code": "94267", + "phone": "(908) 949-6758", + "account_credit": 3565.9999999999995 + }, { + "id": 3, + "name": "Roanna Robinson", + "registered_at": "Fri, 28 Nov 2014 13:14:08 -0800", + "address": "Ap #561-4214 Eget St.", + "city": "Harrisburg", + "state": "PA", + "postal_code": "15867", + "phone": "(323) 336-1841", + "account_credit": 5039 + }, { + "id": 4, + "name": "Carolyn Chandler", + "registered_at": "Fri, 04 Jul 2014 11:05:11 -0700", + "address": "133-8707 Arcu. Avenue", + "city": "Fort Wayne", + "state": "IN", + "postal_code": "73684", + "phone": "(234) 837-2886", + "account_credit": 2079 + }] +} +``` + +###Rental History of Single Movie +- **Endpoint:** GET `/rentals/:title` +- Retrieves data and rental history about the movie identified by the `title` passed in the URL. +- Returns an object with `movie_data`, `availability`, `current_renters` properties. + - `movie_data` contains the following properties: `id`, `title`, `overview`, `release_date`, and `inventory`. + - `availability` contains: `available` (boolean) and `copies_available` (integer). + - `current_renters` is a list of customers who are currently renting the movie. + - Each customer object contains the following propeties: `id`, `name`, `registered_at` (date of registration), `address`, `city`, `state`, `postal_code`, `phone`, and `account_credit` (in cents). +- **Sample:** GET `/rentals/jaws` +```json + +{ + "movie_data": { + "id": 2, + "title": "Jaws", + "overview": "An insatiable great white shark terrorizes the townspeople of Amity Island, The police chief, an oceanographer and a grizzled shark hunter seek to destroy the bloodthirsty beast.", + "release_date": "1975-06-19", + "inventory": 6 + }, + "availability": { + "available": true, + "copies_available": 5 + }, + "current_renters": [{ + "id": 1, + "name": "Shelley Rocha", + "registered_at": "Wed, 29 Apr 2015 07:54:14 -0700", + "address": "Ap #292-5216 Ipsum Rd.", + "city": "Hillsboro", + "state": "OR", + "postal_code": "24309", + "phone": "(322) 510-8695", + "account_credit": 1315 + }] +} +``` + +###Rent Movie +- **Endpoint:** POST `/rentals/checkout/:customer_id/:movie_title` +- Creates a rental record for the customer identified by the `customer_id` and the movie identified by the `movie_title`. + - The `checkout_date` is set to the current day. + - The `due_date` of the rental is set to 5 days from the current day. + - The `returned_date` is set to an empty string `''`. +- Charges the customer's account credit $1.00 (decrements by `100`). +- Returns an object with a `success` property. + - `success` contains the message: `"Yay! You checked out " + movie_title"` +- **Sample:** POST `/rentals/checkout/1/Jaws` +```json +{ + "success": "Yay! You checked out Jaws" +} +``` + +###Return Movie +- **Endpoint:** PUT `/rentals/checkin/:customer_id/:movie_title` +- Updates the first rental record associated with the `customer_id` and `movie_title` to include a `returned_date`. + - `returned_date` is set to the current day. +- Returns an object with a `success` property. + - `success` contains the message: `"Congratulations, you have checked in: " + movie_title` +- **Sample:** PUT `/rentals/checkin/1/jaws` +```json +{ + "success": "Congratulations, you have checked in: jaws" +} +``` + diff --git a/models/customer.js b/models/customer.js new file mode 100644 index 0000000..746bdbc --- /dev/null +++ b/models/customer.js @@ -0,0 +1,9 @@ +"use strict"; + +function Customer() { + this.table_name = "customers"; +} + +Customer.prototype = require('../database_adapter'); + +module.exports = Customer; diff --git a/models/movie.js b/models/movie.js new file mode 100644 index 0000000..12d7431 --- /dev/null +++ b/models/movie.js @@ -0,0 +1,9 @@ +"use strict"; + +function Movie() { + this.table_name = "movies"; +} + +Movie.prototype = require('../database_adapter'); + +module.exports = Movie; diff --git a/models/rental.js b/models/rental.js new file mode 100644 index 0000000..6753719 --- /dev/null +++ b/models/rental.js @@ -0,0 +1,9 @@ +"use strict"; + +function Rental() { + this.table_name = "rentals"; +} + +Rental.prototype = require('../database_adapter'); + +module.exports = Rental; diff --git a/package.json b/package.json new file mode 100644 index 0000000..bb0c0bd --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "C3Projects--VideoStoreAPI", + "version": "0.0.0", + "private": true, + "scripts": { + "start": "nodemon ./bin/www", + "test": "DB=test mocha --recursive", + "db:schema": "node ./utils/schema", + "db:seed": "node ./utils/seed", + "db:setup": "npm run db:schema; npm run db: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", + "sqlite3": "^3.1.0", + "supertest": "^1.1.0" + }, + "devDependencies": { + "should": "^7.1.0" + } +} diff --git a/pseudoRoutes.md b/pseudoRoutes.md new file mode 100644 index 0000000..ea95b5f --- /dev/null +++ b/pseudoRoutes.md @@ -0,0 +1,87 @@ +Customers +--------- + +### GET '/customers' +- returns an object of all customer objects +- no data provided + + +### GET '/customers/:sort_by/:number/:offset' +(GET '/customers?sort_by=:column&number=:number&offset=:offset') + +- returns select collection of customer objects +- how to tell route to stop at the & ?? +- columns are: name, registered_at, postal_code +- number is # of customer records to be returned +- offset is how far into the total records you want to start grabbing records + - can be used for pagination +- need to provide column name, number, and offset + +### GET '/customers/:id' +- returns an object for that customer, which has two objects: current and past + - each of current and past contain a collection of movie objects + - see resources card on Trello for image of architecture +- need to provide: customer id + + +Movies +------ +### GET '/movies' +- returns an object of all movie objects + +### GET '/movies/:sort_by/:number/:offset' +- returns select collection of movie objects +- columns are: title, release_date + +### GET '/movies/:title/:order' +- customer id: /movies/jaws&id=4 +- customer name: /movies/jaws&name=alice +- checkout date: /movies/jaws&date=20150922 (YYYYMMDD) +- returns an object for that movie, has two objects inside: current and past + - current contains a list of customers that have checked out the film + - past contains customers that have previously checked out the film + - :order - allows sorting by: customer id, customer name, or checkout date + +Rental +------ + +### GET '/rental/:title' +- returns a movie object based on title with synopsis, release date, and inventory + - info on availability: boolean + - object with list of customers that currently have that movie checked out + +### POST '/rental/checkout/:customer_id/:movie_id' (checkout) +- creates a record of rental movie object + +### PUT '/rental/checkin/:customer_id/:movie_id' (checkin) +- updates rental record, returned: to true + + +```javascript +var movies = [ + ['title', 'text'], + ['overview', 'text'], + ['release_date', 'text'], + ['inventory', 'integer'] +] + +var customers = [ + ['name', 'text'], + ['registered_at', 'text'], + ['address', 'text'], + ['city', 'text'], + ['state', 'text'], + ['postal_code', 'text'], + ['phone', 'text'], + ['account_credit', 'integer'] // multiplied by 100 to be stored as cents +] + +var rentals = [ + ['customer_id', 'integer'], + ['movie_id', 'integer'], + ['checkout_date', 'text'], + ['return_date', 'text'], + ['returned', 'text'] // boolean +] +``` + 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..8f3fbd8 --- /dev/null +++ b/routes/customers.js @@ -0,0 +1,102 @@ +var express = require('express'); +var router = express.Router(); +// var async = require('async'); + +var Customer = require('../models/customer'), + customer = new Customer(); + +var Rental = require('../models/rental'), + rental = new Rental(); + +var Movie = require('../models/movie'), + movie = new Movie(); + +router.get('/', function(req, res, next) { + customer.find_all(function(err, rows) { + if (rows) { + return res.status(200).json({ customers: rows }); + } else { + return res.status(400).json({ error: "No customers were found." }); + } + }); +}); + +router.get('/:id', function(req, res, next) { + var id = req.params.id; + var customer_info; + // var pastRentalsObject = {}; + + var customerObject = { + customer_data: undefined, + movies: {} + } + + customer.find_by('id', id, function(err, row) { + customerObject.customer_data = row; + + if (row == undefined) { + return res.status(403).json({ error: "Customer " + id + " does not exist." }); + } + + // use where to pull all records that meet the condition + rental.where(["customer_id"], [id], function(err, rows) { + var currentMoviesIDs = []; + var pastMoviesIDs = []; + var pastMovies = {}; + + for (var i = 0; i < rows.length; i++) { + // currently checked out movies + if (rows[i].returned_date == "") { + currentMoviesIDs.push(rows[i].movie_id); + // returned movies + } else { + pastMovies[rows[i].movie_id] = { + dates: { + returned_date: rows[i].returned_date, + checkout_date: rows[i].checkout_date + } + }; + + pastMoviesIDs.push(rows[i].movie_id); + } + } + + movie.where_in('id', currentMoviesIDs, function(err, rows) { + customerObject.movies.current_rentals = rows; // no returned_date + pastMoviesArray = []; + + movie.where_in('id', pastMoviesIDs, function(err, rows) { // unsorted + for (var i = 0; i < rows.length; i++) { + pastMovies[rows[i].id].movie_data = rows[i]; + pastMoviesArray.push(pastMovies[rows[i].id]); + } + + pastMoviesArray.sort(function(a, b) { + return a.dates.checkout_date.localeCompare(b.dates.checkout_date); // this is a good way to sort strings! + }); + + customerObject.movies.past_rentals = pastMoviesArray; + + return res.status(200).json(customerObject); + }); + }); + }); + }); +}); + +router.get('/:sort_by/:limit/:offset', function(req, res, next) { + var values = []; + values.push(req.params.limit); + values.push(req.params.offset); + var column = req.params.sort_by; + + customer.subset(column, values, function(err, rows) { + if (rows) { + return res.status(200).json({ customers: rows} ); + } else { + return res.status(400).json({ error: "No results found or your parameters are inaccurate. Try again." }); + } + }); +}); + +module.exports = router; diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..254eca5 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,13 @@ +var express = require('express'); +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + res.render('index', { title: 'Express' }); +}); + +router.get('/zomg', function(req, res, next) { + return res.status(200).json({ message: "it works!" }); +}); + +module.exports = router; diff --git a/routes/movies.js b/routes/movies.js new file mode 100644 index 0000000..3c1eb83 --- /dev/null +++ b/routes/movies.js @@ -0,0 +1,121 @@ +var express = require('express'); +var router = express.Router(); + +var Movie = require('../models/movie'), + movie = new Movie(); + +var Customer = require('../models/customer'), + customer = new Customer(); + +var Rental = require('../models/rental'), + rental = new Rental(); + +router.get('/', function(req, res, next) { + movie.find_all(function(err, rows) { + if (rows) { + return res.status(200).json({ movies: rows }); + } else { + return res.status(400).json({ error: "No movies were found." }); + } + }); +}); + +router.get('/:title/:order', function(req, res, next) { + var title = req.params.title; + var order = req.params.order; + + var movieObject = { + movie_data: undefined, + customers: { } + }; + + var movieId; + + movie.find_by('title', title, function(err, row) { + movieObject.movie_data = row; + title = title.charAt(0).toUpperCase() + title.slice(1); + + if (row == undefined) { + return res.status(400).json({ error: title + " was not found." }) + } + + movieId = row.id; + + rental.where(["movie_id"], [movieId], function(err, rows) { + var currentRentersIds = []; + var pastRentersIds = []; + var pastRenters = {}; + + for (var i = 0; i < rows.length; i++) { + + if (rows[i].returned_date == "") { + currentRentersIds.push(rows[i].customer_id); + } else { + var checkoutDate = new Date(rows[i].checkout_date); + pastRenters[rows[i].customer_id] = { + dates: { + checkout_date: checkoutDate + } + }; + + pastRentersIds.push(rows[i].customer_id); + } + } + + customer.where_in('id', currentRentersIds, function(err, rows) { + movieObject.customers.current_renters = rows; + var pastRentersArray = []; + + customer.where_in('id', pastRentersIds, function(err, rows) { + for (var i = 0; i < rows.length; i++) { + pastRenters[rows[i].id].customer_data = rows[i]; + pastRentersArray.push(pastRenters[rows[i].id]); + } + + // now sort the array by name, id or checkout date + if (order == "name") { + pastRentersArray.sort(function(a, b) { + return a.customer_data.name + .localeCompare(b.customer_data.name); + }); + } else if (order == "id") { + pastRentersArray.sort(function(a, b) { + return a.customer_data.id - b.customer_data.id; + }); + } else if (order == "checkout_date") { + pastRentersArray.sort(function(a, b) { + return a.dates.checkout_date - b.dates.checkout_date; + }); + } else { + var error_message = { error: "You cannot sort by " + order } + } + + movieObject.customers.past_renters = pastRentersArray; + + if (error_message) { + return res.status(400).json(error_message); + } else { + return res.status(200).json(movieObject); + } + }); + }); + }); + }); +}); + +router.get('/:sort_by/:limit/:offset', function(req, res, next) { + var queries = []; + queries.push(req.params.limit); + queries.push(req.params.offset); + var column = req.params.sort_by; + + movie.subset(column, queries, function(err, rows) { + if (rows) { + return res.status(200).json({ movies: rows} ); + } else { + return res.status(400).json({ error: "No results found or your parameters are inaccurate. Try again." }) + } + }); +}); + +module.exports = router; diff --git a/routes/rentals.js b/routes/rentals.js new file mode 100644 index 0000000..8beaaae --- /dev/null +++ b/routes/rentals.js @@ -0,0 +1,151 @@ +var express = require('express'); +var router = express.Router(); + +var Rental = require('../models/rental'), + rental = new Rental(); + +var Movie = require('../models/movie'), + movie = new Movie(); + +var Customer = require('../models/customer'), + customer = new Customer(); + +var RENTAL_PERIOD = 5; // 5 days + +var today = new Date(); + +router.get('/overdue', function(req, res, next) { + var overdueCustomerIds = []; + + rental.where(['returned_date'], [''], function(err, rows) { + for (var i = 0; i < rows.length; i++) { + var dueDate = new Date(rows[i].due_date); + if (dueDate < today) { // overdue! + overdueCustomerIds.push(rows[i].customer_id); + } + } + + customer.where_in("id", overdueCustomerIds, function(err, rows) { + if (rows) { + return res.status(200).json({ overdue_customers: rows }); + } else { + return res.status(400).json({ error: "No overdue customers were found." }); + } + }); + }); +}); + +router.get('/:title', function(request, response, next) { + var title = request.params.title; + var rentedCount; + var customerIdList = []; + var movieObject = {}; + + // query db for movie and get inventory + movie.find_by('title', title, function(error, row) { + movieObject.movie_data = row; + title = title.charAt(0).toUpperCase() + title.slice(1); + + if (row == undefined) { + return response.status(400).json({ error: title + " was not found."}); + } + + // retrieve rental records for that movie + rental.where(['movie_id', 'returned_date'], [movieObject.movie_data.id, ''], function(error, rows) { + // turn object of rentals into array of ids + for (var i = 0; i < rows.length; i++) { + customerIdList.push(rows[i].customer_id); + } + + rentedCount = customerIdList.length; + var inventory = movieObject.movie_data.inventory; + var availableBool = (rentedCount < inventory) ? true : false; + var availableCount = (rentedCount < inventory) ? (inventory - rentedCount) : 0; + movieObject.availability = { available: availableBool, copies_available: availableCount } + + customer.where_in('id', customerIdList, function(error, rows) { + movieObject.current_renters = rows; + response.status(200).json(movieObject); + }); + }); + }); +}); + +router.post('/checkout/:customer_id/:movie_title', function(request, response, next) { + var customer_id = request.params.customer_id; + var movie_title = request.params.movie_title; + var count, inventory, enoughInventory, movie_id, account_credit; + + movie.find_by('title', movie_title, function(err, row) { + movie_id = row.id; + + // count total # of checked out copies of movie with id, movie_id + rental.where(['movie_id', 'returned_date'], [movie_id, ''], function(err, rows) { + movieCount = rows.length; + + // check if enough inventory of movie is available (true/false) + movie.find_by('id', movie_id, function(err, row) { + // don't need inventory, could call row.inventory on line 29 + inventory = row.inventory; + enoughInventory = (movieCount < inventory) ? true : false; + + // if result, which equals enoughInventory, is false, return message NO + if (enoughInventory == false) { + response.status(403).json({ error: "There are no available copies of that movie for rental." }); + } else { // proceed with checkout + var values = []; + values.push(request.params.customer_id); + values.push(movie_id); + + var checkout_date = new Date(); + var due_date = new Date(); + due_date.setDate(due_date.getDate() + RENTAL_PERIOD); + + var defaults = [checkout_date, due_date, ""]; + values = values.concat(defaults); + + var columns = ['customer_id', 'movie_id', 'checkout_date', 'due_date', 'returned_date']; + + rental.create(columns, values, function(err, results) { + customer.find_by('id', customer_id, function(err, row) { + account_credit = row.account_credit; + var new_credit = account_credit - 100; + customer.update(customer_id, ['account_credit'], [new_credit], function(err, results) { + response.status(200).json({ success: "Yay! You checked out " + movie_title }); + }); + }); + }); + } + }); + }); + }); +}); + +router.put('/checkin/:customer_id/:movie_title', function(req, res, next) { + var customerId = req.params.customer_id; + var movieTitle = req.params.movie_title; + var movieId; + + // retrieve rental record which corresponds to customer id and movie title, + // AND which is not already returned + + // translate movie title to movie id + movie.find_by("title", movieTitle, function(err, row) { + movieId = row.id; + var columns = ["customer_id", "movie_id", "returned_date"]; + var values = [customerId, movieId, ""]; + + rental.where(columns, values, function(err, rows) { + var rentalId = rows[0].id; + + var message = "Congratulations, you have checked in: "; + + rental.update(rentalId, ["returned_date"], [today], function(err, result) { + res.status(200).json({ success: message + movieTitle }); + }) + }); + }); +}); + + +module.exports = router; diff --git a/test/models/customer.js b/test/models/customer.js new file mode 100644 index 0000000..3e62206 --- /dev/null +++ b/test/models/customer.js @@ -0,0 +1,134 @@ +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 ('Dana Scully', 'Wed, 16 Apr 2014 21:40:20 -0700', \ + 'P.O. Box 887, 4257 Lorem Rd.', 'Columbus', 'Ohio', '43201', \ + '(371) 627-1105', 1234), \ + ('Fox Mulder', 'Fri, 10 Jul 2015 15:23:06 -0700', '152-525 Odio St.', \ + 'Seattle', 'Washington', '98109', '(206) 329-4928', 293), \ + ('Alex Krychek', 'Fri, 10 Jul 2015 15:23:06 -0700', '152-525 Odio St.', \ + 'Seattle', 'Washington', '98109', '(206) 329-4928', 293); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }) + + it("can be instantiated", function() { + assert(customer instanceof Customer); + }) + + describe("instance methods", function() { + it("can find all customers", function(done){ + customer.find_all(function(err, res){ + assert.equal(err, undefined); + assert.equal(res.length, 3); + done(); + }); + }); + + it("can find a customer by id", function(done) { + customer.find_by("id", 1, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Object); + assert.equal(res.id, 1); + done(); + }); + }); + + it("can find a customer when given multiple columns and values", function(done) { + var columns = ['name', 'city', 'postal_code']; + var values = ['Fox Mulder', 'Seattle', '98109']; + customer.where(columns, values, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res[0].id, 2); + done(); + }); + }); + + it("can find all customers with specified column=value(s)", function(done) { + var column = 'city'; + var values = ['Seattle']; + customer.where_in(column, values, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 2); + assert.equal(res[0].name, 'Fox Mulder'); + assert.equal(res[1].name, 'Alex Krychek'); + done(); + }); + }); + + var sorted_by = ["name", "registered_at", "postal_code"]; + + for (var i = 0; i < sorted_by.length; i++) { + var column = sorted_by[i]; + + it("can return a subset of customers sorted by " + column, function(done) { + var queries = [1, 0]; + customer.subset(column, queries, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res[0].id, 1); + assert.equal(res[0].name, "Dana Scully") + done(); + }); + }); + } + }); // end of describe instance methods + + describe("class methods", function() { + it("can create a new customer in the database", function(done) { + var columns = ['name', 'registered_at', 'address', 'city', 'state', 'postal_code', 'phone']; + var values = ["Ratboy", "Wed, 24 Feb 2012 18:22:18 -0700", "55 Skinner Ave.", + "Vancouver", "BC", "93840", "(385) 948-9282"]; + + customer.create(columns, values, function(err, res) { + assert.equal(res.inserted_id, 4); //it inserted a new record + + customer.find_by("name", "Ratboy", function(err, res) { + assert.equal(res.name, 'Ratboy'); //we found our new customer + done(); + }); + }); + }); + + it("can update a customer record", function(done) { + var id = 2; + var columns = ['name', 'city', 'state']; + var values = ['Foxy Mulder', 'Victoria', 'BC']; + + customer.update(id, columns, values, function(err, res) { + assert.equal(res.changes, 1); + + customer.find_by("name", "Fox Mulder", function(err, res) { + assert.equal(res, undefined); //we can't find by old name + }); + + customer.find_by("name", "Foxy Mulder", function(err, res) { + assert.equal(res.name, "Foxy Mulder"); + done(); + }); + }); + }); + }); + +}); diff --git a/test/models/movie.js b/test/models/movie.js new file mode 100644 index 0000000..6e1b950 --- /dev/null +++ b/test/models/movie.js @@ -0,0 +1,141 @@ +var assert = require('assert'), + Movie = require('../../models/movie'), + 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) \ + VALUES('Jaws', 'Shark!', '2015', 10), \ + ('Paws', 'Cat!', '1989', 10), \ + ('Maws', 'Worm!', '2009', 11); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + it("can be instantiated", function() { + assert(movie instanceof Movie); + }); + + describe("instance methods", function() { + it("can find all movies", function(done){ + movie.find_all(function(err, res){ + assert.equal(err, undefined); + assert.equal(res.length, 3); + done(); + }); + }); + + it("can find a movie by id", function(done){ + movie.find_by("id", 1, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Object); + assert.equal(res.id, 1); + done(); + }); + }); + + it("can find a movie by title", function(done) { + movie.find_by("title", "Jaws", function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Object); + assert.equal(res.title, 'Jaws'); + done(); + }); + }); + + it("can find all movies where a column has a particular value", function(done) { + movie.where(["title"], ["Jaws"], function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Object); + assert.equal(res.length, 1); + assert.equal(res[0].title, "Jaws"); + done(); + }); + }); + + it("can find all movies with specified column=value(s)", function(done) { + var column = 'inventory'; + var values = ['10']; + movie.where_in(column, values, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res.length, 2); + assert.equal(res[0].title, 'Jaws'); + assert.equal(res[1].title, 'Paws'); + done(); + }); + }); + + it("can return a subset of movies sorted by title", function(done){ + var queries = [1, 0] // number, offset + movie.subset("title", queries, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res[0].id, 1); + assert.equal(res[0].title, "Jaws"); + done(); + }); + }); + + it("can return a subset of movies sorted by release_date", function(done){ + var queries = [1, 0] // number, offset + movie.subset("release_date", queries, function(err, res) { + assert.equal(err, undefined); + assert(res instanceof Array); + assert.equal(res[0].id, 2); + assert.equal(res[0].release_date, "1989"); + done(); + }); + }); + }); + + describe("class methods", function() { + it("can create a new movie in the database", function(done) { + var columns = ['title', 'overview', 'release_date', 'inventory']; + var values = ['The X-Files: Fight the Future', 'A supercool movie.', + '1998', 3]; + + movie.create(columns, values, function(err, res) { + assert.equal(res.inserted_id, 4); //it inserted a new record + + movie.find_by("title", "The X-Files: Fight the Future", function(err, res) { + assert.equal(res.title, "The X-Files: Fight the Future"); //we found our new movie + done(); + }); + }); + }); + + it("can update a movie record", function(done) { + var id = 3; + var columns = ['title', 'release_date']; + var values = ['The X-Files: I Want to Believe', '2008']; + + movie.update(id, columns, values, function(err, res) { + assert.equal(res.changes, 1); + + movie.find_by("title", "Maws", function(err, res) { + assert.equal(res, undefined); //we can't find by old name + }); + + movie.find_by("title", 'The X-Files: I Want to Believe', function(err, res) { + assert.equal(res.title, 'The X-Files: I Want to Believe'); + done(); + }); + }); + }); + }); +}); diff --git a/test/routes/customers.js b/test/routes/customers.js new file mode 100644 index 0000000..521167a --- /dev/null +++ b/test/routes/customers.js @@ -0,0 +1,243 @@ +var request = require('supertest'), + assert = require('assert'), + app = require('../../app'), + sqlite3 = require('sqlite3').verbose(), + agent = request.agent(app); + +describe("customers routes", 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 ('Alex Krychek', 'Wed, 16 Apr 2014 21:40:20 -0700', \ + 'P.O. Box 887, 4257 Lorem Rd.', 'Columbus', 'Ohio', '43201', \ + '(371) 627-1105', 1234), \ + ('Fox Mulder', 'Fri, 10 Jul 2015 15:23:06 -0700', '152-525 Odio St.', \ + 'Seattle', 'Washington', '98109', '(206) 329-4928', 293), \ + ('Walter Skinner', 'Fri, 10 Jul 2000 15:23:06 -0700', '456 Director Ln', \ + 'Washington', 'DC', '01234', '(234) 567-8901', 4000); \ + DELETE FROM movies; \ + INSERT INTO movies (title, overview, release_date, inventory) \ + VALUES ('Fight the Future', 'first xfiles movie', '1998', 2), \ + ('I Want to Believe', 'second xfiles movie', '2008', 4); \ + DELETE FROM rentals; \ + INSERT INTO rentals (customer_id, movie_id, checkout_date, due_date, \ + returned_date) \ + VALUES (1, 1, '2012', '2013', '2013'), \ + (1, 2, '2008', '2009', '2009'), \ + (1, 2, '2014', '2015', ''); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + describe("GET /customers", function() { + + it("responds with json", function(done) { + agent.get('/customers').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns an array of objects", function(done) { + agent.get('/customers').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var customers = response.body.customers; + + assert(customers instanceof Array); + done(); + }); + }); + + it("returns as many customers as there are in the table: 3", function(done) { + agent.get('/customers').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var customers = response.body.customers; + + assert.equal(customers.length, 3); + done(); + }); + }); + + it("the customer objects contain customer data", function(done) { + agent.get('/customers').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var customer = response.body.customers[0]; + + assert.equal(customer.name, "Alex Krychek"); + assert.equal(customer.registered_at, 'Wed, 16 Apr 2014 21:40:20 -0700'); + assert.equal(customer.address, 'P.O. Box 887, 4257 Lorem Rd.'); + assert.equal(customer.city, "Columbus"); + assert.equal(customer.state, "Ohio"); + assert.equal(customer.postal_code, "43201"); + assert.equal(customer.phone, "(371) 627-1105"); + assert.equal(customer.account_credit, 1234); + done(); + }); + }); + }); + + describe("GET /customers/:id", function() { + it("responds with json", function(done) { + agent.get('/customers/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns an object", function(done) { + agent.get('/customers/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var result = response.body; + + assert(result instanceof Object); + done(); + }); + }); + + it("returns an object that has customer_data and movies", function(done) { + agent.get('/customers/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var result = response.body; + var keys = Object.keys(result); + + assert.equal(keys.length, 2); + assert.equal(keys[0], "customer_data"); + assert.equal(keys[1], "movies") + done(); + }); + }); + + it("returns a list of movies currently checked out", function(done) { + agent.get('/customers/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var currentRentals = response.body.movies.current_rentals; + var movie = currentRentals[0]; + + assert.equal(currentRentals.length, 1); + assert.equal(movie.title, "I Want to Believe"); + assert.equal(movie.overview, "second xfiles movie"); + assert.equal(movie.release_date, "2008"); + assert.equal(movie.inventory, 4); + done(); + }); + }); + + it("returns a list of past rentals", function(done) { + agent.get('/customers/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var pastRentals = response.body.movies.past_rentals; + var movie = pastRentals[0].movie_data; + + assert.equal(pastRentals.length, 2); + assert.equal(movie.title, "I Want to Believe"); + assert.equal(movie.overview, "second xfiles movie"); + assert.equal(movie.release_date, "2008"); + assert.equal(movie.inventory, 4); + done(); + }); + }); + + it("sorts past rentals by checkout date", function(done) { + agent.get('/customers/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var pastRental1 = response.body.movies.past_rentals[0].movie_data; + var pastRental2 = response.body.movies.past_rentals[1].movie_data; + + assert.equal(pastRental1.title, "I Want to Believe"); + assert.equal(pastRental2.title, "Fight the Future"); + done(); + }); + }); + + it("includes return date for past rentals", function(done) { + agent.get('/customers/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var pastRentals = response.body.movies.past_rentals; + var movie1ReturnDate = pastRentals[0].dates.returned_date; + var movie2ReturnDate = pastRentals[1].dates.returned_date; + + assert.equal(movie1ReturnDate, 2009); + assert.equal(movie2ReturnDate, 2013); + done(); + }); + }); + }); + + describe("GET /customers/:sort_by/:number/:offset", function() { + it("responds with json", function(done) { + agent.get('/customers/name/1/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns an object", function(done) { + agent.get('/customers/name/1/1').set('Accept', 'application/json') + .expect(200, function(error, response) { + var customers = response.body; + assert(customers instanceof Object); + done(); + }); + }); + + it("returns the number of customers in the number parameter", function(done) { + agent.get('/customers/name/2/0').set('Accept', 'application/json') + .expect(200, function(error, response) { + var customers = response.body.customers; + + assert.equal(customers.length, 2); + done(); + }); + }); + + it("returns customers ordered, here by registered_at (date)", function(done) { + agent.get('/customers/registered_at/2/0').set('Accept', 'application/json') + .expect(200, function(error, response) { + var customers = response.body.customers; + + // this is the third customer by id, but the first by registered_at + assert(customers[0].name, "Walter Skinner"); + done(); + }); + }); + + it("returns customers starting from the id listed in the offset", function(done) { + agent.get('/customers/registered_at/1/1').set('Accept', 'application/json') + .expect(200, function(error, response) { + var customers = response.body.customers; + + // this is the second customer by registered_at + assert(customers[0].name, "Alex Krychek"); + done(); + }); + }); + }); +}); diff --git a/test/routes/movies.js b/test/routes/movies.js new file mode 100644 index 0000000..6ae3c0b --- /dev/null +++ b/test/routes/movies.js @@ -0,0 +1,194 @@ +var request = require('supertest'), + assert = require('assert'), + app = require('../../app'), + sqlite3 = require('sqlite3').verbose(), + agent = request.agent(app); + +describe("movies routes", 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 ('Alex Krychek', 'Wed, 16 Apr 2014 21:40:20 -0700', \ + 'P.O. Box 887, 4257 Lorem Rd.', 'Columbus', 'Ohio', '43201', \ + '(371) 627-1105', 1234), \ + ('Fox Mulder', 'Fri, 10 Jul 2015 15:23:06 -0700', '152-525 Odio St.', \ + 'Seattle', 'Washington', '98109', '(206) 329-4928', 293); \ + DELETE FROM movies; \ + INSERT INTO movies (title, overview, release_date, inventory) \ + VALUES ('Fight the Future', 'first xfiles movie', '1998', 2), \ + ('I Want to Believe', 'second xfiles movie', '2008', 4), \ + ('A Movie', 'some plot', '2009', 3); \ + DELETE FROM rentals; \ + INSERT INTO rentals (customer_id, movie_id, checkout_date, due_date, \ + returned_date) \ + VALUES (1, 1, '2012', '2013', '2013'), \ + (1, 2, '2008', '2009', '2009'), \ + (1, 2, '2014', '2015', ''), \ + (2, 1, '2005', '2006', '2006'), \ + (2, 1, '2015', '2016', ''); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + describe("GET /movies", function() { + + it("responds with json", function(done) { + agent.get('/movies').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns an array of objects", function(done) { + agent.get('/movies').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movies = response.body.movies; + + assert(movies instanceof Array); + done(); + }); + }); + + it("returns as many movies as there are in the table: 3", function(done) { + agent.get('/movies').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movies = response.body.movies; + + assert.equal(movies.length, 3); + done(); + }); + }); + + it("the movie objects contain movie data", function(done) { + agent.get('/movies').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movie = response.body.movies[0]; + + // 'Fight the Future', 'first xfiles movie', '1998', 2 + assert.equal(movie.title, "Fight the Future"); + assert.equal(movie.overview, 'first xfiles movie'); + assert.equal(movie.release_date, '1998'); + assert.equal(movie.inventory, 2); + done(); + }); + }); + }); + + describe("GET /movies/:title/:order", function() { + it("responds with json", function(done) { + agent.get('/movies/Fight the Future/id').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns an object", function(done) { + agent.get('/movies/Fight the Future/id').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movie = response.body; + + assert(movie instanceof Object); + done(); + }); + }); + + it("returns the movie with the title from the url", function(done) { + agent.get('/movies/Fight the Future/id').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movieData = response.body.movie_data; + + assert.equal(movieData.title, 'Fight the Future'); + done(); + }); + }); + + it("returns a currentRenters object with a list of current renters", function(done) { + agent.get('/movies/Fight the Future/id').set('Accept', 'application/json') + .expect(200, function(error, response) { + var currentRenters = response.body.customers.current_renters; + + assert(currentRenters[0].name, 'Fox Mulder'); + done(); + }); + }); + + it("returns a pastRenters object with a list of past renters sorted by the order variable", function(done) { + agent.get('/movies/Fight the Future/id').set('Accept', 'application/json') + .expect(200, function(error, response) { + var pastRenters = response.body.customers.past_renters; + + assert(pastRenters[0].customer_data.name, 'Alex Krychek'); + done(); + }); + }); + }); + +describe("GET /movies/:sort_by/:number/:offset", function() { + it("responds with json", function(done) { + agent.get('/movies/title/1/1').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns an object", function(done) { + agent.get('/movies/title/1/1').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movies = response.body; + assert(movies instanceof Object); + done(); + }); + }); + + it("returns the number of movies in the number parameter", function(done) { + agent.get('/movies/title/2/0').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movies = response.body.movies; + + assert.equal(movies.length, 2); + done(); + }); + }); + + it("returns movies ordered alphabetically", function(done) { + agent.get('/movies/title/1/0').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movies = response.body.movies; + + // this is the third movie by id, but the first alphabetically + assert(movies[0].title, "A Movie"); + done(); + }); + }); + + it("returns movies starting from the id listed in the offset", function(done) { + agent.get('/movies/title/1/1').set('Accept', 'application/json') + .expect(200, function(error, response) { + var movies = response.body.movies; + + // this is the second movie alphabetically + assert(movies[0].title, "Fight the Future"); + done(); + }); + }); + }); +}); diff --git a/test/routes/rentals.js b/test/routes/rentals.js new file mode 100644 index 0000000..47c4a9b --- /dev/null +++ b/test/routes/rentals.js @@ -0,0 +1,248 @@ +var request = require('supertest'), + assert = require('assert'), + app = require('../../app'), + sqlite3 = require('sqlite3').verbose(), + agent = request.agent(app); + +var Rental = require('../../models/rental'); + +describe("rentals routes", function() { + var db_cleaner, rental; + + beforeEach(function(done) { + db_cleaner = new sqlite3.Database('db/test.db'); + rental = new Rental(); + + 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 ('Alex Krychek', 'Wed, 16 Apr 2014 21:40:20 -0700', \ + 'P.O. Box 887, 4257 Lorem Rd.', 'Columbus', 'Ohio', '43201', \ + '(371) 627-1105', 1234), \ + ('Fox Mulder', 'Fri, 10 Jul 2015 15:23:06 -0700', '152-525 Odio St.', \ + 'Seattle', 'Washington', '98109', '(206) 329-4928', 293), \ + ('Dana Scully', 'Fri, 20 Jul 2015 15:23:06 -0700', '234 Piper St.', \ + 'Tulsa', 'Oklahoma', '34566', '(206) 329-4928', 2000); \ + DELETE FROM movies; \ + INSERT INTO movies (title, overview, release_date, inventory) \ + VALUES ('Fight the Future', 'first xfiles movie', '1998', 2), \ + ('I Want to Believe', 'second xfiles movie', '2008', 4); \ + DELETE FROM rentals; \ + INSERT INTO rentals (customer_id, movie_id, checkout_date, due_date, \ + returned_date) \ + VALUES (1, 1, '2012', '2013', '2013'), \ + (1, 2, '2008', '2009', '2009'), \ + (1, 2, '2014', '2015', ''), \ + (2, 1, '2005', '2006', '2006'), \ + (2, 1, '2015', '2016', ''), \ + (2, 3, '2013', '2014', ''), \ + (1, 2, '1989', '1990', ''), \ + (3, 1, '1991', '1992', '1991'); \ + COMMIT;" + , function(err) { + db_cleaner.close(); + done(); + } + ); + }); + }); + + describe("GET /rentals/overdue", function() { + it("responds with json", function(done) { + agent.get('/rentals/overdue').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns an array of objects", function(done) { + agent.get('/rentals/overdue').set('Accept', 'application/json') + .expect(200, function(error, response) { + var overdueCustomers = response.body.overdue_customers; + + assert(overdueCustomers instanceof Array); + done(); + }); + }); + + it("returns a list of the customers with overdue books", function(done) { + agent.get('/rentals/overdue').set('Accept', 'application/json') + .expect(200, function(error, response) { + var overdueCustomers = response.body.overdue_customers; + + // only two of the three customers with rental records have overdue + assert.equal(overdueCustomers.length, 2); + done(); + }); + }); + }); + + describe('GET /rentals/:title', function() { + it("responds with json", function(done) { + agent.get('/rentals/Fight the Future').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns an object", function(done) { + agent.get('/rentals/Fight the Future').set('Accept', 'application/json') + .expect(200, function(error, response) { + assert(response.body instanceof Object); + done(); + }); + }); + + it("returns movie info: overview, release_date, inventory", function(done) { + agent.get('/rentals/Fight the Future').set('Accept', 'application/json') + .expect(200, function(error, response) { + assert.equal(response.body.movie_data.overview, "first xfiles movie"); + assert.equal(response.body.movie_data.release_date, "1998"); + assert.equal(response.body.movie_data.inventory, 2); + done(); + }); + }); + + it("returns availability info: yes/no, and copies available", function(done) { + agent.get('/rentals/Fight the Future').set('Accept', 'application/json') + .expect(200, function(error, response) { + assert.equal(response.body.availability.available, true); + assert.equal(response.body.availability.copies_available, 1); + done(); + }); + }); + + it("returns a list of customers who have currently rented the movie", function(done) { + agent.get('/rentals/Fight the Future').set('Accept', 'application/json') + .expect(200, function(error, response) { + assert(response.body.current_renters instanceof Object); + assert.equal(response.body.current_renters[0].name, "Fox Mulder"); + done(); + }); + }); + }); + + describe("PUT /rentals/checkin/:customer_id/:movie_title", function() { + it("responds with json", function(done) { + agent.put('/rentals/checkin/2/Fight the Future').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + assert.equal(error, undefined); + done(); + }); + }); + + it("returns a message that you checked in that movie", function(done) { + agent.put('/rentals/checkin/2/Fight the Future').set('Accept', 'application/json') + .expect(200, function(error, response) { + assert.equal(response.body.success, "Congratulations, you have checked in: Fight the Future"); + done(); + }); + }); + + it("updates the rental record with the returned_date", function(done) { + // this joins movies and rentals on the movie_id + // and selects all those records with the customer ID and movie title + // from the URI, which have *not* been returned + var statement = + "SELECT * FROM rentals INNER JOIN movies \ + ON rentals.movie_id = movies.id \ + WHERE movies.title = ? \ + AND rentals.customer_id = ? \ + AND rentals.returned_date = '';"; + + var values = ['Fight the Future', 2]; + var rentalsBeforeCheckin, + rentalsAfterCheckin; + + var db = new sqlite3.Database('db/test.db'); + + db.all(statement, values, function(err, rows) { + rentalsBeforeCheckin = rows; + db.close(); + + agent.put('/rentals/checkin/2/Fight the Future').set('Accept', 'application/json') + .expect(200, function(error, response) { + var db = new sqlite3.Database('db/test.db'); + + db.all(statement, values, function(err, rows) { + rentalsAfterCheckin = rows; + db.close(); + + // check to make sure the number of un-returned movies has decreased by 1 + assert(rentalsBeforeCheckin.length - rentalsAfterCheckin.length == 1); + done(); + }); + }); + }); + }); + }); + + describe("POST /rentals/checkout/:customer_id/:movie_title", function() { + it("returns a message that you checked out a movie", function(done) { + agent.post('/rentals/checkout/1/Fight the Future').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + var result = response.body; + + assert.equal(error, undefined); + assert.equal(result.success, 'Yay! You checked out Fight the Future') + done(); + }); + }); + + it("adds a rental record to the rentals table", function(done) { + agent.post('/rentals/checkout/1/Fight the Future').set('Accept', 'application/json') + .expect('Content-Type', /application\/json/) + .expect(200, function(error, response) { + + // there were 8 rental records seeded prior to creating the new rental + rental.find_by("id", 9, function(err, res) { + assert.equal(res.movie_id, 1); + assert.equal(res.customer_id, 1); + done(); + }); + }); + }); + + it("decrements the account_credit for each customer by $1.00", function(done) { + var statement = + "SELECT * FROM customers \ + WHERE id = ?;"; + + var values = [1]; + var creditBeforeCheckout, + creditAfterCheckout; + + var db = new sqlite3.Database('db/test.db'); + + // get account_credit before checking out a movie + db.all(statement, values, function(err, row) { + creditBeforeCheckout = row[0].account_credit; + db.close(); + + agent.post('/rentals/checkout/1/Fight the Future').set('Accept', 'application/json') + .expect(200, function(error, response) { + var db = new sqlite3.Database('db/test.db'); + + // get account_credit after checking out a movie + db.all(statement, values, function(err, row) { + creditAfterCheckout = row[0].account_credit; + db.close(); + + // check to make sure the account_credit has decreased by 100 ($1.00) + assert(creditBeforeCheckout - 100 == creditAfterCheckout); + done(); + }); + }); + }); + }); + }); +}); diff --git a/customers.json b/utils/customers.json similarity index 99% rename from customers.json rename to utils/customers.json index a615fdd..467eb32 100644 --- a/customers.json +++ b/utils/customers.json @@ -12,7 +12,7 @@ }, { "id": "2", - "name": "Curran Stout", + "name": "XCurran Stout", "registered_at": "Wed, 16 Apr 2014 21:40:20 -0700", "address": "Ap #658-1540 Erat Rd.", "city": "San Francisco", 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..7101ad2 --- /dev/null +++ b/utils/rentals.json @@ -0,0 +1,233 @@ +[ + { + "customer_id": 1, + "movie_id": 5, + "checkout_date": "29 Apr 2015 07:54:14 -0700", + "due_date": "3 May 2015 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 1, + "movie_id": 49, + "checkout_date": "01 Nov 2004 06:19:02", + "due_date": "06 Nov 2004 18:19:02", + "returned_date": "" + }, + { + "customer_id": 1, + "movie_id": 72, + "checkout_date": "03 Dec 2009 02:58:43", + "due_date": "08 Dec 2009 06:19:02", + "returned_date": "07 Dec 2009 06:19:02" + }, + { + "customer_id": 1, + "movie_id": 100, + "checkout_date": "10 May 2011 02:58:43", + "due_date": "15 May 2011 02:58:43", + "returned_date": "14 May 2011 02:58:43" + }, + { + "customer_id": 1, + "movie_id": 99, + "checkout_date": "03 Oct 2012 00:25:08", + "due_date": "11 Oct 2012 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 1, + "movie_id": 2, + "checkout_date": "29 Apr 2015 07:54:14 -0700", + "due_date": "3 May 2015 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 1, + "movie_id": 3, + "checkout_date": "01 Nov 2004 06:19:02", + "due_date": "06 Nov 2004 18:19:02", + "returned_date": "" + }, + { + "customer_id": 1, + "movie_id": 39, + "checkout_date": "03 Dec 2008 02:58:43", + "due_date": "08 Dec 2008 06:19:02", + "returned_date": "07 Dec 2008 06:19:02" + }, + { + "customer_id": 1, + "movie_id": 68, + "checkout_date": "10 May 2011 02:58:43", + "due_date": "15 May 2011 02:58:43", + "returned_date": "14 May 2011 02:58:43" + }, + { + "customer_id": 2, + "movie_id": 28, + "checkout_date": "03 Oct 2012 00:25:08", + "due_date": "11 Oct 2012 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 2, + "movie_id": 44, + "checkout_date": "29 Apr 2015 07:54:14 -0700", + "due_date": "3 May 2015 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 2, + "movie_id": 41, + "checkout_date": "01 Nov 2004 06:19:02", + "due_date": "06 Nov 2004 18:19:02", + "returned_date": "" + }, + { + "customer_id": 3, + "movie_id": 89, + "checkout_date": "03 Dec 2009 02:58:43", + "due_date": "08 Dec 2009 06:19:02", + "returned_date": "07 Dec 2009 06:19:02" + }, + { + "customer_id": 2, + "movie_id": 50, + "checkout_date": "10 May 2011 02:58:43", + "due_date": "15 May 2011 02:58:43", + "returned_date": "" + }, + { + "customer_id": 2, + "movie_id": 51, + "checkout_date": "03 Oct 2012 00:25:08", + "due_date": "11 Oct 2012 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 3, + "movie_id": 29, + "checkout_date": "29 Apr 2015 07:54:14 -0700", + "due_date": "3 May 2015 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 3, + "movie_id": 5, + "checkout_date": "01 Nov 2004 06:19:02", + "due_date": "06 Nov 2004 18:19:02", + "returned_date": "" + }, + { + "customer_id": 3, + "movie_id": 16, + "checkout_date": "03 Dec 2009 02:58:43", + "due_date": "08 Dec 2009 06:19:02", + "returned_date": "07 Dec 2009 06:19:02" + }, + { + "customer_id": 3, + "movie_id": 68, + "checkout_date": "10 May 2011 02:58:43", + "due_date": "15 May 2011 02:58:43", + "returned_date": "14 May 2011 02:58:43" + }, + { + "customer_id": 2, + "movie_id": 89, + "checkout_date": "03 Oct 1999 00:25:08", + "due_date": "11 Oct 1999 07:54:14 -0700", + "returned_date": "10 Oct 1999 07:54:14 -0700" + }, + { + "customer_id": 4, + "movie_id": 89, + "checkout_date": "29 Apr 2015 07:54:14 -0700", + "due_date": "3 May 2015 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 4, + "movie_id": 74, + "checkout_date": "01 Nov 2004 06:19:02", + "due_date": "06 Nov 2004 18:19:02", + "returned_date": "" + }, + { + "customer_id": 4, + "movie_id": 54, + "checkout_date": "03 Dec 2009 02:58:43", + "due_date": "08 Dec 2009 06:19:02", + "returned_date": "07 Dec 2009 06:19:02" + }, + { + "customer_id": 4, + "movie_id": 19, + "checkout_date": "10 May 2011 02:58:43", + "due_date": "15 May 2011 02:58:43", + "returned_date": "14 May 2011 02:58:43" + }, + { + "customer_id": 4, + "movie_id": 87, + "checkout_date": "03 Oct 2012 00:25:08", + "due_date": "11 Oct 2012 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 4, + "movie_id": 25, + "checkout_date": "29 Apr 2015 07:54:14 -0700", + "due_date": "3 May 2015 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 4, + "movie_id": 99, + "checkout_date": "19 Sep 2015 06:19:02", + "due_date": "24 Sep 2004 18:19:02", + "returned_date": "" + }, + { + "customer_id": 4, + "movie_id": 85, + "checkout_date": "03 Dec 2009 02:58:43", + "due_date": "08 Dec 2009 06:19:02", + "returned_date": "07 Dec 2009 06:19:02" + }, + { + "customer_id": 4, + "movie_id": 13, + "checkout_date": "10 May 2011 02:58:43", + "due_date": "15 May 2011 02:58:43", + "returned_date": "14 May 2011 02:58:43" + }, + { + "customer_id": 4, + "movie_id": 14, + "checkout_date": "03 Oct 2012 00:25:08", + "due_date": "11 Oct 2012 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 4, + "movie_id": 29, + "checkout_date": "29 Apr 2015 07:54:14 -0700", + "due_date": "3 May 2015 07:54:14 -0700", + "returned_date": "" + }, + { + "customer_id": 4, + "movie_id": 5, + "checkout_date": "01 Nov 2004 06:19:02", + "due_date": "06 Nov 2004 18:19:02", + "returned_date": "" + }, + { + "customer_id": 4, + "movie_id": 82, + "checkout_date": "03 Dec 2009 02:58:43", + "due_date": "08 Dec 2009 06:19:02", + "returned_date": "07 Dec 2009 06:19:02" + } +] diff --git a/utils/schema.js b/utils/schema.js new file mode 100644 index 0000000..47c2a2c --- /dev/null +++ b/utils/schema.js @@ -0,0 +1,55 @@ +"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'] +]; + +var customer_fields = [ + ['name', 'text'], + ['registered_at', 'text'], + ['address', 'text'], + ['city', 'text'], + ['state', 'text'], + ['postal_code', 'text'], + ['phone', 'text'], + ['account_credit', 'integer'] // multiplied by 100 to be stored as cents +]; + +var rental_fields = [ + ['customer_id', 'integer'], + ['movie_id', 'integer'], + ['checkout_date', 'text'], + ['due_date', 'text'], + ['returned_date', 'text'] +]; + +function reset(table_name, table_fields) { + db.serialize(function() { + db.run("DROP TABLE IF EXISTS " + table_name + ";"); + + // create fresh versions of those tables + db.run("CREATE TABLE " + table_name + " (id INTEGER PRIMARY KEY);"); + + // add columns that I need to those tables + for (var i = 0; i < table_fields.length; i++) { + var name = table_fields[i][0], + type = table_fields[i][1]; + + // ALTER TABLE movies ADD COLUMN title text; + db.run("ALTER TABLE " + table_name + " ADD COLUMN " + name + " " + type + ";"); + }; + }); +} + +reset("movies", movie_fields); +reset("customers", customer_fields); +reset("rentals", rental_fields); + +db.close(); diff --git a/utils/seed.js b/utils/seed.js new file mode 100644 index 0000000..3765f0f --- /dev/null +++ b/utils/seed.js @@ -0,0 +1,73 @@ +"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'); +var movie_statement = db.prepare( + "INSERT INTO movies (title, overview, inventory, release_date) \ + VALUES (?, ?, ?, ?);" +); + +var customers = require('./customers'); +var customer_statement = db.prepare( + "INSERT INTO customers (name, registered_at, address, city, state, \ + postal_code, phone, account_credit) VALUES (?, ?, ?, ?, ?, ?, ?, ?);" +); + +var rentals = require('./rentals'); +var rental_statement = db.prepare( + "INSERT INTO rentals(customer_id, movie_id, checkout_date, due_date, returned_date) \ + VALUES (?, ?, ?, ?, ?);" + ); + +db.serialize(function() { + // loop thru 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(); + + for (var i = 0; i < customers.length; i++) { + var customer = customers[i]; + + customer_statement.run( + customer.name, + customer.registered_at, + customer.address, + customer.city, + customer.state, + customer.postal_code, + customer.phone, + customer.account_credit * 100 // convert for storage as integer + ); + } + + customer_statement.finalize(); + + for (var i = 0; i < rentals.length; i++) { + var rental = rentals[i]; + + rental_statement.run( + rental.customer_id, + rental.movie_id, + rental.checkout_date, + rental.due_date, + rental.returned_date + ); + } + + 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