Skip to content

Commit 84485e6

Browse files
authored
Hide order logic in backend: issue 55 (#61)
* style: 💚 fix CI * fix: 🐛 reject unknown field on config * fix: 🐛 fix quick start docker-compose * Follow semantic conventions for GRPC Spans * bound check for add_to_request * rewrite rpc.oj.backend.token/list with normal pagaintor * optimize wasm size using alternative allocator and using different serialization library. * add order to database * change Dockerfile to enable bin feature * add ReOrder trait * change proto and the endpoint issue: #55
1 parent dce1ea0 commit 84485e6

File tree

12 files changed

+266
-6
lines changed

12 files changed

+266
-6
lines changed

backend/migration/src/m20231207_000001_create_table.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ enum Testcase {
115115
Input,
116116
Output,
117117
Score,
118+
Order,
118119
}
119120
#[derive(Iden)]
120121
enum Token {
@@ -488,6 +489,12 @@ impl MigrationTrait for Migration {
488489
.not_null()
489490
.default(0),
490491
)
492+
.col(
493+
ColumnDef::new(Testcase::Order)
494+
.float()
495+
.not_null()
496+
.default(0.0),
497+
)
491498
.to_owned(),
492499
)
493500
.await?;

backend/src/endpoint/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use tonic::*;
2222
use tracing::*;
2323
use uuid::Uuid;
2424

25-
use crate::entity::util::filter::*;
25+
use crate::entity::util::{filter::*, order::*};
2626
use crate::util::with::*;
2727
use crate::util::{auth::RoleLv, bound::BoundCheck, duplicate::*, error::Error, time::*};
2828
use crate::{fill_active_model, fill_exist_active_model, server::ArcServer, TonicStream};

backend/src/endpoint/problem.rs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ impl Problem for ArcServer {
223223
req.get_or_insert(|req| async move {
224224
let (contest, model) = tokio::try_join!(
225225
contest::Entity::write_by_id(req.contest_id, &auth)?
226+
.into_partial_model()
226227
.one(self.db.deref())
227228
.instrument(debug_span!("find_parent").or_current()),
228229
Entity::write_by_id(req.problem_id, &auth)?
@@ -231,13 +232,20 @@ impl Problem for ArcServer {
231232
)
232233
.map_err(Into::<Error>::into)?;
233234

234-
contest.ok_or(Error::NotInDB)?;
235+
let contest: contest::IdModel = contest.ok_or(Error::NotInDB)?;
235236

236237
let mut model = model.ok_or(Error::NotInDB)?.into_active_model();
237238
if let ActiveValue::Set(Some(v)) = model.contest_id {
238239
return Err(Error::AlreadyExist("problem already linked"));
239240
}
240241

242+
let order = contest
243+
.with_db(self.db.deref())
244+
.insert_last()
245+
.await
246+
.map_err(Into::<Error>::into)?;
247+
model.order = ActiveValue::Set(order);
248+
241249
model.contest_id = ActiveValue::Set(Some(req.problem_id));
242250
model
243251
.save(self.db.deref())
@@ -292,6 +300,51 @@ impl Problem for ArcServer {
292300
.with_grpc()
293301
.into()
294302
}
303+
#[instrument(
304+
skip_all,
305+
level = "info",
306+
name = "oj.backend.Problem/insert",
307+
err(level = "debug", Display)
308+
)]
309+
async fn insert(&self, req: Request<InsertProblemRequest>) -> Result<Response<()>, Status> {
310+
let (auth, req) = self.rate_limit(req).in_current_span().await?;
311+
auth.perm().super_user()?;
312+
313+
req.get_or_insert(|req| async move {
314+
let contest: contest::IdModel = contest::Entity::find_by_id(req.contest_id)
315+
.with_auth(&auth)
316+
.write()?
317+
.into_partial_model()
318+
.one(self.db.deref())
319+
.instrument(info_span!("fetch").or_current())
320+
.await
321+
.map_err(Into::<Error>::into)?
322+
.ok_or(Error::NotInDB)?;
323+
324+
let order = match req.pivot_id {
325+
None => contest.with_db(self.db.deref()).insert_front().await,
326+
Some(id) => contest.with_db(self.db.deref()).insert_after(id).await,
327+
}
328+
.map_err(Into::<Error>::into)?;
329+
330+
Entity::write_filter(
331+
Entity::update(ActiveModel {
332+
id: ActiveValue::Set(req.problem_id),
333+
order: ActiveValue::Set(order),
334+
..Default::default()
335+
}),
336+
&auth,
337+
)?
338+
.exec(self.db.deref())
339+
.await
340+
.map_err(Into::<Error>::into)?;
341+
342+
Ok(())
343+
})
344+
.await
345+
.with_grpc()
346+
.into()
347+
}
295348
#[instrument(
296349
skip_all,
297350
level = "info",

backend/src/endpoint/testcase.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ impl Testcase for ArcServer {
185185
req.get_or_insert(|req| async move {
186186
let (problem, model) = tokio::try_join!(
187187
problem::Entity::write_by_id(req.problem_id, &auth)?
188+
.into_partial_model()
188189
.one(self.db.deref())
189190
.instrument(debug_span!("find_parent").or_current()),
190191
Entity::write_by_id(req.testcase_id, &auth)?
@@ -193,12 +194,20 @@ impl Testcase for ArcServer {
193194
)
194195
.map_err(Into::<Error>::into)?;
195196

196-
problem.ok_or(Error::NotInDB)?;
197+
let problem: problem::IdModel = problem.ok_or(Error::NotInDB)?;
198+
197199
let mut model = model.ok_or(Error::NotInDB)?.into_active_model();
198200
if let ActiveValue::Set(Some(v)) = model.problem_id {
199201
return Err(Error::AlreadyExist("testcase already linked"));
200202
}
201203

204+
let order = problem
205+
.with_db(self.db.deref())
206+
.insert_last()
207+
.await
208+
.map_err(Into::<Error>::into)?;
209+
model.order = ActiveValue::Set(order);
210+
202211
model.problem_id = ActiveValue::Set(Some(req.problem_id));
203212
model
204213
.update(self.db.deref())
@@ -247,6 +256,51 @@ impl Testcase for ArcServer {
247256
.with_grpc()
248257
.into()
249258
}
259+
#[instrument(
260+
skip_all,
261+
level = "info",
262+
name = "oj.backend.Testcase/insert",
263+
err(level = "debug", Display)
264+
)]
265+
async fn insert(&self, req: Request<InsertTestcaseRequest>) -> Result<Response<()>, Status> {
266+
let (auth, req) = self.rate_limit(req).in_current_span().await?;
267+
auth.perm().super_user()?;
268+
269+
req.get_or_insert(|req| async move {
270+
let problem: problem::IdModel = problem::Entity::find_by_id(req.problem_id)
271+
.with_auth(&auth)
272+
.write()?
273+
.into_partial_model()
274+
.one(self.db.deref())
275+
.instrument(info_span!("fetch").or_current())
276+
.await
277+
.map_err(Into::<Error>::into)?
278+
.ok_or(Error::NotInDB)?;
279+
280+
let order = match req.pivot_id {
281+
None => problem.with_db(self.db.deref()).insert_front().await,
282+
Some(id) => problem.with_db(self.db.deref()).insert_after(id).await,
283+
}
284+
.map_err(Into::<Error>::into)?;
285+
286+
Entity::write_filter(
287+
Entity::update(ActiveModel {
288+
id: ActiveValue::Set(req.testcase_id),
289+
order: ActiveValue::Set(order),
290+
..Default::default()
291+
}),
292+
&auth,
293+
)?
294+
.exec(self.db.deref())
295+
.await
296+
.map_err(Into::<Error>::into)?;
297+
298+
Ok(())
299+
})
300+
.await
301+
.with_grpc()
302+
.into()
303+
}
250304
#[instrument(
251305
skip_all,
252306
level = "info",

backend/src/entity/testcase.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::*;
22

33
// FIXME: use partial model
4-
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
4+
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
55
#[sea_orm(table_name = "testcase")]
66
pub struct Model {
77
#[sea_orm(primary_key)]
@@ -14,6 +14,7 @@ pub struct Model {
1414
#[sea_orm(column_type = "Binary(BlobSize::Blob(None))")]
1515
pub output: Vec<u8>,
1616
pub score: u32,
17+
pub order: f32,
1718
}
1819

1920
#[derive(DerivePartialModel, FromQueryResult)]

backend/src/entity/util/helper.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
//! a collection of helper function
1+
//! a collection of helper function for low level sql query
2+
//!
3+
//! This module use extensively of [`sea_query`], which make it extreme unsafe to use
24
35
use std::ops::Deref;
46

backend/src/entity/util/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub mod filter;
22
pub mod helper;
3+
pub mod order;
34
pub mod paginator;

backend/src/entity/util/order.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
use crate::util::with::WithDB;
2+
use crate::util::with::WithDBTrait;
3+
use sea_orm::*;
4+
use tonic::async_trait;
5+
6+
#[async_trait]
7+
pub trait ReOrder {
8+
async fn insert_last(self) -> Result<f32, DbErr>;
9+
async fn insert_after(self, pivot: i32) -> Result<f32, DbErr>;
10+
async fn insert_front(self) -> Result<f32, DbErr>;
11+
}
12+
13+
#[derive(Default, EnumIter, DeriveColumn, Clone, Copy, Debug)]
14+
enum RetValue {
15+
#[default]
16+
RetValue,
17+
}
18+
pub mod testcase {
19+
use super::*;
20+
use crate::entity::problem;
21+
use crate::entity::testcase::{Column, Entity};
22+
23+
impl WithDBTrait for problem::IdModel {}
24+
25+
#[async_trait]
26+
impl ReOrder for WithDB<'_, problem::IdModel> {
27+
async fn insert_last(self) -> Result<f32, DbErr> {
28+
Entity::find()
29+
.filter(Column::ProblemId.eq(self.1.id))
30+
.select_only()
31+
.column_as(Column::Order.max(), RetValue::default())
32+
.into_values::<_, RetValue>()
33+
.one(self.0)
34+
.await
35+
.map(|x: Option<f32>| x.unwrap_or_default() + 1.0)
36+
}
37+
async fn insert_after(self, pivot: i32) -> Result<f32, DbErr> {
38+
let vals: Vec<f32> = Entity::find()
39+
.filter(Column::ProblemId.eq(self.1.id))
40+
.filter(Column::Order.gte(pivot))
41+
.select_only()
42+
.column_as(Column::Order.min(), RetValue::default())
43+
.limit(2)
44+
.into_values::<_, RetValue>()
45+
.all(self.0)
46+
.await?;
47+
Ok(match vals.len() {
48+
1 => vals[0] + 1.0,
49+
2 => (vals[0] + vals[1]) * 0.5,
50+
_ => 0.0,
51+
})
52+
}
53+
async fn insert_front(self) -> Result<f32, DbErr> {
54+
Entity::find()
55+
.filter(Column::ProblemId.eq(self.1.id))
56+
.select_only()
57+
.column_as(Column::Order.min(), RetValue::default())
58+
.into_values::<_, RetValue>()
59+
.one(self.0)
60+
.await
61+
.map(|x: Option<f32>| x.unwrap_or_default() - 1.0)
62+
}
63+
}
64+
}
65+
66+
pub mod contest {
67+
use super::*;
68+
use crate::entity::contest;
69+
use crate::entity::problem::{Column, Entity};
70+
71+
impl WithDBTrait for contest::IdModel {}
72+
#[async_trait]
73+
impl ReOrder for WithDB<'_, contest::IdModel> {
74+
async fn insert_last(self) -> Result<f32, DbErr> {
75+
Entity::find()
76+
.filter(Column::ContestId.eq(self.1.id))
77+
.select_only()
78+
.column_as(Column::Order.max(), RetValue::default())
79+
.into_values::<_, RetValue>()
80+
.one(self.0)
81+
.await
82+
.map(|x: Option<f32>| x.unwrap_or_default() + 1.0)
83+
}
84+
async fn insert_after(self, pivot: i32) -> Result<f32, DbErr> {
85+
let vals: Vec<f32> = Entity::find()
86+
.filter(Column::ContestId.eq(self.1.id))
87+
.filter(Column::Order.gte(pivot))
88+
.select_only()
89+
.column_as(Column::Order.min(), RetValue::default())
90+
.limit(2)
91+
.into_values::<_, RetValue>()
92+
.all(self.0)
93+
.await?;
94+
Ok(match vals.len() {
95+
1 => vals[0] + 1.0,
96+
2 => (vals[0] + vals[1]) * 0.5,
97+
_ => 0.0,
98+
})
99+
}
100+
async fn insert_front(self) -> Result<f32, DbErr> {
101+
Entity::find()
102+
.filter(Column::ContestId.eq(self.1.id))
103+
.select_only()
104+
.column_as(Column::Order.min(), RetValue::default())
105+
.into_values::<_, RetValue>()
106+
.one(self.0)
107+
.await
108+
.map(|x: Option<f32>| x.unwrap_or_default() - 1.0)
109+
}
110+
}
111+
}

backend/src/util/duplicate.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,6 @@ create_cache!(AddEducationToProblemRequest, ());
114114
create_cache!(UploadRequest, UploadResponse);
115115
create_cache!(AddTestcaseToProblemRequest, ());
116116
create_cache!(AddProblemToContestRequest, ());
117+
118+
create_cache!(InsertProblemRequest, ());
119+
create_cache!(InsertTestcaseRequest, ());

backend/src/util/rate_limit.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,14 @@ impl RateLimit for RefreshRequest {
214214
50
215215
}
216216
}
217+
218+
impl RateLimit for InsertProblemRequest {
219+
fn get_cost(&self) -> u32 {
220+
3 + (self.pivot_id.is_some() as u32) * 2
221+
}
222+
}
223+
impl RateLimit for InsertTestcaseRequest {
224+
fn get_cost(&self) -> u32 {
225+
3 + (self.pivot_id.is_some() as u32) * 2
226+
}
227+
}

frontend/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ RUN rustup target add ${ARCH}-unknown-linux-musl
2727
ENV LEPTOS_OUTPUT_NAME="mdoj"
2828
ENV LEPTOS_BIN_TARGET_TRIPLE=${ARCH}-unknown-linux-musl
2929

30-
RUN cargo leptos build -p frontend --release --precompress -vv
30+
RUN cargo leptos build -p frontend --bin-features compress --release --precompress -vv
3131

3232
FROM scratch
3333
WORKDIR /config

0 commit comments

Comments
 (0)