wasmer_backend_api/
query.rs

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