use ocre::{Ctx, Error, Page, Query, Result, Validator, params};
use serde::{Deserialize, Serialize};
#[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,
}
#[derive(Debug, Clone, Deserialize)]
pub struct NewQuestion {
pub event_id: i64,
pub body: String,
pub votes: i64,
#[serde(default)]
pub answered: bool,
}
#[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 {
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 {
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
}
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 {
pub async fn event(&self, ctx: &Ctx) -> Result<Option<crate::models::event::Event>> {
crate::models::event::find(ctx, self.event_id).await
}
}
pub fn query() -> Query<Question> {
Query::table("questions")
}
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
}
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)
}
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())
}
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)
}
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
}
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)
}
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)
}
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)
}
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)
}
async fn before_create(_ctx: &Ctx, _new: &mut NewQuestion) -> Result<()> {
Ok(())
}
async fn after_create(_ctx: &Ctx, _question: &Question) -> Result<()> {
Ok(())
}
async fn before_update(_ctx: &Ctx, _id: i64, _changes: &mut QuestionChanges) -> Result<()> {
Ok(())
}
async fn after_update(_ctx: &Ctx, _question: &Question) -> Result<()> {
Ok(())
}
async fn before_delete(_ctx: &Ctx, _id: i64) -> Result<()> {
Ok(())
}
async fn after_delete(_ctx: &Ctx, _question: &Question) -> Result<()> {
Ok(())
}