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    // First 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 the published version numbers of a package, if it exists.
1721///
1722/// Returns `None` when the package does not exist in the registry, and an
1723/// (possibly empty) list of version strings otherwise.
1724pub async fn get_package_version_numbers(
1725    client: &WasmerClient,
1726    name: String,
1727) -> Result<Option<Vec<String>>, anyhow::Error> {
1728    let package = client
1729        .run_graphql_strict(types::GetPackageVersionNumbers::build(
1730            types::GetPackageVars { name },
1731        ))
1732        .await?
1733        .get_package;
1734
1735    Ok(package.map(|p| {
1736        p.versions
1737            .unwrap_or_default()
1738            .into_iter()
1739            .flatten()
1740            .map(|v| v.version)
1741            .collect()
1742    }))
1743}
1744
1745/// Retrieve a package version by its name.
1746pub async fn get_package_version(
1747    client: &WasmerClient,
1748    name: String,
1749    version: String,
1750) -> Result<Option<types::PackageVersionWithPackage>, anyhow::Error> {
1751    client
1752        .run_graphql_strict(types::GetPackageVersion::build(
1753            types::GetPackageVersionVars { name, version },
1754        ))
1755        .await
1756        .map(|x| x.get_package_version)
1757}
1758
1759/// Retrieve package versions for an app.
1760pub async fn get_package_versions(
1761    client: &WasmerClient,
1762    vars: types::AllPackageVersionsVars,
1763) -> Result<PackageVersionConnection, anyhow::Error> {
1764    let res = client
1765        .run_graphql(types::GetAllPackageVersions::build(vars))
1766        .await?;
1767    Ok(res.all_package_versions)
1768}
1769
1770/// Search the registry for packages matching `query`, optionally constrained by
1771/// `filter` (e.g. by owner, curated status, downloads).
1772///
1773/// Returns a single page of matching package versions; pass `first`/`after` for
1774/// pagination. Use [`fetch_all_matching_packages`] to stream every page.
1775pub async fn search_packages(
1776    client: &WasmerClient,
1777    query: impl Into<String>,
1778    filter: Option<types::PackagesFilter>,
1779    first: Option<i32>,
1780    after: Option<String>,
1781) -> Result<types::Paginated<types::SearchPackageVersion>, anyhow::Error> {
1782    let con = client
1783        .run_graphql_strict(types::SearchPackages::build(types::SearchPackagesVars {
1784            query: query.into(),
1785            packages: filter,
1786            first,
1787            after,
1788        }))
1789        .await?
1790        .search;
1791
1792    let items = con
1793        .edges
1794        .into_iter()
1795        .flatten()
1796        .filter_map(|edge| edge.node)
1797        .filter_map(types::SearchResult::into_package_version)
1798        .collect();
1799
1800    let next_cursor = con
1801        .page_info
1802        .end_cursor
1803        .filter(|_| con.page_info.has_next_page);
1804
1805    Ok(types::Paginated { items, next_cursor })
1806}
1807
1808/// Stream every package matching `query`/`filter`, fetching `page_size` results
1809/// per request until the registry is exhausted.
1810pub fn fetch_all_matching_packages(
1811    client: &WasmerClient,
1812    query: impl Into<String>,
1813    filter: Option<types::PackagesFilter>,
1814    page_size: i32,
1815) -> impl futures::Stream<Item = Result<Vec<types::SearchPackageVersion>, anyhow::Error>> + '_ {
1816    let query = query.into();
1817    futures::stream::try_unfold(Some(None), move |state| {
1818        let query = query.clone();
1819        let filter = filter.clone();
1820        async move {
1821            let Some(after) = state else {
1822                return Ok(None);
1823            };
1824
1825            let page = search_packages(client, query, filter, Some(page_size), after).await?;
1826            let next_state = page.next_cursor.map(Some);
1827
1828            Ok::<_, anyhow::Error>(Some((page.items, next_state)))
1829        }
1830    })
1831}
1832
1833/// Retrieve a package release by hash.
1834pub async fn get_package_release(
1835    client: &WasmerClient,
1836    hash: &str,
1837) -> Result<Option<types::PackageWebc>, anyhow::Error> {
1838    let hash = hash.trim_start_matches("sha256:");
1839    client
1840        .run_graphql_strict(types::GetPackageRelease::build(
1841            types::GetPackageReleaseVars {
1842                hash: hash.to_string(),
1843            },
1844        ))
1845        .await
1846        .map(|x| x.get_package_release)
1847}
1848
1849pub async fn get_package_releases(
1850    client: &WasmerClient,
1851    vars: types::AllPackageReleasesVars,
1852) -> Result<types::PackageWebcConnection, anyhow::Error> {
1853    let res = client
1854        .run_graphql(types::GetAllPackageReleases::build(vars))
1855        .await?;
1856    Ok(res.all_package_releases)
1857}
1858
1859/// Retrieve all versions of a package as a stream that auto-paginates.
1860pub fn get_package_versions_stream(
1861    client: &WasmerClient,
1862    vars: types::AllPackageVersionsVars,
1863) -> impl futures::Stream<Item = Result<Vec<types::PackageVersionWithPackage>, anyhow::Error>> + '_
1864{
1865    futures::stream::try_unfold(
1866        Some(vars),
1867        move |vars: Option<types::AllPackageVersionsVars>| async move {
1868            let vars = match vars {
1869                Some(vars) => vars,
1870                None => return Ok(None),
1871            };
1872
1873            let page = get_package_versions(client, vars.clone()).await?;
1874
1875            let end_cursor = page.page_info.end_cursor;
1876
1877            let items = page
1878                .edges
1879                .into_iter()
1880                .filter_map(|x| x.and_then(|x| x.node))
1881                .collect::<Vec<_>>();
1882
1883            let new_vars = end_cursor.map(|cursor| types::AllPackageVersionsVars {
1884                after: Some(cursor),
1885                ..vars
1886            });
1887
1888            Ok(Some((items, new_vars)))
1889        },
1890    )
1891}
1892
1893/// Retrieve all package releases as a stream.
1894pub fn get_package_releases_stream(
1895    client: &WasmerClient,
1896    vars: types::AllPackageReleasesVars,
1897) -> impl futures::Stream<Item = Result<Vec<types::PackageWebc>, anyhow::Error>> + '_ {
1898    futures::stream::try_unfold(
1899        Some(vars),
1900        move |vars: Option<types::AllPackageReleasesVars>| async move {
1901            let vars = match vars {
1902                Some(vars) => vars,
1903                None => return Ok(None),
1904            };
1905
1906            let page = get_package_releases(client, vars.clone()).await?;
1907
1908            let end_cursor = page.page_info.end_cursor;
1909
1910            let items = page
1911                .edges
1912                .into_iter()
1913                .filter_map(|x| x.and_then(|x| x.node))
1914                .collect::<Vec<_>>();
1915
1916            let new_vars = end_cursor.map(|cursor| types::AllPackageReleasesVars {
1917                after: Some(cursor),
1918                ..vars
1919            });
1920
1921            Ok(Some((items, new_vars)))
1922        },
1923    )
1924}
1925
1926#[derive(Debug, PartialEq)]
1927pub enum TokenKind {
1928    SSH,
1929}
1930
1931pub async fn generate_deploy_config_token_raw(
1932    client: &WasmerClient,
1933    token_kind: TokenKind,
1934) -> Result<String, anyhow::Error> {
1935    let res = client
1936        .run_graphql(types::GenerateDeployConfigToken::build(
1937            types::GenerateDeployConfigTokenVars {
1938                input: match token_kind {
1939                    TokenKind::SSH => "{}".to_string(),
1940                },
1941            },
1942        ))
1943        .await?;
1944
1945    res.generate_deploy_config_token
1946        .map(|x| x.token)
1947        .context("no token returned")
1948}
1949
1950/// Generate an SSH token for accessing Edge over SSH or SFTP.
1951///
1952/// If an app id is provided, the token will be scoped to that app,
1953/// and using the token will open an ssh context for that app.
1954pub async fn generate_ssh_token(
1955    client: &WasmerClient,
1956    app_id: Option<String>,
1957) -> Result<String, anyhow::Error> {
1958    let res = client
1959        .run_graphql_strict(types::GenerateSshToken::build(
1960            types::GenerateSshTokenVariables {
1961                app_id: app_id.map(cynic::Id::new),
1962            },
1963        ))
1964        .await?;
1965
1966    res.generate_ssh_token
1967        .map(|x| x.token)
1968        .context("no token returned")
1969}
1970
1971/// Get pages of logs associated with an application that lie within the
1972/// specified date range.
1973// NOTE: this is not public due to severe usability issues.
1974// The stream can loop forever due to re-fetching the same logs over and over.
1975#[tracing::instrument(skip_all, level = "debug")]
1976#[allow(clippy::let_with_type_underscore)]
1977#[allow(clippy::too_many_arguments)]
1978fn get_app_logs(
1979    client: &WasmerClient,
1980    name: String,
1981    owner: String,
1982    tag: Option<String>,
1983    start: OffsetDateTime,
1984    end: Option<OffsetDateTime>,
1985    watch: bool,
1986    streams: Option<Vec<LogStream>>,
1987    request_id: Option<String>,
1988    instance_ids: Option<Vec<String>>,
1989) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
1990    // Note: the backend will limit responses to a certain number of log
1991    // messages, so we use try_unfold() to keep calling it until we stop getting
1992    // new log messages.
1993    let span = tracing::Span::current();
1994
1995    futures::stream::try_unfold(start, move |start| {
1996        let variables = types::GetDeployAppLogsVars {
1997            name: name.clone(),
1998            owner: owner.clone(),
1999            version: tag.clone(),
2000            first: Some(100),
2001            starting_from: unix_timestamp(start),
2002            until: end.map(unix_timestamp),
2003            streams: streams.clone(),
2004            request_id: request_id.clone(),
2005            instance_ids: instance_ids.clone(),
2006        };
2007
2008        let fut = async move {
2009            loop {
2010                let deploy_app_version = client
2011                    .run_graphql(types::GetDeployAppLogs::build(variables.clone()))
2012                    .await?
2013                    .get_deploy_app_version
2014                    .context("app version not found")?;
2015
2016                let page: Vec<_> = deploy_app_version
2017                    .logs
2018                    .edges
2019                    .into_iter()
2020                    .flatten()
2021                    .filter_map(|edge| edge.node)
2022                    .collect();
2023
2024                if page.is_empty() {
2025                    if watch {
2026                        /*
2027                         * [TODO]: The resolution here should be configurable.
2028                         */
2029
2030                        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
2031                        std::thread::sleep(Duration::from_secs(1));
2032
2033                        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
2034                        tokio::time::sleep(Duration::from_secs(1)).await;
2035
2036                        continue;
2037                    }
2038
2039                    break Ok(None);
2040                } else {
2041                    let last_message = page.last().expect("The page is non-empty");
2042                    let timestamp = last_message.timestamp;
2043                    // NOTE: adding 1 microsecond to the timestamp to avoid fetching
2044                    // the last message again.
2045                    let timestamp = OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)
2046                        .with_context(|| {
2047                            format!("Unable to interpret {timestamp} as a unix timestamp")
2048                        })?;
2049
2050                    // FIXME: We need a better way to tell the backend "give me the
2051                    // next set of logs". Adding 1 nanosecond could theoretically
2052                    // mean we miss messages if multiple log messages arrived at
2053                    // the same nanosecond and the page ended midway.
2054
2055                    let next_timestamp = timestamp + Duration::from_nanos(1_000);
2056
2057                    break Ok(Some((page, next_timestamp)));
2058                }
2059            }
2060        };
2061
2062        fut.instrument(span.clone())
2063    })
2064}
2065
2066/// Get pages of logs associated with an application that lie within the
2067/// specified date range.
2068///
2069/// In contrast to `get_app_logs`, this function collects the stream into a
2070/// final vector.
2071#[tracing::instrument(skip_all, level = "debug")]
2072#[allow(clippy::let_with_type_underscore)]
2073#[allow(clippy::too_many_arguments)]
2074pub async fn get_app_logs_paginated(
2075    client: &WasmerClient,
2076    name: String,
2077    owner: String,
2078    tag: Option<String>,
2079    start: OffsetDateTime,
2080    end: Option<OffsetDateTime>,
2081    watch: bool,
2082    streams: Option<Vec<LogStream>>,
2083) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2084    let stream = get_app_logs(
2085        client, name, owner, tag, start, end, watch, streams, None, None,
2086    );
2087
2088    stream.map(|res| {
2089        let mut logs = Vec::new();
2090        let mut hasher = HashSet::new();
2091        let mut page = res?;
2092
2093        // Prevent duplicates.
2094        // TODO: don't clone the message, just hash it.
2095        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
2096
2097        logs.extend(page);
2098
2099        Ok(logs)
2100    })
2101}
2102
2103/// Get pages of logs associated with an application that lie within the
2104/// specified date range with a specific instance identifier.
2105///
2106/// In contrast to `get_app_logs`, this function collects the stream into a
2107/// final vector.
2108#[tracing::instrument(skip_all, level = "debug")]
2109#[allow(clippy::let_with_type_underscore)]
2110#[allow(clippy::too_many_arguments)]
2111pub async fn get_app_logs_paginated_filter_instance(
2112    client: &WasmerClient,
2113    name: String,
2114    owner: String,
2115    tag: Option<String>,
2116    start: OffsetDateTime,
2117    end: Option<OffsetDateTime>,
2118    watch: bool,
2119    streams: Option<Vec<LogStream>>,
2120    instance_ids: Vec<String>,
2121) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2122    let stream = get_app_logs(
2123        client,
2124        name,
2125        owner,
2126        tag,
2127        start,
2128        end,
2129        watch,
2130        streams,
2131        None,
2132        Some(instance_ids),
2133    );
2134
2135    stream.map(|res| {
2136        let mut logs = Vec::new();
2137        let mut hasher = HashSet::new();
2138        let mut page = res?;
2139
2140        // Prevent duplicates.
2141        // TODO: don't clone the message, just hash it.
2142        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
2143
2144        logs.extend(page);
2145
2146        Ok(logs)
2147    })
2148}
2149
2150/// Get pages of logs associated with an specific request for application that lie within the
2151/// specified date range.
2152///
2153/// In contrast to `get_app_logs`, this function collects the stream into a
2154/// final vector.
2155#[tracing::instrument(skip_all, level = "debug")]
2156#[allow(clippy::let_with_type_underscore)]
2157#[allow(clippy::too_many_arguments)]
2158pub async fn get_app_logs_paginated_filter_request(
2159    client: &WasmerClient,
2160    name: String,
2161    owner: String,
2162    tag: Option<String>,
2163    start: OffsetDateTime,
2164    end: Option<OffsetDateTime>,
2165    watch: bool,
2166    streams: Option<Vec<LogStream>>,
2167    request_id: String,
2168) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2169    let stream = get_app_logs(
2170        client,
2171        name,
2172        owner,
2173        tag,
2174        start,
2175        end,
2176        watch,
2177        streams,
2178        Some(request_id),
2179        None,
2180    );
2181
2182    stream.map(|res| {
2183        let mut logs = Vec::new();
2184        let mut hasher = HashSet::new();
2185        let mut page = res?;
2186
2187        // Prevent duplicates.
2188        // TODO: don't clone the message, just hash it.
2189        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
2190
2191        logs.extend(page);
2192
2193        Ok(logs)
2194    })
2195}
2196
2197/// Retrieve a domain by its name.
2198///
2199/// Specify with_records to also retrieve all records for the domain.
2200pub async fn get_domain(
2201    client: &WasmerClient,
2202    domain: String,
2203) -> Result<Option<types::DnsDomain>, anyhow::Error> {
2204    let vars = types::GetDomainVars { domain };
2205
2206    let opt = client
2207        .run_graphql(types::GetDomain::build(vars))
2208        .await?
2209        .get_domain;
2210    Ok(opt)
2211}
2212
2213/// Retrieve a domain by its name.
2214///
2215/// Specify with_records to also retrieve all records for the domain.
2216pub async fn get_domain_zone_file(
2217    client: &WasmerClient,
2218    domain: String,
2219) -> Result<Option<types::DnsDomainWithZoneFile>, anyhow::Error> {
2220    let vars = types::GetDomainVars { domain };
2221
2222    let opt = client
2223        .run_graphql(types::GetDomainWithZoneFile::build(vars))
2224        .await?
2225        .get_domain;
2226    Ok(opt)
2227}
2228
2229/// Retrieve a domain by its name, along with all it's records.
2230pub async fn get_domain_with_records(
2231    client: &WasmerClient,
2232    domain: String,
2233) -> Result<Option<types::DnsDomainWithRecords>, anyhow::Error> {
2234    let vars = types::GetDomainVars { domain };
2235
2236    let opt = client
2237        .run_graphql(types::GetDomainWithRecords::build(vars))
2238        .await?
2239        .get_domain;
2240    Ok(opt)
2241}
2242
2243/// Register a new domain
2244pub async fn register_domain(
2245    client: &WasmerClient,
2246    name: String,
2247    namespace: Option<String>,
2248    import_records: Option<bool>,
2249) -> Result<types::DnsDomain, anyhow::Error> {
2250    let vars = types::RegisterDomainVars {
2251        name,
2252        namespace,
2253        import_records,
2254    };
2255    let opt = client
2256        .run_graphql_strict(types::RegisterDomain::build(vars))
2257        .await?
2258        .register_domain
2259        .context("Domain registration failed")?
2260        .domain
2261        .context("Domain registration failed, no associatede domain found.")?;
2262    Ok(opt)
2263}
2264
2265/// Retrieve all DNS records.
2266///
2267/// NOTE: this is a privileged operation that requires extra permissions.
2268pub async fn get_all_dns_records(
2269    client: &WasmerClient,
2270    vars: types::GetAllDnsRecordsVariables,
2271) -> Result<types::DnsRecordConnection, anyhow::Error> {
2272    client
2273        .run_graphql_strict(types::GetAllDnsRecords::build(vars))
2274        .await
2275        .map(|x| x.get_all_dnsrecords)
2276}
2277
2278/// Retrieve all DNS domains.
2279pub async fn get_all_domains(
2280    client: &WasmerClient,
2281    vars: types::GetAllDomainsVariables,
2282) -> Result<Vec<DnsDomain>, anyhow::Error> {
2283    let connection = client
2284        .run_graphql_strict(types::GetAllDomains::build(vars))
2285        .await
2286        .map(|x| x.get_all_domains)
2287        .context("no domains returned")?;
2288    Ok(connection
2289        .edges
2290        .into_iter()
2291        .flatten()
2292        .filter_map(|x| x.node)
2293        .collect())
2294}
2295
2296/// Retrieve a domain by its name.
2297///
2298/// Specify with_records to also retrieve all records for the domain.
2299pub fn get_all_dns_records_stream(
2300    client: &WasmerClient,
2301    vars: types::GetAllDnsRecordsVariables,
2302) -> impl futures::Stream<Item = Result<Vec<types::DnsRecord>, anyhow::Error>> + '_ {
2303    futures::stream::try_unfold(
2304        Some(vars),
2305        move |vars: Option<types::GetAllDnsRecordsVariables>| async move {
2306            let vars = match vars {
2307                Some(vars) => vars,
2308                None => return Ok(None),
2309            };
2310
2311            let page = get_all_dns_records(client, vars.clone()).await?;
2312
2313            let end_cursor = page.page_info.end_cursor;
2314
2315            let items = page
2316                .edges
2317                .into_iter()
2318                .filter_map(|x| x.and_then(|x| x.node))
2319                .collect::<Vec<_>>();
2320
2321            let new_vars = end_cursor.map(|c| types::GetAllDnsRecordsVariables {
2322                after: Some(c),
2323                ..vars
2324            });
2325
2326            Ok(Some((items, new_vars)))
2327        },
2328    )
2329}
2330
2331pub async fn purge_cache_for_app_version(
2332    client: &WasmerClient,
2333    vars: types::PurgeCacheForAppVersionVars,
2334) -> Result<(), anyhow::Error> {
2335    client
2336        .run_graphql_strict(types::PurgeCacheForAppVersion::build(vars))
2337        .await
2338        .map(|x| x.purge_cache_for_app_version)
2339        .context("backend did not return data")?;
2340
2341    Ok(())
2342}
2343
2344/// Convert a [`OffsetDateTime`] to a unix timestamp that the WAPM backend
2345/// understands.
2346fn unix_timestamp(ts: OffsetDateTime) -> f64 {
2347    let nanos_per_second = 1_000_000_000;
2348    let timestamp = ts.unix_timestamp_nanos();
2349    let nanos = timestamp % nanos_per_second;
2350    let secs = timestamp / nanos_per_second;
2351
2352    (secs as f64) + (nanos as f64 / nanos_per_second as f64)
2353}
2354
2355/// Publish a new app (version).
2356pub async fn upsert_domain_from_zone_file(
2357    client: &WasmerClient,
2358    zone_file_contents: String,
2359    delete_missing_records: bool,
2360) -> Result<DnsDomain, anyhow::Error> {
2361    let vars = UpsertDomainFromZoneFileVars {
2362        zone_file: zone_file_contents,
2363        delete_missing_records: Some(delete_missing_records),
2364    };
2365    let res = client
2366        .run_graphql_strict(types::UpsertDomainFromZoneFile::build(vars))
2367        .await?;
2368
2369    let domain = res
2370        .upsert_domain_from_zone_file
2371        .context("Upserting domain from zonefile failed")?
2372        .domain;
2373
2374    Ok(domain)
2375}