Created
April 23, 2025 09:29
-
-
Save oleggtro/1b54e695baa7117c69bbb2d7dbdc418f to your computer and use it in GitHub Desktop.
musicaldesk paginated query builder fix
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use actix_web::{http::StatusCode, HttpResponse, ResponseError}; | |
use serde::Serialize; | |
use sqlx::{postgres::PgRow, query, query_as, FromRow, Postgres}; | |
use sqlx::{Encode, PgPool, Type}; | |
use sqlx::{QueryBuilder, Row}; | |
use std::error::Error as ErrorTrait; | |
use thiserror::Error; | |
#[derive(Clone, Copy, PartialEq, Eq)] | |
pub enum UserRole { | |
Admin, | |
MemberAdmin, | |
EventAdmin, | |
// Default role for backend users (not for shop users) | |
Default, | |
ShopCustomer, | |
TicketInvalidator, | |
Accountant, | |
} | |
pub const BACKEND_ROLES: [UserRole; 3] = | |
[UserRole::Admin, UserRole::EventAdmin, UserRole::MemberAdmin]; | |
impl ToString for UserRole { | |
fn to_string(&self) -> String { | |
match self { | |
Self::Admin => "admin".to_string(), | |
Self::Default => "default".to_string(), | |
Self::MemberAdmin => "member_admin".to_string(), | |
Self::EventAdmin => "event_admin".to_string(), | |
Self::ShopCustomer => "shop_customer".to_string(), | |
Self::TicketInvalidator => "ticket_invalidator".to_string(), | |
Self::Accountant => "accountant".to_string(), | |
} | |
} | |
} | |
impl TryFrom<&String> for UserRole { | |
type Error = Error; | |
fn try_from(value: &String) -> Result<Self, Self::Error> { | |
match value.as_str() { | |
"admin" => Ok(Self::Admin), | |
"member_admin" => Ok(Self::MemberAdmin), | |
"event_admin" => Ok(Self::EventAdmin), | |
"shop_customer" => Ok(Self::ShopCustomer), | |
"ticket_invalidator" => Ok(Self::TicketInvalidator), | |
"accountant" => Ok(Self::Accountant), | |
_ => Err(Error::BadRequest), | |
} | |
} | |
} | |
#[derive(Debug, Error)] | |
pub enum Error { | |
#[error("ALREADY_EXISTS")] | |
AlreadyExists, | |
#[error("BAD_REQUEST")] | |
BadRequest, | |
#[error("NOT_FOUND")] | |
NotFound, | |
#[error("UNAUTHORIZED")] | |
Unauthorized, | |
#[error("FORBIDDEN")] | |
Forbidden, | |
#[error("INTERNAL_SERVER_ERROR")] | |
InternalServerError { source: Box<dyn ErrorTrait> }, | |
} | |
#[derive(Serialize)] | |
struct ResponseBody { | |
message: String, | |
} | |
impl ResponseError for Error { | |
fn error_response(&self) -> HttpResponse { | |
let response_body = ResponseBody { | |
message: format!("{}", self), | |
}; | |
HttpResponse::build(self.status_code()).json(response_body) | |
} | |
/// Map http statuscodes to the corresponding [`Error`] variants | |
fn status_code(&self) -> StatusCode { | |
use Error::*; | |
match self { | |
AlreadyExists => StatusCode::CONFLICT, | |
BadRequest => StatusCode::BAD_REQUEST, | |
Forbidden => StatusCode::FORBIDDEN, | |
InternalServerError { source: _ } => StatusCode::INTERNAL_SERVER_ERROR, | |
NotFound => StatusCode::NOT_FOUND, | |
Unauthorized => StatusCode::UNAUTHORIZED, | |
} | |
} | |
} | |
impl From<Box<dyn ErrorTrait>> for Error { | |
fn from(v: Box<dyn ErrorTrait>) -> Self { | |
Self::InternalServerError { source: v } | |
} | |
} | |
pub struct Paginated<T> { | |
pub results: Vec<T>, | |
pub total: i64, | |
} | |
impl<T> Paginated<T> | |
where | |
T: for<'r> FromRow<'r, PgRow> + Send + Unpin, | |
{ | |
pub async fn create_paginated_query<U>( | |
select: &str, | |
table: &str, | |
where_clause: Option<&str>, | |
page: i32, | |
page_size: i32, | |
params: Vec<U>, | |
db: &PgPool, | |
) -> Self | |
where | |
U: Encode<'static, sqlx::Postgres> + Type<sqlx::Postgres> + Clone + Send + Sync + 'static, | |
{ | |
let mut query_builder = | |
QueryBuilder::<Postgres>::new(format!("SELECT {} FROM {}", select, table)); | |
if let Some(clause) = where_clause { | |
query_builder.push(" WHERE ").push(clause); | |
} | |
query_builder | |
.push(" LIMIT ") | |
.push_bind(page_size) | |
.push(" OFFSET ") | |
.push_bind(page_size * page); | |
let mut count_builder = | |
QueryBuilder::<Postgres>::new(format!("SELECT COUNT(*) AS count FROM {}", table)); | |
if let Some(clause) = where_clause { | |
count_builder.push(" WHERE ").push(clause); | |
} | |
// Add parameters to both queries | |
for param in params.iter() { | |
query_builder.push_bind(param.clone()); | |
count_builder.push_bind(param.clone()); | |
} | |
let results = query_builder | |
.build_query_as::<T>() | |
.fetch_all(db) | |
.await | |
.expect("Cannot execute select"); | |
let count_row: PgRow = count_builder | |
.build() | |
.fetch_one(db) | |
.await | |
.expect("Cannot fetch count"); | |
let total: i64 = count_row.get("count"); | |
Paginated { results, total } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment