diff --git a/app.rb b/app.rb new file mode 100644 index 00000000..e14068b1 --- /dev/null +++ b/app.rb @@ -0,0 +1,83 @@ +# # file: app.rb +require_relative './lib/item_repository' +require_relative './lib/order_repository' +require_relative 'lib/database_connection' + +class Application + + def initialize(database_name, io, item_repository, order_repository) + DatabaseConnection.connect(database_name) + @io = io + @item_repository = item_repository + @order_repository = order_repository + end + + def run(item_repository = ItemRepository.new, order_repository = OrderRepository.new) + @item_repository = item_repository + @order_repository = order_repository + + @io.puts "Welcome to the shop management program!" + @io.puts "What do you want to do? + \n1 = list all shop items\n2 = create a new item + \n3 = list all orders\n4 = create a new order" + user_choice = @io.gets.chomp + while true do + case user_choice + when "1" + @io.puts "Here is the list of all items:" + @item_repository.all.each do |item| + @io.puts "##{item.id} #{item.name} - Unit price: #{item.price} - Quantity: #{item.quantity}" + end + break + when "2" + item = Item.new + @io.puts "Please enter new item name." + item.name = @io.gets.chomp + @io.puts "Please enter new item price." + item.price = @io.gets.chomp + @io.puts "Please enter new item quantity." + item.quantity = @io.gets.chomp + item_repository = ItemRepository.new + item_repository.create(item) + break + when "3" + @io.puts "Here is the list of all orders:" + @order_repository.all.each do |order| + @io.puts "##{order.id} #{order.customer_name} - Date: #{order.date}" + end + break + when "4" + order = Order.new + @io.puts "Please enter customer name." + order.customer_name = @io.gets.chomp + @io.puts "Please enter date." + order.date = @io.gets.chomp + order_repository = OrderRepository.new + order_repository.create(order) + break + + + else + @io.puts "Please choose 1, 2, 3 or 4." + user_choice = @io.gets.chomp + end + end + + end + +end + +# Don't worry too much about this if statement. It is basically saying "only +# run the following code if this is the main file being run, instead of having +# been required or loaded by another file. +# If you want to learn more about __FILE__ and $0, see here: https://en.wikibooks.org/wiki/Ruby_Programming/Syntax/Variables_and_Constants#Pre-defined_Variables +if __FILE__ == $0 + app = Application.new( + 'shop_manager', + Kernel, + ItemRepository.new, + OrderRepository.new + ) + app.run +end + diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 00000000..c8f1ad1d --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,28 @@ +# file: lib/database_connection.rb + +require 'pg' + +# This class is a thin "wrapper" around the +# PG library. We'll use it in our project to interact +# with the database using SQL. + +class DatabaseConnection + # This method connects to PostgreSQL using the + # PG gem. We connect to 127.0.0.1, and select + # the database name given in argument. + def self.connect(database_name) + @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) + end + + # This method executes an SQL query + # on the database, providing some optional parameters + # (you will learn a bit later about when to provide these parameters). + def self.exec_params(query, params) + if @connection.nil? + raise 'DatabaseConnection.exec_params: Cannot run a SQL query as the connection to'\ + 'the database was never opened. Did you make sure to call first the method '\ + '`DatabaseConnection.connect` in your app.rb file (or in your tests spec_helper.rb)?' + end + @connection.exec_params(query, params) + end +end \ No newline at end of file diff --git a/lib/item.rb b/lib/item.rb new file mode 100644 index 00000000..29819ed1 --- /dev/null +++ b/lib/item.rb @@ -0,0 +1,4 @@ +class Item + + attr_accessor :id, :name, :price, :quantity +end \ No newline at end of file diff --git a/lib/item_repository.rb b/lib/item_repository.rb new file mode 100644 index 00000000..45657c27 --- /dev/null +++ b/lib/item_repository.rb @@ -0,0 +1,63 @@ +require_relative './item' + +class ItemRepository + def all + # shows all items + sql = 'SELECT id, name, price, quantity FROM items;' + result = DatabaseConnection.exec_params(sql, []) + + items = [] + + result.each do |record| + items << record_to_item(record) + end + + return items + end + + def find(id) + sql = 'SELECT id, name, price, quantity FROM items WHERE id = $1;' + sql_params = [id] + + result = DatabaseConnection.exec_params(sql, sql_params) + record = result[0] + + return record_to_item(record) + + end + + def create(item) + sql = 'INSERT INTO items + (name, price, quantity) + VALUES($1, $2, $3);' + sql_params = [item.name, item.price, item.quantity] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + def delete(id) + sql = 'DELETE FROM items + WHERE id = $1;' + sql_params = [id] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + private + + def record_to_item(record) + item = Item.new + item.id = record['id'] + item.name = record['name'] + item.price = record['price'] + item.quantity = record['quantity'] + + return item + end + + +end \ No newline at end of file diff --git a/lib/order.rb b/lib/order.rb new file mode 100644 index 00000000..c1b9cb1b --- /dev/null +++ b/lib/order.rb @@ -0,0 +1,5 @@ +class Order + + attr_accessor :id, :customer_name, :date, :items + @items = [] +end \ No newline at end of file diff --git a/lib/order_repository.rb b/lib/order_repository.rb new file mode 100644 index 00000000..adc8ca07 --- /dev/null +++ b/lib/order_repository.rb @@ -0,0 +1,75 @@ +require_relative './order' + +class OrderRepository + def all + sql = 'SELECT id, customer_name, date FROM orders' + result = DatabaseConnection.exec_params(sql, []) + + orders = [] + + result.each do |record| + + # order = Order.new + # order.id = record['id'] + # order.customer_name = record['customer_name'] + # order.date = record['date'] + + orders << record_to_order(record) + end + + return orders + end + + def find(id) + sql = 'SELECT id, customer_name, date FROM orders WHERE id = $1;' + sql_params = [id] + + result = DatabaseConnection.exec_params(sql, sql_params) + record = result[0] + + return record_to_order(record) + + end + + def create(order) + sql = 'INSERT INTO orders + (customer_name, date) + VALUES($1, $2);' + sql_params = [order.customer_name, order.date] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + def delete(id) + sql = 'DELETE FROM orders + WHERE id = $1;' + sql_params = [id] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + def find_with_items(id) + sql = 'SELECT items.id, items.name, items.price + FROM items + JOIN items_orders ON items_orders.item_id = items.id + JOIN orders ON items_orders.order_id = orders.id + WHERE orders.id = $1;' + sql_params = [id] + result = DatabaseConnection.exec_params(sql, sql_params) + end + + private + def record_to_order(record) + order = Order.new + order.id = record['id'] + order.customer_name = record['customer_name'] + order.date = record['date'] + return order + end + + +end diff --git a/recipes/item_repository_class_recipe.md b/recipes/item_repository_class_recipe.md new file mode 100644 index 00000000..7ee5bed7 --- /dev/null +++ b/recipes/item_repository_class_recipe.md @@ -0,0 +1,314 @@ +# Item Model and Repository Classes Design Recipe + +_Copy this recipe template to design and implement Model and Repository classes for a database table._ + +## 1. Design and create the Table + +If the table is already created in the database, you can skip this step. + +Otherwise, [follow this recipe to design and create the SQL schema for your table](./single_table_design_recipe_template.md). + +*In this template, we'll use an example table `students`* + +``` +# EXAMPLE + +Table: students + +Columns: +id | name | cohort_name +``` + +## 2. Create Test SQL seeds + +Your tests will depend on data stored in PostgreSQL to run. + +If seed data is provided (or you already created it), you can skip this step. + +```sql +-- EXAMPLE +-- (file: spec/seeds_{table_name}.sql) + +-- Write your SQL seed here. +DROP TABLE IF EXISTS items CASCADE; + +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + price numeric, + quantity int +); + +DROP TABLE IF EXISTS orders CASCADE; +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + date date +); + + +DROP TABLE IF EXISTS items_orders CASCADE; +-- Create the join table. +CREATE TABLE items_orders ( + item_id int, + order_id int, + constraint fk_post foreign key(item_id) references items(id) on delete cascade, + constraint fk_tag foreign key(order_id) references orders(id) on delete cascade, + PRIMARY KEY (item_id, order_id) +); + +TRUNCATE TABLE items RESTART IDENTITY CASCADE; +TRUNCATE TABLE orders RESTART IDENTITY CASCADE; +TRUNCATE TABLE items_orders RESTART IDENTITY CASCADE; + + +INSERT INTO items (name, price, quantity) VALUES ('Eggs', '2.99', '10'); +INSERT INTO items (name, price, quantity) VALUES ('Coffee', '5.99', '5'); +INSERT INTO items (name, price, quantity) VALUES ('Bread', '3.99', '15'); +INSERT INTO items (name, price, quantity) VALUES ('Orange Juice', '2.99', '10'); +-- New post inserted with id 3 + +INSERT INTO orders (customer_name, date) VALUES ('John Key', 'Jan-08-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Sally Smith', 'Jan-09-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Eddie Man', 'Jan-09-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Sam Baker', 'Jan-09-2023'); +-- New tag inserted with id 5 + +INSERT INTO items_orders (item_id, order_id) VALUES +(1, 1), +(1, 2), +(2, 2), +(1, 3), +(3, 3), +(4, 4); + +ALTER TABLE items_orders ADD FOREIGN KEY (item_id) REFERENCES items(id); +ALTER TABLE items_orders ADD FOREIGN KEY (order_id) REFERENCES orders(id); +``` + +Run this SQL file on the database to truncate (empty) the table, and insert the seed data. Be mindful of the fact any existing records in the table will be deleted. + +```bash +psql -h 127.0.0.1 your_database_name < seeds_recipes.sql +``` + +## 3. Define the class names + +Usually, the Model class name will be the capitalised table name (single instead of plural). The same name is then suffixed by `Repository` for the Repository class name. + +```ruby +# EXAMPLE +# Table name: items + +# Model class +# (in lib/item.rb) +class Item +end + +# Repository class +# (in lib/item_repository.rb) +class ItemRepository +end +``` + +## 4. Implement the Model class + +Define the attributes of your Model class. You can usually map the table columns to the attributes of the class, including primary and foreign keys. + +```ruby +# EXAMPLE +# Table name: items + +# Model class +# (in lib/item.rb) + +class Item + + attr_accessor :id, :name, :price, :quantity +end + +``` + +*You may choose to test-drive this class, but unless it contains any more logic than the example above, it is probably not needed.* + +## 5. Define the Repository Class interface + +Your Repository class will need to implement methods for each "read" or "write" operation you'd like to run against the database. + +Using comments, define the method signatures (arguments and return value) and what they do - write up the SQL queries that will be used by each method. + +```ruby + +# Table name: items + +# Repository class +# (in lib/item_repository.rb) + +class ItemRepository + def all + # shows all items + sql = 'SELECT id, name, price, quantity FROM items;' + result = DatabaseConnection.exec_params(sql, []) + + items = [] + + result.each do |record| + item = Item.new + item.id = record['id'] + item.name = record['name'] + item.price = record['price'] + item.quantity = record['quantity'] + end + end + + def find(id) + end + + def create(item) + def create(item) + sql = 'INSERT INTO items + (name, price, quantity) + VALUES($1, $2, $3);' + sql_params = [item.name, item.price, item.quantity] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + def delete(item) + end + +end + +# EXAMPLE +# Table name: recipe + +# Repository class +# (in lib/recipe_repository.rb) + + def find(id) + # SELECT id, name, price, quantity FROM items WHERE id = $1; + end + + def create(album) + sql = 'INSERT INTO albums + (title, release_year, artist_id) + VALUES($1, $2, $3);' + sql_params = [album.title, album.release_year, album.artist_id] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + + def delete(id) + sql = 'DELETE FROM accounts WHERE id = $1;' + sql_params = [id] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end +``` + +## 6. Write Test Examples + +Write Ruby code that defines the expected behaviour of the Repository class, following your design from the table written in step 5. + +These examples will later be encoded as RSpec tests. + +```ruby + + # 1 + it 'shows all items' do + repo = ItemRepository.new + + items = repo.all + expect(items.length).to eq 4 + expect(items.first.id).to eq '1' + expect(items.first.name).to eq 'Eggs' + end + # 2 + it 'returns a single item' do + repo = ItemRepository.new + + item = repo.find(1) + expect(item.name).to eq 'Eggs' + expect(item.price).to eq '2.99' + end + # 3 + it 'returns another item' do + repo = ItemRepository.new + + item = repo.find(2) + expect(item.name).to eq 'Coffee' + expect(item.price).to eq '5.99' + end + + # 4 + it 'creates and item' do + repo = ItemRepository.new + + item = Item.new + item.name = 'Bananas' + item.price = '1.99' + item.quantity = '10' + repo.create(item) + + expect(repo.all.length).to eq 5 + end + + # 5 + it 'deletes an item' do + repo = ItemRepository.new + id_to_delete = 1 + repo.delete(id_to_delete) + + expect(repo.all.length).to eq 3 + end + + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +Running the SQL code present in the seed file will empty the table and re-insert the seed data. + +This is so you get a fresh table contents every time you run the test suite. + +```ruby +# EXAMPLE + +# file: spec/recipe_repository_spec.rb + +def reset_recipes_table + seed_sql = File.read('spec/seeds_recipes.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'recipes_directory_test' }) + connection.exec(seed_sql) +end + +describe RecipeRepository do + before(:each) do + reset_recipes_table + end + + # (your tests will go here). +end +``` + +## 8. Test-drive and implement the Repository class behaviour + +_After each test you write, follow the test-driving process of red, green, refactor to implement the behaviour._ + + + +--- + +**How was this resource?** +[😫](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=😫) [😕](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=😕) [😐](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=😐) [🙂](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=🙂) [😀](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=😀) +Click an emoji to tell us. + + \ No newline at end of file diff --git a/recipes/order_repository_class_recipe.md b/recipes/order_repository_class_recipe.md new file mode 100644 index 00000000..7126346f --- /dev/null +++ b/recipes/order_repository_class_recipe.md @@ -0,0 +1,288 @@ +# Order Model and Repository Classes Design Recipe + +_Copy this recipe template to design and implement Model and Repository classes for a database table._ + +## 1. Design and create the Table + +If the table is already created in the database, you can skip this step. + +Otherwise, [follow this recipe to design and create the SQL schema for your table](./single_table_design_recipe_template.md). + +*In this template, we'll use an example table `students`* + +``` +# EXAMPLE + +Table: students + +Columns: +id | name | cohort_name +``` + +## 2. Create Test SQL seeds + +Your tests will depend on data stored in PostgreSQL to run. + +If seed data is provided (or you already created it), you can skip this step. + +```sql +-- EXAMPLE +-- (file: spec/seeds_{table_name}.sql) + +-- Write your SQL seed here. + +DROP TABLE IF EXISTS items CASCADE; + +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + price numeric, + quantity int +); + +DROP TABLE IF EXISTS orders CASCADE; +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + date date +); + + +DROP TABLE IF EXISTS items_orders CASCADE; +-- Create the join table. +CREATE TABLE items_orders ( + item_id int, + order_id int, + constraint fk_post foreign key(item_id) references items(id) on delete cascade, + constraint fk_tag foreign key(order_id) references orders(id) on delete cascade, + PRIMARY KEY (item_id, order_id) +); + +TRUNCATE TABLE items RESTART IDENTITY CASCADE; +TRUNCATE TABLE orders RESTART IDENTITY CASCADE; +TRUNCATE TABLE items_orders RESTART IDENTITY CASCADE; + + +INSERT INTO items (name, price, quantity) VALUES ('Eggs', '2.99', '10'); +INSERT INTO items (name, price, quantity) VALUES ('Coffee', '5.99', '5'); +INSERT INTO items (name, price, quantity) VALUES ('Bread', '3.99', '15'); +INSERT INTO items (name, price, quantity) VALUES ('Orange Juice', '2.99', '10'); +-- New post inserted with id 3 + +INSERT INTO orders (customer_name, date) VALUES ('John Key', 'Jan-08-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Sally Smith', 'Jan-09-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Eddie Man', 'Jan-09-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Sam Baker', 'Jan-09-2023'); +-- New tag inserted with id 5 + +INSERT INTO items_orders (item_id, order_id) VALUES +(1, 1), +(1, 2), +(2, 2), +(1, 3), +(3, 3), +(4, 4); + +ALTER TABLE items_orders ADD FOREIGN KEY (item_id) REFERENCES items(id); +ALTER TABLE items_orders ADD FOREIGN KEY (order_id) REFERENCES orders(id); +``` + +Run this SQL file on the database to truncate (empty) the table, and insert the seed data. Be mindful of the fact any existing records in the table will be deleted. + +```bash +psql -h 127.0.0.1 your_database_name < seeds_recipes.sql +``` + +## 3. Define the class names + +Usually, the Model class name will be the capitalised table name (single instead of plural). The same name is then suffixed by `Repository` for the Repository class name. + +```ruby +# EXAMPLE +# Table name: orders + +# Model class +# (in lib/order.rb) +class Order +end + +# Repository class +# (in lib/order_repository.rb) +class OrderRepository +end +``` + +## 4. Implement the Model class + +Define the attributes of your Model class. You can usually map the table columns to the attributes of the class, including primary and foreign keys. + +```ruby +# EXAMPLE +# Table name: iorder + +# Model class +# (in lib/order.rb) + +class Order + + attr_accessor :id, :customer_name, :date +end + +``` + +*You may choose to test-drive this class, but unless it contains any more logic than the example above, it is probably not needed.* + +## 5. Define the Repository Class interface + +Your Repository class will need to implement methods for each "read" or "write" operation you'd like to run against the database. + +Using comments, define the method signatures (arguments and return value) and what they do - write up the SQL queries that will be used by each method. + +```ruby + +# Table name: orders + +# Repository class +# (in lib/order_repository.rb) + +class orderRepository + def all + # shows all orders + sql = 'SELECT id, customer_name, date FROM orders;' + result = DatabaseConnection.exec_params(sql, []) + + orders = [] + + result.each do |record| + order = Order.new + order.id = record['id'] + order.customer_name = record['customer_name'] + order.date = record['date'] + + orders << order + end + end + + def find(id) + end + + def create(item) + def create(item) + sql = 'INSERT INTO items + (name, price, quantity) + VALUES($1, $2, $3);' + sql_params = [item.name, item.price, item.quantity] + + DatabaseConnection.exec_params(sql, sql_params) + + return nil + end + + def delete(item) + # 'DELETE FROM items + # WHERE id = $1;' + end + +end + +``` + +## 6. Write Test Examples + +Write Ruby code that defines the expected behaviour of the Repository class, following your design from the table written in step 5. + +These examples will later be encoded as RSpec tests. + +```ruby + + # 1 + it 'shows all items' do + repo = ItemRepository.new + + items = repo.all + expect(items.length).to eq 4 + expect(items.first.id).to eq '1' + expect(items.first.name).to eq 'Eggs' + end + # 2 + it 'returns a single item' do + repo = ItemRepository.new + + item = repo.find(1) + expect(item.name).to eq 'Eggs' + expect(item.price).to eq '2.99' + end + # 3 + it 'returns another item' do + repo = ItemRepository.new + + item = repo.find(2) + expect(item.name).to eq 'Coffee' + expect(item.price).to eq '5.99' + end + + # 4 + it 'creates and item' do + repo = ItemRepository.new + + item = Item.new + item.name = 'Bananas' + item.price = '1.99' + item.quantity = '10' + repo.create(item) + + expect(repo.all.length).to eq 5 + end + + # 5 + it 'deletes an item' do + repo = ItemRepository.new + id_to_delete = 1 + repo.delete(id_to_delete) + + expect(repo.all.length).to eq 3 + end + + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +Running the SQL code present in the seed file will empty the table and re-insert the seed data. + +This is so you get a fresh table contents every time you run the test suite. + +```ruby +# EXAMPLE + +# file: spec/recipe_repository_spec.rb + +def reset_recipes_table + seed_sql = File.read('spec/seeds_recipes.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'recipes_directory_test' }) + connection.exec(seed_sql) +end + +describe RecipeRepository do + before(:each) do + reset_recipes_table + end + + # (your tests will go here). +end +``` + +## 8. Test-drive and implement the Repository class behaviour + +_After each test you write, follow the test-driving process of red, green, refactor to implement the behaviour._ + + + +--- + +**How was this resource?** +[😫](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=😫) [😕](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=😕) [😐](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=😐) [🙂](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=🙂) [😀](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Frepository_class_recipe_template.md&prefill_Sentiment=😀) +Click an emoji to tell us. + + \ No newline at end of file diff --git a/recipes/table_recipe.md b/recipes/table_recipe.md new file mode 100644 index 00000000..21ccf937 --- /dev/null +++ b/recipes/table_recipe.md @@ -0,0 +1,158 @@ + +# Two Tables (Many-to-Many) Design Recipe Template + +_Copy this recipe template to design and create two related database tables having a Many-to-Many relationship._ + +## 1. Extract nouns from the user stories or specification + +``` +As a shop manager +So I can know which items I have in stock +I want to keep a list of my shop items with their name and unit price. + +As a shop manager +So I can know which items I have in stock +I want to know which quantity (a number) I have for each item. + +As a shop manager +So I can manage items +I want to be able to create a new item. + +As a shop manager +So I can know which orders were made +I want to keep a list of orders with their customer name. + +As a shop manager +So I can know which orders were made +I want to assign each order to their corresponding item. + +As a shop manager +So I can know which orders were made +I want to know on which date an order was placed. + +As a shop manager +So I can manage orders +I want to be able to create a new order. +``` +items, name, price, item quantity (number), orders, customer name, date placed (for order) +``` + +## 2. Infer the Table Name and Columns + +Put the different nouns in this table. Replace the example with your own nouns. + +| Record | Properties | +| --------------------- | ------------------ | +| items | name, price, quanitity +| orders | customer_name, date + +1. Name of the first table (always plural): `items` + + Column names: `customer_name`, `price`, `quantity` + +2. Name of the second table (always plural): `customer_orders` + + Column names: `name`, `date` + +## 3. Decide the column types. + +[Here's a full documentation of PostgreSQL data types](https://www.postgresql.org/docs/current/datatype.html). + +Most of the time, you'll need either `text`, `int`, `bigint`, `numeric`, or `boolean`. If you're in doubt, do some research or ask your peers. + +Remember to **always** have the primary key `id` as a first column. Its type will always be `SERIAL`. + +``` +# EXAMPLE: + +Table: items +id: SERIAL +name: text +price: numeric +quantity: int + +Table: orders +id: SERIAL +customer_name: text +date: date +``` + +## 4. Design the Many-to-Many relationship + +Make sure you can answer YES to these two questions: + +1. Can one [TABLE ONE] have many [TABLE TWO]? (Yes/No) +2. Can one [TABLE TWO] have many [TABLE ONE]? (Yes/No) + +``` +# EXAMPLE + +1. Can one item have many orders? YES +2. Can one order have many items? YES +``` + +_If you would answer "No" to one of these questions, you'll probably have to implement a One-to-Many relationship, which is simpler. Use the relevant design recipe in that case._ + +## 5. Design the Join Table + +The join table usually contains two columns, which are two foreign keys, each one linking to a record in the two other tables. + +The naming convention is `table1_table2`. + +``` +# EXAMPLE + +Join table for tables: items and orders +Join table name: items_orders +Columns: item_id, order_id +``` + +## 4. Write the SQL. + +```sql +-- EXAMPLE +-- file: items_orders.sql + +-- Replace the table name, columm names and types. + +-- Create the first table. +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + price numeric, + quantity int +); + +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + date date +); + +-- Create the join table. +CREATE TABLE items_orders ( + item_id int, + order_id int, + constraint fk_post foreign key(item_id) references items(id) on delete cascade, + constraint fk_tag foreign key(order_id) references orders(id) on delete cascade, + PRIMARY KEY (item_id, order_id) +); + +``` + +## 5. Create the tables. + +```bash +psql -h 127.0.0.1 database_name < posts_tags.sql +``` + + + +--- + +**How was this resource?** +[😫](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_tables_many_to_many_design_recipe_template.md&prefill_Sentiment=😫) [😕](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_tables_many_to_many_design_recipe_template.md&prefill_Sentiment=😕) [😐](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_tables_many_to_many_design_recipe_template.md&prefill_Sentiment=😐) [🙂](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_tables_many_to_many_design_recipe_template.md&prefill_Sentiment=🙂) [😀](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_tables_many_to_many_design_recipe_template.md&prefill_Sentiment=😀) +Click an emoji to tell us. + + \ No newline at end of file diff --git a/spec/app_spec.rb b/spec/app_spec.rb new file mode 100644 index 00000000..6a3261dc --- /dev/null +++ b/spec/app_spec.rb @@ -0,0 +1,104 @@ +require_relative '../app' + +RSpec.describe Application do + def reset_items_table + seed_sql = File.read('spec/items_orders.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'shop_manager_test' }) + connection.exec(seed_sql) + end + + def reset_orders_table + seed_sql = File.read('spec/items_orders.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'shop_manager_test' }) + connection.exec(seed_sql) +end + + describe Application do + before(:each) do + reset_items_table + end + + before(:each) do + reset_orders_table + end + + it "returns list of all items if user choice is 1" do + io = double :io + item = double :fake_item, id: "1", name: "Eggs", price: "2.99", quantity: "10" + item_list = double :fake_item_list, all: [item] + order_list = double :fake_order_list + + expect(io).to receive(:puts).with("Welcome to the shop management program!").ordered + expect(io).to receive(:puts).with("What do you want to do? + \n1 = list all shop items\n2 = create a new item + \n3 = list all orders\n4 = create a new order").ordered + expect(io).to receive(:gets).and_return("1").ordered + expect(io).to receive(:puts).with("Here is the list of all items:") + expect(io).to receive(:puts).with("#1 Eggs - Unit price: 2.99 - Quantity: 10").ordered + + app = Application.new('shop_manager_test', io, 'item_repository', 'order_repository') + app.run(item_list, order_list) + end + + it "returns list of orders if choice is 3" do + io = double :io + item_list = double :fake_item_list + order = double :fake_order, id: "1", customer_name: "John Key", date: "2023-01-08" + order_list = double :fake_order_list, all: [order] + + expect(io).to receive(:puts).with("Welcome to the shop management program!").ordered + expect(io).to receive(:puts).with("What do you want to do? + \n1 = list all shop items\n2 = create a new item + \n3 = list all orders\n4 = create a new order").ordered + expect(io).to receive(:gets).and_return("3").ordered + expect(io).to receive(:puts).with("Here is the list of all orders:") + expect(io).to receive(:puts).with("#1 John Key - Date: 2023-01-08").ordered + + app = Application.new('shop_manager_test', io, 'item_repository', 'order_repository') + app.run(item_list, order_list) + end + + it "creates a new item when user choice is 2" do + io = double :io + new_item = double :item_object, name: 'Bananas', price: '1.99', quantity: '10' + item_list = double :fake_item_list, create: new_item + order_list = double :fake_order_list + + expect(io).to receive(:puts).with("Welcome to the shop management program!").ordered + expect(io).to receive(:puts).with("What do you want to do? + \n1 = list all shop items\n2 = create a new item + \n3 = list all orders\n4 = create a new order").ordered + expect(io).to receive(:gets).and_return("2").ordered + expect(io).to receive(:puts).with("Please enter new item name.").ordered + expect(io).to receive(:gets).and_return("Bananas").ordered + expect(io).to receive(:puts).with("Please enter new item price.").ordered + expect(io).to receive(:gets).and_return("1.99").ordered + expect(io).to receive(:puts).with("Please enter new item quantity.").ordered + expect(io).to receive(:gets).and_return("10").ordered + + app = Application.new('shop_manager_test', io, 'item_repository', 'order_repository') + app.run(item_list, order_list) + end + + it "creates a new order when user choice is 4" do + io = double :io + new_order = double :order_object, customer_name: 'Daisy Jones', date: '2023-01-08' + item_list = double :fake_item_list + order_list = double :fake_order_list, create: new_order + + expect(io).to receive(:puts).with("Welcome to the shop management program!").ordered + expect(io).to receive(:puts).with("What do you want to do? + \n1 = list all shop items\n2 = create a new item + \n3 = list all orders\n4 = create a new order").ordered + expect(io).to receive(:gets).and_return("4").ordered + expect(io).to receive(:puts).with("Please enter customer name.").ordered + expect(io).to receive(:gets).and_return("Daisy Jones").ordered + expect(io).to receive(:puts).with("Please enter date.").ordered + expect(io).to receive(:gets).and_return("2023-01-08").ordered + + app = Application.new('shop_manager_test', io, 'item_repository', 'order_repository') + app.run(item_list, order_list) + end + + end +end \ No newline at end of file diff --git a/spec/item_repository_spec.rb b/spec/item_repository_spec.rb new file mode 100644 index 00000000..46ddf3c1 --- /dev/null +++ b/spec/item_repository_spec.rb @@ -0,0 +1,63 @@ +require 'item_repository' + +RSpec.describe ItemRepository do + + def reset_items_table + seed_sql = File.read('spec/items_orders.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'shop_manager_test' }) + connection.exec(seed_sql) + end + + describe ItemRepository do + before(:each) do + reset_items_table + end + + + it 'shows all items' do + repo = ItemRepository.new + + items = repo.all + expect(items.length).to eq 4 + expect(items.first.id).to eq '1' + expect(items.first.name).to eq 'Eggs' + end + + it 'returns a single item' do + repo = ItemRepository.new + + item = repo.find(1) + expect(item.name).to eq 'Eggs' + expect(item.price).to eq '2.99' + end + + it 'returns another item' do + repo = ItemRepository.new + + item = repo.find(2) + expect(item.name).to eq 'Coffee' + expect(item.price).to eq '5.99' + end + + it 'creates a new item' do + repo = ItemRepository.new + + item = Item.new + item.name = 'Bananas' + item.price = '1.99' + item.quantity = '10' + repo.create(item) + + expect(repo.all.length).to eq 5 + end + + it 'deletes an item' do + repo = ItemRepository.new + id_to_delete = 1 + repo.delete(id_to_delete) + + expect(repo.all.length).to eq 3 + end + + end +end \ No newline at end of file diff --git a/spec/items_orders.sql b/spec/items_orders.sql new file mode 100644 index 00000000..1cb25560 --- /dev/null +++ b/spec/items_orders.sql @@ -0,0 +1,56 @@ +DROP TABLE IF EXISTS items CASCADE; + +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + price numeric, + quantity int +); + +DROP TABLE IF EXISTS orders CASCADE; +-- Create the second table. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + date date +); + + +DROP TABLE IF EXISTS items_orders CASCADE; +-- Create the join table. +CREATE TABLE items_orders ( + item_id int, + order_id int, + constraint fk_post foreign key(item_id) references items(id) on delete cascade, + constraint fk_tag foreign key(order_id) references orders(id) on delete cascade, + PRIMARY KEY (item_id, order_id) +); + +TRUNCATE TABLE items RESTART IDENTITY CASCADE; +TRUNCATE TABLE orders RESTART IDENTITY CASCADE; +TRUNCATE TABLE items_orders RESTART IDENTITY CASCADE; + + +INSERT INTO items (name, price, quantity) VALUES ('Eggs', '2.99', '10'); +INSERT INTO items (name, price, quantity) VALUES ('Coffee', '5.99', '5'); +INSERT INTO items (name, price, quantity) VALUES ('Bread', '3.99', '15'); +INSERT INTO items (name, price, quantity) VALUES ('Orange Juice', '2.99', '10'); +-- New post inserted with id 3 + +INSERT INTO orders (customer_name, date) VALUES ('John Key', 'Jan-08-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Sally Smith', 'Jan-09-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Eddie Man', 'Jan-09-2023'); +INSERT INTO orders (customer_name, date) VALUES ('Sam Baker', 'Jan-09-2023'); +-- New tag inserted with id 5 + +INSERT INTO items_orders (item_id, order_id) VALUES +(1, 1), +(1, 2), +(2, 2), +(1, 3), +(3, 3), +(4, 4); + +ALTER TABLE items_orders ADD FOREIGN KEY (item_id) REFERENCES items(id); +ALTER TABLE items_orders ADD FOREIGN KEY (order_id) REFERENCES orders(id); + diff --git a/spec/order_repository_spec.rb b/spec/order_repository_spec.rb new file mode 100644 index 00000000..955f0894 --- /dev/null +++ b/spec/order_repository_spec.rb @@ -0,0 +1,64 @@ +require 'order_repository' + +RSpec.describe OrderRepository do + def reset_orders_table + seed_sql = File.read('spec/items_orders.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'shop_manager_test' }) + connection.exec(seed_sql) + end + + describe OrderRepository do + before(:each) do + reset_orders_table + end + + it 'returns a list of orders' do + repo = OrderRepository.new + expect(repo.all.length).to eq 4 + end + + it 'returns a single order' do + repo = OrderRepository.new + first_order = repo.find(1) + expect(first_order.customer_name).to eq 'John Key' + expect(first_order.date).to eq '2023-01-08' + end + + it 'creates a new order' do + repo = OrderRepository.new + + order = Order.new + order.customer_name = 'Jo Blogs' + order.date = '2023-01-11' + + repo.create(order) + + expect(repo.all.length).to eq 5 + end + + it 'deletes an order' do + repo = OrderRepository.new + id_to_delete = 1 + repo.delete(id_to_delete) + + expect(repo.all.length).to eq 3 + end + + it 'returns order 1 alongside related items' do + repo = OrderRepository.new + order = repo.find_with_items(1) + item = double :item, name: 'Eggs' + + expect(item.name).to eq 'Eggs' + end + + it 'returns order 2 alongside related items' do + repo = OrderRepository.new + order = repo.find_with_items(2) + item = double :item, name: ['Eggs' 'Coffee'] + + expect(item.name).to eq ['Eggs' 'Coffee'] + end + + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d8..d486d48c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,10 +1,13 @@ +require 'database_connection' +DatabaseConnection.connect('shop_manager_test') + require 'simplecov' require 'simplecov-console' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter + SimpleCov::Formatter::HTMLFormatter ]) SimpleCov.start @@ -15,3 +18,102 @@ puts "\e[33mTry it now! Just run: rubocop\e[0m" end end + +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end