ocre-cli 0.2.0

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
//! Question model: the `questions` table. Generated by `ocre g scaffold Question event:references body:text votes:integer answered:boolean --realtime`.
//!
//! Every query and rule about questions lives here. Controllers (HTML
//! pages, JSON API, GraphQL) call these functions instead of writing SQL.

use ocre::{Ctx, Error, Page, Query, Result, Validator, params};
use serde::{Deserialize, Serialize};

/// A row of the `questions` table.
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Question {
    pub id: i64,
    pub event_id: i64,
    pub body: String,
    pub votes: i64,
    #[serde(deserialize_with = "ocre::bool_from_sql")]
    pub answered: bool,
    pub created_at: String,
    pub updated_at: String,
}

/// Values for a new question.
#[derive(Debug, Clone, Deserialize)]
pub struct NewQuestion {
    pub event_id: i64,
    pub body: String,
    pub votes: i64,
    #[serde(default)]
    pub answered: bool,
}

/// Changes to a question: absent fields keep their value; for optional fields
/// `Some(None)` clears it.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct QuestionChanges {
    pub event_id: Option<i64>,
    pub body: Option<String>,
    pub votes: Option<i64>,
    pub answered: Option<bool>,
}

impl NewQuestion {
    /// Checks that need no database; `create` adds uniqueness and references.
    pub fn validate(&self) -> Validator {
        let mut v = Validator::new();
        v.required("body", &self.body);
        v.min_length("body", &self.body, 3);
        v.max_length("body", &self.body, 280);
        v.safe_integer("votes", self.votes);
        v
    }
}

impl QuestionChanges {
    /// Checks the fields being changed; `update` adds the database checks.
    pub fn validate(&self) -> Validator {
        let mut v = Validator::new();
        if let Some(body) = &self.body {
            v.required("body", body);
        }
        if let Some(votes) = &self.votes {
            v.safe_integer("votes", *votes);
        }
        v
    }

    /// Names of the fields this change sets (Rails' `changed`), e.g. to
    /// run a callback only when `title` changes.
    pub fn changed(&self) -> Vec<&'static str> {
        let mut fields = Vec::new();
        if self.event_id.is_some() {
            fields.push("event_id");
        }
        if self.body.is_some() {
            fields.push("body");
        }
        if self.votes.is_some() {
            fields.push("votes");
        }
        if self.answered.is_some() {
            fields.push("answered");
        }
        fields
    }
}

impl Question {
    /// The event this question belongs to.
    pub async fn event(&self, ctx: &Ctx) -> Result<Option<crate::models::event::Event>> {
        crate::models::event::find(ctx, self.event_id).await
    }

    // ocre:associations
}

/// Every query on questions starts here, e.g.
/// `query().eq("id", id).first(&ctx.db()?)`. Scopes are functions taking and
/// returning a `Query<Question>`; add them below.
pub fn query() -> Query<Question> {
    Query::table("questions")
}

/// Questions, newest first.
pub async fn all(ctx: &Ctx, page: Page) -> Result<Vec<Question>> {
    query().order_desc("id").page(page).all(&ctx.db()?).await
}

pub async fn count(ctx: &Ctx) -> Result<i64> {
    query().count(&ctx.db()?).await
}

pub async fn find(ctx: &Ctx, id: i64) -> Result<Option<Question>> {
    query().eq("id", id).first(&ctx.db()?).await
}

/// Loads many questions in few queries (100 ids per query, D1's limit on
/// parameters): use it instead of calling `find` in a loop.
pub async fn find_many(ctx: &Ctx, ids: &[i64]) -> Result<Vec<Question>> {
    let db = ctx.db()?;
    let mut rows = Vec::with_capacity(ids.len());
    for chunk in ids.chunks(100) {
        rows.extend(query().is_in("id", chunk.iter().copied()).all(&db).await?);
    }
    Ok(rows)
}

/// The events of `records` by id, in one query per 100 ids: show a
/// list of questions with their event without one query per row.
pub async fn preload_events(
    ctx: &Ctx,
    records: &[Question],
) -> Result<std::collections::HashMap<i64, crate::models::event::Event>> {
    let mut ids: Vec<i64> = records.iter().map(|record| record.event_id).collect();
    ids.sort_unstable();
    ids.dedup();
    let rows = crate::models::event::find_many(ctx, &ids).await?;
    Ok(rows.into_iter().map(|row| (row.id, row)).collect())
}

/// Every question of the given events, newest first, in one query per
/// 100 ids: the has-many side preloaded for a list. Not paginated: keep the
/// list of ids short.
pub async fn for_events(ctx: &Ctx, event_ids: &[i64]) -> Result<Vec<Question>> {
    let db = ctx.db()?;
    let mut rows = Vec::new();
    for chunk in event_ids.chunks(100) {
        rows.extend(query().is_in("event_id", chunk.iter().copied()).order_desc("id").all(&db).await?);
    }
    Ok(rows)
}

/// The questions of an event as the room shows them: open ones first, most
/// voted first (oldest first on a tie), then answered ones. At most 200.
pub async fn for_event(ctx: &Ctx, event_id: i64) -> Result<Vec<Question>> {
    query()
        .eq("event_id", event_id)
        .order_asc("answered")
        .order_desc("votes")
        .order_asc("id")
        .limit(200)
        .all(&ctx.db()?)
        .await
}

/// Adds one vote. A single UPDATE, so votes arriving at the same moment are
/// all counted. `None` when there is no question with this id.
pub async fn vote(ctx: &Ctx, id: i64) -> Result<Option<Question>> {
    let sql = "UPDATE questions SET votes = votes + 1, updated_at = datetime('now') WHERE id = ?1 RETURNING *";
    ctx.db()?.first(sql, params![id]).await
}

pub async fn create(ctx: &Ctx, mut new: NewQuestion) -> Result<Question> {
    before_create(ctx, &mut new).await?;
    let db = ctx.db()?;
    let mut v = new.validate();
    {
        let event_id = &new.event_id;
        v.check(
            "event_id",
            !db.exists("SELECT 1 FROM events WHERE id = ?1 LIMIT 1", params![*event_id]).await?,
            "must exist",
        );
    }
    v.finish()?;
    let record: Question = db
        .first(
            "INSERT INTO questions (event_id, body, votes, answered) VALUES (?1, ?2, ?3, ?4) RETURNING *",
            params![new.event_id, new.body, new.votes, new.answered],
        )
        .await?
        .ok_or_else(|| Error::internal("INSERT ... RETURNING returned no row"))?;
    after_create(ctx, &record).await?;
    Ok(record)
}

/// `None` when there is no question with this id.
pub async fn update(ctx: &Ctx, id: i64, mut changes: QuestionChanges) -> Result<Option<Question>> {
    before_update(ctx, id, &mut changes).await?;
    let db = ctx.db()?;
    let mut v = changes.validate();
    if let Some(event_id) = &changes.event_id {
        v.check(
            "event_id",
            !db.exists("SELECT 1 FROM events WHERE id = ?1 LIMIT 1", params![*event_id]).await?,
            "must exist",
        );
    }
    v.finish()?;
    let updated: Option<Question> = db
        .first(
            "UPDATE questions SET event_id = CASE WHEN ?1 THEN ?2 ELSE event_id END, body = CASE WHEN ?3 THEN ?4 ELSE body END, votes = CASE WHEN ?5 THEN ?6 ELSE votes END, answered = CASE WHEN ?7 THEN ?8 ELSE answered END, updated_at = datetime('now') WHERE id = ?9 RETURNING *",
            params![changes.event_id.is_some(), changes.event_id, changes.body.is_some(), changes.body, changes.votes.is_some(), changes.votes, changes.answered.is_some(), changes.answered, id],
        )
        .await?;
    if let Some(record) = &updated {
        after_update(ctx, record).await?;
    }
    Ok(updated)
}

/// `false` when there is no question with this id.
pub async fn delete(ctx: &Ctx, id: i64) -> Result<bool> {
    before_delete(ctx, id).await?;
    let deleted: Option<Question> =
        ctx.db()?.first("DELETE FROM questions WHERE id = ?1 RETURNING *", params![id]).await?;
    let Some(record) = deleted else { return Ok(false) };
    after_delete(ctx, &record).await?;
    Ok(true)
}

/// Sets `updated_at` to now, without validation or callbacks (Rails'
/// `touch`): call it from another model's callbacks, e.g. a child's
/// `after_create`, so cache keys built from this question's `updated_at`
/// change. `false` when there is no question with this id.
pub async fn touch(ctx: &Ctx, id: i64) -> Result<bool> {
    let sql = "UPDATE questions SET updated_at = datetime('now') WHERE id = ?1";
    Ok(ctx.db()?.execute(sql, params![id]).await? > 0)
}

// Callbacks: `create`, `update` and `delete` call these, so every controller
// gets them. Return an error to stop the operation. D1 keeps no transaction
// open between queries: an `after_*` error does not undo the write, so put
// writes that must succeed together in one `ctx.db()?.batch(...)`.

/// Before validation and the INSERT: normalize or fill in values.
async fn before_create(_ctx: &Ctx, _new: &mut NewQuestion) -> Result<()> {
    Ok(())
}

/// After the INSERT: send email, enqueue jobs, update counters.
async fn after_create(_ctx: &Ctx, _question: &Question) -> Result<()> {
    Ok(())
}

/// Before validation and the UPDATE of the question `id`.
async fn before_update(_ctx: &Ctx, _id: i64, _changes: &mut QuestionChanges) -> Result<()> {
    Ok(())
}

/// After a successful UPDATE.
async fn after_update(_ctx: &Ctx, _question: &Question) -> Result<()> {
    Ok(())
}

/// Before the DELETE of the question `id`: return an error to keep it.
async fn before_delete(_ctx: &Ctx, _id: i64) -> Result<()> {
    Ok(())
}

/// After the DELETE, with the deleted row.
async fn after_delete(_ctx: &Ctx, _question: &Question) -> Result<()> {
    Ok(())
}