diff --git a/Gemfile.lock b/Gemfile.lock index 76d220da..bbe45785 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,28 +3,28 @@ GEM specs: ansi (1.5.0) ast (2.4.2) - diff-lcs (1.4.4) + diff-lcs (1.5.0) docile (1.4.0) - parallel (1.20.1) - parser (3.0.2.0) + parallel (1.23.0) + parser (3.2.2.1) ast (~> 2.4.1) - pg (1.3.5) - rainbow (3.0.0) - regexp_parser (2.1.1) + pg (1.5.3) + rainbow (3.1.1) + regexp_parser (2.8.0) rexml (3.2.5) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-support (3.10.2) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) rubocop (1.20.0) parallel (~> 1.10) parser (>= 3.0.0.0) @@ -34,10 +34,10 @@ GEM rubocop-ast (>= 1.9.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.11.0) - parser (>= 3.0.1.1) - ruby-progressbar (1.11.0) - simplecov (0.21.2) + rubocop-ast (1.28.1) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) @@ -46,13 +46,13 @@ GEM simplecov terminal-table simplecov-html (0.12.3) - simplecov_json_formatter (0.1.3) - terminal-table (3.0.1) + simplecov_json_formatter (0.1.4) + terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.0.0) + unicode-display_width (2.4.2) PLATFORMS - ruby + x86_64-darwin-19 DEPENDENCIES pg (~> 1.3) @@ -65,4 +65,4 @@ RUBY VERSION ruby 3.0.2p107 BUNDLED WITH - 2.2.26 + 2.2.22 diff --git a/app.rb b/app.rb new file mode 100644 index 00000000..d8d39353 --- /dev/null +++ b/app.rb @@ -0,0 +1,108 @@ +require_relative 'lib/database_connection' +require_relative 'lib/orders_repo' +require_relative 'lib/item_repo.rb' + +# We need to give the database name to the method `connect`. +DatabaseConnection.connect('database_orders') + +# Perform a SQL query on the database and get the result set. +order_repository = OrderRepository.new +item_repository = ItemRepository.new + +class Application + def initialize(io, order_repository, item_repository) + @io = io + @order_repository = order_repository + @item_repository = item_repository + end + + def run + @io.puts <<-TEXT + Welcome to the shop management program! + What do you want to do? + 1 = list all shop items + 2 = create a new item + 3 = list all orders + 4 = create a new order + TEXT + + user_input = @io.gets.chomp + + case user_input + when '1' + @io.puts "Here is a list of all shop items:" + @item_repository.all.each do |item| + @io.puts <<-TEXT + Name: #{item.name} + Unit Price: #{item.unit_price} + Quantity: #{item.quantity} + Order ID: #{item.order_id} + + TEXT + end + + when '2' + @io.puts "Create a new item" + @io.puts "Type name of item:" + name = @io.gets.chomp + @io.puts "Type unit price of item:" + unit_price = @io.gets.chomp + @io.puts "Type quantity:" + quantity = @io.gets.chomp + + @item_repository.create(name, unit_price, quantity) + + when '3' + @io.puts 'Here is a list of all shop orders:' + @order_repository.all.each do |order| + @io.puts <<-TEXT + Customer Name: #{order.customer_name} + Date of Order: #{order.date_of_order} + + TEXT + end + + when '4' + @io.puts 'Create a new order' + @io.puts "Type customer name:" + customer_name = @io.gets.chomp + @io.puts "Type date of order" + date_of_order = @io.gets.chomp + + @order_repository.create(customer_name, date_of_order) + end + end + + if __FILE__ == $0 + app = Application.new( + Kernel, + OrderRepository.new, + ItemRepository.new, + ) + app.run + end +end + +# Print out each record from the result set. + +=begin +order_repository.all.each do |order| +p "Customer #{order.customer_name}, made an order #{order.date_of_order}" +end + +#item_repository.all.each do |item| +#p "" +#end + +order = order_repository.find(1) +puts order.customer_name + +RSpec.describe Application do + + it 'returns a list of all shop items' + io = double : io + expect(io).to receive(:puts). with("Here is a list of all shop items:") + expect(items_repository).to recieve(:gets).and_return() +end + +=end diff --git a/app_spec.rb b/app_spec.rb new file mode 100644 index 00000000..e69de29b diff --git a/items_repo_recipe.md b/items_repo_recipe.md new file mode 100644 index 00000000..91b39bc1 --- /dev/null +++ b/items_repo_recipe.md @@ -0,0 +1,225 @@ +# Items 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 `items`* + +``` +# EXAMPLE + +Table: items + +Columns: +name | unit_price | quantity | order_id +``` + +## 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. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE items RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO items (name, unit_price, quantity, order_id) VALUES('Orange', '0.85', '5', '1'); +INSERT INTO items (name, unit_price, quantity, order_id) VALUES('Apple', '2', '3', '1'); +``` + +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_{table_name}.sql +e.g psql -h 127.0.0.1 database_orders < spec/seeds_items.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_repo.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 + + # Replace the attributes by your own columns. + attr_accessor :id, :name, :unit_price, :quantity, :order_id +end + +# The keyword attr_accessor is a special Ruby feature +# which allows us to set and get attributes on an object, +# here's an example: +# +# order = order.new +# order.name = 'Trompe le Monde' +# student.name +``` + +*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 +# EXAMPLE +# Table name: items + +# Repository class +# (in lib/item_repo.rb) + +class ItemRepository + + # Selecting all records + # No arguments + def all + # Executes the SQL query: + # SELECT id, name, unit_price, quantity, order_id FROM items; + + # Returns an array of order objects. + end + + def create + # INSERT INTO items (name, unit_price, quantity, order_id) VALUES ($1, $2, $3, $4) RETURNING id, name, unit_price, quantity, order_id' + end + + # Select a single record + # Given the id in argument(a number) + + def find(id) + # Executes the SQL query: + # SELECT id, name, unit_price, quantity, order_id 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 +# EXAMPLES + +# 1 +# Get all items + +repo = ItemRepository.new + +items = repo.all +items.length # => 2 +items.first.id # => '1' +items.first.name # => 'Orange' +items.first.unit_price # => '0.85' +items.quantity # => '5' +items.order_id # => '1' + +# 2 +# Get a single order + +repo = ItemRepository.new +item = repo.find(1) +item.id # => '2' +item.name # => 'Apple' +item.unit_price # => '2' +item.quantity # => => '3' +item.order_id # => '1' +#3 +# Get another single artist + +repo = ItemRepository.new +item = repo.find(0) +item.id # => '1' +item.name # => 'Orange' +item.unit_price #=> '0.85' +item.quantity #=> '5' +item.order_id #=> '1' + +#4 +# Creates a new item +repo = ItemRepository.new +new_item = repo.create(name: 'Biscuit', unit_price: '3.50', quantity: '5', order_id: '2') + +new_item.id +new_item.name +new_item.unit_prce +new_item.quantity +new_item.order_id + +# 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/items_repo_spec.rb + +def reset_items_table + seed_sql = File.read('spec/seeds_items.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'database_items_test' }) + connection.exec(seed_sql) +end + +describe OrderRepository do + before(:each) do + reset_items_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._ + + \ No newline at end of file diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 00000000..ac9ec32a --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,26 @@ +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..58467d77 --- /dev/null +++ b/lib/item.rb @@ -0,0 +1,5 @@ +class Item + + # Replace the attributes by your own columns. + attr_accessor :id, :name, :unit_price, :quantity, :order_id +end \ No newline at end of file diff --git a/lib/item_repo.rb b/lib/item_repo.rb new file mode 100644 index 00000000..59d72f9a --- /dev/null +++ b/lib/item_repo.rb @@ -0,0 +1,72 @@ +require_relative './item' +require_relative './database_connection' +require 'pg' + +class ItemRepository + + # Selecting all records + # No arguments + def all + sql = 'SELECT id, name, unit_price, quantity, order_id FROM items;' + DatabaseConnection.connect('database_orders_test') + result_set = DatabaseConnection.exec_params(sql, []) + items = [] + + result_set.each do |record| + item = Item.new + item.id = record['id'] + item.name = record['name'] + item.unit_price = record['unit_price'] + item.quantity = record['quantity'] + item.order_id = record['order_id'] + + items << item + end + + return items + # Returns an array of orditemer objects. + end + + # Select a single record + # Given the id in argument(a number) + def find(id) + sql = 'SELECT id, name, unit_price, quantity, order_id FROM items WHERE id = $1' + sql_params = [id] + + + result_set = DatabaseConnection.exec_params(sql, sql_params) + + record = result_set[0] + item = Item.new + item.id = record['id'] + item.name = record['name'] + item.unit_price = record['unit_price'] + item.quantity = record['quantity'] + item.order_id = record['order_id'] + + return item + + end + + def create(item) + + sql = 'INSERT INTO items (name, unit_price, quantity, order_id) VALUES ($1, $2, $3, $4) RETURNING id, name, unit_price, quantity, order_id' + sql_params = [item[:name], item[:unit_price], item[:quantity], item[:order_id]] + + result_set = DatabaseConnection.exec_params(sql, sql_params) + return nil if result_set.ntuples.zero? + + record = result_set[0] + + return nil if record.nil? + + new_item = Item.new + new_item.id = record['id'].to_i + new_item.name = record['name'] + new_item.unit_price = record['unit_price'] + new_item.quantity = record['quantity'] + new_item.order_id = record['order_id'] + + new_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..9c83eda6 --- /dev/null +++ b/lib/order.rb @@ -0,0 +1,5 @@ +class Order + + # Replace the attributes by your own columns. + attr_accessor :id, :customer_name, :date_of_order +end \ No newline at end of file diff --git a/lib/orders_repo.rb b/lib/orders_repo.rb new file mode 100644 index 00000000..b0216131 --- /dev/null +++ b/lib/orders_repo.rb @@ -0,0 +1,59 @@ +require_relative './order' +require_relative './database_connection' +require 'pg' + +class OrderRepository + + # Selecting all records + # No arguments + def all + sql = 'SELECT id, customer_name, date_of_order FROM orders;' + DatabaseConnection.connect('database_orders_test') + result_set = DatabaseConnection.exec_params(sql, []) + orders = [] + + result_set.each do |record| + order = Order.new + order.id = record['id'] + order.customer_name = record['customer_name'] + order.date_of_order = record['date_of_order'] + + orders << order + end + # Returns an array of Album objects. + return orders + end + + # Select a single record + # Given the id in argument(a number) + + def find(id) + sql = 'SELECT id, customer_name, date_of_order FROM orders WHERE id = $1' + sql_params = [id] + + result_set = DatabaseConnection.exec_params(sql, sql_params) + + record = result_set[0] + order = Order.new + order.id = record['id'] + order.customer_name = record['customer_name'] + order.date_of_order = record['date_of_order'] + + return order + end + + def create(order) + sql = 'INSERT INTO orders (customer_name, date_of_order) VALUES ($1, $2) RETURNING id, customer_name, date_of_order' + sql_params = [order[:customer_name], order[:date_of_order]] + + result_set = DatabaseConnection.exec_params(sql, sql_params) + + record = result_set[0] + new_order = Order.new + new_order.id = record['id'] + new_order.customer_name = record['customer_name'] + new_order.date_of_order = record['date_of_order'] + + return new_order + end +end diff --git a/orders_repo_recipe.md b/orders_repo_recipe.md new file mode 100644 index 00000000..aa06616d --- /dev/null +++ b/orders_repo_recipe.md @@ -0,0 +1,217 @@ +# Orders 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: orders + +Columns: +customer_name | date_of_order | order_id +``` + +## 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. + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE orders RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO orders (customer_name, date_of_order) VALUES('Khuslen', '2023-05-26'); +INSERT INTO orders (customer_name, date_of_order) VALUES('John', '2023-05-26'); +``` + +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_{table_name}.sql +e.g psql -h 127.0.0.1 database_orders < spec/seeds.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_repo.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: orders + +# Model class +# (in lib/order.rb) + +class Order + + # Replace the attributes by your own columns. + attr_accessor :id, :customer_name, :date_of_order +end + +# The keyword attr_accessor is a special Ruby feature +# which allows us to set and get attributes on an object, +# here's an example: +# +# order = order.new +# order.name = 'Trompe le Monde' +# student.name +``` + +*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 +# EXAMPLE +# Table name: orders + +# Repository class +# (in lib/order_repo.rb) + +class OrderRepository + + # Selecting all records + # No arguments + def all + # Executes the SQL query: + # SELECT id, customer_name, date_of_order FROM orders; + + # Returns an array of order objects. + end + + def create(order) + end + + # Select a single record + # 'INSERT INTO orders (customer_name, date_of_order) VALUES ($1, $2) RETURNING id, customer_name, date_of_order' + # Given the id in argument(a number) + + def find(id) + # Executes the SQL query: + # SELECT id, customer_name, date_of_order FROM orders 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 +# EXAMPLES + +# 1 +# Get all orders + +repo = OrderRepository.new + +orders = repo.all +orders.length # => 2 +orders.first.id # => '1' +orders.first.customer_name # => 'Khuslen' +orders.first.date_of_order # => '23-05-26' + +# 2 +# Get a single order + +repo = OrderRepository.new +order = repo.find(1) +order.id # => '1' +order.customer_name # => 'Khuslen' +order.date_of_order # => '23-05-26' +#3 +# Get another single artist + +repo = OrderRepository.new +order = repo.find(2) +order.id # => '2' +order.customer_name # => 'John' +order.date_of_order #=> '2023-05-25' + +#4 +# Creates a new order +repo = OrderRepository.new +new_order = repo.create(customer_name: 'Billy', date_of_order: '2023-05-01') + +new_order.id +new_order.customer_name +new_order.date_of_order + +# 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/orders_repo_spec.rb + +def reset_orders_table + seed_sql = File.read('spec/seeds_orders.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'database_orders_test' }) + connection.exec(seed_sql) +end + +describe OrderRepository do + before(:each) do + reset_orders_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._ + + \ No newline at end of file diff --git a/orders_table.sql b/orders_table.sql new file mode 100644 index 00000000..3aee32cc --- /dev/null +++ b/orders_table.sql @@ -0,0 +1,18 @@ +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + date_of_order date +); + +-- Then the table with the foreign key first. +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + unit_price numeric, + quantity int, +-- The foreign key name is always {other_table_singular}_id + order_id int, + constraint fk_order foreign key(order_id) + references orders(id) + on delete cascade +); \ No newline at end of file diff --git a/spec/item_repo_spec.rb b/spec/item_repo_spec.rb new file mode 100644 index 00000000..0c2b1267 --- /dev/null +++ b/spec/item_repo_spec.rb @@ -0,0 +1,68 @@ +require_relative '../lib/item_repo.rb' + +RSpec.describe ItemRepository do + + def reset_items_table + seed_sql = File.read('./spec/seeds_items.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'database_orders_test' }) + connection.exec(seed_sql) + end + + before(:each) do + reset_items_table + end + + it 'returns the list of items' do + repo = ItemRepository.new + + items = repo.all + + expect(items.length).to eq(2) # => 2 + expect(items.first.id).to eq('1') # => '1' + expect(items.first.name).to eq('Orange') # => 'Orange' + expect(items.first.unit_price).to eq('0.85')# => '0.85' + expect(items.first.quantity).to eq('5') # => '5' + expect(items.first.order_id).to eq('1') # => '1' + end + # 2 + # Get a single order + it 'returns item with id number 1' do + + repo = ItemRepository.new + + item = repo.find(1) + expect(item.id).to eq('1') # => '1' + expect(item.name).to eq('Orange') # => 'Orange' + expect(item.unit_price).to eq('0.85')# => '0.85' + expect(item.quantity).to eq('5') # => '5' + expect(item.order_id).to eq('1') # => '1' + #3 + # Get another single artist + end + + it 'returns item with id number 2' do + repo = ItemRepository.new + + item = repo.find(2) + + expect(item.id).to eq('2') # => '1' + expect(item.name).to eq('Apple') # => 'Orange' + expect(item.unit_price).to eq('2')# => '0.85' + expect(item.quantity).to eq('3') # => '5' + expect(item.order_id).to eq('1') # => '1' + end + + it 'creates a new item called Biscuit' do + + repo = ItemRepository.new + + new_item = repo.create(name: 'Biscuit', unit_price: '3.50', quantity: '5', order_id: '2') + + expect(new_item.id).to eq(3) + expect(new_item.name).to eq('Biscuit') + expect(new_item.unit_price).to eq('3.50') + expect(new_item.quantity).to eq('5') + expect(new_item.order_id).to eq('2') + end + end + diff --git a/spec/orders_repo_spec.rb b/spec/orders_repo_spec.rb new file mode 100644 index 00000000..f01f5011 --- /dev/null +++ b/spec/orders_repo_spec.rb @@ -0,0 +1,60 @@ +require_relative '../lib/orders_repo' + +RSpec.describe OrderRepository do + + def reset_orders_table + seed_sql = File.read('./spec/seeds_orders.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'database_orders_test' }) + connection.exec(seed_sql) + end + + before(:each) do + reset_orders_table + end + + it 'returns the list of orders' do + repo = OrderRepository.new + + orders = repo.all + + expect(orders.length).to eq(2) + expect(orders.first.id).to eq('1') # => '1' + expect(orders.first.customer_name).to eq('Khuslen')# => 'Khuslen' + expect(orders.first.date_of_order).to eq('2023-05-26') + end + + # 2 + # Get a single order + it 'returns Khuslen as a single order' do + + repo = OrderRepository.new + + order = repo.find(1) + + expect(order.id).to eq('1')# => '1' + expect(order.customer_name).to eq('Khuslen') # => 'Khuslen' + expect(order.date_of_order).to eq('2023-05-26') + end + #3 + # Get another single artist + it 'returns John as another order' do + repo = OrderRepository.new + order = repo.find(2) + + expect(order.id).to eq('2')# => '2' + expect(order.customer_name).to eq('John') # => 'John' + expect(order.date_of_order).to eq('2023-05-25') + end + + #4 + # Creates a new order + it 'creates a new order' do + + repo = OrderRepository.new + new_order = repo.create(customer_name: 'Billy', date_of_order: '2023-05-01') + + expect(new_order.id).to eq('3') + expect(new_order.customer_name).to eq('Billy') + expect(new_order.date_of_order).to eq('2023-05-01') + end +end diff --git a/spec/seeds_items.sql b/spec/seeds_items.sql new file mode 100644 index 00000000..e3750a55 --- /dev/null +++ b/spec/seeds_items.sql @@ -0,0 +1,7 @@ +TRUNCATE TABLE items RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO items (name, unit_price, quantity, order_id) VALUES('Orange', '0.85', '5', '1'); +INSERT INTO items (name, unit_price, quantity, order_id) VALUES('Apple', '2', '3', '1'); \ No newline at end of file diff --git a/spec/seeds_orders.sql b/spec/seeds_orders.sql new file mode 100644 index 00000000..9145075d --- /dev/null +++ b/spec/seeds_orders.sql @@ -0,0 +1,7 @@ +TRUNCATE TABLE orders RESTART IDENTITY; -- replace with your own table name. + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO orders (customer_name, date_of_order) VALUES('Khuslen', '2023-05-26'); +INSERT INTO orders (customer_name, date_of_order) VALUES('John', '2023-05-25'); \ No newline at end of file diff --git a/two_table_schema.md b/two_table_schema.md new file mode 100644 index 00000000..51328316 --- /dev/null +++ b/two_table_schema.md @@ -0,0 +1,176 @@ +# Two Tables Design Recipe Template + +_Copy this recipe template to design and create two related database tables from a specification._ + +## 1. Extract nouns from the user stories or specification + +``` +# EXAMPLE USER STORY: +# (analyse only the relevant part - here the final line). + +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. +``` + +``` +Nouns: + +items, name, unit price, quantity, orders, customer name, date of 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, unit_price, quantity +| orders | customer_name, date_of_order + +1. Name of the first table (always plural): `items` + + Column names: `name`, `unit_price`, `quantity`, + +2. Name of the second table (always plural): `orders` + + Column names: `customer_name`, `date_of_order` + +## 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 +unit_price: numeric +quantity: int + +Table: orders +id: SERIAL +customer_name: text +date_of_order: date +``` + +## 4. Decide on The Tables Relationship + +Most of the time, you'll be using a **one-to-many** relationship, and will need a **foreign key** on one of the two tables. + +To decide on which one, answer 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) + +You'll then be able to say that: + +1. **[A] has many [B]** +2. And on the other side, **[B] belongs to [A]** +3. In that case, the foreign key is in the table [B] + +Replace the relevant bits in this example with your own: + +``` +# EXAMPLE + +1. Can one order have many items? YES +2. Can one item have many orders? No + +-> Therefore, +-> An order HAS MANY items +-> An item BELONGS TO an order + +-> Therefore, the foreign key is on the items table. + +order -> one to many -> item +item -> many to one -> order + +Foreign key items) - 'order_id' + +orders +1 Jane +2 Bill + items + orders_id +1 Apple 2 +2 Peacher 1 +``` + +*If you can answer YES to the two questions, you'll probably have to implement a Many-to-Many relationship, which is more complex and needs a third table (called a join table).* + +## 4. Write the SQL. + +```sql +-- EXAMPLE +-- file: databases_orders.sql + +-- Replace the table name, columm names and types. + +-- Create the table without the foreign key first. +CREATE TABLE orders ( + id SERIAL PRIMARY KEY, + customer_name text, + date_of_order date +); + +-- Then the table with the foreign key first. +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + name text, + unit_price numeric, + quantity int, +-- The foreign key name is always {other_table_singular}_id + order_id int, + constraint fk_order foreign key(order_id) + references orders(id) + on delete cascade +); + +``` + +## 5. Create the tables. + +```bash +psql -h 127.0.0.1 database_orders + +--- + +**How was this resource?** +[😫](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=😫) [😕](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=😕) [😐](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=😐) [🙂](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=🙂) [😀](https://airtable.com/shrUJ3t7KLMqVRFKR?prefill_Repository=makersacademy%2Fdatabases&prefill_File=resources%2Ftwo_table_design_recipe_template.md&prefill_Sentiment=😀) +Click an emoji to tell us. + + \ No newline at end of file