From 1596ae336c9d1a5cbb1b0756bc937428840059af Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 4 Nov 2024 09:07:18 -0800 Subject: [PATCH 1/2] Fix links --- docs/docs/background-jobs/cron.md | 2 +- docs/docs/configuration.md | 8 +-- docs/docs/controllers/REST/index.md | 2 +- docs/docs/controllers/authentication.md | 6 +- docs/docs/controllers/cookies.md | 6 +- docs/docs/controllers/index.md | 6 +- docs/docs/controllers/middleware.md | 6 +- docs/docs/controllers/request.md | 4 +- docs/docs/controllers/response.md | 8 +-- docs/docs/controllers/sessions.md | 10 +-- docs/docs/controllers/static-files.md | 2 +- docs/docs/controllers/websockets.md | 4 +- docs/docs/encryption.md | 2 +- docs/docs/index.md | 10 +-- docs/docs/logging.md | 2 +- docs/docs/migrating-from-python.md | 2 +- docs/docs/models/.pages | 1 + docs/docs/models/connection-pool.md | 8 +-- docs/docs/models/create-records.md | 22 ++++++- docs/docs/models/custom-queries.md | 20 +++++- docs/docs/models/grouping.md | 62 +++++++++++++++++++ docs/docs/models/index.md | 2 +- docs/docs/models/join-models.md | 2 +- docs/docs/models/update-records.md | 2 +- docs/docs/views/index.md | 10 +-- docs/docs/views/templates/caching.md | 2 +- docs/docs/views/templates/context.md | 6 +- docs/docs/views/templates/for-loops.md | 2 +- docs/docs/views/templates/if-statements.md | 2 +- docs/docs/views/templates/index.md | 6 +- .../templates/templates-in-controllers.md | 6 +- docs/docs/views/templates/variables.md | 4 +- docs/docs/views/turbo/building-pages.md | 4 +- docs/docs/views/turbo/index.md | 4 +- docs/docs/views/turbo/streams.md | 2 +- examples/orm/src/main.rs | 7 +++ 36 files changed, 179 insertions(+), 75 deletions(-) create mode 100644 docs/docs/models/grouping.md diff --git a/docs/docs/background-jobs/cron.md b/docs/docs/background-jobs/cron.md index f654e376..f0b18593 100644 --- a/docs/docs/background-jobs/cron.md +++ b/docs/docs/background-jobs/cron.md @@ -4,7 +4,7 @@ Cron jobs, or scheduled jobs, are background jobs that are performed automatical ## Defining scheduled jobs -A scheduled job is a regular [background job](../), for example: +A scheduled job is a regular [background job](index.md), for example: ```rust use rwf::prelude::*; diff --git a/docs/docs/configuration.md b/docs/docs/configuration.md index 734154e3..8cb9f5fc 100644 --- a/docs/docs/configuration.md +++ b/docs/docs/configuration.md @@ -11,16 +11,16 @@ Rwf will automatically load configuration settings from that file, as they are n The configuration file is using the [TOML language](https://toml.io/). If you're not familiar with TOML, it's pretty simple and expressive language commonly used in the world of Rust programming. -Rwf configuration file is split into multiple sections. The `[general]` section controls various options such as logging settings, and which secret key to use for [encryption](../encryption). The `[database]` +Rwf configuration file is split into multiple sections. The `[general]` section controls various options such as logging settings, and which secret key to use for [encryption](encryption.md). The `[database]` section configures database connection settings, like the database URL, connection pool size, and others. ### `[general]` | Setting | Description | Default | |---------|-------------|---------| -| `log_queries` | Toggles logging of all SQL queries executed by the [ORM](../models/). | `false` | -| `secret_key` | Secret key, encoded using base64, used for [encryption](../encryption). | Randomly generated | -| `cache_templates` | Toggle caching of [dynamic templates](/views/templates/). | `false` in debug, `true` in release | +| `log_queries` | Toggles logging of all SQL queries executed by the [ORM](models/index.md). | `false` | +| `secret_key` | Secret key, encoded using base64, used for [encryption](encryption.md). | Randomly generated | +| `cache_templates` | Toggle caching of [dynamic templates](views/templates/index.md). | `false` in debug, `true` in release | #### Secret key diff --git a/docs/docs/controllers/REST/index.md b/docs/docs/controllers/REST/index.md index 50de64f5..8d33ad68 100644 --- a/docs/docs/controllers/REST/index.md +++ b/docs/docs/controllers/REST/index.md @@ -107,7 +107,7 @@ The delete method, implemented using the HTTP `DELETE /endpoint/` call, dele ## REST controller -Rwf comes with a REST [controller](../), which has the six aforementioned methods separated into individual functions. For example, writing a `/users` endpoint controller could be done like so: +Rwf comes with a REST [controller](../index.md), which has the six aforementioned methods separated into individual functions. For example, writing a `/users` endpoint controller could be done like so: ```rust use rwf::prelude::*; diff --git a/docs/docs/controllers/authentication.md b/docs/docs/controllers/authentication.md index cd225449..032bb7ca 100644 --- a/docs/docs/controllers/authentication.md +++ b/docs/docs/controllers/authentication.md @@ -1,10 +1,10 @@ # Authentication -Rwf has multiple authentication and authorization mechanisms. Different kinds of authentication require their own kinds of user-supplied credentials. The most commonly used mechanism is [Session](../sessions) authentication, which has built-in methods for easy use in [controllers](../). +Rwf has multiple authentication and authorization mechanisms. Different kinds of authentication require their own kinds of user-supplied credentials. The most commonly used mechanism is [Session](sessions.md) authentication, which has built-in methods for easy use in [controllers](index.md). ## Session authentication -[Session](../sessions) authentication checks that the user-supplied session cookie is valid (not expired) and contains an authenticated session. If that's not the case, the request is either rejected with a `403 - Forbidden` or provided an endpoint to re-authenticate, e.g., using a username and password, with a `302 - Found` redirect. +[Session](sessions.md) authentication checks that the user-supplied session cookie is valid (not expired) and contains an authenticated session. If that's not the case, the request is either rejected with a `403 - Forbidden` or provided an endpoint to re-authenticate, e.g., using a username and password, with a `302 - Found` redirect. ### Enable session authentication @@ -43,4 +43,4 @@ impl Controller for Private { ## Basic authentication HTTP Basic is a form of authentication using a global username and password. It's not particularly secure, but it's good enough to protect an endpoint quickly against random visitors. Enabling basic authentication is as simple -as setting an [`AuthHandler`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.AuthHandler.html) with [`BasicAuth`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.BasicAuth.html) on your [controller](../). See [examples/auth](https://github.com/levkk/rwf/tree/main/examples/auth) for examples on how to do this. +as setting an [`AuthHandler`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.AuthHandler.html) with [`BasicAuth`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.BasicAuth.html) on your [controller](index.md). See [examples/auth](https://github.com/levkk/rwf/tree/main/examples/auth) for examples on how to do this. diff --git a/docs/docs/controllers/cookies.md b/docs/docs/controllers/cookies.md index 671bd2a0..35c3ae41 100644 --- a/docs/docs/controllers/cookies.md +++ b/docs/docs/controllers/cookies.md @@ -6,7 +6,7 @@ Cookies allow persisting information between what are otherwise stateless HTTP r ## Read cookies -Cookies sent by the browser can be read inside a [controller](../) by calling the [`cookies`](https://docs.rs/rwf/latest/rwf/http/request/struct.Request.html#method.cookies) method: +Cookies sent by the browser can be read inside a [controller](index.md) by calling the [`cookies`](https://docs.rs/rwf/latest/rwf/http/request/struct.Request.html#method.cookies) method: ```rust let cookies = request.cookies(); @@ -26,7 +26,7 @@ More often than not, cookies are used to store plain text information, so no spe ## Set cookies -Setting cookies on the server can be done when crafting a [response](../response): +Setting cookies on the server can be done when crafting a [response](response.md): ```rust use rwf::prelude::*; @@ -61,7 +61,7 @@ response .add_private(cookie)?; ``` -Cookies are [encrypted](../../encryption) with AES-128, using the security key set in the [configuration](../../configuration). +Cookies are [encrypted](../encryption.md) with AES-128, using the security key set in the [configuration](../configuration.md). ### Read private cookies diff --git a/docs/docs/controllers/index.md b/docs/docs/controllers/index.md index efd63ee8..09ba8df7 100644 --- a/docs/docs/controllers/index.md +++ b/docs/docs/controllers/index.md @@ -50,7 +50,7 @@ The `Controller` trait is asynchronous. Support for async traits in Rust is stil #### `handle` -The `handle` method accepts a [`Request`](request) and must return a [`Response`](response). The response can be any valid HTTP response, including `404` or even `500`. +The `handle` method accepts a [`Request`](request.md) and must return a [`Response`](response.md). The response can be any valid HTTP response, including `404` or even `500`. ##### Errors @@ -124,5 +124,5 @@ impl PageController for Login { Read more about working with controllers, requests, and responses: -- [Requests](request) -- [Responses](response) +- [Requests](request.md) +- [Responses](response.md) diff --git a/docs/docs/controllers/middleware.md b/docs/docs/controllers/middleware.md index ba1bd38c..d4d8739d 100644 --- a/docs/docs/controllers/middleware.md +++ b/docs/docs/controllers/middleware.md @@ -9,11 +9,11 @@ Middleware needs to be specified on each controller. By default, all controllers ### Define middleware -Middleware, similar to [controllers](../), is any struct which implements the [`Middleware`](https://docs.rs/rwf/latest/rwf/controller/middleware/trait.Middleware.html) trait. The only method that needs -implementation is the [`async fn handle_request`](https://docs.rs/rwf/latest/rwf/controller/middleware/trait.Middleware.html#tymethod.handle_request) method, which accepts a [`Request`](../request) and must return an [`Outcome`](https://docs.rs/rwf/latest/rwf/controller/middleware/enum.Outcome.html). +Middleware, similar to [controllers](index.md), is any struct which implements the [`Middleware`](https://docs.rs/rwf/latest/rwf/controller/middleware/trait.Middleware.html) trait. The only method that needs +implementation is the [`async fn handle_request`](https://docs.rs/rwf/latest/rwf/controller/middleware/trait.Middleware.html#tymethod.handle_request) method, which accepts a [`Request`](request.md) and must return an [`Outcome`](https://docs.rs/rwf/latest/rwf/controller/middleware/enum.Outcome.html). If the request is allowed to proceed, [`Outcome::Forward`](https://docs.rs/rwf/latest/rwf/controller/middleware/enum.Outcome.html#variant.Forward) is returned, containing the request, in its modified or unchanged form. -If on the other hand, the request failed some kind of validation, [`Outcome::Stop`](https://docs.rs/rwf/latest/rwf/controller/middleware/enum.Outcome.html#variant.Stop) must be returned with a [`Response`](../response), for example: +If on the other hand, the request failed some kind of validation, [`Outcome::Stop`](https://docs.rs/rwf/latest/rwf/controller/middleware/enum.Outcome.html#variant.Stop) must be returned with a [`Response`](response.md), for example: ```rust use rwf::controller::middleware::prelude::*; diff --git a/docs/docs/controllers/request.md b/docs/docs/controllers/request.md index e1b8d5a4..8abd9641 100644 --- a/docs/docs/controllers/request.md +++ b/docs/docs/controllers/request.md @@ -1,7 +1,7 @@ # Requests For each HTTP request served by Rwf, a new [`Request`](https://docs.rs/rwf/latest/rwf/http/request/struct.Request.html) struct is created. It contains the client IP address, -browser headers, [cookies](../cookies), [session](../sessions) information, and the request body. +browser headers, [cookies](cookies.md), [session](sessions.md) information, and the request body. ## Headers @@ -32,7 +32,7 @@ impl Controller for Index { Headers in Rwf are case-insensitive, so `accept` and `Accept` are equivalent. Most browsers send required headers like `Origin`, `Accept`, and `User-Agent`, but that doesn't mean all HTTP clients will. -Checking for valid headers is good practice to avoid bad actors like bots. Read more about intercepting HTTP requests with [Middleware](../middleware). +Checking for valid headers is good practice to avoid bad actors like bots. Read more about intercepting HTTP requests with [Middleware](middleware.md). ## Request body diff --git a/docs/docs/controllers/response.md b/docs/docs/controllers/response.md index 9807e05f..9d5bbf32 100644 --- a/docs/docs/controllers/response.md +++ b/docs/docs/controllers/response.md @@ -1,7 +1,7 @@ # Responses -Each HTTP request served by Rwf is expected to return a response. If your app is using a [REST](../REST/) API, responses -are typically JSON. If you prefer [HTML over the wire](../../views/turbo/) or plain old websites, the responses will contain HTML or text. +Each HTTP request served by Rwf is expected to return a response. If your app is using a [REST](REST/index.md) API, responses +are typically JSON. If you prefer [HTML over the wire](../views/turbo/index.md) or plain old websites, the responses will contain HTML or text. ## Creating responses @@ -56,5 +56,5 @@ let response = Response::new() ## Learn more -- [Cookies](../cookies) -- [Sessions](../sessions) +- [Cookies](cookies.md) +- [Sessions](sessions.md) diff --git a/docs/docs/controllers/sessions.md b/docs/docs/controllers/sessions.md index 58d32120..2efe8ce4 100644 --- a/docs/docs/controllers/sessions.md +++ b/docs/docs/controllers/sessions.md @@ -1,6 +1,6 @@ # Sessions -A session is an [encrypted](../../encryption) [cookie](../cookies) managed by Rwf. It contains a unique identifier for each browser using your web app. All standard-compliant browsers connecting to Rwf-powered apps will have a Rwf session set automatically, and should send it back on each request. +A session is an [encrypted](../encryption.md) [cookie](cookies.md) managed by Rwf. It contains a unique identifier for each browser using your web app. All standard-compliant browsers connecting to Rwf-powered apps will have a Rwf session set automatically, and should send it back on each request. ## Session types @@ -29,7 +29,7 @@ async fn handle(&self, request: &Request) -> Result { ## Check for valid session -All [controllers](../) can check for the presence of a valid session: +All [controllers](index.md) can check for the presence of a valid session: ```rust let session = request.session(); @@ -46,11 +46,11 @@ If the session is expired, it's advisable not to trust its point of origin. Whil ### Session authentication -Rwf can ensure all requests have valid and current (not expired) sessions. To enable this feature, enable the [`SessionAuth`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.SessionAuth.html) [authentication](../authentication) on your controllers. Guest sessions will be refused access, while authenticated sessions will be allowed through. +Rwf can ensure all requests have valid and current (not expired) sessions. To enable this feature, enable the [`SessionAuth`](https://docs.rs/rwf/latest/rwf/controller/auth/struct.SessionAuth.html) [authentication](authentication.md) on your controllers. Guest sessions will be refused access, while authenticated sessions will be allowed through. ## Store data in session -Rwf sessions allow you to privately store arbitrary JSON-encoded data. Since browsers place limits on cookie sizes, this data should be relatively small. To store some data in the session, you can set it on the [response](../response): +Rwf sessions allow you to privately store arbitrary JSON-encoded data. Since browsers place limits on cookie sizes, this data should be relatively small. To store some data in the session, you can set it on the [response](response.md): ```rust let session = Session::new( @@ -65,6 +65,6 @@ let response = Response::new() ## Renew sessions -Sessions are automatically renewed on each request. This allows your active users to remain "logged in", while inactive ones would be redirected to a login page if session [authentication](../authentication) is enabled. +Sessions are automatically renewed on each request. This allows your active users to remain "logged in", while inactive ones would be redirected to a login page if session [authentication](authentication.md) is enabled. Expired sessions are not renewed, so a user holding an expired session will need to use an authentication controller to get a new valid session. diff --git a/docs/docs/controllers/static-files.md b/docs/docs/controllers/static-files.md index 29d4dc20..2e7a67c7 100644 --- a/docs/docs/controllers/static-files.md +++ b/docs/docs/controllers/static-files.md @@ -5,7 +5,7 @@ and will automatically return the right `Content-Type` header (also known as [MI ## Serve static files -The static files server is just another [controller](../), implemented internally. To add it to your app, you can +The static files server is just another [controller](index.md), implemented internally. To add it to your app, you can add it to the server at startup: ```rust diff --git a/docs/docs/controllers/websockets.md b/docs/docs/controllers/websockets.md index 1e623fa6..4630f009 100644 --- a/docs/docs/controllers/websockets.md +++ b/docs/docs/controllers/websockets.md @@ -92,7 +92,7 @@ to send a [`Message`](https://docs.rs/rwf/latest/rwf/http/websocket/enum.Message ## Sending messages to clients -All WebSocket clients have a unique [session](../sessions) identifier. Sending a message to a client only requires that you know their session ID, which you can obtain from the [`Request`](../request), for example: +All WebSocket clients have a unique [session](sessions.md) identifier. Sending a message to a client only requires that you know their session ID, which you can obtain from the [`Request`](request.md), for example: ```rust if let Some(session_id) = request.session_id() { @@ -103,7 +103,7 @@ if let Some(session_id) = request.session_id() { } ``` -WebSocket messages can be delivered to any client from anywhere in the application, including [controllers](../) and [background jobs](../../background-jobs/). +WebSocket messages can be delivered to any client from anywhere in the application, including [controllers](index.md) and [background jobs](../background-jobs/index.md). ## Starting a WebSocket server diff --git a/docs/docs/encryption.md b/docs/docs/encryption.md index f4d181a9..3f1b1232 100644 --- a/docs/docs/encryption.md +++ b/docs/docs/encryption.md @@ -1,6 +1,6 @@ # Encryption -Rwf uses [AES-128](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) for encrypting user [sessions](../controllers/sessions) and private [cookies](../controllers/cookies). The same functionality is available through the [`rwf::crypto`](https://docs.rs/rwf/latest/rwf/crypto/index.html) module to encrypt and decrypt arbitrary data. +Rwf uses [AES-128](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) for encrypting user [sessions](controllers/sessions.md) and private [cookies](controllers/cookies.md). The same functionality is available through the [`rwf::crypto`](https://docs.rs/rwf/latest/rwf/crypto/index.html) module to encrypt and decrypt arbitrary data. ## Encrypt data diff --git a/docs/docs/index.md b/docs/docs/index.md index 2ebfba2b..eac0e369 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -56,9 +56,9 @@ impl Controller for Index { `rwf::prelude::*` includes the vast majority of types, structs, traits and functions you'll be using when building controllers with Rwf. Adding this declaration to your source files will make handling imports easier, but it's not required. -Rwf controllers are defined as Rust structs that implement the [`Controller`](../controllers/) trait. The trait is asynchronous, hence the `#[async_trait]` macro[^2], +Rwf controllers are defined as Rust structs that implement the [`Controller`](controllers/index.md) trait. The trait is asynchronous, hence the `#[async_trait]` macro[^2], and has only one method you need to implement: `async fn handle`. This method -accepts a [`Request`](../controllers/request), and must return a [`Response`](../controllers/response). +accepts a [`Request`](controllers/request.md), and must return a [`Response`](controllers/response.md). In this example, we are returning HTTP `200 - OK` with the body `

My first Rwf app

`. This is not strictly valid HTML, but it'll work in all browsers for our demo purposes. @@ -100,6 +100,6 @@ The full code for this is available on GitHub in [examples/quick-start](https:// ## Learn more -- [Controllers](controllers/) -- [Models](models/) -- [Views](views/) +- [Controllers](controllers/index.md) +- [Models](models/index.md) +- [Views](views/index.md) diff --git a/docs/docs/logging.md b/docs/docs/logging.md index 1519f3c3..b9845a26 100644 --- a/docs/docs/logging.md +++ b/docs/docs/logging.md @@ -16,7 +16,7 @@ async fn main() { ## Log queries -By default, queries executed against the database are not logged. If you want to see what's being executed (and how long queries are taking to return results), toggle the `log_queries` setting in the [configuration](../configuration). +By default, queries executed against the database are not logged. If you want to see what's being executed (and how long queries are taking to return results), toggle the `log_queries` setting in the [configuration](configuration.md). ## Log requests diff --git a/docs/docs/migrating-from-python.md b/docs/docs/migrating-from-python.md index 63d67f72..d8fe8222 100644 --- a/docs/docs/migrating-from-python.md +++ b/docs/docs/migrating-from-python.md @@ -1,6 +1,6 @@ # Migrating from Python -Rwf is written in Rust, so if you have an existing application you want to migrate to Rust, you have options. Rwf comes with its own [WSGI](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) server, so you can run your existing Django or Flask apps without modifications, side by side with Rwf [controllers](../controllers/). +Rwf is written in Rust, so if you have an existing application you want to migrate to Rust, you have options. Rwf comes with its own [WSGI](https://en.wikipedia.org/wiki/Web_Server_Gateway_Interface) server, so you can run your existing Django or Flask apps without modifications, side by side with Rwf [controllers](controllers/index.md). ## Using WSGI diff --git a/docs/docs/models/.pages b/docs/docs/models/.pages index 897a6a0a..b7e06385 100644 --- a/docs/docs/models/.pages +++ b/docs/docs/models/.pages @@ -7,4 +7,5 @@ nav: - 'scopes.md' - 'debug-queries.md' - 'custom-queries.md' + - 'grouping.md' - '...' diff --git a/docs/docs/models/connection-pool.md b/docs/docs/models/connection-pool.md index 2db9cc8f..52d40a00 100644 --- a/docs/docs/models/connection-pool.md +++ b/docs/docs/models/connection-pool.md @@ -23,14 +23,12 @@ let users = User::all() Returning the connection to the pool is done automatically when the `conn` variable goes out of scope. In Rust semantics, the `conn` variable is "dropped". For example, to checkout a connection for only one query, you can do so inside its own scope: ```rust -async fn get_users() -> Result, Error> { +let users = { let mut conn = Pool::connection().await?; let users = User::all() .fetch_all(&mut conn) - .await?; - Ok(users) - // The connection is returned to the pool here. -} + .await? +}; ``` ## Transactions diff --git a/docs/docs/models/create-records.md b/docs/docs/models/create-records.md index 84e8c20d..159db5c6 100644 --- a/docs/docs/models/create-records.md +++ b/docs/docs/models/create-records.md @@ -7,7 +7,7 @@ Rwf can create model records in one of two ways: ## Saving models -Using our `User` model from our [previous example](../), we can create a new record by instantiating a new instance of the `User` struct and calling `save`: +Using our `User` model from our [previous example](index.md), we can create a new record by instantiating a new instance of the `User` struct and calling `save`: ```rust let user = User { @@ -54,7 +54,25 @@ method instead: ``` Any columns not specified in the `INSERT` statement will be automatically filled in with column defaults. For example, the `created_at` column -specified in our [previous example](../) has a default value `NOW()`, the current database time. +specified in our [previous example](index.md) has a default value `NOW()`, the current database time. + +## Mixing data types + +When using `Model::create`, Rwf automatically converts values from Rust to database types. Due to how Rust works, it's not possible to build slices containing values of different types. If you try, you will get `error[E0308]: mismatched types`. To get around this, you can call [`ToValue::to_value`](https://docs.rs/rwf/latest/rwf/model/value/trait.ToValue.html#tymethod.to_value) on each column, for example: + +=== "Rust" + ```rust + let user = User::create(&[ + ("email", "user@example.com".to_value()), + ("created_at", OffsetDateTime::now_utc().to_value()), + ]) + .fetch(&mut conn) + .await?; + ``` +=== "SQL" + ```postgresql + INSERT INTO "users" ("email", "created_at") VALUES ($1, $2) RETURNING * + ``` ## Unique constraints diff --git a/docs/docs/models/custom-queries.md b/docs/docs/models/custom-queries.md index 93bbadb0..89a32947 100644 --- a/docs/docs/models/custom-queries.md +++ b/docs/docs/models/custom-queries.md @@ -4,7 +4,10 @@ Sometimes the ORM is not enough and you need to write a complex query by hand. R and map the results to a model struct: ```rust -let users = User::find_by_sql("SELECT * FROM users ORDER BY RANDOM() LIMIT 1") +let users = User::find_by_sql( + "SELECT * FROM users ORDER BY RANDOM() LIMIT 1", + &[] +) .fetch_all(&mut conn) .await?; ``` @@ -13,6 +16,21 @@ let users = User::find_by_sql("SELECT * FROM users ORDER BY RANDOM() LIMIT 1") Since this query is not generated by the ORM, you need to make sure to return all the necessary columns and correct data types to map the results to the Rust struct. +## Passing parameters + +Custom queries accept parameters just like any other ORM query. Postgres uses the dollar notation (`$1`) for value placeholders, for example: + +```rust +let users = User::find_by_sql( + "SELECT * FROM users + WHERE id BETWEEN $1 AND $2 + ORDER BY RANDOM()", + &[25, 50], +).fetch_all(&mut conn) +.await?; +``` + +Make sure the parameter values are passed in the same order as the placeholders in the query. ## Use the database driver directly diff --git a/docs/docs/models/grouping.md b/docs/docs/models/grouping.md new file mode 100644 index 00000000..c1f74efc --- /dev/null +++ b/docs/docs/models/grouping.md @@ -0,0 +1,62 @@ +# Group by + +Group by queries allow you to perform analysis of your data directly inside the database. They typically don't return original records, but some kind of aggregate instead. For example, the query below calculates how many users are creating accounts every hour: + +=== "SQL" + ```postgresql + SELECT + COUNT(*) AS count, + DATE_TRUNC('hour', created_at) AS created_at + FROM users + GROUP BY 2 + ORDER BY 2 + ``` +=== "Output" + ``` + count | created_at + -------+------------------------ + 1 | 2024-11-04 08:00:00-08 + 5 | 2024-11-04 09:00:00-08 + 17 | 2024-11-04 10:00:00-08 + ``` + +## Write a group by + +Ergonomic support for group by queries in Rwf is still a work in progress, so for now, you'll need to use [custom queries](custom-queries.md). + +### Define a struct + +Since Rust is a typed language, it would be best to define a struct for your aggregate. Using the example above, we can create a model like so: + +```rust +#[derive(Clone, macros::Model)] +struct UsersPerHour { + count: i64, + created_at: OffsetDateTime, +} +``` + +Using the `Model` macro allows this struct use all ORM features, just like regular models. In fact, any query result can be mapped to a model in Rwf, as long as you define a struct for it. + +### Calculate aggregate + +Calculating the aggregate using the database can be done with `Model::find_by_sql`, for example: + +```rust +let stats = UsersPerHour::find_by_sql(" + SELECT + COUNT(*) AS count, + DATE_TRUNC('hour', created_at) AS created_at + FROM users + GROUP BY 2 + ORDER BY 2 +", &[]) +.fetch_all(&mut conn) +.await?; +``` + +Just like with [custom queries](custom-queries.md), make sure the query returns all columns specified by the struct, with the correct data types. + +## Learn more + +- [Group by in rwf-admin](https://github.com/levkk/rwf/blob/main/rwf-admin/src/models/mod.rs) diff --git a/docs/docs/models/index.md b/docs/docs/models/index.md index ba051db7..dbe16b7c 100644 --- a/docs/docs/models/index.md +++ b/docs/docs/models/index.md @@ -51,7 +51,7 @@ CREATE TABLE users ( for both inserting and selecting data from the table. When inserting, the `id` column should be `None` and will be automatically assigned by the database. This ensures that all rows in your tables have a unique primary key. -[^1]: See [migrations](migrations) to learn how to create tables in your database reliably. +[^1]: See [migrations](migrations.md) to learn how to create tables in your database reliably. ### Naming conventions The struct fields have the same name as the database columns, and the data types match their respective Rust types. The table name in the database corresponds to the name of the struct, lowercase and pluralized. For example, `User` model will refer to the `"users"` table in the database. diff --git a/docs/docs/models/join-models.md b/docs/docs/models/join-models.md index c7ca36bf..45ede15e 100644 --- a/docs/docs/models/join-models.md +++ b/docs/docs/models/join-models.md @@ -5,7 +5,7 @@ models out of the box, but requires a couple of annotations to declare relations ## Define model relationship -Using the `User` model from our [previous example](../), let's define a `Project` model, which will record projects created by the users of our +Using the `User` model from our [previous example](index.md), let's define a `Project` model, which will record projects created by the users of our fictional web app: ```rust diff --git a/docs/docs/models/update-records.md b/docs/docs/models/update-records.md index b9a9aa74..602de7f8 100644 --- a/docs/docs/models/update-records.md +++ b/docs/docs/models/update-records.md @@ -47,7 +47,7 @@ let user = user .await?; ``` -This is very similar to [creating new records](../create-records), except that we set the `id` field to a known value. +This is very similar to [creating new records](create-records.md), except that we set the `id` field to a known value. When the `id` is set to `Some(i64)`, Rwf assumes the record exists in the database, meanwhile if the `id` is `None`, Rwf will attempt to create one instead. ## Update multiple records diff --git a/docs/docs/views/index.md b/docs/docs/views/index.md index 4d5a9229..fbc23bd5 100644 --- a/docs/docs/views/index.md +++ b/docs/docs/views/index.md @@ -1,17 +1,17 @@ # Views basics -Rwf comes with a [templating](templates/) library that can generate dynamic pages. Dynamic templates allow you to create unique HTML pages on the fly, and libraries like [Turbo](turbo) can use it in a way that feels like a native frontend application. +Rwf comes with a [templating](templates/index.md) library that can generate dynamic pages. Dynamic templates allow you to create unique HTML pages on the fly, and libraries like [Turbo](turbo/index.md) can use it in a way that feels like a native frontend application. ## What are views? -Views are the **V** in MVC: they control what your users see and how they experience your web app. Separating views from [controllers](../controllers/) allows controllers to reuse similar parts of your web app on different pages without code duplication. +Views are the **V** in MVC: they control what your users see and how they experience your web app. Separating views from [controllers](../controllers/index.md) allows controllers to reuse similar parts of your web app on different pages without code duplication. ## Using JavaScript frontends -If you prefer to build your frontend with JavaScript libraries like React or Vue, take a look at Rwf's [REST](../controllers/REST/) API documentation. Rwf templates are not required to build web applications. +If you prefer to build your frontend with JavaScript libraries like React or Vue, take a look at Rwf's [REST](../controllers/REST/index.md) API documentation. Rwf templates are not required to build web applications. ## Learn more -- [Templates](templates/) -- [Turbo](turbo/) +- [Templates](templates/index.md) +- [Turbo](turbo/index.md) diff --git a/docs/docs/views/templates/caching.md b/docs/docs/views/templates/caching.md index eba8a686..f0dec7f8 100644 --- a/docs/docs/views/templates/caching.md +++ b/docs/docs/views/templates/caching.md @@ -16,6 +16,6 @@ The first time the template is loaded, it will be fetched from disk and compiled ## Enable the cache -The template cache is disabled by default in development, and enabled in production[^1]. To change this behavior, toggle the `cache_templates` setting in [configuration](../../../configuration). +The template cache is disabled by default in development, and enabled in production[^1]. To change this behavior, toggle the `cache_templates` setting in [configuration](../../configuration.md). [^1]: This assumes you build your application using the `release` profile, e.g. `cargo build --release`. diff --git a/docs/docs/views/templates/context.md b/docs/docs/views/templates/context.md index 2891411b..026f9779 100644 --- a/docs/docs/views/templates/context.md +++ b/docs/docs/views/templates/context.md @@ -45,6 +45,6 @@ let rendered = template.render(&ctx)?; ## Learn more -- [For loops](../for-loops) -- [If statements](../if-statements) -- [Templates in controllers](../templates-in-controllers) +- [For loops](for-loops.md) +- [If statements](if-statements.md) +- [Templates in controllers](templates-in-controllers.md) diff --git a/docs/docs/views/templates/for-loops.md b/docs/docs/views/templates/for-loops.md index acb9e0f6..8acf6a61 100644 --- a/docs/docs/views/templates/for-loops.md +++ b/docs/docs/views/templates/for-loops.md @@ -12,7 +12,7 @@ A for loop can iterate over a list of values, for example: ``` -Template lists, unlike Rust's `Vec`, can hold [variables](../variables) of different data types, and are dynamically evaluated at runtime: +Template lists, unlike Rust's `Vec`, can hold [variables](variables.md) of different data types, and are dynamically evaluated at runtime: === "Template" ```erb diff --git a/docs/docs/views/templates/if-statements.md b/docs/docs/views/templates/if-statements.md index c564276a..d2af188e 100644 --- a/docs/docs/views/templates/if-statements.md +++ b/docs/docs/views/templates/if-statements.md @@ -1,6 +1,6 @@ # If statements -If statements allow you to control the flow of templates, conditionally displaying some elements while hiding others. For example, if a [variable](../variables) is "falsy", you can hide entire sections of your website: +If statements allow you to control the flow of templates, conditionally displaying some elements while hiding others. For example, if a [variable](variables.md) is "falsy", you can hide entire sections of your website: ```erb <% if logged_in %> diff --git a/docs/docs/views/templates/index.md b/docs/docs/views/templates/index.md index c9dee339..5b64f0d0 100644 --- a/docs/docs/views/templates/index.md +++ b/docs/docs/views/templates/index.md @@ -44,6 +44,6 @@ reuse code. ## Learn more -- [Variables](variables) -- [For loops](for-loops) -- [If statements](if-statements) +- [Variables](variables.md) +- [For loops](for-loops.md) +- [If statements](if-statements.md) diff --git a/docs/docs/views/templates/templates-in-controllers.md b/docs/docs/views/templates/templates-in-controllers.md index d8ac384a..12a46429 100644 --- a/docs/docs/views/templates/templates-in-controllers.md +++ b/docs/docs/views/templates/templates-in-controllers.md @@ -1,6 +1,6 @@ # Templates in controllers -Using templates in [controllers](../../../controllers) typically involves rendering them inside a request handler and returning the result as HTML, for example: +Using templates in [controllers](../../controllers/index.md) typically involves rendering them inside a request handler and returning the result as HTML, for example: ```rust struct Index; @@ -17,7 +17,7 @@ impl Controller for Index { } ``` -The template will be loaded from the [template cache](../caching), rendered with the provided context, and used as a body for a response with the correct `Content-Type` header. +The template will be loaded from the [template cache](caching.md), rendered with the provided context, and used as a body for a response with the correct `Content-Type` header. Since this is a very common way to use templates in controllers, Rwf has the `render!` macro to make this less verbose: @@ -30,7 +30,7 @@ impl Controller for Index { } ``` -The `render!` macro takes the template path as the first argument, and optionally, a mapping of variable names and values as subsequent arguments. It creates a [`Response`](../../../controllers/response) automatically, so there is no need to return one manually. +The `render!` macro takes the template path as the first argument, and optionally, a mapping of variable names and values as subsequent arguments. It creates a [`Response`](../../controllers/response.md) automatically, so there is no need to return one manually. If the template doesn't have any variables, you can use `render!` with just the template name: diff --git a/docs/docs/views/templates/variables.md b/docs/docs/views/templates/variables.md index 67e22002..d6dd7e5d 100644 --- a/docs/docs/views/templates/variables.md +++ b/docs/docs/views/templates/variables.md @@ -76,7 +76,7 @@ Rust's `f32` and `f64` are converted to 64-bit double precision floating point. 501.5 ``` -Numbers can be [converted](functions.md) to strings, floored, ceiled and rounded, for example: +Numbers can be [converted](functions/index.md) to strings, floored, ceiled and rounded, for example: === "Template" ```erb @@ -182,4 +182,4 @@ All other variables evaluate to true. - [Context](context.md) - [If statements](if-statements.md) - [For loops](for-loops.md) -- [Functions](functions.md) +- [Functions](functions/index.md) diff --git a/docs/docs/views/turbo/building-pages.md b/docs/docs/views/turbo/building-pages.md index a41c456f..72895fb4 100644 --- a/docs/docs/views/turbo/building-pages.md +++ b/docs/docs/views/turbo/building-pages.md @@ -1,5 +1,5 @@ # Building pages -Turbo can be used to update parts of the page, without having to render the entire page on every request. This is useful when you want to update sections of the page from any endpoint, without having to load several [partials](../../templates/partials) or performing redirects. +Turbo can be used to update parts of the page, without having to render the entire page on every request. This is useful when you want to update sections of the page from any endpoint, without having to load several [partials](../templates/partials.md) or performing redirects. -Partial updates uses Turbo Streams, a feature of Turbo that sends page updates via [forms](../../../controllers/forms) or [WebSockets](../../../controllers/websockets). +Partial updates uses Turbo Streams, a feature of Turbo that sends page updates via [forms](../../controllers/request.md#forms) or [WebSockets](../../controllers/websockets.md). diff --git a/docs/docs/views/turbo/index.md b/docs/docs/views/turbo/index.md index 5270a59c..986f1ea9 100644 --- a/docs/docs/views/turbo/index.md +++ b/docs/docs/views/turbo/index.md @@ -1,10 +1,10 @@ # Turbo basics -[Hotwired Turbo](https://turbo.hotwired.dev/) is a JavaScript library that can intercept HTTP requests to your backend and perform updates to the frontend without reloading the browser page. The backend produces HTML, generated with [dynamic templates](../templates/), and Turbo updates only the sections of the page that changed. This simulates the behavior of [Single-page applications](https://en.wikipedia.org/wiki/Single-page_application) (like the ones written with React or Vue) without using JavaScript on the frontend. +[Hotwired Turbo](https://turbo.hotwired.dev/) is a JavaScript library that can intercept HTTP requests to your backend and perform updates to the frontend without reloading the browser page. The backend produces HTML, generated with [dynamic templates](../templates/index.md), and Turbo updates only the sections of the page that changed. This simulates the behavior of [Single-page applications](https://en.wikipedia.org/wiki/Single-page_application) (like the ones written with React or Vue) without using JavaScript on the frontend. ## Enabling Turbo -If you're building pages using Rwf's [dynamic templates](../templates/), you can enable Turbo by adding a declaration into the `` element of your pages: +If you're building pages using Rwf's [dynamic templates](../templates/index.md), you can enable Turbo by adding a declaration into the `` element of your pages: ```html diff --git a/docs/docs/views/turbo/streams.md b/docs/docs/views/turbo/streams.md index 71dbc0ca..7d2e98d2 100644 --- a/docs/docs/views/turbo/streams.md +++ b/docs/docs/views/turbo/streams.md @@ -85,6 +85,6 @@ If you need to send updates to the client from somewhere else besides a controll ## Learn more - [WebSockets](../../controllers/websockets.md) -- [Template functions](../templates/functions.md) +- [Template functions](../templates/functions/index.md) - [Sessions](../../controllers/sessions.md) - [Hotwired Turbo Streams](https://turbo.hotwired.dev/handbook/streams) diff --git a/examples/orm/src/main.rs b/examples/orm/src/main.rs index 7db85e61..3a93c00e 100644 --- a/examples/orm/src/main.rs +++ b/examples/orm/src/main.rs @@ -24,6 +24,13 @@ mod models { .fetch(&mut conn) .await?; + let user = User::create(&[ + ("email", "user@example.com".to_value()), + ("created_at", OffsetDateTime::now_utc().to_value()), + ]) + .fetch(&mut conn) + .await?; + Ok(user) } From 89e0fa5fccadd271dcc4326466ff2a32388b1aeb Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Mon, 4 Nov 2024 09:09:21 -0800 Subject: [PATCH 2/2] remove edit to main.rs --- examples/orm/src/main.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/examples/orm/src/main.rs b/examples/orm/src/main.rs index 3a93c00e..7db85e61 100644 --- a/examples/orm/src/main.rs +++ b/examples/orm/src/main.rs @@ -24,13 +24,6 @@ mod models { .fetch(&mut conn) .await?; - let user = User::create(&[ - ("email", "user@example.com".to_value()), - ("created_at", OffsetDateTime::now_utc().to_value()), - ]) - .fetch(&mut conn) - .await?; - Ok(user) }