-
Notifications
You must be signed in to change notification settings - Fork 5
General Backend Workflow
With the feature creep of this project being what it is, a large fraction of the written code deals with storing and retrieving data with a database. What follows is the process used to create a new table and make its related data available to the frontend.
To add a new table to the database, run diesel migration generate create_<TABLE_NAME>
. This will create a new directory inside of the migrations
directory with the name CURRENT_DATE_TIMESTAMP_create_TABLE_NAME
. Inside that directory, will be two files, an up.sql
and a down.sql
. In up.sql
you should write SQL to create your table, usually in the form:
CREATE TABLE users (
...
...
...
);
And in the down.sql
you should drop the table, which should look like
DROP TABLE users;
After the table creation statements are set up, you can run diesel migration run
to apply any migrations that haven't been run yet. This will run your up.sql
file. If you don't like the way your table is set up, you can run diesel migration revert
to undo the most recently applied migration by running the corresponding down.sql
file.
With the database set up the way you want, you should create a new file in src/db/
with your table's name (singular). In this file you should define your struct that maps to your table. It should contain fields that are in the same order, have corresponding types, and have the same names as the table entry. The following traits should be derived for the struct.
#[derive(Debug, Clone, Identifiable, Queryable)]
#[table_name="users"]
pub struct User {
pub id: i32,
...
This allows the infer_schema!()
macro to find the table entry in the database and associate its columns with your type.
Because you want to be able to insert a struct into the database, while letting the database take care of assigning it an id, you can declare another struct and derive Insertable for it like so:
#[derive(Insertable, Debug, Clone)]
#[table_name="users"]
pub struct NewUser {
... // everything except the id
For some types, it makes sense for the API to allow its caller to supply a set of changes to a given row. For this, you can derive AsChangeset for yet another struct type.
#[derive(AsChangeset, Debug, Clone)]
#[table_name=""]
pub struct UserChangeset {
pub id: i32,
... // fields that you want to be able to be changed.
Then inside of the impl
block for the main struct representing your table, implement the functions that will alter the database.
In the case of the User
struct it should return some permutation of Result<User, WeekendAtJoesError>
or Result<Vec<User>, WeekendAtJoesError>
.
With the database functions implemented, return types should be defined.
In the /requests_and_responses/src/
library, create a new file with the same name as the /src/db/
entry.
In the case of the User
example, create /requests_and_responses/src/user.rs
.
In this file, define what types of data your future routes will accept and return.
Derive Serialize and Deserialize so that they may be used by both the frontend and the backend.
For the User struct, you want to return less data than you get from the database (you probably don't want to send the password hash to the client).
This looks like:
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserResponse {
pub user_name: String,
pub display_name: String,
pub id: i32,
}
With your return types defined, you should create a file for your routes to this table.
You should implement From
or Into
for your requests and responses in this file to convert too and from your canonical table type, or insert type, or changeset type.
Then set up your route and have it return something like Result<Json<UserResponse>, WeekendAtJoesError>
.
Call your database method and convert it to the expected return type like so:
User::database_function(user_id, &conn)
.and_then(|user| Ok(Json(user.into())))
This will convert your successful response from your table type, to your response type, and then convert it to JSON when it is turned into an actual HTTP response.
Finally, inside of the /routes/
file, implement Routable for your type, by putting the names of the route functions into the ROUTES constant, and the route path they should all be superseded by in the PATH constant like so:
impl Routable for User {
const ROUTES: &'static Fn() -> Vec<Route> = &|| routes!
[
create_user,
delete_user,
...
...
];
const PATH: &'static str = "/user/";
}