1use std::{collections::HashSet, time::Duration};
2
3use anyhow::{Context, bail};
4use cynic::{MutationBuilder, QueryBuilder};
5use futures::StreamExt;
6use merge_streams::MergeStreams;
7use time::OffsetDateTime;
8use tracing::Instrument;
9use url::Url;
10use wasmer_config::package::PackageIdent;
11use wasmer_package::utils::from_bytes;
12use webc::Container;
13
14use crate::{
15 GraphQLApiFailure, WasmerClient,
16 types::{self, *},
17};
18
19pub const CRON_JOB_PAGE_SIZE: i32 = 100;
20
21pub async fn get_deploy_app_volumes(
29 client: &WasmerClient,
30 owner: impl Into<String>,
31 name: impl Into<String>,
32) -> Result<Vec<types::AppVolume>, anyhow::Error> {
33 let mut vars = types::GetDeployAppVolumesVars {
34 owner: owner.into(),
35 name: name.into(),
36 after: None,
37 };
38
39 let mut volumes = Vec::new();
40 loop {
41 let connection = client
42 .run_graphql_strict(types::GetDeployAppVolumes::build(vars.clone()))
43 .await?
44 .get_deploy_app
45 .context("app not found")?
46 .volumes;
47
48 volumes.extend(connection.edges.into_iter().map(|edge| edge.node));
49
50 match connection
51 .page_info
52 .end_cursor
53 .filter(|_| connection.page_info.has_next_page)
54 {
55 Some(after) => vars.after = Some(after),
56 None => break,
57 }
58 }
59
60 Ok(volumes)
61}
62
63pub async fn rotate_s3_credentials(
66 client: &WasmerClient,
67 volume_id: types::Id,
68) -> Result<types::RotateS3CredentialsPayload, anyhow::Error> {
69 let payload = client
70 .run_graphql_strict(types::RotateS3Credentials::build(
71 RotateS3CredentialsVariables { id: volume_id },
72 ))
73 .await?
74 .rotate_s3_credentials;
75
76 Ok(payload)
77}
78
79pub async fn update_volume_s3_enabled(
82 client: &WasmerClient,
83 volume_id: types::Id,
84 s3_enabled: bool,
85) -> Result<types::UpdateVolumePayload, anyhow::Error> {
86 let payload = client
87 .run_graphql_strict(types::UpdateVolume::build(types::UpdateVolumeVariables {
88 id: volume_id,
89 s3_enabled: Some(s3_enabled),
90 }))
91 .await?
92 .update_volume;
93
94 Ok(payload)
95}
96
97pub async fn viewer_can_deploy_to_namespace(
98 client: &WasmerClient,
99 owner_name: &str,
100) -> Result<bool, anyhow::Error> {
101 client
102 .run_graphql_strict(types::ViewerCan::build(ViewerCanVariables {
103 action: OwnerAction::DeployApp,
104 owner_name,
105 }))
106 .await
107 .map(|v| v.viewer_can)
108}
109
110pub async fn redeploy_app_by_id(
111 client: &WasmerClient,
112 app_id: impl Into<String>,
113) -> Result<Option<DeployApp>, anyhow::Error> {
114 client
115 .run_graphql_strict(types::RedeployActiveApp::build(
116 RedeployActiveAppVariables {
117 id: types::Id::from(app_id),
118 },
119 ))
120 .await
121 .map(|v| v.redeploy_active_version.map(|v| v.app))
122}
123
124pub async fn list_bindings(
129 client: &WasmerClient,
130 name: &str,
131 version: Option<&str>,
132) -> Result<Vec<Bindings>, anyhow::Error> {
133 client
134 .run_graphql_strict(types::GetBindingsQuery::build(GetBindingsQueryVariables {
135 name,
136 version,
137 }))
138 .await
139 .and_then(|b| {
140 b.package_version
141 .ok_or(anyhow::anyhow!("No bindings found!"))
142 })
143 .map(|v| {
144 let mut bindings_packages = Vec::new();
145
146 for b in v.bindings.into_iter().flatten() {
147 let pkg = Bindings {
148 id: b.id.into_inner(),
149 url: b.url,
150 language: b.language,
151 generator: b.generator,
152 };
153 bindings_packages.push(pkg);
154 }
155
156 bindings_packages
157 })
158}
159
160pub async fn revoke_token(
162 client: &WasmerClient,
163 token: String,
164) -> Result<Option<bool>, anyhow::Error> {
165 client
166 .run_graphql_strict(types::RevokeToken::build(RevokeTokenVariables { token }))
167 .await
168 .map(|v| v.revoke_api_token.and_then(|v| v.success))
169}
170
171pub async fn create_nonce(
175 client: &WasmerClient,
176 name: String,
177 callback_url: String,
178) -> Result<Option<Nonce>, anyhow::Error> {
179 client
180 .run_graphql_strict(types::CreateNewNonce::build(CreateNewNonceVariables {
181 callback_url,
182 name,
183 }))
184 .await
185 .map(|v| v.new_nonce.map(|v| v.nonce))
186}
187
188pub async fn get_app_secret_value_by_id(
189 client: &WasmerClient,
190 secret_id: impl Into<String>,
191) -> Result<Option<String>, anyhow::Error> {
192 client
193 .run_graphql_strict(types::GetAppSecretValue::build(
194 GetAppSecretValueVariables {
195 id: types::Id::from(secret_id),
196 },
197 ))
198 .await
199 .map(|v| v.get_secret_value)
200}
201
202pub async fn get_app_secret_by_name(
203 client: &WasmerClient,
204 app_id: impl Into<String>,
205 name: impl Into<String>,
206) -> Result<Option<Secret>, anyhow::Error> {
207 client
208 .run_graphql_strict(types::GetAppSecret::build(GetAppSecretVariables {
209 app_id: types::Id::from(app_id),
210 secret_name: name.into(),
211 }))
212 .await
213 .map(|v| v.get_app_secret)
214}
215
216pub async fn upsert_app_secret(
218 client: &WasmerClient,
219 app_id: impl Into<String>,
220 name: impl Into<String>,
221 value: impl Into<String>,
222) -> Result<Option<UpsertAppSecretPayload>, anyhow::Error> {
223 client
224 .run_graphql_strict(types::UpsertAppSecret::build(UpsertAppSecretVariables {
225 app_id: cynic::Id::from(app_id.into()),
226 name: name.into().as_str(),
227 value: value.into().as_str(),
228 }))
229 .await
230 .map(|v| v.upsert_app_secret)
231}
232
233pub async fn upsert_app_secrets(
235 client: &WasmerClient,
236 app_id: impl Into<String>,
237 secrets: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
238) -> Result<Option<UpsertAppSecretsPayload>, anyhow::Error> {
239 client
240 .run_graphql_strict(types::UpsertAppSecrets::build(UpsertAppSecretsVariables {
241 app_id: cynic::Id::from(app_id.into()),
242 secrets: Some(
243 secrets
244 .into_iter()
245 .map(|(name, value)| SecretInput {
246 name: name.into(),
247 value: value.into(),
248 })
249 .collect(),
250 ),
251 }))
252 .await
253 .map(|v| v.upsert_app_secrets)
254}
255
256pub async fn get_all_app_secrets_filtered(
260 client: &WasmerClient,
261 app_id: impl Into<String>,
262 names: impl IntoIterator<Item = impl Into<String>>,
263) -> Result<Vec<Secret>, anyhow::Error> {
264 let mut vars = GetAllAppSecretsVariables {
265 after: None,
266 app_id: types::Id::from(app_id),
267 before: None,
268 first: None,
269 last: None,
270 offset: None,
271 names: Some(names.into_iter().map(|s| s.into()).collect()),
272 };
273
274 let mut all_secrets = Vec::<Secret>::new();
275
276 loop {
277 let page = get_app_secrets(client, vars.clone()).await?;
278 if page.edges.is_empty() {
279 break;
280 }
281
282 for edge in page.edges {
283 let edge = match edge {
284 Some(edge) => edge,
285 None => continue,
286 };
287 let version = match edge.node {
288 Some(item) => item,
289 None => continue,
290 };
291
292 all_secrets.push(version);
293
294 vars.after = Some(edge.cursor);
296 }
297 }
298
299 Ok(all_secrets)
300}
301
302pub async fn get_app_volumes(
304 client: &WasmerClient,
305 owner: impl Into<String>,
306 name: impl Into<String>,
307) -> Result<Vec<types::AppVersionVolume>, anyhow::Error> {
308 let vars = types::GetAppVolumesVars {
309 owner: owner.into(),
310 name: name.into(),
311 };
312 let res = client
313 .run_graphql_strict(types::GetAppVolumes::build(vars))
314 .await?;
315 let volumes = res
316 .get_deploy_app
317 .context("app not found")?
318 .active_version
319 .and_then(|v| v.volumes)
320 .unwrap_or_default()
321 .into_iter()
322 .flatten()
323 .collect();
324 Ok(volumes)
325}
326
327pub async fn get_app_databases(
329 client: &WasmerClient,
330 owner: impl Into<String>,
331 name: impl Into<String>,
332) -> Result<Vec<types::AppDatabase>, anyhow::Error> {
333 let vars = types::GetAppDatabasesVars {
334 owner: owner.into(),
335 name: name.into(),
336 after: None,
337 };
338 let res = client
339 .run_graphql_strict(types::GetAppDatabases::build(vars))
340 .await?;
341
342 let app = res.get_deploy_app.context("app not found")?;
343 let dbs = app.databases;
344 let _ = dbs.page_info;
345
346 let dbs = dbs
347 .edges
348 .into_iter()
349 .flatten()
350 .flat_map(|edge| edge.node)
351 .collect::<Vec<_>>();
352 Ok(dbs)
353}
354
355pub async fn get_app_cron_jobs(
357 client: &WasmerClient,
358 owner: impl Into<String>,
359 name: impl Into<String>,
360) -> Result<Vec<types::CronJob>, anyhow::Error> {
361 let owner = owner.into();
362 let name = name.into();
363 let mut after = None;
364 let mut cron_jobs = Vec::new();
365
366 loop {
367 let vars = types::GetAppCronJobsVars {
368 owner: owner.clone(),
369 name: name.clone(),
370 after,
371 first: Some(CRON_JOB_PAGE_SIZE),
372 };
373 let res = client
374 .run_graphql_strict(types::GetAppCronJobs::build(vars))
375 .await?;
376
377 let app = res.get_deploy_app.context("app not found")?;
378 let con = app.cron_jobs;
379 let page_info = con.page_info;
380 cron_jobs.extend(con.edges.into_iter().flatten().flat_map(|edge| edge.node));
381
382 if !page_info.has_next_page {
383 break;
384 }
385 after = Some(page_info.end_cursor.context("cron jobs cursor missing")?);
386 }
387
388 Ok(cron_jobs)
389}
390
391pub async fn toggle_cron_job(
393 client: &WasmerClient,
394 cron_job_id: impl Into<String>,
395 enabled: bool,
396) -> Result<types::CronJob, anyhow::Error> {
397 let res = client
398 .run_graphql_strict(types::ToggleCronJob::build(types::ToggleCronJobVars {
399 cron_job_id: types::Id::from(cron_job_id),
400 enabled,
401 }))
402 .await?;
403
404 Ok(res
405 .toggle_cron_job
406 .context("backend did not return toggled cron job")?
407 .cron_job)
408}
409
410pub async fn get_cron_job_by_id(
411 client: &WasmerClient,
412 cron_job_id: impl Into<String>,
413) -> Result<types::CronJob, anyhow::Error> {
414 let cron_job_id = cron_job_id.into();
415 let res = client
416 .run_graphql_strict(types::GetCronJobById::build(types::GetCronJobByIdVars {
417 id: types::Id::from(cron_job_id.clone()),
418 }))
419 .await?;
420
421 res.cron_job
422 .and_then(types::NodeCronJob::into_cron_job)
423 .with_context(|| format!("cron job '{cron_job_id}' not found"))
424}
425
426#[allow(clippy::too_many_arguments)]
428pub async fn get_cron_job_invocations_page(
429 client: &WasmerClient,
430 owner: impl Into<String>,
431 name: impl Into<String>,
432 cron_job: impl AsRef<str>,
433 invocation_after: Option<String>,
434 invocation_first: Option<i32>,
435 start: Option<OffsetDateTime>,
436 end: Option<OffsetDateTime>,
437) -> Result<
438 (
439 types::CronJobWithInvocations,
440 Paginated<types::CronJobInvocation>,
441 ),
442 anyhow::Error,
443> {
444 let cron_job = cron_job.as_ref();
445 let owner = owner.into();
446 let name = name.into();
447 let (start, end) = default_cron_invocation_window(start, end)?;
448 let start = types::DateTime::try_from(start)?;
449 let end = types::DateTime::try_from(end)?;
450 let mut cron_after = None;
451
452 loop {
453 let vars = types::GetCronJobInvocationsVars {
454 owner: owner.clone(),
455 name: name.clone(),
456 cron_after: cron_after.clone(),
457 cron_first: Some(CRON_JOB_PAGE_SIZE),
458 invocation_start: Some(start.clone()),
459 invocation_end: Some(end.clone()),
460 invocation_after: invocation_after.clone(),
461 invocation_first,
462 };
463 let res = client
464 .run_graphql_strict(types::GetCronJobInvocations::build(vars))
465 .await?;
466
467 let app = res.get_deploy_app.context("app not found")?;
468 let con = app.cron_jobs;
469 let page_info = con.page_info;
470 if let Some(cron) = con
471 .nodes
472 .into_iter()
473 .find(|node| node.id.inner() == cron_job || node.name == cron_job)
474 {
475 let invocations = cron
476 .invocations
477 .edges
478 .iter()
479 .flatten()
480 .flat_map(|edge| edge.node.clone())
481 .collect::<Vec<_>>();
482 let next_cursor = cron
483 .invocations
484 .page_info
485 .has_next_page
486 .then(|| cron.invocations.page_info.end_cursor.clone())
487 .flatten();
488
489 return Ok((
490 cron,
491 Paginated {
492 items: invocations,
493 next_cursor,
494 },
495 ));
496 }
497
498 if !page_info.has_next_page {
499 break;
500 }
501 cron_after = Some(page_info.end_cursor.context("cron jobs cursor missing")?);
502 }
503
504 bail!("cron job '{cron_job}' not found")
505}
506
507pub async fn get_cron_job_invocations_page_by_id(
509 client: &WasmerClient,
510 cron_job_id: impl Into<String>,
511 invocation_after: Option<String>,
512 invocation_first: Option<i32>,
513 start: Option<OffsetDateTime>,
514 end: Option<OffsetDateTime>,
515) -> Result<
516 (
517 types::CronJobWithInvocationsById,
518 Paginated<types::CronJobInvocation>,
519 ),
520 anyhow::Error,
521> {
522 let cron_job_id = cron_job_id.into();
523 let (start, end) = default_cron_invocation_window(start, end)?;
524
525 let res = client
526 .run_graphql_strict(types::GetCronJobInvocationsById::build(
527 types::GetCronJobInvocationsByIdVars {
528 id: types::Id::from(cron_job_id.clone()),
529 invocation_start: Some(types::DateTime::try_from(start)?),
530 invocation_end: Some(types::DateTime::try_from(end)?),
531 invocation_after,
532 invocation_first,
533 },
534 ))
535 .await?;
536
537 let cron = res
538 .cron_job
539 .and_then(types::NodeCronJobWithInvocations::into_cron_job)
540 .with_context(|| format!("cron job '{cron_job_id}' not found"))?;
541 let invocations = cron
542 .invocations
543 .edges
544 .iter()
545 .flatten()
546 .flat_map(|edge| edge.node.clone())
547 .collect::<Vec<_>>();
548 let next_cursor = cron
549 .invocations
550 .page_info
551 .has_next_page
552 .then(|| cron.invocations.page_info.end_cursor.clone())
553 .flatten();
554
555 Ok((
556 cron,
557 Paginated {
558 items: invocations,
559 next_cursor,
560 },
561 ))
562}
563
564#[allow(clippy::too_many_arguments)]
565pub async fn get_cron_job_invocation_logs(
566 client: &WasmerClient,
567 owner: impl Into<String>,
568 name: impl Into<String>,
569 cron_job: impl AsRef<str>,
570 invocation: impl AsRef<str>,
571 log_first: Option<i32>,
572 start: Option<OffsetDateTime>,
573 end: Option<OffsetDateTime>,
574) -> Result<Vec<types::CronJobLog>, anyhow::Error> {
575 let owner = owner.into();
576 let name = name.into();
577 let cron_job = cron_job.as_ref().to_string();
578 let invocation = invocation.as_ref().to_string();
579 let (start, end) = default_cron_invocation_window(start, end)?;
580 let start = types::DateTime::try_from(start)?;
581 let end = types::DateTime::try_from(end)?;
582 let mut cron_after = None;
583
584 loop {
585 let mut invocation_after = None;
586 loop {
587 let vars = types::GetCronJobInvocationLogsVars {
588 owner: owner.clone(),
589 name: name.clone(),
590 cron_after: cron_after.clone(),
591 cron_first: Some(CRON_JOB_PAGE_SIZE),
592 invocation_start: Some(start.clone()),
593 invocation_end: Some(end.clone()),
594 invocation_after: invocation_after.clone(),
595 invocation_first: Some(1),
596 log_first,
597 };
598 let res = client
599 .run_graphql_strict(types::GetCronJobInvocationLogs::build(vars))
600 .await?;
601 let app = res.get_deploy_app.context("app not found")?;
602 let con = app.cron_jobs;
603 let page_info = con.page_info;
604
605 if let Some(cron) = con
606 .nodes
607 .into_iter()
608 .find(|node| node.id.inner() == cron_job || node.name == cron_job)
609 {
610 if let Some(invocation_logs) = cron
611 .invocations
612 .edges
613 .into_iter()
614 .flatten()
615 .filter_map(|edge| edge.node)
616 .find(|node| node.id.inner() == invocation || node.edge_job_id == invocation)
617 {
618 return Ok(logs_from_connection(invocation_logs.logs));
619 }
620
621 if !cron.invocations.page_info.has_next_page {
622 bail!("cron job invocation '{invocation}' not found");
623 }
624 invocation_after = Some(
625 cron.invocations
626 .page_info
627 .end_cursor
628 .context("cron job invocations cursor missing")?,
629 );
630 continue;
631 }
632
633 if !page_info.has_next_page {
634 bail!("cron job '{cron_job}' not found");
635 }
636 cron_after = Some(page_info.end_cursor.context("cron jobs cursor missing")?);
637 break;
638 }
639 }
640}
641
642pub async fn get_cron_job_invocation_logs_by_id(
643 client: &WasmerClient,
644 cron_job_id: impl Into<String>,
645 invocation: impl AsRef<str>,
646 log_first: Option<i32>,
647 start: Option<OffsetDateTime>,
648 end: Option<OffsetDateTime>,
649) -> Result<Vec<types::CronJobLog>, anyhow::Error> {
650 let cron_job_id = cron_job_id.into();
651 let invocation = invocation.as_ref().to_string();
652 let (start, end) = default_cron_invocation_window(start, end)?;
653 let start = types::DateTime::try_from(start)?;
654 let end = types::DateTime::try_from(end)?;
655 let mut invocation_after = None;
656
657 loop {
658 let res = client
659 .run_graphql_strict(types::GetCronJobInvocationLogsById::build(
660 types::GetCronJobInvocationLogsByIdVars {
661 id: types::Id::from(cron_job_id.clone()),
662 invocation_start: Some(start.clone()),
663 invocation_end: Some(end.clone()),
664 invocation_after: invocation_after.clone(),
665 invocation_first: Some(1),
666 log_first,
667 },
668 ))
669 .await?;
670 let cron = res
671 .cron_job
672 .and_then(types::NodeCronJobWithInvocationLogs::into_cron_job)
673 .with_context(|| format!("cron job '{cron_job_id}' not found"))?;
674
675 if let Some(invocation_logs) = cron
676 .invocations
677 .edges
678 .into_iter()
679 .flatten()
680 .filter_map(|edge| edge.node)
681 .find(|node| node.id.inner() == invocation || node.edge_job_id == invocation)
682 {
683 return Ok(logs_from_connection(invocation_logs.logs));
684 }
685
686 if !cron.invocations.page_info.has_next_page {
687 bail!("cron job invocation '{invocation}' not found");
688 }
689 invocation_after = Some(
690 cron.invocations
691 .page_info
692 .end_cursor
693 .context("cron job invocations cursor missing")?,
694 );
695 }
696}
697
698fn default_cron_invocation_window(
699 start: Option<OffsetDateTime>,
700 end: Option<OffsetDateTime>,
701) -> Result<(OffsetDateTime, OffsetDateTime), anyhow::Error> {
702 let (start, end) = match (start, end) {
703 (Some(start), Some(end)) => (start, end),
704 (Some(start), None) => (start, OffsetDateTime::now_utc()),
705 (None, Some(end)) => (end - time::Duration::days(31), end),
706 (None, None) => {
707 let end = OffsetDateTime::now_utc();
708 (end - time::Duration::days(31), end)
709 }
710 };
711 if start > end {
712 bail!("invocation start must not be after end");
713 }
714 Ok((start, end))
715}
716
717fn logs_from_connection(connection: types::CronJobLogConnection) -> Vec<types::CronJobLog> {
718 connection
719 .edges
720 .into_iter()
721 .flatten()
722 .filter_map(|edge| edge.node)
723 .collect()
724}
725
726pub async fn get_app_s3_credentials(
730 client: &WasmerClient,
731 app_id: impl Into<String>,
732) -> Result<types::S3Credentials, anyhow::Error> {
733 let app_id = app_id.into();
734
735 let app1 = get_app_by_id(client, app_id.clone()).await?;
737
738 let vars = types::GetDeployAppVars {
739 owner: app1.owner.global_name,
740 name: app1.name,
741 };
742 client
743 .run_graphql_strict(types::GetDeployAppS3Credentials::build(vars))
744 .await?
745 .get_deploy_app
746 .context("app not found")?
747 .s3_credentials
748 .context("app does not have S3 credentials")
749}
750
751pub async fn get_all_app_regions(client: &WasmerClient) -> Result<Vec<AppRegion>, anyhow::Error> {
755 let mut vars = GetAllAppRegionsVariables {
756 after: None,
757 before: None,
758 first: None,
759 last: None,
760 offset: None,
761 };
762
763 let mut all_regions = Vec::<AppRegion>::new();
764
765 loop {
766 let page = get_regions(client, vars.clone()).await?;
767 if page.edges.is_empty() {
768 break;
769 }
770
771 for edge in page.edges {
772 let edge = match edge {
773 Some(edge) => edge,
774 None => continue,
775 };
776 let version = match edge.node {
777 Some(item) => item,
778 None => continue,
779 };
780
781 all_regions.push(version);
782
783 vars.after = Some(edge.cursor);
785 }
786 }
787
788 Ok(all_regions)
789}
790
791pub async fn get_regions(
793 client: &WasmerClient,
794 vars: GetAllAppRegionsVariables,
795) -> Result<AppRegionConnection, anyhow::Error> {
796 let res = client
797 .run_graphql_strict(types::GetAllAppRegions::build(vars))
798 .await?;
799 Ok(res.get_app_regions)
800}
801
802pub async fn get_all_app_secrets(
806 client: &WasmerClient,
807 app_id: impl Into<String>,
808) -> Result<Vec<Secret>, anyhow::Error> {
809 let mut vars = GetAllAppSecretsVariables {
810 after: None,
811 app_id: types::Id::from(app_id),
812 before: None,
813 first: None,
814 last: None,
815 offset: None,
816 names: None,
817 };
818
819 let mut all_secrets = Vec::<Secret>::new();
820
821 loop {
822 let page = get_app_secrets(client, vars.clone()).await?;
823 if page.edges.is_empty() {
824 break;
825 }
826
827 for edge in page.edges {
828 let edge = match edge {
829 Some(edge) => edge,
830 None => continue,
831 };
832 let version = match edge.node {
833 Some(item) => item,
834 None => continue,
835 };
836
837 all_secrets.push(version);
838
839 vars.after = Some(edge.cursor);
841 }
842 }
843
844 Ok(all_secrets)
845}
846
847pub async fn get_app_secrets(
849 client: &WasmerClient,
850 vars: GetAllAppSecretsVariables,
851) -> Result<SecretConnection, anyhow::Error> {
852 let res = client
853 .run_graphql_strict(types::GetAllAppSecrets::build(vars))
854 .await?;
855 res.get_app_secrets.context("app not found")
856}
857
858pub async fn delete_app_secret(
859 client: &WasmerClient,
860 secret_id: impl Into<String>,
861) -> Result<Option<DeleteAppSecretPayload>, anyhow::Error> {
862 client
863 .run_graphql_strict(types::DeleteAppSecret::build(DeleteAppSecretVariables {
864 id: types::Id::from(secret_id.into()),
865 }))
866 .await
867 .map(|v| v.delete_app_secret)
868}
869
870pub async fn fetch_webc_package(
875 client: &WasmerClient,
876 ident: &PackageIdent,
877 default_registry: &Url,
878) -> Result<Container, anyhow::Error> {
879 let url = match ident {
880 PackageIdent::Named(n) => Url::parse(&format!(
881 "{default_registry}/{}:{}",
882 n.full_name(),
883 n.version_or_default()
884 ))?,
885 PackageIdent::Hash(h) => match get_package_release(client, &h.to_string()).await? {
886 Some(webc) => Url::parse(&webc.webc_url)?,
887 None => anyhow::bail!("Could not find package with hash '{h}'"),
888 },
889 };
890
891 let data = client
892 .client
893 .get(url)
894 .header(reqwest::header::USER_AGENT, &client.user_agent)
895 .header(reqwest::header::ACCEPT, "application/webc")
896 .send()
897 .await?
898 .error_for_status()?
899 .bytes()
900 .await?;
901
902 from_bytes(data).context("failed to parse webc package")
903}
904
905pub async fn fetch_app_template_from_slug(
907 client: &WasmerClient,
908 slug: String,
909) -> Result<Option<types::AppTemplate>, anyhow::Error> {
910 client
911 .run_graphql_strict(types::GetAppTemplateFromSlug::build(
912 GetAppTemplateFromSlugVariables { slug },
913 ))
914 .await
915 .map(|v| v.get_app_template)
916}
917
918pub async fn fetch_app_templates_from_framework(
920 client: &WasmerClient,
921 framework_slug: String,
922 first: i32,
923 after: Option<String>,
924 sort_by: Option<types::AppTemplatesSortBy>,
925) -> Result<Option<types::AppTemplateConnection>, anyhow::Error> {
926 client
927 .run_graphql_strict(types::GetAppTemplatesFromFramework::build(
928 GetAppTemplatesFromFrameworkVars {
929 framework_slug,
930 first,
931 after,
932 sort_by,
933 },
934 ))
935 .await
936 .map(|r| r.get_app_templates)
937}
938
939pub async fn fetch_app_templates(
941 client: &WasmerClient,
942 category_slug: String,
943 first: i32,
944 after: Option<String>,
945 sort_by: Option<types::AppTemplatesSortBy>,
946) -> Result<Option<types::AppTemplateConnection>, anyhow::Error> {
947 client
948 .run_graphql_strict(types::GetAppTemplates::build(GetAppTemplatesVars {
949 category_slug,
950 first,
951 after,
952 sort_by,
953 }))
954 .await
955 .map(|r| r.get_app_templates)
956}
957
958pub fn fetch_all_app_templates(
962 client: &WasmerClient,
963 page_size: i32,
964 sort_by: Option<types::AppTemplatesSortBy>,
965) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
966 let vars = GetAppTemplatesVars {
967 category_slug: String::new(),
968 first: page_size,
969 sort_by,
970 after: None,
971 };
972
973 futures::stream::try_unfold(
974 Some(vars),
975 move |vars: Option<types::GetAppTemplatesVars>| async move {
976 let vars = match vars {
977 Some(vars) => vars,
978 None => return Ok(None),
979 };
980
981 let con = client
982 .run_graphql_strict(types::GetAppTemplates::build(vars.clone()))
983 .await?
984 .get_app_templates
985 .context("backend did not return any data")?;
986
987 let items = con
988 .edges
989 .into_iter()
990 .flatten()
991 .filter_map(|edge| edge.node)
992 .collect::<Vec<_>>();
993
994 let next_cursor = con
995 .page_info
996 .end_cursor
997 .filter(|_| con.page_info.has_next_page);
998
999 let next_vars = next_cursor.map(|after| types::GetAppTemplatesVars {
1000 after: Some(after),
1001 ..vars
1002 });
1003
1004 #[allow(clippy::type_complexity)]
1005 let res: Result<
1006 Option<(Vec<types::AppTemplate>, Option<types::GetAppTemplatesVars>)>,
1007 anyhow::Error,
1008 > = Ok(Some((items, next_vars)));
1009
1010 res
1011 },
1012 )
1013}
1014
1015pub fn fetch_all_app_templates_from_language(
1019 client: &WasmerClient,
1020 page_size: i32,
1021 sort_by: Option<types::AppTemplatesSortBy>,
1022 language: String,
1023) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
1024 let vars = GetAppTemplatesFromLanguageVars {
1025 language_slug: language.clone().to_string(),
1026 first: page_size,
1027 sort_by,
1028 after: None,
1029 };
1030
1031 futures::stream::try_unfold(
1032 Some(vars),
1033 move |vars: Option<types::GetAppTemplatesFromLanguageVars>| async move {
1034 let vars = match vars {
1035 Some(vars) => vars,
1036 None => return Ok(None),
1037 };
1038
1039 let con = client
1040 .run_graphql_strict(types::GetAppTemplatesFromLanguage::build(vars.clone()))
1041 .await?
1042 .get_app_templates
1043 .context("backend did not return any data")?;
1044
1045 let items = con
1046 .edges
1047 .into_iter()
1048 .flatten()
1049 .filter_map(|edge| edge.node)
1050 .collect::<Vec<_>>();
1051
1052 let next_cursor = con
1053 .page_info
1054 .end_cursor
1055 .filter(|_| con.page_info.has_next_page);
1056
1057 let next_vars = next_cursor.map(|after| types::GetAppTemplatesFromLanguageVars {
1058 after: Some(after),
1059 ..vars
1060 });
1061
1062 #[allow(clippy::type_complexity)]
1063 let res: Result<
1064 Option<(
1065 Vec<types::AppTemplate>,
1066 Option<types::GetAppTemplatesFromLanguageVars>,
1067 )>,
1068 anyhow::Error,
1069 > = Ok(Some((items, next_vars)));
1070
1071 res
1072 },
1073 )
1074}
1075
1076pub async fn fetch_app_template_languages(
1078 client: &WasmerClient,
1079 after: Option<String>,
1080 first: Option<i32>,
1081) -> Result<Option<types::TemplateLanguageConnection>, anyhow::Error> {
1082 client
1083 .run_graphql_strict(types::GetTemplateLanguages::build(
1084 GetTemplateLanguagesVars { after, first },
1085 ))
1086 .await
1087 .map(|r| r.get_template_languages)
1088}
1089
1090pub fn fetch_all_app_template_languages(
1094 client: &WasmerClient,
1095 page_size: Option<i32>,
1096) -> impl futures::Stream<Item = Result<Vec<types::TemplateLanguage>, anyhow::Error>> + '_ {
1097 let vars = GetTemplateLanguagesVars {
1098 after: None,
1099 first: page_size,
1100 };
1101
1102 futures::stream::try_unfold(
1103 Some(vars),
1104 move |vars: Option<types::GetTemplateLanguagesVars>| async move {
1105 let vars = match vars {
1106 Some(vars) => vars,
1107 None => return Ok(None),
1108 };
1109
1110 let con = client
1111 .run_graphql_strict(types::GetTemplateLanguages::build(vars.clone()))
1112 .await?
1113 .get_template_languages
1114 .context("backend did not return any data")?;
1115
1116 let items = con
1117 .edges
1118 .into_iter()
1119 .flatten()
1120 .filter_map(|edge| edge.node)
1121 .collect::<Vec<_>>();
1122
1123 let next_cursor = con
1124 .page_info
1125 .end_cursor
1126 .filter(|_| con.page_info.has_next_page);
1127
1128 let next_vars = next_cursor.map(|after| types::GetTemplateLanguagesVars {
1129 after: Some(after),
1130 ..vars
1131 });
1132
1133 #[allow(clippy::type_complexity)]
1134 let res: Result<
1135 Option<(
1136 Vec<types::TemplateLanguage>,
1137 Option<types::GetTemplateLanguagesVars>,
1138 )>,
1139 anyhow::Error,
1140 > = Ok(Some((items, next_vars)));
1141
1142 res
1143 },
1144 )
1145}
1146
1147pub fn fetch_all_app_templates_from_framework(
1151 client: &WasmerClient,
1152 page_size: i32,
1153 sort_by: Option<types::AppTemplatesSortBy>,
1154 framework: String,
1155) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
1156 let vars = GetAppTemplatesFromFrameworkVars {
1157 framework_slug: framework.clone().to_string(),
1158 first: page_size,
1159 sort_by,
1160 after: None,
1161 };
1162
1163 futures::stream::try_unfold(
1164 Some(vars),
1165 move |vars: Option<types::GetAppTemplatesFromFrameworkVars>| async move {
1166 let vars = match vars {
1167 Some(vars) => vars,
1168 None => return Ok(None),
1169 };
1170
1171 let con = client
1172 .run_graphql_strict(types::GetAppTemplatesFromFramework::build(vars.clone()))
1173 .await?
1174 .get_app_templates
1175 .context("backend did not return any data")?;
1176
1177 let items = con
1178 .edges
1179 .into_iter()
1180 .flatten()
1181 .filter_map(|edge| edge.node)
1182 .collect::<Vec<_>>();
1183
1184 let next_cursor = con
1185 .page_info
1186 .end_cursor
1187 .filter(|_| con.page_info.has_next_page);
1188
1189 let next_vars = next_cursor.map(|after| types::GetAppTemplatesFromFrameworkVars {
1190 after: Some(after),
1191 ..vars
1192 });
1193
1194 #[allow(clippy::type_complexity)]
1195 let res: Result<
1196 Option<(
1197 Vec<types::AppTemplate>,
1198 Option<types::GetAppTemplatesFromFrameworkVars>,
1199 )>,
1200 anyhow::Error,
1201 > = Ok(Some((items, next_vars)));
1202
1203 res
1204 },
1205 )
1206}
1207
1208pub async fn fetch_app_template_frameworks(
1210 client: &WasmerClient,
1211 after: Option<String>,
1212 first: Option<i32>,
1213) -> Result<Option<types::TemplateFrameworkConnection>, anyhow::Error> {
1214 client
1215 .run_graphql_strict(types::GetTemplateFrameworks::build(
1216 GetTemplateFrameworksVars { after, first },
1217 ))
1218 .await
1219 .map(|r| r.get_template_frameworks)
1220}
1221
1222pub fn fetch_all_app_template_frameworks(
1226 client: &WasmerClient,
1227 page_size: Option<i32>,
1228) -> impl futures::Stream<Item = Result<Vec<types::TemplateFramework>, anyhow::Error>> + '_ {
1229 let vars = GetTemplateFrameworksVars {
1230 after: None,
1231 first: page_size,
1232 };
1233
1234 futures::stream::try_unfold(
1235 Some(vars),
1236 move |vars: Option<types::GetTemplateFrameworksVars>| async move {
1237 let vars = match vars {
1238 Some(vars) => vars,
1239 None => return Ok(None),
1240 };
1241
1242 let con = client
1243 .run_graphql_strict(types::GetTemplateFrameworks::build(vars.clone()))
1244 .await?
1245 .get_template_frameworks
1246 .context("backend did not return any data")?;
1247
1248 let items = con
1249 .edges
1250 .into_iter()
1251 .flatten()
1252 .filter_map(|edge| edge.node)
1253 .collect::<Vec<_>>();
1254
1255 let next_cursor = con
1256 .page_info
1257 .end_cursor
1258 .filter(|_| con.page_info.has_next_page);
1259
1260 let next_vars = next_cursor.map(|after| types::GetTemplateFrameworksVars {
1261 after: Some(after),
1262 ..vars
1263 });
1264
1265 #[allow(clippy::type_complexity)]
1266 let res: Result<
1267 Option<(
1268 Vec<types::TemplateFramework>,
1269 Option<types::GetTemplateFrameworksVars>,
1270 )>,
1271 anyhow::Error,
1272 > = Ok(Some((items, next_vars)));
1273
1274 res
1275 },
1276 )
1277}
1278
1279#[derive(Debug)]
1281pub enum UploadMethod {
1282 R2,
1283}
1284
1285impl UploadMethod {
1286 pub fn as_str(&self) -> &'static str {
1287 match self {
1288 UploadMethod::R2 => "R2",
1289 }
1290 }
1291}
1292
1293pub async fn get_signed_url_for_package_upload(
1295 client: &WasmerClient,
1296 expires_after_seconds: Option<i32>,
1297 filename: Option<&str>,
1298 name: Option<&str>,
1299 version: Option<&str>,
1300 method: Option<UploadMethod>,
1301) -> Result<Option<SignedUrl>, anyhow::Error> {
1302 client
1303 .run_graphql_strict(types::GetSignedUrlForPackageUpload::build(
1304 GetSignedUrlForPackageUploadVariables {
1305 expires_after_seconds,
1306 filename,
1307 name,
1308 version,
1309 method: method.map(|m| m.as_str()),
1310 },
1311 ))
1312 .await
1313 .map(|r| r.get_signed_url_for_package_upload)
1314}
1315
1316pub async fn generate_upload_url(
1318 client: &WasmerClient,
1319 filename: &str,
1320 name: Option<&str>,
1321 version: Option<&str>,
1322 expires_after_seconds: Option<i32>,
1323 method: Option<UploadMethod>,
1324) -> Result<SignedUrl, anyhow::Error> {
1325 let payload = client
1326 .run_graphql_strict(types::GenerateUploadUrl::build(
1327 GenerateUploadUrlVariables {
1328 expires_after_seconds,
1329 filename,
1330 name,
1331 version,
1332 method: method.map(|m| m.as_str()),
1333 },
1334 ))
1335 .await
1336 .and_then(|res| {
1337 res.generate_upload_url
1338 .context("generateUploadUrl mutation did not return data")
1339 })?;
1340
1341 Ok(payload.signed_url)
1342}
1343
1344pub async fn autobuild_config_for_zip_upload(
1346 client: &WasmerClient,
1347 upload_url: &str,
1348) -> Result<Option<types::AutobuildConfigForZipUploadPayload>, anyhow::Error> {
1349 client
1350 .run_graphql_strict(types::AutobuildConfigForZipUpload::build(
1351 AutobuildConfigForZipUploadVariables { upload_url },
1352 ))
1353 .await
1354 .map(|res| res.autobuild_config_for_zip_upload)
1355}
1356
1357pub async fn deploy_via_autobuild(
1359 client: &WasmerClient,
1360 vars: DeployViaAutobuildVars,
1361) -> Result<Option<types::DeployViaAutobuildPayload>, anyhow::Error> {
1362 client
1363 .run_graphql_strict(types::DeployViaAutobuild::build(vars))
1364 .await
1365 .map(|res| res.deploy_via_autobuild)
1366}
1367pub async fn push_package_release(
1369 client: &WasmerClient,
1370 name: Option<&str>,
1371 namespace: &str,
1372 signed_url: &str,
1373 private: Option<bool>,
1374) -> Result<Option<PushPackageReleasePayload>, anyhow::Error> {
1375 client
1376 .run_graphql_strict(types::PushPackageRelease::build(
1377 types::PushPackageReleaseVariables {
1378 name,
1379 namespace,
1380 private,
1381 signed_url,
1382 },
1383 ))
1384 .await
1385 .map(|r| r.push_package_release)
1386}
1387
1388#[allow(clippy::too_many_arguments)]
1389pub async fn tag_package_release(
1390 client: &WasmerClient,
1391 description: Option<&str>,
1392 homepage: Option<&str>,
1393 license: Option<&str>,
1394 license_file: Option<&str>,
1395 manifest: Option<&str>,
1396 name: &str,
1397 namespace: Option<&str>,
1398 package_release_id: &cynic::Id,
1399 private: Option<bool>,
1400 readme: Option<&str>,
1401 repository: Option<&str>,
1402 version: &str,
1403) -> Result<Option<TagPackageReleasePayload>, anyhow::Error> {
1404 client
1405 .run_graphql_strict(types::TagPackageRelease::build(
1406 types::TagPackageReleaseVariables {
1407 description,
1408 homepage,
1409 license,
1410 license_file,
1411 manifest,
1412 name,
1413 namespace,
1414 package_release_id,
1415 private,
1416 readme,
1417 repository,
1418 version,
1419 },
1420 ))
1421 .await
1422 .map(|r| r.tag_package_release)
1423}
1424
1425pub async fn current_user(client: &WasmerClient) -> Result<Option<types::User>, anyhow::Error> {
1427 client
1428 .run_graphql(types::GetCurrentUser::build(()))
1429 .await
1430 .map(|x| x.viewer)
1431}
1432
1433pub async fn current_user_with_namespaces(
1437 client: &WasmerClient,
1438 namespace_role: Option<types::GrapheneRole>,
1439) -> Result<types::UserWithNamespaces, anyhow::Error> {
1440 client
1441 .run_graphql(types::GetCurrentUserWithNamespaces::build(
1442 types::GetCurrentUserWithNamespacesVars { namespace_role },
1443 ))
1444 .await?
1445 .viewer
1446 .context("not logged in")
1447}
1448
1449pub async fn get_app(
1451 client: &WasmerClient,
1452 owner: String,
1453 name: String,
1454) -> Result<Option<types::DeployApp>, anyhow::Error> {
1455 client
1456 .run_graphql(types::GetDeployApp::build(types::GetDeployAppVars {
1457 name,
1458 owner,
1459 }))
1460 .await
1461 .map(|x| x.get_deploy_app)
1462}
1463
1464pub async fn get_app_by_alias(
1466 client: &WasmerClient,
1467 alias: String,
1468) -> Result<Option<types::DeployApp>, anyhow::Error> {
1469 client
1470 .run_graphql(types::GetDeployAppByAlias::build(
1471 types::GetDeployAppByAliasVars { alias },
1472 ))
1473 .await
1474 .map(|x| x.get_app_by_global_alias)
1475}
1476
1477pub async fn get_app_version(
1479 client: &WasmerClient,
1480 owner: String,
1481 name: String,
1482 version: String,
1483) -> Result<Option<types::DeployAppVersion>, anyhow::Error> {
1484 client
1485 .run_graphql(types::GetDeployAppVersion::build(
1486 types::GetDeployAppVersionVars {
1487 name,
1488 owner,
1489 version,
1490 },
1491 ))
1492 .await
1493 .map(|x| x.get_deploy_app_version)
1494}
1495
1496pub async fn get_app_with_version(
1498 client: &WasmerClient,
1499 owner: String,
1500 name: String,
1501 version: String,
1502) -> Result<GetDeployAppAndVersion, anyhow::Error> {
1503 client
1504 .run_graphql(types::GetDeployAppAndVersion::build(
1505 types::GetDeployAppAndVersionVars {
1506 name,
1507 owner,
1508 version,
1509 },
1510 ))
1511 .await
1512}
1513
1514pub async fn get_app_and_package_by_name(
1516 client: &WasmerClient,
1517 vars: types::GetPackageAndAppVars,
1518) -> Result<(Option<types::Package>, Option<types::DeployApp>), anyhow::Error> {
1519 let res = client
1520 .run_graphql(types::GetPackageAndApp::build(vars))
1521 .await?;
1522 Ok((res.get_package, res.get_deploy_app))
1523}
1524
1525pub async fn get_deploy_apps(
1527 client: &WasmerClient,
1528 vars: types::GetDeployAppsVars,
1529) -> Result<DeployAppConnection, anyhow::Error> {
1530 let res = client
1531 .run_graphql(types::GetDeployApps::build(vars))
1532 .await?;
1533 res.get_deploy_apps.context("no apps returned")
1534}
1535
1536pub fn get_deploy_apps_stream(
1538 client: &WasmerClient,
1539 vars: types::GetDeployAppsVars,
1540) -> impl futures::Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + '_ {
1541 futures::stream::try_unfold(
1542 Some(vars),
1543 move |vars: Option<types::GetDeployAppsVars>| async move {
1544 let vars = match vars {
1545 Some(vars) => vars,
1546 None => return Ok(None),
1547 };
1548
1549 let page = get_deploy_apps(client, vars.clone()).await?;
1550
1551 let end_cursor = page.page_info.end_cursor;
1552
1553 let items = page
1554 .edges
1555 .into_iter()
1556 .filter_map(|x| x.and_then(|x| x.node))
1557 .collect::<Vec<_>>();
1558
1559 let new_vars = end_cursor.map(|c| types::GetDeployAppsVars {
1560 after: Some(c),
1561 ..vars
1562 });
1563
1564 Ok(Some((items, new_vars)))
1565 },
1566 )
1567}
1568
1569pub async fn get_deploy_app_versions(
1571 client: &WasmerClient,
1572 vars: GetDeployAppVersionsVars,
1573) -> Result<DeployAppVersionConnection, anyhow::Error> {
1574 let res = client
1575 .run_graphql_strict(types::GetDeployAppVersions::build(vars))
1576 .await?;
1577 let versions = res.get_deploy_app.context("app not found")?.versions;
1578 Ok(versions)
1579}
1580
1581pub async fn app_deployments(
1583 client: &WasmerClient,
1584 vars: types::GetAppDeploymentsVariables,
1585) -> Result<Vec<types::Deployment>, anyhow::Error> {
1586 let res = client
1587 .run_graphql_strict(types::GetAppDeployments::build(vars))
1588 .await?;
1589 let builds = res
1590 .get_deploy_app
1591 .and_then(|x| x.deployments)
1592 .context("no data returned")?
1593 .edges
1594 .into_iter()
1595 .flatten()
1596 .filter_map(|x| x.node)
1597 .collect();
1598
1599 Ok(builds)
1600}
1601
1602pub async fn app_deployment(
1604 client: &WasmerClient,
1605 id: String,
1606) -> Result<types::AutobuildRepository, anyhow::Error> {
1607 let node = get_node(client, id.clone())
1608 .await?
1609 .with_context(|| format!("app deployment with id '{id}' not found"))?;
1610 match node {
1611 types::Node::AutobuildRepository(x) => Ok(*x),
1612 _ => anyhow::bail!("invalid node type returned"),
1613 }
1614}
1615
1616pub async fn all_app_versions(
1620 client: &WasmerClient,
1621 owner: String,
1622 name: String,
1623) -> Result<Vec<DeployAppVersion>, anyhow::Error> {
1624 let mut vars = GetDeployAppVersionsVars {
1625 owner,
1626 name,
1627 offset: None,
1628 before: None,
1629 after: None,
1630 first: Some(10),
1631 last: None,
1632 sort_by: None,
1633 };
1634
1635 let mut all_versions = Vec::<DeployAppVersion>::new();
1636
1637 loop {
1638 let page = get_deploy_app_versions(client, vars.clone()).await?;
1639 if page.edges.is_empty() {
1640 break;
1641 }
1642
1643 for edge in page.edges {
1644 let edge = match edge {
1645 Some(edge) => edge,
1646 None => continue,
1647 };
1648 let version = match edge.node {
1649 Some(item) => item,
1650 None => continue,
1651 };
1652
1653 if all_versions.iter().any(|v| v.id == version.id) == false {
1655 all_versions.push(version);
1656 }
1657
1658 vars.after = Some(edge.cursor);
1660 }
1661 }
1662
1663 Ok(all_versions)
1664}
1665
1666pub async fn get_deploy_app_versions_by_id(
1668 client: &WasmerClient,
1669 vars: types::GetDeployAppVersionsByIdVars,
1670) -> Result<DeployAppVersionConnection, anyhow::Error> {
1671 let res = client
1672 .run_graphql_strict(types::GetDeployAppVersionsById::build(vars))
1673 .await?;
1674 let versions = res
1675 .node
1676 .context("app not found")?
1677 .into_app()
1678 .context("invalid node type returned")?
1679 .versions;
1680 Ok(versions)
1681}
1682
1683pub async fn all_app_versions_by_id(
1687 client: &WasmerClient,
1688 app_id: impl Into<String>,
1689) -> Result<Vec<DeployAppVersion>, anyhow::Error> {
1690 let mut vars = types::GetDeployAppVersionsByIdVars {
1691 id: cynic::Id::new(app_id),
1692 offset: None,
1693 before: None,
1694 after: None,
1695 first: Some(10),
1696 last: None,
1697 sort_by: None,
1698 };
1699
1700 let mut all_versions = Vec::<DeployAppVersion>::new();
1701
1702 loop {
1703 let page = get_deploy_app_versions_by_id(client, vars.clone()).await?;
1704 if page.edges.is_empty() {
1705 break;
1706 }
1707
1708 for edge in page.edges {
1709 let edge = match edge {
1710 Some(edge) => edge,
1711 None => continue,
1712 };
1713 let version = match edge.node {
1714 Some(item) => item,
1715 None => continue,
1716 };
1717
1718 if all_versions.iter().any(|v| v.id == version.id) == false {
1720 all_versions.push(version);
1721 }
1722
1723 vars.after = Some(edge.cursor);
1725 }
1726 }
1727
1728 Ok(all_versions)
1729}
1730
1731pub async fn app_version_activate(
1733 client: &WasmerClient,
1734 version: String,
1735) -> Result<DeployApp, anyhow::Error> {
1736 let res = client
1737 .run_graphql_strict(types::MarkAppVersionAsActive::build(
1738 types::MarkAppVersionAsActiveVars {
1739 input: types::MarkAppVersionAsActiveInput {
1740 app_version: version.into(),
1741 },
1742 },
1743 ))
1744 .await?;
1745 res.mark_app_version_as_active
1746 .context("app not found")
1747 .map(|x| x.app)
1748}
1749
1750pub async fn get_node(
1752 client: &WasmerClient,
1753 id: String,
1754) -> Result<Option<types::Node>, anyhow::Error> {
1755 client
1756 .run_graphql(types::GetNode::build(types::GetNodeVars { id: id.into() }))
1757 .await
1758 .map(|x| x.node)
1759}
1760
1761pub async fn get_app_by_id(
1763 client: &WasmerClient,
1764 app_id: String,
1765) -> Result<DeployApp, anyhow::Error> {
1766 get_app_by_id_opt(client, app_id)
1767 .await?
1768 .context("app not found")
1769}
1770
1771pub async fn get_app_by_id_opt(
1773 client: &WasmerClient,
1774 app_id: String,
1775) -> Result<Option<DeployApp>, anyhow::Error> {
1776 let app_opt = client
1777 .run_graphql(types::GetDeployAppById::build(
1778 types::GetDeployAppByIdVars {
1779 app_id: app_id.into(),
1780 },
1781 ))
1782 .await?
1783 .app;
1784
1785 if let Some(app) = app_opt {
1786 let app = app.into_deploy_app().context("app conversion failed")?;
1787 Ok(Some(app))
1788 } else {
1789 Ok(None)
1790 }
1791}
1792
1793pub async fn get_app_with_version_by_id(
1795 client: &WasmerClient,
1796 app_id: String,
1797 version_id: String,
1798) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
1799 let res = client
1800 .run_graphql(types::GetDeployAppAndVersionById::build(
1801 types::GetDeployAppAndVersionByIdVars {
1802 app_id: app_id.into(),
1803 version_id: version_id.into(),
1804 },
1805 ))
1806 .await?;
1807
1808 let app = res
1809 .app
1810 .context("app not found")?
1811 .into_deploy_app()
1812 .context("app conversion failed")?;
1813 let version = res
1814 .version
1815 .context("version not found")?
1816 .into_deploy_app_version()
1817 .context("version conversion failed")?;
1818
1819 Ok((app, version))
1820}
1821
1822pub async fn get_app_version_by_id(
1824 client: &WasmerClient,
1825 version_id: String,
1826) -> Result<DeployAppVersion, anyhow::Error> {
1827 client
1828 .run_graphql(types::GetDeployAppVersionById::build(
1829 types::GetDeployAppVersionByIdVars {
1830 version_id: version_id.into(),
1831 },
1832 ))
1833 .await?
1834 .version
1835 .context("app not found")?
1836 .into_deploy_app_version()
1837 .context("app version conversion failed")
1838}
1839
1840pub async fn get_app_version_by_id_with_app(
1841 client: &WasmerClient,
1842 version_id: String,
1843) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
1844 let version = client
1845 .run_graphql(types::GetDeployAppVersionById::build(
1846 types::GetDeployAppVersionByIdVars {
1847 version_id: version_id.into(),
1848 },
1849 ))
1850 .await?
1851 .version
1852 .context("app not found")?
1853 .into_deploy_app_version()
1854 .context("app version conversion failed")?;
1855
1856 let app_id = version
1857 .app
1858 .as_ref()
1859 .context("could not load app for version")?
1860 .id
1861 .clone();
1862
1863 let app = get_app_by_id(client, app_id.into_inner()).await?;
1864
1865 Ok((app, version))
1866}
1867
1868pub async fn user_apps_page(
1869 client: &WasmerClient,
1870 sort: types::DeployAppsSortBy,
1871 cursor: Option<String>,
1872) -> Result<Paginated<types::DeployApp>, anyhow::Error> {
1873 let user = client
1874 .run_graphql(types::GetCurrentUserWithApps::build(
1875 GetCurrentUserWithAppsVars {
1876 after: cursor,
1877 first: Some(10),
1878 sort: Some(sort),
1879 },
1880 ))
1881 .await?
1882 .viewer
1883 .context("not logged in")?;
1884
1885 let apps: Vec<_> = user
1886 .apps
1887 .edges
1888 .into_iter()
1889 .flatten()
1890 .filter_map(|x| x.node)
1891 .collect();
1892
1893 let out = Paginated {
1894 items: apps,
1895 next_cursor: user.apps.page_info.end_cursor,
1896 };
1897
1898 Ok(out)
1899}
1900
1901pub async fn user_apps(
1905 client: &WasmerClient,
1906 sort: types::DeployAppsSortBy,
1907) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
1908 futures::stream::try_unfold(None, move |cursor| async move {
1909 let user = client
1910 .run_graphql(types::GetCurrentUserWithApps::build(
1911 GetCurrentUserWithAppsVars {
1912 first: Some(10),
1913 after: cursor,
1914 sort: Some(sort),
1915 },
1916 ))
1917 .await?
1918 .viewer
1919 .context("not logged in")?;
1920
1921 let apps: Vec<_> = user
1922 .apps
1923 .edges
1924 .into_iter()
1925 .flatten()
1926 .filter_map(|x| x.node)
1927 .collect();
1928
1929 let cursor = user.apps.page_info.end_cursor;
1930
1931 if apps.is_empty() {
1932 Ok(None)
1933 } else {
1934 Ok(Some((apps, cursor)))
1935 }
1936 })
1937}
1938
1939pub async fn user_accessible_apps(
1941 client: &WasmerClient,
1942 sort: types::DeployAppsSortBy,
1943) -> Result<
1944 impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_,
1945 anyhow::Error,
1946> {
1947 let user_apps = user_apps(client, sort).await;
1948
1949 let namespace_res = client
1951 .run_graphql(types::GetCurrentUserWithNamespaces::build(
1952 types::GetCurrentUserWithNamespacesVars {
1953 namespace_role: None,
1954 },
1955 ))
1956 .await?;
1957 let active_user = namespace_res.viewer.context("not logged in")?;
1958 let namespace_names = active_user
1959 .namespaces
1960 .edges
1961 .iter()
1962 .filter_map(|edge| edge.as_ref())
1963 .filter_map(|edge| edge.node.as_ref())
1964 .map(|node| node.name.clone())
1965 .collect::<Vec<_>>();
1966
1967 let mut ns_apps = vec![];
1968 for ns in namespace_names {
1969 let apps = namespace_apps(client, ns, sort).await;
1970 ns_apps.push(apps);
1971 }
1972
1973 Ok((user_apps, ns_apps.merge()).merge())
1974}
1975
1976pub async fn namespace_apps_page(
1980 client: &WasmerClient,
1981 namespace: String,
1982 sort: types::DeployAppsSortBy,
1983 cursor: Option<String>,
1984) -> Result<Paginated<types::DeployApp>, anyhow::Error> {
1985 let namespace = namespace.clone();
1986
1987 let res = client
1988 .run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
1989 name: namespace.to_string(),
1990 after: cursor,
1991 sort: Some(sort),
1992 }))
1993 .await?
1994 .get_namespace
1995 .context("namespace not found")?
1996 .apps;
1997
1998 let apps: Vec<_> = res
1999 .edges
2000 .into_iter()
2001 .flatten()
2002 .filter_map(|x| x.node)
2003 .collect();
2004
2005 let out = Paginated {
2006 items: apps,
2007 next_cursor: res.page_info.end_cursor,
2008 };
2009
2010 Ok(out)
2011}
2012
2013pub async fn namespace_apps(
2017 client: &WasmerClient,
2018 namespace: String,
2019 sort: types::DeployAppsSortBy,
2020) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
2021 let namespace = namespace.clone();
2022
2023 futures::stream::try_unfold((None, namespace), move |(cursor, namespace)| async move {
2024 let res = client
2025 .run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
2026 name: namespace.to_string(),
2027 after: cursor,
2028 sort: Some(sort),
2029 }))
2030 .await?;
2031
2032 let ns = res
2033 .get_namespace
2034 .with_context(|| format!("failed to get namespace '{namespace}'"))?;
2035
2036 let apps: Vec<_> = ns
2037 .apps
2038 .edges
2039 .into_iter()
2040 .flatten()
2041 .filter_map(|x| x.node)
2042 .collect();
2043
2044 let cursor = ns.apps.page_info.end_cursor;
2045
2046 if apps.is_empty() {
2047 Ok(None)
2048 } else {
2049 Ok(Some((apps, (cursor, namespace))))
2050 }
2051 })
2052}
2053
2054pub async fn publish_deploy_app(
2056 client: &WasmerClient,
2057 vars: PublishDeployAppVars,
2058) -> Result<DeployAppVersion, anyhow::Error> {
2059 let res = client
2060 .run_graphql_raw(types::PublishDeployApp::build(vars))
2061 .await?;
2062
2063 if let Some(app) = res
2064 .data
2065 .and_then(|d| d.publish_deploy_app)
2066 .map(|d| d.deploy_app_version)
2067 {
2068 Ok(app)
2069 } else {
2070 Err(GraphQLApiFailure::from_errors(
2071 "could not publish app",
2072 res.errors,
2073 ))
2074 }
2075}
2076
2077pub async fn delete_app(client: &WasmerClient, app_id: String) -> Result<(), anyhow::Error> {
2079 let res = client
2080 .run_graphql_strict(types::DeleteApp::build(types::DeleteAppVars {
2081 app_id: app_id.into(),
2082 }))
2083 .await?
2084 .delete_app
2085 .context("API did not return data for the delete_app mutation")?;
2086
2087 if !res.success {
2088 bail!("App deletion failed for an unknown reason");
2089 }
2090
2091 Ok(())
2092}
2093
2094pub async fn user_namespaces(
2096 client: &WasmerClient,
2097) -> Result<Vec<types::Namespace>, anyhow::Error> {
2098 let user = client
2099 .run_graphql(types::GetCurrentUserWithNamespaces::build(
2100 types::GetCurrentUserWithNamespacesVars {
2101 namespace_role: None,
2102 },
2103 ))
2104 .await?
2105 .viewer
2106 .context("not logged in")?;
2107
2108 let ns = user
2109 .namespaces
2110 .edges
2111 .into_iter()
2112 .flatten()
2113 .filter_map(|x| x.node)
2115 .collect();
2116
2117 Ok(ns)
2118}
2119
2120pub async fn get_namespace(
2122 client: &WasmerClient,
2123 name: String,
2124) -> Result<Option<types::Namespace>, anyhow::Error> {
2125 client
2126 .run_graphql(types::GetNamespace::build(types::GetNamespaceVars { name }))
2127 .await
2128 .map(|x| x.get_namespace)
2129}
2130
2131pub async fn create_namespace(
2133 client: &WasmerClient,
2134 vars: CreateNamespaceVars,
2135) -> Result<types::Namespace, anyhow::Error> {
2136 client
2137 .run_graphql(types::CreateNamespace::build(vars))
2138 .await?
2139 .create_namespace
2140 .map(|x| x.namespace)
2141 .context("no namespace returned")
2142}
2143
2144pub async fn get_package(
2146 client: &WasmerClient,
2147 name: String,
2148) -> Result<Option<types::Package>, anyhow::Error> {
2149 client
2150 .run_graphql_strict(types::GetPackage::build(types::GetPackageVars { name }))
2151 .await
2152 .map(|x| x.get_package)
2153}
2154
2155pub async fn get_package_version_numbers(
2160 client: &WasmerClient,
2161 name: String,
2162) -> Result<Option<Vec<String>>, anyhow::Error> {
2163 let package = client
2164 .run_graphql_strict(types::GetPackageVersionNumbers::build(
2165 types::GetPackageVars { name },
2166 ))
2167 .await?
2168 .get_package;
2169
2170 Ok(package.map(|p| {
2171 p.versions
2172 .unwrap_or_default()
2173 .into_iter()
2174 .flatten()
2175 .map(|v| v.version)
2176 .collect()
2177 }))
2178}
2179
2180pub async fn get_package_version(
2182 client: &WasmerClient,
2183 name: String,
2184 version: String,
2185) -> Result<Option<types::PackageVersionWithPackage>, anyhow::Error> {
2186 client
2187 .run_graphql_strict(types::GetPackageVersion::build(
2188 types::GetPackageVersionVars { name, version },
2189 ))
2190 .await
2191 .map(|x| x.get_package_version)
2192}
2193
2194pub async fn get_package_versions(
2196 client: &WasmerClient,
2197 vars: types::AllPackageVersionsVars,
2198) -> Result<PackageVersionConnection, anyhow::Error> {
2199 let res = client
2200 .run_graphql(types::GetAllPackageVersions::build(vars))
2201 .await?;
2202 Ok(res.all_package_versions)
2203}
2204
2205pub async fn search_packages(
2211 client: &WasmerClient,
2212 query: impl Into<String>,
2213 filter: Option<types::PackagesFilter>,
2214 first: Option<i32>,
2215 after: Option<String>,
2216) -> Result<types::Paginated<types::SearchPackageVersion>, anyhow::Error> {
2217 let con = client
2218 .run_graphql_strict(types::SearchPackages::build(types::SearchPackagesVars {
2219 query: query.into(),
2220 packages: filter,
2221 first,
2222 after,
2223 }))
2224 .await?
2225 .search;
2226
2227 let items = con
2228 .edges
2229 .into_iter()
2230 .flatten()
2231 .filter_map(|edge| edge.node)
2232 .filter_map(types::SearchResult::into_package_version)
2233 .collect();
2234
2235 let next_cursor = con
2236 .page_info
2237 .end_cursor
2238 .filter(|_| con.page_info.has_next_page);
2239
2240 Ok(types::Paginated { items, next_cursor })
2241}
2242
2243pub fn fetch_all_matching_packages(
2246 client: &WasmerClient,
2247 query: impl Into<String>,
2248 filter: Option<types::PackagesFilter>,
2249 page_size: i32,
2250) -> impl futures::Stream<Item = Result<Vec<types::SearchPackageVersion>, anyhow::Error>> + '_ {
2251 let query = query.into();
2252 futures::stream::try_unfold(Some(None), move |state| {
2253 let query = query.clone();
2254 let filter = filter.clone();
2255 async move {
2256 let Some(after) = state else {
2257 return Ok(None);
2258 };
2259
2260 let page = search_packages(client, query, filter, Some(page_size), after).await?;
2261 let next_state = page.next_cursor.map(Some);
2262
2263 Ok::<_, anyhow::Error>(Some((page.items, next_state)))
2264 }
2265 })
2266}
2267
2268pub async fn get_package_release(
2270 client: &WasmerClient,
2271 hash: &str,
2272) -> Result<Option<types::PackageWebc>, anyhow::Error> {
2273 let hash = hash.trim_start_matches("sha256:");
2274 client
2275 .run_graphql_strict(types::GetPackageRelease::build(
2276 types::GetPackageReleaseVars {
2277 hash: hash.to_string(),
2278 },
2279 ))
2280 .await
2281 .map(|x| x.get_package_release)
2282}
2283
2284pub async fn get_package_releases(
2285 client: &WasmerClient,
2286 vars: types::AllPackageReleasesVars,
2287) -> Result<types::PackageWebcConnection, anyhow::Error> {
2288 let res = client
2289 .run_graphql(types::GetAllPackageReleases::build(vars))
2290 .await?;
2291 Ok(res.all_package_releases)
2292}
2293
2294pub fn get_package_versions_stream(
2296 client: &WasmerClient,
2297 vars: types::AllPackageVersionsVars,
2298) -> impl futures::Stream<Item = Result<Vec<types::PackageVersionWithPackage>, anyhow::Error>> + '_
2299{
2300 futures::stream::try_unfold(
2301 Some(vars),
2302 move |vars: Option<types::AllPackageVersionsVars>| async move {
2303 let vars = match vars {
2304 Some(vars) => vars,
2305 None => return Ok(None),
2306 };
2307
2308 let page = get_package_versions(client, vars.clone()).await?;
2309
2310 let end_cursor = page.page_info.end_cursor;
2311
2312 let items = page
2313 .edges
2314 .into_iter()
2315 .filter_map(|x| x.and_then(|x| x.node))
2316 .collect::<Vec<_>>();
2317
2318 let new_vars = end_cursor.map(|cursor| types::AllPackageVersionsVars {
2319 after: Some(cursor),
2320 ..vars
2321 });
2322
2323 Ok(Some((items, new_vars)))
2324 },
2325 )
2326}
2327
2328pub fn get_package_releases_stream(
2330 client: &WasmerClient,
2331 vars: types::AllPackageReleasesVars,
2332) -> impl futures::Stream<Item = Result<Vec<types::PackageWebc>, anyhow::Error>> + '_ {
2333 futures::stream::try_unfold(
2334 Some(vars),
2335 move |vars: Option<types::AllPackageReleasesVars>| async move {
2336 let vars = match vars {
2337 Some(vars) => vars,
2338 None => return Ok(None),
2339 };
2340
2341 let page = get_package_releases(client, vars.clone()).await?;
2342
2343 let end_cursor = page.page_info.end_cursor;
2344
2345 let items = page
2346 .edges
2347 .into_iter()
2348 .filter_map(|x| x.and_then(|x| x.node))
2349 .collect::<Vec<_>>();
2350
2351 let new_vars = end_cursor.map(|cursor| types::AllPackageReleasesVars {
2352 after: Some(cursor),
2353 ..vars
2354 });
2355
2356 Ok(Some((items, new_vars)))
2357 },
2358 )
2359}
2360
2361#[derive(Debug, PartialEq)]
2362pub enum TokenKind {
2363 SSH,
2364}
2365
2366pub async fn generate_deploy_config_token_raw(
2367 client: &WasmerClient,
2368 token_kind: TokenKind,
2369) -> Result<String, anyhow::Error> {
2370 let res = client
2371 .run_graphql(types::GenerateDeployConfigToken::build(
2372 types::GenerateDeployConfigTokenVars {
2373 input: match token_kind {
2374 TokenKind::SSH => "{}".to_string(),
2375 },
2376 },
2377 ))
2378 .await?;
2379
2380 res.generate_deploy_config_token
2381 .map(|x| x.token)
2382 .context("no token returned")
2383}
2384
2385pub async fn generate_ssh_token(
2390 client: &WasmerClient,
2391 app_id: Option<String>,
2392) -> Result<String, anyhow::Error> {
2393 let res = client
2394 .run_graphql_strict(types::GenerateSshToken::build(
2395 types::GenerateSshTokenVariables {
2396 app_id: app_id.map(cynic::Id::new),
2397 },
2398 ))
2399 .await?;
2400
2401 res.generate_ssh_token
2402 .map(|x| x.token)
2403 .context("no token returned")
2404}
2405
2406#[tracing::instrument(skip_all, level = "debug")]
2411#[allow(clippy::let_with_type_underscore)]
2412#[allow(clippy::too_many_arguments)]
2413fn get_app_logs(
2414 client: &WasmerClient,
2415 name: String,
2416 owner: String,
2417 tag: Option<String>,
2418 start: OffsetDateTime,
2419 end: Option<OffsetDateTime>,
2420 watch: bool,
2421 streams: Option<Vec<LogStream>>,
2422 request_id: Option<String>,
2423 instance_ids: Option<Vec<String>>,
2424) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2425 let span = tracing::Span::current();
2429
2430 futures::stream::try_unfold(start, move |start| {
2431 let variables = types::GetDeployAppLogsVars {
2432 name: name.clone(),
2433 owner: owner.clone(),
2434 version: tag.clone(),
2435 first: Some(100),
2436 starting_from: unix_timestamp(start),
2437 until: end.map(unix_timestamp),
2438 streams: streams.clone(),
2439 request_id: request_id.clone(),
2440 instance_ids: instance_ids.clone(),
2441 };
2442
2443 let fut = async move {
2444 loop {
2445 let deploy_app_version = client
2446 .run_graphql(types::GetDeployAppLogs::build(variables.clone()))
2447 .await?
2448 .get_deploy_app_version
2449 .context("app version not found")?;
2450
2451 let page: Vec<_> = deploy_app_version
2452 .logs
2453 .edges
2454 .into_iter()
2455 .flatten()
2456 .filter_map(|edge| edge.node)
2457 .collect();
2458
2459 if page.is_empty() {
2460 if watch {
2461 #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
2466 std::thread::sleep(Duration::from_secs(1));
2467
2468 #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
2469 tokio::time::sleep(Duration::from_secs(1)).await;
2470
2471 continue;
2472 }
2473
2474 break Ok(None);
2475 } else {
2476 let last_message = page.last().expect("The page is non-empty");
2477 let timestamp = last_message.timestamp;
2478 let timestamp = OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)
2481 .with_context(|| {
2482 format!("Unable to interpret {timestamp} as a unix timestamp")
2483 })?;
2484
2485 let next_timestamp = timestamp + Duration::from_nanos(1_000);
2491
2492 break Ok(Some((page, next_timestamp)));
2493 }
2494 }
2495 };
2496
2497 fut.instrument(span.clone())
2498 })
2499}
2500
2501#[tracing::instrument(skip_all, level = "debug")]
2507#[allow(clippy::let_with_type_underscore)]
2508#[allow(clippy::too_many_arguments)]
2509pub async fn get_app_logs_paginated(
2510 client: &WasmerClient,
2511 name: String,
2512 owner: String,
2513 tag: Option<String>,
2514 start: OffsetDateTime,
2515 end: Option<OffsetDateTime>,
2516 watch: bool,
2517 streams: Option<Vec<LogStream>>,
2518) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2519 let stream = get_app_logs(
2520 client, name, owner, tag, start, end, watch, streams, None, None,
2521 );
2522
2523 stream.map(|res| {
2524 let mut logs = Vec::new();
2525 let mut hasher = HashSet::new();
2526 let mut page = res?;
2527
2528 page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
2531
2532 logs.extend(page);
2533
2534 Ok(logs)
2535 })
2536}
2537
2538#[tracing::instrument(skip_all, level = "debug")]
2544#[allow(clippy::let_with_type_underscore)]
2545#[allow(clippy::too_many_arguments)]
2546pub async fn get_app_logs_paginated_filter_instance(
2547 client: &WasmerClient,
2548 name: String,
2549 owner: String,
2550 tag: Option<String>,
2551 start: OffsetDateTime,
2552 end: Option<OffsetDateTime>,
2553 watch: bool,
2554 streams: Option<Vec<LogStream>>,
2555 instance_ids: Vec<String>,
2556) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2557 let stream = get_app_logs(
2558 client,
2559 name,
2560 owner,
2561 tag,
2562 start,
2563 end,
2564 watch,
2565 streams,
2566 None,
2567 Some(instance_ids),
2568 );
2569
2570 stream.map(|res| {
2571 let mut logs = Vec::new();
2572 let mut hasher = HashSet::new();
2573 let mut page = res?;
2574
2575 page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
2578
2579 logs.extend(page);
2580
2581 Ok(logs)
2582 })
2583}
2584
2585#[tracing::instrument(skip_all, level = "debug")]
2591#[allow(clippy::let_with_type_underscore)]
2592#[allow(clippy::too_many_arguments)]
2593pub async fn get_app_logs_paginated_filter_request(
2594 client: &WasmerClient,
2595 name: String,
2596 owner: String,
2597 tag: Option<String>,
2598 start: OffsetDateTime,
2599 end: Option<OffsetDateTime>,
2600 watch: bool,
2601 streams: Option<Vec<LogStream>>,
2602 request_id: String,
2603) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2604 let stream = get_app_logs(
2605 client,
2606 name,
2607 owner,
2608 tag,
2609 start,
2610 end,
2611 watch,
2612 streams,
2613 Some(request_id),
2614 None,
2615 );
2616
2617 stream.map(|res| {
2618 let mut logs = Vec::new();
2619 let mut hasher = HashSet::new();
2620 let mut page = res?;
2621
2622 page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
2625
2626 logs.extend(page);
2627
2628 Ok(logs)
2629 })
2630}
2631
2632pub async fn get_domain(
2636 client: &WasmerClient,
2637 domain: String,
2638) -> Result<Option<types::DnsDomain>, anyhow::Error> {
2639 let vars = types::GetDomainVars { domain };
2640
2641 let opt = client
2642 .run_graphql(types::GetDomain::build(vars))
2643 .await?
2644 .get_domain;
2645 Ok(opt)
2646}
2647
2648pub async fn get_domain_zone_file(
2652 client: &WasmerClient,
2653 domain: String,
2654) -> Result<Option<types::DnsDomainWithZoneFile>, anyhow::Error> {
2655 let vars = types::GetDomainVars { domain };
2656
2657 let opt = client
2658 .run_graphql(types::GetDomainWithZoneFile::build(vars))
2659 .await?
2660 .get_domain;
2661 Ok(opt)
2662}
2663
2664pub async fn get_domain_with_records(
2666 client: &WasmerClient,
2667 domain: String,
2668) -> Result<Option<types::DnsDomainWithRecords>, anyhow::Error> {
2669 let vars = types::GetDomainVars { domain };
2670
2671 let opt = client
2672 .run_graphql(types::GetDomainWithRecords::build(vars))
2673 .await?
2674 .get_domain;
2675 Ok(opt)
2676}
2677
2678pub async fn register_domain(
2680 client: &WasmerClient,
2681 name: String,
2682 namespace: Option<String>,
2683 import_records: Option<bool>,
2684) -> Result<types::DnsDomain, anyhow::Error> {
2685 let vars = types::RegisterDomainVars {
2686 name,
2687 namespace,
2688 import_records,
2689 };
2690 let opt = client
2691 .run_graphql_strict(types::RegisterDomain::build(vars))
2692 .await?
2693 .register_domain
2694 .context("Domain registration failed")?
2695 .domain
2696 .context("Domain registration failed, no associatede domain found.")?;
2697 Ok(opt)
2698}
2699
2700pub async fn get_all_dns_records(
2704 client: &WasmerClient,
2705 vars: types::GetAllDnsRecordsVariables,
2706) -> Result<types::DnsRecordConnection, anyhow::Error> {
2707 client
2708 .run_graphql_strict(types::GetAllDnsRecords::build(vars))
2709 .await
2710 .map(|x| x.get_all_dnsrecords)
2711}
2712
2713pub async fn get_all_domains(
2715 client: &WasmerClient,
2716 vars: types::GetAllDomainsVariables,
2717) -> Result<Vec<DnsDomain>, anyhow::Error> {
2718 let connection = client
2719 .run_graphql_strict(types::GetAllDomains::build(vars))
2720 .await
2721 .map(|x| x.get_all_domains)
2722 .context("no domains returned")?;
2723 Ok(connection
2724 .edges
2725 .into_iter()
2726 .flatten()
2727 .filter_map(|x| x.node)
2728 .collect())
2729}
2730
2731pub fn get_all_dns_records_stream(
2735 client: &WasmerClient,
2736 vars: types::GetAllDnsRecordsVariables,
2737) -> impl futures::Stream<Item = Result<Vec<types::DnsRecord>, anyhow::Error>> + '_ {
2738 futures::stream::try_unfold(
2739 Some(vars),
2740 move |vars: Option<types::GetAllDnsRecordsVariables>| async move {
2741 let vars = match vars {
2742 Some(vars) => vars,
2743 None => return Ok(None),
2744 };
2745
2746 let page = get_all_dns_records(client, vars.clone()).await?;
2747
2748 let end_cursor = page.page_info.end_cursor;
2749
2750 let items = page
2751 .edges
2752 .into_iter()
2753 .filter_map(|x| x.and_then(|x| x.node))
2754 .collect::<Vec<_>>();
2755
2756 let new_vars = end_cursor.map(|c| types::GetAllDnsRecordsVariables {
2757 after: Some(c),
2758 ..vars
2759 });
2760
2761 Ok(Some((items, new_vars)))
2762 },
2763 )
2764}
2765
2766pub async fn purge_cache_for_app_version(
2767 client: &WasmerClient,
2768 vars: types::PurgeCacheForAppVersionVars,
2769) -> Result<(), anyhow::Error> {
2770 client
2771 .run_graphql_strict(types::PurgeCacheForAppVersion::build(vars))
2772 .await
2773 .map(|x| x.purge_cache_for_app_version)
2774 .context("backend did not return data")?;
2775
2776 Ok(())
2777}
2778
2779pub async fn configure_app_cdn_cache(
2780 client: &WasmerClient,
2781 vars: types::ConfigureAppCdnCacheVars,
2782) -> Result<types::AppCdnCacheMutationPayload, anyhow::Error> {
2783 client
2784 .run_graphql_strict(types::ConfigureAppCdnCache::build(vars))
2785 .await
2786 .map(|x| x.configure_app_cdn_cache)
2787}
2788
2789pub async fn purge_app_cdn_cache(
2790 client: &WasmerClient,
2791 vars: types::PurgeAppCdnCacheVars,
2792) -> Result<types::AppCdnCacheMutationPayload, anyhow::Error> {
2793 client
2794 .run_graphql_strict(types::PurgeAppCdnCache::build(vars))
2795 .await
2796 .map(|x| x.purge_app_cdn_cache)
2797}
2798
2799pub async fn app_cdn_cache_status(
2800 client: &WasmerClient,
2801 vars: types::GetAppCdnCacheStatusVars,
2802) -> Result<types::AppCdnCacheStatus, anyhow::Error> {
2803 client
2804 .run_graphql_strict(types::GetAppCdnCacheStatus::build(vars))
2805 .await?
2806 .app
2807 .context("app not found")?
2808 .into_app()
2809 .context("invalid node type returned")
2810}
2811
2812pub async fn app_cdn_cache_metrics(
2813 client: &WasmerClient,
2814 vars: types::GetAppCdnCacheMetricsVars,
2815) -> Result<types::AppCdnCacheMetrics, anyhow::Error> {
2816 client
2817 .run_graphql_strict(types::GetAppCdnCacheMetrics::build(vars))
2818 .await?
2819 .app
2820 .context("app not found")?
2821 .into_app()
2822 .context("invalid node type returned")
2823}
2824
2825fn unix_timestamp(ts: OffsetDateTime) -> f64 {
2828 let nanos_per_second = 1_000_000_000;
2829 let timestamp = ts.unix_timestamp_nanos();
2830 let nanos = timestamp % nanos_per_second;
2831 let secs = timestamp / nanos_per_second;
2832
2833 (secs as f64) + (nanos as f64 / nanos_per_second as f64)
2834}
2835
2836pub async fn upsert_domain_from_zone_file(
2838 client: &WasmerClient,
2839 zone_file_contents: String,
2840 delete_missing_records: bool,
2841) -> Result<DnsDomain, anyhow::Error> {
2842 let vars = UpsertDomainFromZoneFileVars {
2843 zone_file: zone_file_contents,
2844 delete_missing_records: Some(delete_missing_records),
2845 };
2846 let res = client
2847 .run_graphql_strict(types::UpsertDomainFromZoneFile::build(vars))
2848 .await?;
2849
2850 let domain = res
2851 .upsert_domain_from_zone_file
2852 .context("Upserting domain from zonefile failed")?
2853 .domain;
2854
2855 Ok(domain)
2856}