1use std::collections::HashSet;
2
3use futures::{Stream, future::BoxFuture};
4use serde_json::Value;
5use url::Url;
6
7use crate::{
8 CourseOrExamId,
9 exercise_service_info::{self, ExerciseServiceInfoApi},
10 exercise_services, exercise_slide_submissions,
11 exercise_tasks::{CourseMaterialExerciseTask, ExerciseTask},
12 library::custom_view_exercises::{CustomViewExerciseTaskSubmission, CustomViewExerciseTasks},
13 peer_or_self_review_question_submissions::PeerOrSelfReviewQuestionSubmission,
14 peer_or_self_review_questions::PeerOrSelfReviewQuestion,
15 peer_or_self_review_submissions::PeerOrSelfReviewSubmission,
16 prelude::*,
17};
18
19#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
20#[cfg_attr(feature = "ts_rs", derive(TS))]
21pub struct ExerciseTaskSubmission {
22 pub id: Uuid,
23 pub created_at: DateTime<Utc>,
24 pub updated_at: DateTime<Utc>,
25 pub deleted_at: Option<DateTime<Utc>>,
26 pub exercise_slide_submission_id: Uuid,
27 pub exercise_task_id: Uuid,
28 pub exercise_slide_id: Uuid,
29 pub data_json: Option<serde_json::Value>,
30 pub exercise_task_grading_id: Option<Uuid>,
31 pub metadata: Option<serde_json::Value>,
32}
33
34#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
35#[cfg_attr(feature = "ts_rs", derive(TS))]
36pub struct PeerOrSelfReviewsReceived {
37 pub peer_or_self_review_questions: Vec<PeerOrSelfReviewQuestion>,
38 pub peer_or_self_review_question_submissions: Vec<PeerOrSelfReviewQuestionSubmission>,
39 pub peer_or_self_review_submissions: Vec<PeerOrSelfReviewSubmission>,
40}
41
42#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
43#[cfg_attr(feature = "ts_rs", derive(TS))]
44pub struct SubmissionData {
45 pub exercise_id: Uuid,
46 pub course_id: Uuid,
47 pub exercise_slide_submission_id: Uuid,
48 pub exercise_slide_id: Uuid,
49 pub exercise_task_id: Uuid,
50 pub user_id: Uuid,
51 pub course_instance_id: Uuid,
52 pub data_json: Value,
53 pub id: Uuid,
54}
55
56#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
57#[cfg_attr(feature = "ts_rs", derive(TS))]
58pub struct ExportedSubmission {
59 pub id: Uuid,
60 pub user_id: Uuid,
61 pub created_at: DateTime<Utc>,
62 pub exercise_id: Uuid,
63 pub exercise_task_id: Uuid,
64 pub score_given: Option<f32>,
65 pub data_json: Option<serde_json::Value>,
66}
67
68#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
69#[cfg_attr(feature = "ts_rs", derive(TS))]
70pub struct ExportedCourseSubmission {
71 pub exercise_slide_submission_id: Uuid,
72 pub id: Uuid,
73 pub user_id: Uuid,
74 pub created_at: DateTime<Utc>,
75 pub course_id: Option<Uuid>,
76 pub exercise_id: Uuid,
77 pub exercise_task_id: Uuid,
78 pub score_given: Option<f32>,
79 pub data_json: Option<serde_json::Value>,
80}
81
82#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
84pub struct ExerciseTaskSubmissionCsvExportData {
85 pub exercise_slide_submission_id: Uuid,
86 pub exercise_task_submission_id: Uuid,
87 pub exercise_task_id: Uuid,
88 pub exercise_id: Uuid,
89 pub user_id: Uuid,
90 pub submitted_at: DateTime<Utc>,
91 pub answer: Option<serde_json::Value>,
92}
93
94pub async fn get_submission(
95 conn: &mut PgConnection,
96 submission_id: Uuid,
97) -> ModelResult<ExerciseTaskSubmission> {
98 let res = sqlx::query_as!(
99 ExerciseTaskSubmission,
100 "
101SELECT *
102FROM exercise_task_submissions
103WHERE id = $1
104",
105 submission_id
106 )
107 .fetch_one(conn)
108 .await?;
109 Ok(res)
110}
111
112pub async fn insert_with_id(
114 conn: &mut PgConnection,
115 submission_data: &SubmissionData,
116) -> ModelResult<Uuid> {
117 let res = sqlx::query!(
118 "
119INSERT INTO exercise_task_submissions (
120 id,
121 exercise_slide_submission_id,
122 exercise_slide_id,
123 exercise_task_id,
124 data_json
125 )
126VALUES ($1, $2, $3, $4, $5)
127RETURNING id
128 ",
129 submission_data.id,
130 submission_data.exercise_slide_submission_id,
131 submission_data.exercise_slide_id,
132 submission_data.exercise_task_id,
133 submission_data.data_json,
134 )
135 .fetch_one(conn)
136 .await?;
137 Ok(res.id)
138}
139
140pub async fn insert(
141 conn: &mut PgConnection,
142 pkey_policy: PKeyPolicy<Uuid>,
143 exercise_slide_submission_id: Uuid,
144 exercise_slide_id: Uuid,
145 exercise_task_id: Uuid,
146 data_json: &Value,
147) -> ModelResult<Uuid> {
148 let res = sqlx::query!(
149 "
150INSERT INTO exercise_task_submissions (
151 id,
152 exercise_slide_submission_id,
153 exercise_slide_id,
154 exercise_task_id,
155 data_json
156 )
157 VALUES ($1, $2, $3, $4, $5)
158 RETURNING id
159 ",
160 pkey_policy.into_uuid(),
161 exercise_slide_submission_id,
162 exercise_slide_id,
163 exercise_task_id,
164 data_json,
165 )
166 .fetch_one(conn)
167 .await?;
168 Ok(res.id)
169}
170
171pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<ExerciseTaskSubmission> {
172 let submission = sqlx::query_as!(
173 ExerciseTaskSubmission,
174 "
175SELECT *
176FROM exercise_task_submissions
177WHERE id = $1
178",
179 id
180 )
181 .fetch_one(conn)
182 .await?;
183 Ok(submission)
184}
185
186pub async fn get_by_exercise_slide_submission_id(
187 conn: &mut PgConnection,
188 exercise_slide_submission_id: Uuid,
189) -> ModelResult<Vec<ExerciseTaskSubmission>> {
190 let submissions = sqlx::query_as!(
191 ExerciseTaskSubmission,
192 "
193SELECT *
194FROM exercise_task_submissions
195WHERE exercise_slide_submission_id = $1
196 ",
197 exercise_slide_submission_id
198 )
199 .fetch_all(conn)
200 .await?;
201 Ok(submissions)
202}
203
204pub async fn get_csv_export_data_by_exercise_and_task(
206 conn: &mut PgConnection,
207 exercise_id: Uuid,
208 exercise_task_id: Uuid,
209) -> ModelResult<Vec<ExerciseTaskSubmissionCsvExportData>> {
210 let submissions = sqlx::query_as!(
211 ExerciseTaskSubmissionCsvExportData,
212 r#"
213SELECT ets.exercise_slide_submission_id,
214 ets.id AS exercise_task_submission_id,
215 ets.exercise_task_id,
216 ess.exercise_id,
217 ess.user_id,
218 ets.created_at AS submitted_at,
219 ets.data_json AS answer
220FROM exercise_task_submissions ets
221 JOIN exercise_slide_submissions ess ON ets.exercise_slide_submission_id = ess.id
222WHERE ess.exercise_id = $1
223 AND ets.exercise_task_id = $2
224 AND ess.deleted_at IS NULL
225 AND ets.deleted_at IS NULL
226ORDER BY ets.created_at ASC
227 "#,
228 exercise_id,
229 exercise_task_id
230 )
231 .fetch_all(conn)
232 .await?;
233 Ok(submissions)
234}
235
236pub async fn get_csv_export_data_by_exercise_and_task_latest_per_user(
238 conn: &mut PgConnection,
239 exercise_id: Uuid,
240 exercise_task_id: Uuid,
241) -> ModelResult<Vec<ExerciseTaskSubmissionCsvExportData>> {
242 let submissions = sqlx::query_as!(
243 ExerciseTaskSubmissionCsvExportData,
244 r#"
245WITH latest AS (
246 SELECT DISTINCT ON (ess.user_id) ets.id AS exercise_task_submission_id
247 FROM exercise_task_submissions ets
248 JOIN exercise_slide_submissions ess ON ets.exercise_slide_submission_id = ess.id
249 WHERE ess.exercise_id = $1
250 AND ets.exercise_task_id = $2
251 AND ess.deleted_at IS NULL
252 AND ets.deleted_at IS NULL
253 ORDER BY ess.user_id, ets.created_at DESC
254)
255SELECT ets.exercise_slide_submission_id,
256 ets.id AS exercise_task_submission_id,
257 ets.exercise_task_id,
258 ess.exercise_id,
259 ess.user_id,
260 ets.created_at AS submitted_at,
261 ets.data_json AS answer
262FROM exercise_task_submissions ets
263JOIN exercise_slide_submissions ess ON ets.exercise_slide_submission_id = ess.id
264JOIN latest ON latest.exercise_task_submission_id = ets.id
265WHERE ess.exercise_id = $1
266 AND ets.exercise_task_id = $2
267 AND ess.deleted_at IS NULL
268 AND ets.deleted_at IS NULL
269ORDER BY ets.created_at ASC
270 "#,
271 exercise_id,
272 exercise_task_id
273 )
274 .fetch_all(conn)
275 .await?;
276 Ok(submissions)
277}
278
279pub async fn get_users_latest_exercise_task_submissions_for_exercise_slide(
280 conn: &mut PgConnection,
281 exercise_slide_id: Uuid,
282 user_id: Uuid,
283) -> ModelResult<Option<Vec<ExerciseTaskSubmission>>> {
284 let exercise_slide_submission =
285 exercise_slide_submissions::try_to_get_users_latest_exercise_slide_submission(
286 conn,
287 exercise_slide_id,
288 user_id,
289 )
290 .await?;
291 if let Some(exercise_slide_submission) = exercise_slide_submission {
292 let task_submissions = sqlx::query_as!(
293 ExerciseTaskSubmission,
294 "
295SELECT *
296FROM exercise_task_submissions
297WHERE exercise_slide_submission_id = $1
298 AND deleted_at IS NULL
299 ",
300 exercise_slide_submission.id
301 )
302 .fetch_all(conn)
303 .await?;
304 Ok(Some(task_submissions))
305 } else {
306 Ok(None)
307 }
308}
309
310pub async fn get_course_and_exam_id(
311 conn: &mut PgConnection,
312 id: Uuid,
313) -> ModelResult<CourseOrExamId> {
314 let res = sqlx::query!(
315 "
316SELECT ess.course_id,
317 ess.exam_id
318FROM exercise_task_submissions ets
319 JOIN exercise_slide_submissions ess ON ets.exercise_slide_submission_id = ess.id
320WHERE ets.id = $1
321 AND ets.deleted_at IS NULL
322 AND ess.deleted_at IS NULL
323 ",
324 id
325 )
326 .fetch_one(conn)
327 .await?;
328 CourseOrExamId::from_course_and_exam_ids(res.course_id, res.exam_id)
329}
330
331pub async fn get_peer_reviews_received(
332 conn: &mut PgConnection,
333 exercise_id: Uuid,
334 exercise_slide_submission_id: Uuid,
335 user_id: Uuid,
336) -> ModelResult<PeerOrSelfReviewsReceived> {
337 let exercise = crate::exercises::get_by_id(&mut *conn, exercise_id).await?;
338 let peer_or_self_review_config =
339 crate::peer_or_self_review_configs::get_by_exercise_or_course_id(
340 &mut *conn,
341 &exercise,
342 exercise.course_id.ok_or_else(|| {
343 ModelError::new(
344 ModelErrorType::InvalidRequest,
345 "Peer reviews work only on courses (and not, for example, on exams)"
346 .to_string(),
347 None,
348 )
349 })?,
350 )
351 .await?;
352 let peer_or_self_review_questions =
353 crate::peer_or_self_review_questions::get_by_peer_or_self_review_configs_id(
354 &mut *conn,
355 peer_or_self_review_config.id,
356 )
357 .await?;
358
359 let peer_or_self_review_question_ids = peer_or_self_review_questions
360 .iter()
361 .map(|x| x.id)
362 .collect::<Vec<_>>();
363
364 let peer_or_self_review_submissions =
365 crate::peer_or_self_review_submissions::get_received_peer_or_self_review_submissions_for_user_by_peer_or_self_review_config_id_and_exercise_slide_submission(
366 &mut *conn,
367 user_id,
368 exercise_slide_submission_id,
369 peer_or_self_review_config.id,
370 )
371 .await?;
372
373 let peer_or_self_review_question_submissions =
374 crate::peer_or_self_review_question_submissions::get_by_peer_reviews_question_ids(
375 &mut *conn,
376 &peer_or_self_review_question_ids,
377 user_id,
378 exercise_slide_submission_id,
379 )
380 .await?;
381
382 Ok(PeerOrSelfReviewsReceived {
383 peer_or_self_review_questions,
384 peer_or_self_review_question_submissions,
385 peer_or_self_review_submissions,
386 })
387}
388
389pub async fn set_grading_id(
390 conn: &mut PgConnection,
391 grading_id: Uuid,
392 submission_id: Uuid,
393) -> ModelResult<ExerciseTaskSubmission> {
394 let res = sqlx::query_as!(
395 ExerciseTaskSubmission,
396 "
397UPDATE exercise_task_submissions
398SET exercise_task_grading_id = $1
399WHERE id = $2
400RETURNING *
401",
402 grading_id,
403 submission_id
404 )
405 .fetch_one(conn)
406 .await?;
407 Ok(res)
408}
409
410pub fn stream_exam_submissions(
411 conn: &mut PgConnection,
412 exam_id: Uuid,
413) -> impl Stream<Item = sqlx::Result<ExportedSubmission>> + '_ {
414 sqlx::query_as!(
415 ExportedSubmission,
416 "
417SELECT exercise_task_submissions.id,
418 user_id,
419 exercise_task_submissions.created_at,
420 exercise_slide_submissions.exercise_id,
421 exercise_task_submissions.exercise_task_id,
422 exercise_task_gradings.score_given,
423 exercise_task_submissions.data_json
424FROM exercise_task_submissions
425 JOIN exercise_slide_submissions ON exercise_task_submissions.exercise_slide_submission_id = exercise_slide_submissions.id
426 JOIN exercise_task_gradings on exercise_task_submissions.exercise_task_grading_id = exercise_task_gradings.id
427 JOIN exercises on exercise_slide_submissions.exercise_id = exercises.id
428WHERE exercise_slide_submissions.exam_id = $1
429 AND exercise_task_submissions.deleted_at IS NULL
430 AND exercise_task_gradings.deleted_at IS NULL
431 AND exercises.deleted_at IS NULL;
432 ",
433 exam_id
434 )
435 .fetch(conn)
436}
437
438pub fn stream_course_submissions(
439 conn: &mut PgConnection,
440 course_id: Uuid,
441) -> impl Stream<Item = sqlx::Result<ExportedCourseSubmission>> + '_ {
442 sqlx::query_as!(
443 ExportedCourseSubmission,
444 "
445SELECT exercise_task_submissions.exercise_slide_submission_id,
446 exercise_task_submissions.id,
447 user_id,
448 exercise_task_submissions.created_at,
449 exercise_slide_submissions.course_id,
450 exercise_slide_submissions.exercise_id,
451 exercise_task_submissions.exercise_task_id,
452 exercise_task_gradings.score_given,
453 exercise_task_submissions.data_json
454FROM exercise_task_submissions
455 JOIN exercise_slide_submissions ON exercise_task_submissions.exercise_slide_submission_id = exercise_slide_submissions.id
456 JOIN exercise_task_gradings ON exercise_task_submissions.exercise_task_grading_id = exercise_task_gradings.id
457 JOIN exercises ON exercise_slide_submissions.exercise_id = exercises.id
458WHERE exercise_slide_submissions.course_id = $1
459 AND exercise_slide_submissions.deleted_at IS NULL
460 AND exercise_task_submissions.deleted_at IS NULL
461 AND exercise_task_gradings.deleted_at IS NULL
462 AND exercises.deleted_at IS NULL;
463 ",
464 course_id
465 )
466 .fetch(conn)
467}
468
469pub async fn get_exercise_task_submission_info_by_exercise_slide_submission_id(
471 conn: &mut PgConnection,
472 exercise_slide_submission_id: Uuid,
473 viewer_user_id: Uuid,
474 fetch_service_info: impl Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>>,
475 include_deleted_tasks: bool,
476) -> ModelResult<Vec<CourseMaterialExerciseTask>> {
477 let task_submisssions = crate::exercise_task_submissions::get_by_exercise_slide_submission_id(
478 &mut *conn,
479 exercise_slide_submission_id,
480 )
481 .await?;
482 let exercise_task_gradings =
483 crate::exercise_task_gradings::get_all_gradings_by_exercise_slide_submission_id(
484 &mut *conn,
485 exercise_slide_submission_id,
486 )
487 .await?;
488
489 let exercise_tasks = if include_deleted_tasks {
490 crate::exercise_tasks::get_exercise_tasks_by_exercise_slide_id_including_deleted::<
491 Vec<ExerciseTask>,
492 >(&mut *conn, &task_submisssions[0].exercise_slide_id)
493 .await?
494 } else {
495 crate::exercise_tasks::get_exercise_tasks_by_exercise_slide_id::<Vec<ExerciseTask>>(
496 &mut *conn,
497 &task_submisssions[0].exercise_slide_id,
498 )
499 .await?
500 };
501
502 let mut res = Vec::with_capacity(task_submisssions.len());
503
504 let unique_exercise_service_slugs = exercise_tasks
505 .iter()
506 .cloned()
507 .map(|et| et.exercise_type)
508 .collect::<HashSet<_>>()
509 .into_iter()
510 .collect::<Vec<_>>();
511 let exercise_service_slug_to_service_and_info =
512 exercise_service_info::get_selected_exercise_services_by_type(
513 &mut *conn,
514 &unique_exercise_service_slugs,
515 fetch_service_info,
516 )
517 .await?;
518
519 for ts in task_submisssions {
520 let grading = exercise_task_gradings
521 .iter()
522 .find(|g| Some(g.id) == ts.exercise_task_grading_id)
523 .ok_or_else(|| {
524 ModelError::new(
525 ModelErrorType::NotFound,
526 "Grading not found".to_string(),
527 None,
528 )
529 })?;
530 let task = exercise_tasks
531 .iter()
532 .find(|t| t.id == ts.exercise_task_id)
533 .ok_or_else(|| {
534 ModelError::new(
535 ModelErrorType::NotFound,
536 "Exercise task not found".to_string(),
537 None,
538 )
539 })?;
540 let (exercise_service, service_info) = exercise_service_slug_to_service_and_info
541 .get(&task.exercise_type)
542 .ok_or_else(|| {
543 ModelError::new(
544 ModelErrorType::InvalidRequest,
545 "Exercise service not found".to_string(),
546 None,
547 )
548 })?;
549 let mut exercise_iframe_url =
550 exercise_services::get_exercise_service_externally_preferred_baseurl(exercise_service)?;
551 exercise_iframe_url.set_path(&service_info.user_interface_iframe_path);
552 let course_material_exercise_task = CourseMaterialExerciseTask {
553 id: task.id,
554 exercise_service_slug: task.exercise_type.clone(),
555 exercise_slide_id: task.exercise_slide_id,
556 exercise_iframe_url: Some(exercise_iframe_url.to_string()),
557 pseudonumous_user_id: Some(Uuid::new_v5(
558 &service_info.exercise_service_id,
559 viewer_user_id.as_bytes(),
560 )),
561 assignment: task.assignment.clone(),
562 public_spec: task.public_spec.clone(),
563 model_solution_spec: task.model_solution_spec.clone(),
564 previous_submission: Some(ts),
565 previous_submission_grading: Some(grading.clone()),
566 order_number: task.order_number,
567 deleted_at: task.deleted_at,
568 };
569 res.push(course_material_exercise_task);
570 }
571 Ok(res)
572}
573
574pub async fn get_user_custom_view_exercise_tasks_by_module_and_exercise_type(
575 conn: &mut PgConnection,
576 exercise_type: &str,
577 course_module_id: Uuid,
578 user_id: Uuid,
579 course_id: Uuid,
580) -> ModelResult<CustomViewExerciseTasks> {
581 let task_submissions =
582 crate::exercise_task_submissions::get_user_latest_exercise_task_submissions_by_course_module_and_exercise_type(
583 &mut *conn,
584 user_id,
585 exercise_type,
586 course_module_id,
587 course_id,
588 )
589 .await?;
590 let task_gradings =
591 crate::exercise_task_gradings::get_user_exercise_task_gradings_by_module_and_exercise_type(
592 &mut *conn,
593 user_id,
594 exercise_type,
595 course_module_id,
596 course_id,
597 )
598 .await?;
599
600 let exercise_tasks = crate::exercise_tasks::get_all_exercise_tasks_by_module_and_exercise_type(
601 &mut *conn,
602 exercise_type,
603 course_module_id,
604 )
605 .await?;
606 let res: CustomViewExerciseTasks = CustomViewExerciseTasks {
607 exercise_tasks,
608 task_submissions,
609 task_gradings,
610 };
611 Ok(res)
612}
613
614pub async fn get_user_latest_exercise_task_submissions_by_course_module_and_exercise_type(
616 conn: &mut PgConnection,
617 user_id: Uuid,
618 exercise_type: &str,
619 module_id: Uuid,
620 course_id: Uuid,
621) -> ModelResult<Vec<CustomViewExerciseTaskSubmission>> {
622 let res: Vec<CustomViewExerciseTaskSubmission> = sqlx::query_as!(
623 CustomViewExerciseTaskSubmission,
624 r#"
625 SELECT DISTINCT ON (g.exercise_task_id)
626 g.id,
627 g.created_at,
628 g.exercise_slide_submission_id,
629 g.exercise_slide_id,
630 g.exercise_task_id,
631 g.exercise_task_grading_id,
632 g.data_json
633 FROM exercise_task_submissions g
634 JOIN exercise_tasks et ON et.id = g.exercise_task_id
635 JOIN exercise_slide_submissions ess ON ess.id = g.exercise_slide_submission_id
636 JOIN exercises e ON e.id = ess.exercise_id
637 JOIN chapters c ON c.id = e.chapter_id
638 WHERE ess.user_id = $1
639 AND ess.course_id = $2
640 AND et.exercise_type = $3
641 AND c.course_module_id = $4
642 AND g.deleted_at IS NULL
643 AND et.deleted_at IS NULL
644 AND ess.deleted_at IS NULL
645 AND e.deleted_at IS NULL
646 AND c.deleted_at IS NULL
647 ORDER BY g.exercise_task_id, g.created_at DESC
648 "#,
649 user_id,
650 course_id,
651 exercise_type,
652 module_id
653 )
654 .fetch_all(conn)
655 .await?;
656 Ok(res)
657}
658
659pub async fn get_ids_by_exercise_id(
660 conn: &mut PgConnection,
661 exercise_id: Uuid,
662) -> ModelResult<Vec<Uuid>> {
663 let res = sqlx::query!(
664 "
665SELECT id
666FROM exercise_task_submissions
667WHERE exercise_slide_submission_id IN (
668 SELECT id
669 FROM exercise_slide_submissions
670 WHERE exercise_id = $1
671)
672AND deleted_at IS NULL
673",
674 &exercise_id
675 )
676 .fetch_all(conn)
677 .await?;
678 Ok(res.iter().map(|x| x.id).collect())
679}
680
681pub async fn get_latest_submission_ids_by_exercise_id(
683 conn: &mut PgConnection,
684 exercise_id: Uuid,
685) -> ModelResult<Vec<Uuid>> {
686 let res = sqlx::query!(
687 "
688SELECT id
689FROM exercise_task_submissions
690WHERE exercise_slide_submission_id IN (SELECT id
691 FROM (SELECT DISTINCT ON (user_id, exercise_id) *
692 FROM exercise_slide_submissions
693 WHERE exercise_id = $1
694 AND deleted_at IS NULL
695 ORDER BY user_id, exercise_id, created_at DESC) a )
696 AND deleted_at IS NULL
697",
698 &exercise_id
699 )
700 .fetch_all(conn)
701 .await?;
702 Ok(res.iter().map(|x| x.id).collect())
703}