wasmer_backend_api/
query.rs

1use std::{collections::HashSet, time::Duration};
2
3use anyhow::{bail, Context};
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    types::{self, *},
16    GraphQLApiFailure, WasmerClient,
17};
18
19/// Rotate the s3 secrets tied to an app given its id.
20pub async fn rotate_s3_secrets(
21    client: &WasmerClient,
22    app_id: types::Id,
23) -> Result<(), anyhow::Error> {
24    client
25        .run_graphql_strict(types::RotateS3SecretsForApp::build(
26            RotateS3SecretsForAppVariables { id: app_id },
27        ))
28        .await?;
29
30    Ok(())
31}
32
33pub async fn viewer_can_deploy_to_namespace(
34    client: &WasmerClient,
35    owner_name: &str,
36) -> Result<bool, anyhow::Error> {
37    client
38        .run_graphql_strict(types::ViewerCan::build(ViewerCanVariables {
39            action: OwnerAction::DeployApp,
40            owner_name,
41        }))
42        .await
43        .map(|v| v.viewer_can)
44}
45
46pub async fn redeploy_app_by_id(
47    client: &WasmerClient,
48    app_id: impl Into<String>,
49) -> Result<Option<DeployApp>, anyhow::Error> {
50    client
51        .run_graphql_strict(types::RedeployActiveApp::build(
52            RedeployActiveAppVariables {
53                id: types::Id::from(app_id),
54            },
55        ))
56        .await
57        .map(|v| v.redeploy_active_version.map(|v| v.app))
58}
59
60/// List all bindings associated with a particular package.
61///
62/// If a version number isn't provided, this will default to the most recently
63/// published version.
64pub async fn list_bindings(
65    client: &WasmerClient,
66    name: &str,
67    version: Option<&str>,
68) -> Result<Vec<Bindings>, anyhow::Error> {
69    client
70        .run_graphql_strict(types::GetBindingsQuery::build(GetBindingsQueryVariables {
71            name,
72            version,
73        }))
74        .await
75        .and_then(|b| {
76            b.package_version
77                .ok_or(anyhow::anyhow!("No bindings found!"))
78        })
79        .map(|v| {
80            let mut bindings_packages = Vec::new();
81
82            for b in v.bindings.into_iter().flatten() {
83                let pkg = Bindings {
84                    id: b.id.into_inner(),
85                    url: b.url,
86                    language: b.language,
87                    generator: b.generator,
88                };
89                bindings_packages.push(pkg);
90            }
91
92            bindings_packages
93        })
94}
95
96/// Revoke an existing token
97pub async fn revoke_token(
98    client: &WasmerClient,
99    token: String,
100) -> Result<Option<bool>, anyhow::Error> {
101    client
102        .run_graphql_strict(types::RevokeToken::build(RevokeTokenVariables { token }))
103        .await
104        .map(|v| v.revoke_api_token.and_then(|v| v.success))
105}
106
107/// Generate a new Nonce
108///
109/// Takes a name and a callbackUrl and returns a nonce
110pub async fn create_nonce(
111    client: &WasmerClient,
112    name: String,
113    callback_url: String,
114) -> Result<Option<Nonce>, anyhow::Error> {
115    client
116        .run_graphql_strict(types::CreateNewNonce::build(CreateNewNonceVariables {
117            callback_url,
118            name,
119        }))
120        .await
121        .map(|v| v.new_nonce.map(|v| v.nonce))
122}
123
124pub async fn get_app_secret_value_by_id(
125    client: &WasmerClient,
126    secret_id: impl Into<String>,
127) -> Result<Option<String>, anyhow::Error> {
128    client
129        .run_graphql_strict(types::GetAppSecretValue::build(
130            GetAppSecretValueVariables {
131                id: types::Id::from(secret_id),
132            },
133        ))
134        .await
135        .map(|v| v.get_secret_value)
136}
137
138pub async fn get_app_secret_by_name(
139    client: &WasmerClient,
140    app_id: impl Into<String>,
141    name: impl Into<String>,
142) -> Result<Option<Secret>, anyhow::Error> {
143    client
144        .run_graphql_strict(types::GetAppSecret::build(GetAppSecretVariables {
145            app_id: types::Id::from(app_id),
146            secret_name: name.into(),
147        }))
148        .await
149        .map(|v| v.get_app_secret)
150}
151
152/// Update or create an app secret.
153pub async fn upsert_app_secret(
154    client: &WasmerClient,
155    app_id: impl Into<String>,
156    name: impl Into<String>,
157    value: impl Into<String>,
158) -> Result<Option<UpsertAppSecretPayload>, anyhow::Error> {
159    client
160        .run_graphql_strict(types::UpsertAppSecret::build(UpsertAppSecretVariables {
161            app_id: cynic::Id::from(app_id.into()),
162            name: name.into().as_str(),
163            value: value.into().as_str(),
164        }))
165        .await
166        .map(|v| v.upsert_app_secret)
167}
168
169/// Update or create app secrets in bulk.
170pub async fn upsert_app_secrets(
171    client: &WasmerClient,
172    app_id: impl Into<String>,
173    secrets: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
174) -> Result<Option<UpsertAppSecretsPayload>, anyhow::Error> {
175    client
176        .run_graphql_strict(types::UpsertAppSecrets::build(UpsertAppSecretsVariables {
177            app_id: cynic::Id::from(app_id.into()),
178            secrets: Some(
179                secrets
180                    .into_iter()
181                    .map(|(name, value)| SecretInput {
182                        name: name.into(),
183                        value: value.into(),
184                    })
185                    .collect(),
186            ),
187        }))
188        .await
189        .map(|v| v.upsert_app_secrets)
190}
191
192/// Load all secrets of an app.
193///
194/// Will paginate through all versions and return them in a single list.
195pub async fn get_all_app_secrets_filtered(
196    client: &WasmerClient,
197    app_id: impl Into<String>,
198    names: impl IntoIterator<Item = impl Into<String>>,
199) -> Result<Vec<Secret>, anyhow::Error> {
200    let mut vars = GetAllAppSecretsVariables {
201        after: None,
202        app_id: types::Id::from(app_id),
203        before: None,
204        first: None,
205        last: None,
206        offset: None,
207        names: Some(names.into_iter().map(|s| s.into()).collect()),
208    };
209
210    let mut all_secrets = Vec::<Secret>::new();
211
212    loop {
213        let page = get_app_secrets(client, vars.clone()).await?;
214        if page.edges.is_empty() {
215            break;
216        }
217
218        for edge in page.edges {
219            let edge = match edge {
220                Some(edge) => edge,
221                None => continue,
222            };
223            let version = match edge.node {
224                Some(item) => item,
225                None => continue,
226            };
227
228            all_secrets.push(version);
229
230            // Update pagination.
231            vars.after = Some(edge.cursor);
232        }
233    }
234
235    Ok(all_secrets)
236}
237
238/// Retrieve volumes for an app.
239pub async fn get_app_volumes(
240    client: &WasmerClient,
241    owner: impl Into<String>,
242    name: impl Into<String>,
243) -> Result<Vec<types::AppVersionVolume>, anyhow::Error> {
244    let vars = types::GetAppVolumesVars {
245        owner: owner.into(),
246        name: name.into(),
247    };
248    let res = client
249        .run_graphql_strict(types::GetAppVolumes::build(vars))
250        .await?;
251    let volumes = res
252        .get_deploy_app
253        .context("app not found")?
254        .active_version
255        .and_then(|v| v.volumes)
256        .unwrap_or_default()
257        .into_iter()
258        .flatten()
259        .collect();
260    Ok(volumes)
261}
262
263/// Load the S3 credentials.
264///
265/// S3 can be used to get access to an apps volumes.
266pub async fn get_app_s3_credentials(
267    client: &WasmerClient,
268    app_id: impl Into<String>,
269) -> Result<types::S3Credentials, anyhow::Error> {
270    let app_id = app_id.into();
271
272    // Firt load the app to get the s3 url.
273    let app1 = get_app_by_id(client, app_id.clone()).await?;
274
275    let vars = types::GetDeployAppVars {
276        owner: app1.owner.global_name,
277        name: app1.name,
278    };
279    client
280        .run_graphql_strict(types::GetDeployAppS3Credentials::build(vars))
281        .await?
282        .get_deploy_app
283        .context("app not found")?
284        .s3_credentials
285        .context("app does not have S3 credentials")
286}
287
288/// Load all available regions.
289///
290/// Will paginate through all versions and return them in a single list.
291pub async fn get_all_app_regions(client: &WasmerClient) -> Result<Vec<AppRegion>, anyhow::Error> {
292    let mut vars = GetAllAppRegionsVariables {
293        after: None,
294        before: None,
295        first: None,
296        last: None,
297        offset: None,
298    };
299
300    let mut all_regions = Vec::<AppRegion>::new();
301
302    loop {
303        let page = get_regions(client, vars.clone()).await?;
304        if page.edges.is_empty() {
305            break;
306        }
307
308        for edge in page.edges {
309            let edge = match edge {
310                Some(edge) => edge,
311                None => continue,
312            };
313            let version = match edge.node {
314                Some(item) => item,
315                None => continue,
316            };
317
318            all_regions.push(version);
319
320            // Update pagination.
321            vars.after = Some(edge.cursor);
322        }
323    }
324
325    Ok(all_regions)
326}
327
328/// Retrieve regions.
329pub async fn get_regions(
330    client: &WasmerClient,
331    vars: GetAllAppRegionsVariables,
332) -> Result<AppRegionConnection, anyhow::Error> {
333    let res = client
334        .run_graphql_strict(types::GetAllAppRegions::build(vars))
335        .await?;
336    Ok(res.get_app_regions)
337}
338
339/// Load all secrets of an app.
340///
341/// Will paginate through all versions and return them in a single list.
342pub async fn get_all_app_secrets(
343    client: &WasmerClient,
344    app_id: impl Into<String>,
345) -> Result<Vec<Secret>, anyhow::Error> {
346    let mut vars = GetAllAppSecretsVariables {
347        after: None,
348        app_id: types::Id::from(app_id),
349        before: None,
350        first: None,
351        last: None,
352        offset: None,
353        names: None,
354    };
355
356    let mut all_secrets = Vec::<Secret>::new();
357
358    loop {
359        let page = get_app_secrets(client, vars.clone()).await?;
360        if page.edges.is_empty() {
361            break;
362        }
363
364        for edge in page.edges {
365            let edge = match edge {
366                Some(edge) => edge,
367                None => continue,
368            };
369            let version = match edge.node {
370                Some(item) => item,
371                None => continue,
372            };
373
374            all_secrets.push(version);
375
376            // Update pagination.
377            vars.after = Some(edge.cursor);
378        }
379    }
380
381    Ok(all_secrets)
382}
383
384/// Retrieve secrets for an app.
385pub async fn get_app_secrets(
386    client: &WasmerClient,
387    vars: GetAllAppSecretsVariables,
388) -> Result<SecretConnection, anyhow::Error> {
389    let res = client
390        .run_graphql_strict(types::GetAllAppSecrets::build(vars))
391        .await?;
392    res.get_app_secrets.context("app not found")
393}
394
395pub async fn delete_app_secret(
396    client: &WasmerClient,
397    secret_id: impl Into<String>,
398) -> Result<Option<DeleteAppSecretPayload>, anyhow::Error> {
399    client
400        .run_graphql_strict(types::DeleteAppSecret::build(DeleteAppSecretVariables {
401            id: types::Id::from(secret_id.into()),
402        }))
403        .await
404        .map(|v| v.delete_app_secret)
405}
406
407/// Load a webc package from the registry.
408///
409/// NOTE: this uses the public URL instead of the download URL available through
410/// the API, and should not be used where possible.
411pub async fn fetch_webc_package(
412    client: &WasmerClient,
413    ident: &PackageIdent,
414    default_registry: &Url,
415) -> Result<Container, anyhow::Error> {
416    let url = match ident {
417        PackageIdent::Named(n) => Url::parse(&format!(
418            "{default_registry}/{}:{}",
419            n.full_name(),
420            n.version_or_default()
421        ))?,
422        PackageIdent::Hash(h) => match get_package_release(client, &h.to_string()).await? {
423            Some(webc) => Url::parse(&webc.webc_url)?,
424            None => anyhow::bail!("Could not find package with hash '{}'", h),
425        },
426    };
427
428    let data = client
429        .client
430        .get(url)
431        .header(reqwest::header::USER_AGENT, &client.user_agent)
432        .header(reqwest::header::ACCEPT, "application/webc")
433        .send()
434        .await?
435        .error_for_status()?
436        .bytes()
437        .await?;
438
439    from_bytes(data).context("failed to parse webc package")
440}
441
442/// Fetch app templates.
443pub async fn fetch_app_template_from_slug(
444    client: &WasmerClient,
445    slug: String,
446) -> Result<Option<types::AppTemplate>, anyhow::Error> {
447    client
448        .run_graphql_strict(types::GetAppTemplateFromSlug::build(
449            GetAppTemplateFromSlugVariables { slug },
450        ))
451        .await
452        .map(|v| v.get_app_template)
453}
454
455/// Fetch app templates.
456pub async fn fetch_app_templates_from_framework(
457    client: &WasmerClient,
458    framework_slug: String,
459    first: i32,
460    after: Option<String>,
461    sort_by: Option<types::AppTemplatesSortBy>,
462) -> Result<Option<types::AppTemplateConnection>, anyhow::Error> {
463    client
464        .run_graphql_strict(types::GetAppTemplatesFromFramework::build(
465            GetAppTemplatesFromFrameworkVars {
466                framework_slug,
467                first,
468                after,
469                sort_by,
470            },
471        ))
472        .await
473        .map(|r| r.get_app_templates)
474}
475
476/// Fetch app templates.
477pub async fn fetch_app_templates(
478    client: &WasmerClient,
479    category_slug: String,
480    first: i32,
481    after: Option<String>,
482    sort_by: Option<types::AppTemplatesSortBy>,
483) -> Result<Option<types::AppTemplateConnection>, anyhow::Error> {
484    client
485        .run_graphql_strict(types::GetAppTemplates::build(GetAppTemplatesVars {
486            category_slug,
487            first,
488            after,
489            sort_by,
490        }))
491        .await
492        .map(|r| r.get_app_templates)
493}
494
495/// Fetch all app templates by paginating through the responses.
496///
497/// Will fetch at most `max` templates.
498pub fn fetch_all_app_templates(
499    client: &WasmerClient,
500    page_size: i32,
501    sort_by: Option<types::AppTemplatesSortBy>,
502) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
503    let vars = GetAppTemplatesVars {
504        category_slug: String::new(),
505        first: page_size,
506        sort_by,
507        after: None,
508    };
509
510    futures::stream::try_unfold(
511        Some(vars),
512        move |vars: Option<types::GetAppTemplatesVars>| async move {
513            let vars = match vars {
514                Some(vars) => vars,
515                None => return Ok(None),
516            };
517
518            let con = client
519                .run_graphql_strict(types::GetAppTemplates::build(vars.clone()))
520                .await?
521                .get_app_templates
522                .context("backend did not return any data")?;
523
524            let items = con
525                .edges
526                .into_iter()
527                .flatten()
528                .filter_map(|edge| edge.node)
529                .collect::<Vec<_>>();
530
531            let next_cursor = con
532                .page_info
533                .end_cursor
534                .filter(|_| con.page_info.has_next_page);
535
536            let next_vars = next_cursor.map(|after| types::GetAppTemplatesVars {
537                after: Some(after),
538                ..vars
539            });
540
541            #[allow(clippy::type_complexity)]
542            let res: Result<
543                Option<(Vec<types::AppTemplate>, Option<types::GetAppTemplatesVars>)>,
544                anyhow::Error,
545            > = Ok(Some((items, next_vars)));
546
547            res
548        },
549    )
550}
551
552/// Fetch all app templates by paginating through the responses.
553///
554/// Will fetch at most `max` templates.
555pub fn fetch_all_app_templates_from_language(
556    client: &WasmerClient,
557    page_size: i32,
558    sort_by: Option<types::AppTemplatesSortBy>,
559    language: String,
560) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
561    let vars = GetAppTemplatesFromLanguageVars {
562        language_slug: language.clone().to_string(),
563        first: page_size,
564        sort_by,
565        after: None,
566    };
567
568    futures::stream::try_unfold(
569        Some(vars),
570        move |vars: Option<types::GetAppTemplatesFromLanguageVars>| async move {
571            let vars = match vars {
572                Some(vars) => vars,
573                None => return Ok(None),
574            };
575
576            let con = client
577                .run_graphql_strict(types::GetAppTemplatesFromLanguage::build(vars.clone()))
578                .await?
579                .get_app_templates
580                .context("backend did not return any data")?;
581
582            let items = con
583                .edges
584                .into_iter()
585                .flatten()
586                .filter_map(|edge| edge.node)
587                .collect::<Vec<_>>();
588
589            let next_cursor = con
590                .page_info
591                .end_cursor
592                .filter(|_| con.page_info.has_next_page);
593
594            let next_vars = next_cursor.map(|after| types::GetAppTemplatesFromLanguageVars {
595                after: Some(after),
596                ..vars
597            });
598
599            #[allow(clippy::type_complexity)]
600            let res: Result<
601                Option<(
602                    Vec<types::AppTemplate>,
603                    Option<types::GetAppTemplatesFromLanguageVars>,
604                )>,
605                anyhow::Error,
606            > = Ok(Some((items, next_vars)));
607
608            res
609        },
610    )
611}
612
613/// Fetch languages from available app templates.
614pub async fn fetch_app_template_languages(
615    client: &WasmerClient,
616    after: Option<String>,
617    first: Option<i32>,
618) -> Result<Option<types::TemplateLanguageConnection>, anyhow::Error> {
619    client
620        .run_graphql_strict(types::GetTemplateLanguages::build(
621            GetTemplateLanguagesVars { after, first },
622        ))
623        .await
624        .map(|r| r.get_template_languages)
625}
626
627/// Fetch all languages from available app templates by paginating through the responses.
628///
629/// Will fetch at most `max` templates.
630pub fn fetch_all_app_template_languages(
631    client: &WasmerClient,
632    page_size: Option<i32>,
633) -> impl futures::Stream<Item = Result<Vec<types::TemplateLanguage>, anyhow::Error>> + '_ {
634    let vars = GetTemplateLanguagesVars {
635        after: None,
636        first: page_size,
637    };
638
639    futures::stream::try_unfold(
640        Some(vars),
641        move |vars: Option<types::GetTemplateLanguagesVars>| async move {
642            let vars = match vars {
643                Some(vars) => vars,
644                None => return Ok(None),
645            };
646
647            let con = client
648                .run_graphql_strict(types::GetTemplateLanguages::build(vars.clone()))
649                .await?
650                .get_template_languages
651                .context("backend did not return any data")?;
652
653            let items = con
654                .edges
655                .into_iter()
656                .flatten()
657                .filter_map(|edge| edge.node)
658                .collect::<Vec<_>>();
659
660            let next_cursor = con
661                .page_info
662                .end_cursor
663                .filter(|_| con.page_info.has_next_page);
664
665            let next_vars = next_cursor.map(|after| types::GetTemplateLanguagesVars {
666                after: Some(after),
667                ..vars
668            });
669
670            #[allow(clippy::type_complexity)]
671            let res: Result<
672                Option<(
673                    Vec<types::TemplateLanguage>,
674                    Option<types::GetTemplateLanguagesVars>,
675                )>,
676                anyhow::Error,
677            > = Ok(Some((items, next_vars)));
678
679            res
680        },
681    )
682}
683
684/// Fetch all app templates by paginating through the responses.
685///
686/// Will fetch at most `max` templates.
687pub fn fetch_all_app_templates_from_framework(
688    client: &WasmerClient,
689    page_size: i32,
690    sort_by: Option<types::AppTemplatesSortBy>,
691    framework: String,
692) -> impl futures::Stream<Item = Result<Vec<types::AppTemplate>, anyhow::Error>> + '_ {
693    let vars = GetAppTemplatesFromFrameworkVars {
694        framework_slug: framework.clone().to_string(),
695        first: page_size,
696        sort_by,
697        after: None,
698    };
699
700    futures::stream::try_unfold(
701        Some(vars),
702        move |vars: Option<types::GetAppTemplatesFromFrameworkVars>| async move {
703            let vars = match vars {
704                Some(vars) => vars,
705                None => return Ok(None),
706            };
707
708            let con = client
709                .run_graphql_strict(types::GetAppTemplatesFromFramework::build(vars.clone()))
710                .await?
711                .get_app_templates
712                .context("backend did not return any data")?;
713
714            let items = con
715                .edges
716                .into_iter()
717                .flatten()
718                .filter_map(|edge| edge.node)
719                .collect::<Vec<_>>();
720
721            let next_cursor = con
722                .page_info
723                .end_cursor
724                .filter(|_| con.page_info.has_next_page);
725
726            let next_vars = next_cursor.map(|after| types::GetAppTemplatesFromFrameworkVars {
727                after: Some(after),
728                ..vars
729            });
730
731            #[allow(clippy::type_complexity)]
732            let res: Result<
733                Option<(
734                    Vec<types::AppTemplate>,
735                    Option<types::GetAppTemplatesFromFrameworkVars>,
736                )>,
737                anyhow::Error,
738            > = Ok(Some((items, next_vars)));
739
740            res
741        },
742    )
743}
744
745/// Fetch frameworks from available app templates.
746pub async fn fetch_app_template_frameworks(
747    client: &WasmerClient,
748    after: Option<String>,
749    first: Option<i32>,
750) -> Result<Option<types::TemplateFrameworkConnection>, anyhow::Error> {
751    client
752        .run_graphql_strict(types::GetTemplateFrameworks::build(
753            GetTemplateFrameworksVars { after, first },
754        ))
755        .await
756        .map(|r| r.get_template_frameworks)
757}
758
759/// Fetch all frameworks from available app templates by paginating through the responses.
760///
761/// Will fetch at most `max` templates.
762pub fn fetch_all_app_template_frameworks(
763    client: &WasmerClient,
764    page_size: Option<i32>,
765) -> impl futures::Stream<Item = Result<Vec<types::TemplateFramework>, anyhow::Error>> + '_ {
766    let vars = GetTemplateFrameworksVars {
767        after: None,
768        first: page_size,
769    };
770
771    futures::stream::try_unfold(
772        Some(vars),
773        move |vars: Option<types::GetTemplateFrameworksVars>| async move {
774            let vars = match vars {
775                Some(vars) => vars,
776                None => return Ok(None),
777            };
778
779            let con = client
780                .run_graphql_strict(types::GetTemplateFrameworks::build(vars.clone()))
781                .await?
782                .get_template_frameworks
783                .context("backend did not return any data")?;
784
785            let items = con
786                .edges
787                .into_iter()
788                .flatten()
789                .filter_map(|edge| edge.node)
790                .collect::<Vec<_>>();
791
792            let next_cursor = con
793                .page_info
794                .end_cursor
795                .filter(|_| con.page_info.has_next_page);
796
797            let next_vars = next_cursor.map(|after| types::GetTemplateFrameworksVars {
798                after: Some(after),
799                ..vars
800            });
801
802            #[allow(clippy::type_complexity)]
803            let res: Result<
804                Option<(
805                    Vec<types::TemplateFramework>,
806                    Option<types::GetTemplateFrameworksVars>,
807                )>,
808                anyhow::Error,
809            > = Ok(Some((items, next_vars)));
810
811            res
812        },
813    )
814}
815
816/// Get a signed URL to upload packages.
817pub async fn get_signed_url_for_package_upload(
818    client: &WasmerClient,
819    expires_after_seconds: Option<i32>,
820    filename: Option<&str>,
821    name: Option<&str>,
822    version: Option<&str>,
823) -> Result<Option<SignedUrl>, anyhow::Error> {
824    client
825        .run_graphql_strict(types::GetSignedUrlForPackageUpload::build(
826            GetSignedUrlForPackageUploadVariables {
827                expires_after_seconds,
828                filename,
829                name,
830                version,
831            },
832        ))
833        .await
834        .map(|r| r.get_signed_url_for_package_upload)
835}
836/// Push a package to the registry.
837pub async fn push_package_release(
838    client: &WasmerClient,
839    name: Option<&str>,
840    namespace: &str,
841    signed_url: &str,
842    private: Option<bool>,
843) -> Result<Option<PushPackageReleasePayload>, anyhow::Error> {
844    client
845        .run_graphql_strict(types::PushPackageRelease::build(
846            types::PushPackageReleaseVariables {
847                name,
848                namespace,
849                private,
850                signed_url,
851            },
852        ))
853        .await
854        .map(|r| r.push_package_release)
855}
856
857#[allow(clippy::too_many_arguments)]
858pub async fn tag_package_release(
859    client: &WasmerClient,
860    description: Option<&str>,
861    homepage: Option<&str>,
862    license: Option<&str>,
863    license_file: Option<&str>,
864    manifest: Option<&str>,
865    name: &str,
866    namespace: Option<&str>,
867    package_release_id: &cynic::Id,
868    private: Option<bool>,
869    readme: Option<&str>,
870    repository: Option<&str>,
871    version: &str,
872) -> Result<Option<TagPackageReleasePayload>, anyhow::Error> {
873    client
874        .run_graphql_strict(types::TagPackageRelease::build(
875            types::TagPackageReleaseVariables {
876                description,
877                homepage,
878                license,
879                license_file,
880                manifest,
881                name,
882                namespace,
883                package_release_id,
884                private,
885                readme,
886                repository,
887                version,
888            },
889        ))
890        .await
891        .map(|r| r.tag_package_release)
892}
893
894/// Get the currently logged in user.
895pub async fn current_user(client: &WasmerClient) -> Result<Option<types::User>, anyhow::Error> {
896    client
897        .run_graphql(types::GetCurrentUser::build(()))
898        .await
899        .map(|x| x.viewer)
900}
901
902/// Get the currently logged in user, together with all accessible namespaces.
903///
904/// You can optionally filter the namespaces by the user role.
905pub async fn current_user_with_namespaces(
906    client: &WasmerClient,
907    namespace_role: Option<types::GrapheneRole>,
908) -> Result<types::UserWithNamespaces, anyhow::Error> {
909    client
910        .run_graphql(types::GetCurrentUserWithNamespaces::build(
911            types::GetCurrentUserWithNamespacesVars { namespace_role },
912        ))
913        .await?
914        .viewer
915        .context("not logged in")
916}
917
918/// Retrieve an app.
919pub async fn get_app(
920    client: &WasmerClient,
921    owner: String,
922    name: String,
923) -> Result<Option<types::DeployApp>, anyhow::Error> {
924    client
925        .run_graphql(types::GetDeployApp::build(types::GetDeployAppVars {
926            name,
927            owner,
928        }))
929        .await
930        .map(|x| x.get_deploy_app)
931}
932
933/// Retrieve an app by its global alias.
934pub async fn get_app_by_alias(
935    client: &WasmerClient,
936    alias: String,
937) -> Result<Option<types::DeployApp>, anyhow::Error> {
938    client
939        .run_graphql(types::GetDeployAppByAlias::build(
940            types::GetDeployAppByAliasVars { alias },
941        ))
942        .await
943        .map(|x| x.get_app_by_global_alias)
944}
945
946/// Retrieve an app version.
947pub async fn get_app_version(
948    client: &WasmerClient,
949    owner: String,
950    name: String,
951    version: String,
952) -> Result<Option<types::DeployAppVersion>, anyhow::Error> {
953    client
954        .run_graphql(types::GetDeployAppVersion::build(
955            types::GetDeployAppVersionVars {
956                name,
957                owner,
958                version,
959            },
960        ))
961        .await
962        .map(|x| x.get_deploy_app_version)
963}
964
965/// Retrieve an app together with a specific version.
966pub async fn get_app_with_version(
967    client: &WasmerClient,
968    owner: String,
969    name: String,
970    version: String,
971) -> Result<GetDeployAppAndVersion, anyhow::Error> {
972    client
973        .run_graphql(types::GetDeployAppAndVersion::build(
974            types::GetDeployAppAndVersionVars {
975                name,
976                owner,
977                version,
978            },
979        ))
980        .await
981}
982
983/// Retrieve an app together with a specific version.
984pub async fn get_app_and_package_by_name(
985    client: &WasmerClient,
986    vars: types::GetPackageAndAppVars,
987) -> Result<(Option<types::Package>, Option<types::DeployApp>), anyhow::Error> {
988    let res = client
989        .run_graphql(types::GetPackageAndApp::build(vars))
990        .await?;
991    Ok((res.get_package, res.get_deploy_app))
992}
993
994/// Retrieve apps.
995pub async fn get_deploy_apps(
996    client: &WasmerClient,
997    vars: types::GetDeployAppsVars,
998) -> Result<DeployAppConnection, anyhow::Error> {
999    let res = client
1000        .run_graphql(types::GetDeployApps::build(vars))
1001        .await?;
1002    res.get_deploy_apps.context("no apps returned")
1003}
1004
1005/// Retrieve apps as a stream that will automatically paginate.
1006pub fn get_deploy_apps_stream(
1007    client: &WasmerClient,
1008    vars: types::GetDeployAppsVars,
1009) -> impl futures::Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + '_ {
1010    futures::stream::try_unfold(
1011        Some(vars),
1012        move |vars: Option<types::GetDeployAppsVars>| async move {
1013            let vars = match vars {
1014                Some(vars) => vars,
1015                None => return Ok(None),
1016            };
1017
1018            let page = get_deploy_apps(client, vars.clone()).await?;
1019
1020            let end_cursor = page.page_info.end_cursor;
1021
1022            let items = page
1023                .edges
1024                .into_iter()
1025                .filter_map(|x| x.and_then(|x| x.node))
1026                .collect::<Vec<_>>();
1027
1028            let new_vars = end_cursor.map(|c| types::GetDeployAppsVars {
1029                after: Some(c),
1030                ..vars
1031            });
1032
1033            Ok(Some((items, new_vars)))
1034        },
1035    )
1036}
1037
1038/// Retrieve versions for an app.
1039pub async fn get_deploy_app_versions(
1040    client: &WasmerClient,
1041    vars: GetDeployAppVersionsVars,
1042) -> Result<DeployAppVersionConnection, anyhow::Error> {
1043    let res = client
1044        .run_graphql_strict(types::GetDeployAppVersions::build(vars))
1045        .await?;
1046    let versions = res.get_deploy_app.context("app not found")?.versions;
1047    Ok(versions)
1048}
1049
1050/// Get app deployments for an app.
1051pub async fn app_deployments(
1052    client: &WasmerClient,
1053    vars: types::GetAppDeploymentsVariables,
1054) -> Result<Vec<types::Deployment>, anyhow::Error> {
1055    let res = client
1056        .run_graphql_strict(types::GetAppDeployments::build(vars))
1057        .await?;
1058    let builds = res
1059        .get_deploy_app
1060        .and_then(|x| x.deployments)
1061        .context("no data returned")?
1062        .edges
1063        .into_iter()
1064        .flatten()
1065        .filter_map(|x| x.node)
1066        .collect();
1067
1068    Ok(builds)
1069}
1070
1071/// Get an app deployment by ID.
1072pub async fn app_deployment(
1073    client: &WasmerClient,
1074    id: String,
1075) -> Result<types::AutobuildRepository, anyhow::Error> {
1076    let node = get_node(client, id.clone())
1077        .await?
1078        .with_context(|| format!("app deployment with id '{id}' not found"))?;
1079    match node {
1080        types::Node::AutobuildRepository(x) => Ok(*x),
1081        _ => anyhow::bail!("invalid node type returned"),
1082    }
1083}
1084
1085/// Load all versions of an app.
1086///
1087/// Will paginate through all versions and return them in a single list.
1088pub async fn all_app_versions(
1089    client: &WasmerClient,
1090    owner: String,
1091    name: String,
1092) -> Result<Vec<DeployAppVersion>, anyhow::Error> {
1093    let mut vars = GetDeployAppVersionsVars {
1094        owner,
1095        name,
1096        offset: None,
1097        before: None,
1098        after: None,
1099        first: Some(10),
1100        last: None,
1101        sort_by: None,
1102    };
1103
1104    let mut all_versions = Vec::<DeployAppVersion>::new();
1105
1106    loop {
1107        let page = get_deploy_app_versions(client, vars.clone()).await?;
1108        if page.edges.is_empty() {
1109            break;
1110        }
1111
1112        for edge in page.edges {
1113            let edge = match edge {
1114                Some(edge) => edge,
1115                None => continue,
1116            };
1117            let version = match edge.node {
1118                Some(item) => item,
1119                None => continue,
1120            };
1121
1122            // Sanity check to avoid duplication.
1123            if all_versions.iter().any(|v| v.id == version.id) == false {
1124                all_versions.push(version);
1125            }
1126
1127            // Update pagination.
1128            vars.after = Some(edge.cursor);
1129        }
1130    }
1131
1132    Ok(all_versions)
1133}
1134
1135/// Retrieve versions for an app.
1136pub async fn get_deploy_app_versions_by_id(
1137    client: &WasmerClient,
1138    vars: types::GetDeployAppVersionsByIdVars,
1139) -> Result<DeployAppVersionConnection, anyhow::Error> {
1140    let res = client
1141        .run_graphql_strict(types::GetDeployAppVersionsById::build(vars))
1142        .await?;
1143    let versions = res
1144        .node
1145        .context("app not found")?
1146        .into_app()
1147        .context("invalid node type returned")?
1148        .versions;
1149    Ok(versions)
1150}
1151
1152/// Load all versions of an app id.
1153///
1154/// Will paginate through all versions and return them in a single list.
1155pub async fn all_app_versions_by_id(
1156    client: &WasmerClient,
1157    app_id: impl Into<String>,
1158) -> Result<Vec<DeployAppVersion>, anyhow::Error> {
1159    let mut vars = types::GetDeployAppVersionsByIdVars {
1160        id: cynic::Id::new(app_id),
1161        offset: None,
1162        before: None,
1163        after: None,
1164        first: Some(10),
1165        last: None,
1166        sort_by: None,
1167    };
1168
1169    let mut all_versions = Vec::<DeployAppVersion>::new();
1170
1171    loop {
1172        let page = get_deploy_app_versions_by_id(client, vars.clone()).await?;
1173        if page.edges.is_empty() {
1174            break;
1175        }
1176
1177        for edge in page.edges {
1178            let edge = match edge {
1179                Some(edge) => edge,
1180                None => continue,
1181            };
1182            let version = match edge.node {
1183                Some(item) => item,
1184                None => continue,
1185            };
1186
1187            // Sanity check to avoid duplication.
1188            if all_versions.iter().any(|v| v.id == version.id) == false {
1189                all_versions.push(version);
1190            }
1191
1192            // Update pagination.
1193            vars.after = Some(edge.cursor);
1194        }
1195    }
1196
1197    Ok(all_versions)
1198}
1199
1200/// Activate a particular version of an app.
1201pub async fn app_version_activate(
1202    client: &WasmerClient,
1203    version: String,
1204) -> Result<DeployApp, anyhow::Error> {
1205    let res = client
1206        .run_graphql_strict(types::MarkAppVersionAsActive::build(
1207            types::MarkAppVersionAsActiveVars {
1208                input: types::MarkAppVersionAsActiveInput {
1209                    app_version: version.into(),
1210                },
1211            },
1212        ))
1213        .await?;
1214    res.mark_app_version_as_active
1215        .context("app not found")
1216        .map(|x| x.app)
1217}
1218
1219/// Retrieve a node based on its global id.
1220pub async fn get_node(
1221    client: &WasmerClient,
1222    id: String,
1223) -> Result<Option<types::Node>, anyhow::Error> {
1224    client
1225        .run_graphql(types::GetNode::build(types::GetNodeVars { id: id.into() }))
1226        .await
1227        .map(|x| x.node)
1228}
1229
1230/// Retrieve an app by its global id.
1231pub async fn get_app_by_id(
1232    client: &WasmerClient,
1233    app_id: String,
1234) -> Result<DeployApp, anyhow::Error> {
1235    get_app_by_id_opt(client, app_id)
1236        .await?
1237        .context("app not found")
1238}
1239
1240/// Retrieve an app by its global id.
1241pub async fn get_app_by_id_opt(
1242    client: &WasmerClient,
1243    app_id: String,
1244) -> Result<Option<DeployApp>, anyhow::Error> {
1245    let app_opt = client
1246        .run_graphql(types::GetDeployAppById::build(
1247            types::GetDeployAppByIdVars {
1248                app_id: app_id.into(),
1249            },
1250        ))
1251        .await?
1252        .app;
1253
1254    if let Some(app) = app_opt {
1255        let app = app.into_deploy_app().context("app conversion failed")?;
1256        Ok(Some(app))
1257    } else {
1258        Ok(None)
1259    }
1260}
1261
1262/// Retrieve an app together with a specific version.
1263pub async fn get_app_with_version_by_id(
1264    client: &WasmerClient,
1265    app_id: String,
1266    version_id: String,
1267) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
1268    let res = client
1269        .run_graphql(types::GetDeployAppAndVersionById::build(
1270            types::GetDeployAppAndVersionByIdVars {
1271                app_id: app_id.into(),
1272                version_id: version_id.into(),
1273            },
1274        ))
1275        .await?;
1276
1277    let app = res
1278        .app
1279        .context("app not found")?
1280        .into_deploy_app()
1281        .context("app conversion failed")?;
1282    let version = res
1283        .version
1284        .context("version not found")?
1285        .into_deploy_app_version()
1286        .context("version conversion failed")?;
1287
1288    Ok((app, version))
1289}
1290
1291/// Retrieve an app version by its global id.
1292pub async fn get_app_version_by_id(
1293    client: &WasmerClient,
1294    version_id: String,
1295) -> Result<DeployAppVersion, anyhow::Error> {
1296    client
1297        .run_graphql(types::GetDeployAppVersionById::build(
1298            types::GetDeployAppVersionByIdVars {
1299                version_id: version_id.into(),
1300            },
1301        ))
1302        .await?
1303        .version
1304        .context("app not found")?
1305        .into_deploy_app_version()
1306        .context("app version conversion failed")
1307}
1308
1309pub async fn get_app_version_by_id_with_app(
1310    client: &WasmerClient,
1311    version_id: String,
1312) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
1313    let version = client
1314        .run_graphql(types::GetDeployAppVersionById::build(
1315            types::GetDeployAppVersionByIdVars {
1316                version_id: version_id.into(),
1317            },
1318        ))
1319        .await?
1320        .version
1321        .context("app not found")?
1322        .into_deploy_app_version()
1323        .context("app version conversion failed")?;
1324
1325    let app_id = version
1326        .app
1327        .as_ref()
1328        .context("could not load app for version")?
1329        .id
1330        .clone();
1331
1332    let app = get_app_by_id(client, app_id.into_inner()).await?;
1333
1334    Ok((app, version))
1335}
1336
1337/// List all apps that are accessible by the current user.
1338///
1339/// NOTE: this will only include the first pages and does not provide pagination.
1340pub async fn user_apps(
1341    client: &WasmerClient,
1342    sort: types::DeployAppsSortBy,
1343) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
1344    futures::stream::try_unfold(None, move |cursor| async move {
1345        let user = client
1346            .run_graphql(types::GetCurrentUserWithApps::build(
1347                GetCurrentUserWithAppsVars {
1348                    after: cursor,
1349                    sort: Some(sort),
1350                },
1351            ))
1352            .await?
1353            .viewer
1354            .context("not logged in")?;
1355
1356        let apps: Vec<_> = user
1357            .apps
1358            .edges
1359            .into_iter()
1360            .flatten()
1361            .filter_map(|x| x.node)
1362            .collect();
1363
1364        let cursor = user.apps.page_info.end_cursor;
1365
1366        if apps.is_empty() {
1367            Ok(None)
1368        } else {
1369            Ok(Some((apps, cursor)))
1370        }
1371    })
1372}
1373
1374/// List all apps that are accessible by the current user.
1375pub async fn user_accessible_apps(
1376    client: &WasmerClient,
1377    sort: types::DeployAppsSortBy,
1378) -> Result<
1379    impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_,
1380    anyhow::Error,
1381> {
1382    let user_apps = user_apps(client, sort).await;
1383
1384    // Get all aps in user-accessible namespaces.
1385    let namespace_res = client
1386        .run_graphql(types::GetCurrentUserWithNamespaces::build(
1387            types::GetCurrentUserWithNamespacesVars {
1388                namespace_role: None,
1389            },
1390        ))
1391        .await?;
1392    let active_user = namespace_res.viewer.context("not logged in")?;
1393    let namespace_names = active_user
1394        .namespaces
1395        .edges
1396        .iter()
1397        .filter_map(|edge| edge.as_ref())
1398        .filter_map(|edge| edge.node.as_ref())
1399        .map(|node| node.name.clone())
1400        .collect::<Vec<_>>();
1401
1402    let mut ns_apps = vec![];
1403    for ns in namespace_names {
1404        let apps = namespace_apps(client, ns, sort).await;
1405        ns_apps.push(apps);
1406    }
1407
1408    Ok((user_apps, ns_apps.merge()).merge())
1409}
1410
1411/// Get apps for a specific namespace.
1412///
1413/// NOTE: only retrieves the first page and does not do pagination.
1414pub async fn namespace_apps(
1415    client: &WasmerClient,
1416    namespace: String,
1417    sort: types::DeployAppsSortBy,
1418) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
1419    let namespace = namespace.clone();
1420
1421    futures::stream::try_unfold((None, namespace), move |(cursor, namespace)| async move {
1422        let res = client
1423            .run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
1424                name: namespace.to_string(),
1425                after: cursor,
1426                sort: Some(sort),
1427            }))
1428            .await?;
1429
1430        let ns = res
1431            .get_namespace
1432            .with_context(|| format!("failed to get namespace '{namespace}'"))?;
1433
1434        let apps: Vec<_> = ns
1435            .apps
1436            .edges
1437            .into_iter()
1438            .flatten()
1439            .filter_map(|x| x.node)
1440            .collect();
1441
1442        let cursor = ns.apps.page_info.end_cursor;
1443
1444        if apps.is_empty() {
1445            Ok(None)
1446        } else {
1447            Ok(Some((apps, (cursor, namespace))))
1448        }
1449    })
1450}
1451
1452/// Publish a new app (version).
1453pub async fn publish_deploy_app(
1454    client: &WasmerClient,
1455    vars: PublishDeployAppVars,
1456) -> Result<DeployAppVersion, anyhow::Error> {
1457    let res = client
1458        .run_graphql_raw(types::PublishDeployApp::build(vars))
1459        .await?;
1460
1461    if let Some(app) = res
1462        .data
1463        .and_then(|d| d.publish_deploy_app)
1464        .map(|d| d.deploy_app_version)
1465    {
1466        Ok(app)
1467    } else {
1468        Err(GraphQLApiFailure::from_errors(
1469            "could not publish app",
1470            res.errors,
1471        ))
1472    }
1473}
1474
1475/// Delete an app.
1476pub async fn delete_app(client: &WasmerClient, app_id: String) -> Result<(), anyhow::Error> {
1477    let res = client
1478        .run_graphql_strict(types::DeleteApp::build(types::DeleteAppVars {
1479            app_id: app_id.into(),
1480        }))
1481        .await?
1482        .delete_app
1483        .context("API did not return data for the delete_app mutation")?;
1484
1485    if !res.success {
1486        bail!("App deletion failed for an unknown reason");
1487    }
1488
1489    Ok(())
1490}
1491
1492/// Get all namespaces accessible by the current user.
1493pub async fn user_namespaces(
1494    client: &WasmerClient,
1495) -> Result<Vec<types::Namespace>, anyhow::Error> {
1496    let user = client
1497        .run_graphql(types::GetCurrentUserWithNamespaces::build(
1498            types::GetCurrentUserWithNamespacesVars {
1499                namespace_role: None,
1500            },
1501        ))
1502        .await?
1503        .viewer
1504        .context("not logged in")?;
1505
1506    let ns = user
1507        .namespaces
1508        .edges
1509        .into_iter()
1510        .flatten()
1511        // .filter_map(|x| x)
1512        .filter_map(|x| x.node)
1513        .collect();
1514
1515    Ok(ns)
1516}
1517
1518/// Retrieve a namespace by its name.
1519pub async fn get_namespace(
1520    client: &WasmerClient,
1521    name: String,
1522) -> Result<Option<types::Namespace>, anyhow::Error> {
1523    client
1524        .run_graphql(types::GetNamespace::build(types::GetNamespaceVars { name }))
1525        .await
1526        .map(|x| x.get_namespace)
1527}
1528
1529/// Create a new namespace.
1530pub async fn create_namespace(
1531    client: &WasmerClient,
1532    vars: CreateNamespaceVars,
1533) -> Result<types::Namespace, anyhow::Error> {
1534    client
1535        .run_graphql(types::CreateNamespace::build(vars))
1536        .await?
1537        .create_namespace
1538        .map(|x| x.namespace)
1539        .context("no namespace returned")
1540}
1541
1542/// Retrieve a package by its name.
1543pub async fn get_package(
1544    client: &WasmerClient,
1545    name: String,
1546) -> Result<Option<types::Package>, anyhow::Error> {
1547    client
1548        .run_graphql_strict(types::GetPackage::build(types::GetPackageVars { name }))
1549        .await
1550        .map(|x| x.get_package)
1551}
1552
1553/// Retrieve a package version by its name.
1554pub async fn get_package_version(
1555    client: &WasmerClient,
1556    name: String,
1557    version: String,
1558) -> Result<Option<types::PackageVersionWithPackage>, anyhow::Error> {
1559    client
1560        .run_graphql_strict(types::GetPackageVersion::build(
1561            types::GetPackageVersionVars { name, version },
1562        ))
1563        .await
1564        .map(|x| x.get_package_version)
1565}
1566
1567/// Retrieve package versions for an app.
1568pub async fn get_package_versions(
1569    client: &WasmerClient,
1570    vars: types::AllPackageVersionsVars,
1571) -> Result<PackageVersionConnection, anyhow::Error> {
1572    let res = client
1573        .run_graphql(types::GetAllPackageVersions::build(vars))
1574        .await?;
1575    Ok(res.all_package_versions)
1576}
1577
1578/// Retrieve a package release by hash.
1579pub async fn get_package_release(
1580    client: &WasmerClient,
1581    hash: &str,
1582) -> Result<Option<types::PackageWebc>, anyhow::Error> {
1583    let hash = hash.trim_start_matches("sha256:");
1584    client
1585        .run_graphql_strict(types::GetPackageRelease::build(
1586            types::GetPackageReleaseVars {
1587                hash: hash.to_string(),
1588            },
1589        ))
1590        .await
1591        .map(|x| x.get_package_release)
1592}
1593
1594pub async fn get_package_releases(
1595    client: &WasmerClient,
1596    vars: types::AllPackageReleasesVars,
1597) -> Result<types::PackageWebcConnection, anyhow::Error> {
1598    let res = client
1599        .run_graphql(types::GetAllPackageReleases::build(vars))
1600        .await?;
1601    Ok(res.all_package_releases)
1602}
1603
1604/// Retrieve all versions of a package as a stream that auto-paginates.
1605pub fn get_package_versions_stream(
1606    client: &WasmerClient,
1607    vars: types::AllPackageVersionsVars,
1608) -> impl futures::Stream<Item = Result<Vec<types::PackageVersionWithPackage>, anyhow::Error>> + '_
1609{
1610    futures::stream::try_unfold(
1611        Some(vars),
1612        move |vars: Option<types::AllPackageVersionsVars>| async move {
1613            let vars = match vars {
1614                Some(vars) => vars,
1615                None => return Ok(None),
1616            };
1617
1618            let page = get_package_versions(client, vars.clone()).await?;
1619
1620            let end_cursor = page.page_info.end_cursor;
1621
1622            let items = page
1623                .edges
1624                .into_iter()
1625                .filter_map(|x| x.and_then(|x| x.node))
1626                .collect::<Vec<_>>();
1627
1628            let new_vars = end_cursor.map(|cursor| types::AllPackageVersionsVars {
1629                after: Some(cursor),
1630                ..vars
1631            });
1632
1633            Ok(Some((items, new_vars)))
1634        },
1635    )
1636}
1637
1638/// Retrieve all package releases as a stream.
1639pub fn get_package_releases_stream(
1640    client: &WasmerClient,
1641    vars: types::AllPackageReleasesVars,
1642) -> impl futures::Stream<Item = Result<Vec<types::PackageWebc>, anyhow::Error>> + '_ {
1643    futures::stream::try_unfold(
1644        Some(vars),
1645        move |vars: Option<types::AllPackageReleasesVars>| async move {
1646            let vars = match vars {
1647                Some(vars) => vars,
1648                None => return Ok(None),
1649            };
1650
1651            let page = get_package_releases(client, vars.clone()).await?;
1652
1653            let end_cursor = page.page_info.end_cursor;
1654
1655            let items = page
1656                .edges
1657                .into_iter()
1658                .filter_map(|x| x.and_then(|x| x.node))
1659                .collect::<Vec<_>>();
1660
1661            let new_vars = end_cursor.map(|cursor| types::AllPackageReleasesVars {
1662                after: Some(cursor),
1663                ..vars
1664            });
1665
1666            Ok(Some((items, new_vars)))
1667        },
1668    )
1669}
1670
1671#[derive(Debug, PartialEq)]
1672pub enum TokenKind {
1673    SSH,
1674}
1675
1676pub async fn generate_deploy_config_token_raw(
1677    client: &WasmerClient,
1678    token_kind: TokenKind,
1679) -> Result<String, anyhow::Error> {
1680    let res = client
1681        .run_graphql(types::GenerateDeployConfigToken::build(
1682            types::GenerateDeployConfigTokenVars {
1683                input: match token_kind {
1684                    TokenKind::SSH => "{}".to_string(),
1685                },
1686            },
1687        ))
1688        .await?;
1689
1690    res.generate_deploy_config_token
1691        .map(|x| x.token)
1692        .context("no token returned")
1693}
1694
1695/// Get pages of logs associated with an application that lie within the
1696/// specified date range.
1697// NOTE: this is not public due to severe usability issues.
1698// The stream can loop forever due to re-fetching the same logs over and over.
1699#[tracing::instrument(skip_all, level = "debug")]
1700#[allow(clippy::let_with_type_underscore)]
1701#[allow(clippy::too_many_arguments)]
1702fn get_app_logs(
1703    client: &WasmerClient,
1704    name: String,
1705    owner: String,
1706    tag: Option<String>,
1707    start: OffsetDateTime,
1708    end: Option<OffsetDateTime>,
1709    watch: bool,
1710    streams: Option<Vec<LogStream>>,
1711    request_id: Option<String>,
1712    instance_ids: Option<Vec<String>>,
1713) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
1714    // Note: the backend will limit responses to a certain number of log
1715    // messages, so we use try_unfold() to keep calling it until we stop getting
1716    // new log messages.
1717    let span = tracing::Span::current();
1718
1719    futures::stream::try_unfold(start, move |start| {
1720        let variables = types::GetDeployAppLogsVars {
1721            name: name.clone(),
1722            owner: owner.clone(),
1723            version: tag.clone(),
1724            first: Some(100),
1725            starting_from: unix_timestamp(start),
1726            until: end.map(unix_timestamp),
1727            streams: streams.clone(),
1728            request_id: request_id.clone(),
1729            instance_ids: instance_ids.clone(),
1730        };
1731
1732        let fut = async move {
1733            loop {
1734                let deploy_app_version = client
1735                    .run_graphql(types::GetDeployAppLogs::build(variables.clone()))
1736                    .await?
1737                    .get_deploy_app_version
1738                    .context("app version not found")?;
1739
1740                let page: Vec<_> = deploy_app_version
1741                    .logs
1742                    .edges
1743                    .into_iter()
1744                    .flatten()
1745                    .filter_map(|edge| edge.node)
1746                    .collect();
1747
1748                if page.is_empty() {
1749                    if watch {
1750                        /*
1751                         * [TODO]: The resolution here should be configurable.
1752                         */
1753
1754                        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1755                        std::thread::sleep(Duration::from_secs(1));
1756
1757                        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1758                        tokio::time::sleep(Duration::from_secs(1)).await;
1759
1760                        continue;
1761                    }
1762
1763                    break Ok(None);
1764                } else {
1765                    let last_message = page.last().expect("The page is non-empty");
1766                    let timestamp = last_message.timestamp;
1767                    // NOTE: adding 1 microsecond to the timestamp to avoid fetching
1768                    // the last message again.
1769                    let timestamp = OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)
1770                        .with_context(|| {
1771                            format!("Unable to interpret {timestamp} as a unix timestamp")
1772                        })?;
1773
1774                    // FIXME: We need a better way to tell the backend "give me the
1775                    // next set of logs". Adding 1 nanosecond could theoretically
1776                    // mean we miss messages if multiple log messages arrived at
1777                    // the same nanosecond and the page ended midway.
1778
1779                    let next_timestamp = timestamp + Duration::from_nanos(1_000);
1780
1781                    break Ok(Some((page, next_timestamp)));
1782                }
1783            }
1784        };
1785
1786        fut.instrument(span.clone())
1787    })
1788}
1789
1790/// Get pages of logs associated with an application that lie within the
1791/// specified date range.
1792///
1793/// In contrast to [`get_app_logs`], this function collects the stream into a
1794/// final vector.
1795#[tracing::instrument(skip_all, level = "debug")]
1796#[allow(clippy::let_with_type_underscore)]
1797#[allow(clippy::too_many_arguments)]
1798pub async fn get_app_logs_paginated(
1799    client: &WasmerClient,
1800    name: String,
1801    owner: String,
1802    tag: Option<String>,
1803    start: OffsetDateTime,
1804    end: Option<OffsetDateTime>,
1805    watch: bool,
1806    streams: Option<Vec<LogStream>>,
1807) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
1808    let stream = get_app_logs(
1809        client, name, owner, tag, start, end, watch, streams, None, None,
1810    );
1811
1812    stream.map(|res| {
1813        let mut logs = Vec::new();
1814        let mut hasher = HashSet::new();
1815        let mut page = res?;
1816
1817        // Prevent duplicates.
1818        // TODO: don't clone the message, just hash it.
1819        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
1820
1821        logs.extend(page);
1822
1823        Ok(logs)
1824    })
1825}
1826
1827/// Get pages of logs associated with an application that lie within the
1828/// specified date range with a specific instance identifier.
1829///
1830/// In contrast to [`get_app_logs`], this function collects the stream into a
1831/// final vector.
1832#[tracing::instrument(skip_all, level = "debug")]
1833#[allow(clippy::let_with_type_underscore)]
1834#[allow(clippy::too_many_arguments)]
1835pub async fn get_app_logs_paginated_filter_instance(
1836    client: &WasmerClient,
1837    name: String,
1838    owner: String,
1839    tag: Option<String>,
1840    start: OffsetDateTime,
1841    end: Option<OffsetDateTime>,
1842    watch: bool,
1843    streams: Option<Vec<LogStream>>,
1844    instance_ids: Vec<String>,
1845) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
1846    let stream = get_app_logs(
1847        client,
1848        name,
1849        owner,
1850        tag,
1851        start,
1852        end,
1853        watch,
1854        streams,
1855        None,
1856        Some(instance_ids),
1857    );
1858
1859    stream.map(|res| {
1860        let mut logs = Vec::new();
1861        let mut hasher = HashSet::new();
1862        let mut page = res?;
1863
1864        // Prevent duplicates.
1865        // TODO: don't clone the message, just hash it.
1866        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
1867
1868        logs.extend(page);
1869
1870        Ok(logs)
1871    })
1872}
1873
1874/// Get pages of logs associated with an specific request for application that lie within the
1875/// specified date range.
1876///
1877/// In contrast to [`get_app_logs`], this function collects the stream into a
1878/// final vector.
1879#[tracing::instrument(skip_all, level = "debug")]
1880#[allow(clippy::let_with_type_underscore)]
1881#[allow(clippy::too_many_arguments)]
1882pub async fn get_app_logs_paginated_filter_request(
1883    client: &WasmerClient,
1884    name: String,
1885    owner: String,
1886    tag: Option<String>,
1887    start: OffsetDateTime,
1888    end: Option<OffsetDateTime>,
1889    watch: bool,
1890    streams: Option<Vec<LogStream>>,
1891    request_id: String,
1892) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
1893    let stream = get_app_logs(
1894        client,
1895        name,
1896        owner,
1897        tag,
1898        start,
1899        end,
1900        watch,
1901        streams,
1902        Some(request_id),
1903        None,
1904    );
1905
1906    stream.map(|res| {
1907        let mut logs = Vec::new();
1908        let mut hasher = HashSet::new();
1909        let mut page = res?;
1910
1911        // Prevent duplicates.
1912        // TODO: don't clone the message, just hash it.
1913        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
1914
1915        logs.extend(page);
1916
1917        Ok(logs)
1918    })
1919}
1920
1921/// Retrieve a domain by its name.
1922///
1923/// Specify with_records to also retrieve all records for the domain.
1924pub async fn get_domain(
1925    client: &WasmerClient,
1926    domain: String,
1927) -> Result<Option<types::DnsDomain>, anyhow::Error> {
1928    let vars = types::GetDomainVars { domain };
1929
1930    let opt = client
1931        .run_graphql(types::GetDomain::build(vars))
1932        .await
1933        .map_err(anyhow::Error::from)?
1934        .get_domain;
1935    Ok(opt)
1936}
1937
1938/// Retrieve a domain by its name.
1939///
1940/// Specify with_records to also retrieve all records for the domain.
1941pub async fn get_domain_zone_file(
1942    client: &WasmerClient,
1943    domain: String,
1944) -> Result<Option<types::DnsDomainWithZoneFile>, anyhow::Error> {
1945    let vars = types::GetDomainVars { domain };
1946
1947    let opt = client
1948        .run_graphql(types::GetDomainWithZoneFile::build(vars))
1949        .await
1950        .map_err(anyhow::Error::from)?
1951        .get_domain;
1952    Ok(opt)
1953}
1954
1955/// Retrieve a domain by its name, along with all it's records.
1956pub async fn get_domain_with_records(
1957    client: &WasmerClient,
1958    domain: String,
1959) -> Result<Option<types::DnsDomainWithRecords>, anyhow::Error> {
1960    let vars = types::GetDomainVars { domain };
1961
1962    let opt = client
1963        .run_graphql(types::GetDomainWithRecords::build(vars))
1964        .await
1965        .map_err(anyhow::Error::from)?
1966        .get_domain;
1967    Ok(opt)
1968}
1969
1970/// Register a new domain
1971pub async fn register_domain(
1972    client: &WasmerClient,
1973    name: String,
1974    namespace: Option<String>,
1975    import_records: Option<bool>,
1976) -> Result<types::DnsDomain, anyhow::Error> {
1977    let vars = types::RegisterDomainVars {
1978        name,
1979        namespace,
1980        import_records,
1981    };
1982    let opt = client
1983        .run_graphql_strict(types::RegisterDomain::build(vars))
1984        .await
1985        .map_err(anyhow::Error::from)?
1986        .register_domain
1987        .context("Domain registration failed")?
1988        .domain
1989        .context("Domain registration failed, no associatede domain found.")?;
1990    Ok(opt)
1991}
1992
1993/// Retrieve all DNS records.
1994///
1995/// NOTE: this is a privileged operation that requires extra permissions.
1996pub async fn get_all_dns_records(
1997    client: &WasmerClient,
1998    vars: types::GetAllDnsRecordsVariables,
1999) -> Result<types::DnsRecordConnection, anyhow::Error> {
2000    client
2001        .run_graphql_strict(types::GetAllDnsRecords::build(vars))
2002        .await
2003        .map_err(anyhow::Error::from)
2004        .map(|x| x.get_all_dnsrecords)
2005}
2006
2007/// Retrieve all DNS domains.
2008pub async fn get_all_domains(
2009    client: &WasmerClient,
2010    vars: types::GetAllDomainsVariables,
2011) -> Result<Vec<DnsDomain>, anyhow::Error> {
2012    let connection = client
2013        .run_graphql_strict(types::GetAllDomains::build(vars))
2014        .await
2015        .map_err(anyhow::Error::from)
2016        .map(|x| x.get_all_domains)
2017        .context("no domains returned")?;
2018    Ok(connection
2019        .edges
2020        .into_iter()
2021        .flatten()
2022        .filter_map(|x| x.node)
2023        .collect())
2024}
2025
2026/// Retrieve a domain by its name.
2027///
2028/// Specify with_records to also retrieve all records for the domain.
2029pub fn get_all_dns_records_stream(
2030    client: &WasmerClient,
2031    vars: types::GetAllDnsRecordsVariables,
2032) -> impl futures::Stream<Item = Result<Vec<types::DnsRecord>, anyhow::Error>> + '_ {
2033    futures::stream::try_unfold(
2034        Some(vars),
2035        move |vars: Option<types::GetAllDnsRecordsVariables>| async move {
2036            let vars = match vars {
2037                Some(vars) => vars,
2038                None => return Ok(None),
2039            };
2040
2041            let page = get_all_dns_records(client, vars.clone()).await?;
2042
2043            let end_cursor = page.page_info.end_cursor;
2044
2045            let items = page
2046                .edges
2047                .into_iter()
2048                .filter_map(|x| x.and_then(|x| x.node))
2049                .collect::<Vec<_>>();
2050
2051            let new_vars = end_cursor.map(|c| types::GetAllDnsRecordsVariables {
2052                after: Some(c),
2053                ..vars
2054            });
2055
2056            Ok(Some((items, new_vars)))
2057        },
2058    )
2059}
2060
2061pub async fn purge_cache_for_app_version(
2062    client: &WasmerClient,
2063    vars: types::PurgeCacheForAppVersionVars,
2064) -> Result<(), anyhow::Error> {
2065    client
2066        .run_graphql_strict(types::PurgeCacheForAppVersion::build(vars))
2067        .await
2068        .map_err(anyhow::Error::from)
2069        .map(|x| x.purge_cache_for_app_version)
2070        .context("backend did not return data")?;
2071
2072    Ok(())
2073}
2074
2075/// Convert a [`OffsetDateTime`] to a unix timestamp that the WAPM backend
2076/// understands.
2077fn unix_timestamp(ts: OffsetDateTime) -> f64 {
2078    let nanos_per_second = 1_000_000_000;
2079    let timestamp = ts.unix_timestamp_nanos();
2080    let nanos = timestamp % nanos_per_second;
2081    let secs = timestamp / nanos_per_second;
2082
2083    (secs as f64) + (nanos as f64 / nanos_per_second as f64)
2084}
2085
2086/// Publish a new app (version).
2087pub async fn upsert_domain_from_zone_file(
2088    client: &WasmerClient,
2089    zone_file_contents: String,
2090    delete_missing_records: bool,
2091) -> Result<DnsDomain, anyhow::Error> {
2092    let vars = UpsertDomainFromZoneFileVars {
2093        zone_file: zone_file_contents,
2094        delete_missing_records: Some(delete_missing_records),
2095    };
2096    let res = client
2097        .run_graphql_strict(types::UpsertDomainFromZoneFile::build(vars))
2098        .await?;
2099
2100    let domain = res
2101        .upsert_domain_from_zone_file
2102        .context("Upserting domain from zonefile failed")?
2103        .domain;
2104
2105    Ok(domain)
2106}