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