There are several approaches to getting data from the database. Depending on the complexity of your app and how you like to organize things.
The following three setups are identical as far as delivering results. We will get a list of people and the company they work for.
Given the following SQL:
CREATE TABLE person (
id serial primary key,
company_id int,
first_name varchar(32),
last_name varchar(32),
email varchar(254)
);
CREATE TABLE company (
id serial primary key,
name varchar(64)
);
INSERT INTO company (name) VALUES
('ePark Labs'),
('Met Gallery'),
('OneAero');
INSERT INTO person (company_id, first_name, last_name, email) VALUES
(1, 'Dan', 'Fitzpatrick', 'dan@eparklabs.com'),
(1, 'Kim', 'Jones', 'dan@eparklabs.com'),
(2, 'David', 'Bowie', 'david@met.gallery'),
(2, 'Danny', 'Elfman', 'danny@met.gallery'),
(3, 'Barry', 'White', 'bary@met.gallery'),
(3, 'Jane', 'Dawson', 'jane@one.aero'),
(3, 'Li', 'Wong', 'li@one.aero');
The results of a request for each URL should be:
Return all records sorted by the default (last_name).
http://localhost:7777/person/list
ID | First Name | Last Name | Company | |
---|---|---|---|---|
3 | David | Bowie | david@met.gallery | Met Gallery |
6 | Jane | Dawson | jane@one.aero | OneAero |
4 | Danny | Elfman | danny@met.gallery | Met Gallery |
1 | Dan | Fitzpatrick | dan@eparklabs.com | ePark Labs |
2 | Kim | Jones | dan@eparklabs.com | ePark Labs |
5 | Barry | White | bary@met.gallery | OneAero |
7 | Li | Wong | li@one.aero | OneAero |
Return a set with first name, last_name, or company matching 'da'.
http://localhost:7777/person/list?q=da
ID | First Name | Last Name | Company | |
---|---|---|---|---|
3 | David | Bowie | Met Gallery | david@met.gallery |
6 | Jane | Dawson | OneAero | jane@one.aero |
4 | Danny | Elfman | Met Gallery | danny@met.gallery |
1 | Dan | Fitzpatrick | ePark Labs | dan@eparklabs.com |
Same as above sorted by first name.
http://localhost:7777/person/list?q=da&order=first_name
ID | First Name | Last Name | Company | |
---|---|---|---|---|
1 | Dan | Fitzpatrick | ePark Labs | dan@eparklabs.com |
4 | Danny | Elfman | Met Gallery | danny@met.gallery |
3 | David | Bowie | Met Gallery | david@met.gallery |
6 | Jane | Dawson | OneAero | jane@one.aero |
app/person/person.rb
module App::Person
extend Waxx::Pg
extend self
# Write your own get method that includes the company name
def list_with_company(x, q: nil, order: nil)
# Guard against SQL injection by only allowing certain sort params
order = %w(id last_name first_name company_name).include? order ? order : 'last_name'
if q.to_s.empty?
sql_where = ['',[]]
else
sql_where = ['
WHERE first_name ILIKE $1
OR last_name ILIKE $1
OR company_name ILIKE $1',
["#{filter}%"] # Doing a starts with query
]
end
x.db.app.exec("
SELECT person.id, person.first_name, person.last_name,
person.email, company.name as company_name
FROM person LEFT JOIN company ON person.company_id = company.id
#{sql_where[0]}
ORDER BY #{order}",
sql_where[1]
)
end
runs(
default: "list",
list: {
desc: "List people with their company name",
get: -> (x) {
# Return HTML
if x.ext == 'html'
# Output the table header
x << %(<table>
<tr>
<th>ID</th>
<th>First Name</th>
<th>Last Name</th>
<th>Company</th>
<th>Email</th>
</tr>
)
# Output the data rows
list_with_company_name(x, x['q'], x['order']).each{|rec|
x << %(
<tr>
<td>#{ rec['id'] }</td>
<td>#{ rec['first_name'].h }</td>
<td>#{ rec['last_name'].h }</td>
<td>#{ rec['company'].h }</td>
<td>#{ rec['email'].h }</td>
</tr>
)
# NOTE: The '.h' method will escape the html
}
# End the table
x << %(</table>)
# Return JSON
else
x << list_with_company_name(x, x['q'], x['order']).map{|rec| rec}.to_json
end
}
}
)
end
Some things to note:
The app will have two objects (company and person) and one view (person list).
Company object -- define the columns of the company table.
app/company/company.rb
module App::Company
extend Waxx::Pg
extend self
# Define what columns/fields/attributes this object has
has(
id: {pkey: true},
name: {label: "Company Name"},
)
# Normally you would have a runs method here. For this example it is not needed.
# runs(
# ...
# )
end
Person object -- Define the columns the person table has and the relationship to the company table. Add what external interfaces the person object runs.
app/person/person.rb
module App::Person
extend Waxx::Pg
extend self
# Define what columns/fields/attributes this object has
has(
id: {pkey: true},
company_id: {is: "company: company.id+"}, # Left join company
first_name: {},
last_name: {},
email: {}
)
runs(
default: "list",
list: {
desc: "List people with their company name",
get: -> (x) {
# 'List.run' is a 'Waxx::View' method and will call 'List.get' to get the data and 'List::Html.get' to lay it out as html.
# 'get' because we are doing a 'get' request and 'Html' because the extension of this request is 'html'.
# 'List' here is 'App::Person::List' because we are in the 'App::Person' module.
x << List.run(x)
}
}
)
end
# Require the view
require_relative 'list'
Person list view -- Define the columns on the view.
Add the Html layout.
The get
method is injected into the view by extend Waxx::View
.
app/person/list.rb
module App::Person::List
extend Waxx::View
extend self
has(
:id,
:first_name,
:last_name,
"company_name: company.name",
:email
)
# Define what columns are searchable with the 'q' parameter
search_in %w(first_name last_name company_name)
module Html
extend Waxx::Html
extend self
# Get the Html layout. Note that the 'view' variable is a pointer to App::Person::List
# The 'h' method escapes the Html.
def get(x, data, message:{})
%(
<table>
<tr> #{view.columns.map{|n, c| %(<th>#{h c/:label}</th>)}.join} </tr>
#{rows(x, data)}
</table>
)
end
def rows(x, data)
# Output the data rows
data.map{|rec|
%(<tr>#{ view.columns.map{|n, c| %(<td>#{h rec/n}</td>) }.join }</tr>)
}.join
end
end
end
If you are not outputting HTML, and just have a Json web service, you can shrink the view to:
module App::Person::List
extend Waxx::View
extend self
has(
:id,
:first_name,
:last_name,
"company_name: company.name",
:email
)
# Define what columns are searchable with the x['q'] parameter
search_in %w(first_name last_name company_name)
# This will create the code to layout the data as a Json array of hashes
as :json
end
Some things to note:
See the docs about everything a Waxx::View can do.
In some cases you need more power than the standard view.
For example if you need to call database functions or build a hairy SQL statement.
For this scenario, you still use a view but you write your own get
method.
Define the object. Have the list run method get the data from the view and then pass it to the layout.
app/person/person.rb
module App::Person
extend Waxx::Pg
extend self
# Define what columns/fields/attributes this object has
has(
id: {pkey: true},
company_id: {is: "company: company.id+"}, # Left join company (not needed for option 3)
first_name: {},
last_name: {},
email: {}
)
runs(
default: "list",
list: {
desc: "List people with their company name",
get: -> (x) {
# Get the data from the view. View.get can return any enumerable data type.
data = List.get(x, q: x['q'], order: x['order'])
# Return not_found if the layout does not exist
begin
layout = List.const_get(x.ext.capitalize)
rescue NameError => e
return App.not_found(x)
end
# Send the data to the layout in the requested format (Html, Json, etc.)
x << layout.get(x, data)
}
}
)
end
For this example we will use a database function to get the data we want. This is may not be the approach I would take for this situation but you'll get an idea how the function is used in the view.
The PLPGSQL function (PostgreSQL-specific) is defined as:
CREATE FUNCTION company_people(q varchar, sort varchar) RETURNS
TABLE(id int, first_name varchar, last_name varchar, company_name varchar, email varchar) AS $$
DECLARE
starts_with varchar := q || '%';
contains varchar := '%' || q || '%';
BEGIN
RETURN QUERY EXECUTE 'SELECT person.id, person.first_name, person.last_name, company.name as company_name, person.email
FROM person LEFT JOIN company ON person.company_id = company.id
WHERE person.first_name ilike $1
OR person.last_name ilike $1
OR company.name ilike $1
OR person.email ilike $2
ORDER BY ' || quote_ident(sort)
USING starts_with, contains;
END;
$$ LANGUAGE plpgsql;
Then in the View we will make our own get method to call the function.
app/person/list.rb
module App::Person::List
extend Waxx::View
extend self
as :json
has(
:id,
:first_name,
:last_name,
"company_name: company.name",
:email
)
# Get can return any enumerable object
def get(x, q: nil, order: nil)
x.db.app.exec("SELECT * FROM company_person($1, $2)",[q, order])
end
# This is the same as the Html in Option Two above
module Html
extend Waxx::Html
extend self
def get(x, data, message:{})
%(
<table>
<tr> #{view.columns.map{|n, c| %(<th>#{h c/:label}</th>)}.join} </tr>
#{rows(x, data)}
</table>
)
end
def rows(x, data)
# Output the data rows
data.map{|rec|
%(<tr>#{ view.columns.map{|n, c| %(<td>#{h rec/n}</td>) }.join }</tr>)
}.join
end
end
end