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/// Get a signed URL to upload packages.
845pub async fn get_signed_url_for_package_upload(
846    client: &WasmerClient,
847    expires_after_seconds: Option<i32>,
848    filename: Option<&str>,
849    name: Option<&str>,
850    version: Option<&str>,
851) -> Result<Option<SignedUrl>, anyhow::Error> {
852    client
853        .run_graphql_strict(types::GetSignedUrlForPackageUpload::build(
854            GetSignedUrlForPackageUploadVariables {
855                expires_after_seconds,
856                filename,
857                name,
858                version,
859            },
860        ))
861        .await
862        .map(|r| r.get_signed_url_for_package_upload)
863}
864
865/// Request a signed URL for uploading an app archive via the autobuild flow.
866pub async fn generate_upload_url(
867    client: &WasmerClient,
868    filename: &str,
869    name: Option<&str>,
870    version: Option<&str>,
871    expires_after_seconds: Option<i32>,
872) -> Result<SignedUrl, anyhow::Error> {
873    let payload = client
874        .run_graphql_strict(types::GenerateUploadUrl::build(
875            GenerateUploadUrlVariables {
876                expires_after_seconds,
877                filename,
878                name,
879                version,
880            },
881        ))
882        .await
883        .and_then(|res| {
884            res.generate_upload_url
885                .context("generateUploadUrl mutation did not return data")
886        })?;
887
888    Ok(payload.signed_url)
889}
890
891/// Retrieve autobuild metadata derived from a previously uploaded archive.
892pub async fn autobuild_config_for_zip_upload(
893    client: &WasmerClient,
894    upload_url: &str,
895) -> Result<Option<types::AutobuildConfigForZipUploadPayload>, anyhow::Error> {
896    client
897        .run_graphql_strict(types::AutobuildConfigForZipUpload::build(
898            AutobuildConfigForZipUploadVariables { upload_url },
899        ))
900        .await
901        .map(|res| res.autobuild_config_for_zip_upload)
902}
903
904/// Trigger an autobuild deployment for an uploaded archive or repository.
905pub async fn deploy_via_autobuild(
906    client: &WasmerClient,
907    vars: DeployViaAutobuildVars,
908) -> Result<Option<types::DeployViaAutobuildPayload>, anyhow::Error> {
909    client
910        .run_graphql_strict(types::DeployViaAutobuild::build(vars))
911        .await
912        .map(|res| res.deploy_via_autobuild)
913}
914/// Push a package to the registry.
915pub async fn push_package_release(
916    client: &WasmerClient,
917    name: Option<&str>,
918    namespace: &str,
919    signed_url: &str,
920    private: Option<bool>,
921) -> Result<Option<PushPackageReleasePayload>, anyhow::Error> {
922    client
923        .run_graphql_strict(types::PushPackageRelease::build(
924            types::PushPackageReleaseVariables {
925                name,
926                namespace,
927                private,
928                signed_url,
929            },
930        ))
931        .await
932        .map(|r| r.push_package_release)
933}
934
935#[allow(clippy::too_many_arguments)]
936pub async fn tag_package_release(
937    client: &WasmerClient,
938    description: Option<&str>,
939    homepage: Option<&str>,
940    license: Option<&str>,
941    license_file: Option<&str>,
942    manifest: Option<&str>,
943    name: &str,
944    namespace: Option<&str>,
945    package_release_id: &cynic::Id,
946    private: Option<bool>,
947    readme: Option<&str>,
948    repository: Option<&str>,
949    version: &str,
950) -> Result<Option<TagPackageReleasePayload>, anyhow::Error> {
951    client
952        .run_graphql_strict(types::TagPackageRelease::build(
953            types::TagPackageReleaseVariables {
954                description,
955                homepage,
956                license,
957                license_file,
958                manifest,
959                name,
960                namespace,
961                package_release_id,
962                private,
963                readme,
964                repository,
965                version,
966            },
967        ))
968        .await
969        .map(|r| r.tag_package_release)
970}
971
972/// Get the currently logged in user.
973pub async fn current_user(client: &WasmerClient) -> Result<Option<types::User>, anyhow::Error> {
974    client
975        .run_graphql(types::GetCurrentUser::build(()))
976        .await
977        .map(|x| x.viewer)
978}
979
980/// Get the currently logged in user, together with all accessible namespaces.
981///
982/// You can optionally filter the namespaces by the user role.
983pub async fn current_user_with_namespaces(
984    client: &WasmerClient,
985    namespace_role: Option<types::GrapheneRole>,
986) -> Result<types::UserWithNamespaces, anyhow::Error> {
987    client
988        .run_graphql(types::GetCurrentUserWithNamespaces::build(
989            types::GetCurrentUserWithNamespacesVars { namespace_role },
990        ))
991        .await?
992        .viewer
993        .context("not logged in")
994}
995
996/// Retrieve an app.
997pub async fn get_app(
998    client: &WasmerClient,
999    owner: String,
1000    name: String,
1001) -> Result<Option<types::DeployApp>, anyhow::Error> {
1002    client
1003        .run_graphql(types::GetDeployApp::build(types::GetDeployAppVars {
1004            name,
1005            owner,
1006        }))
1007        .await
1008        .map(|x| x.get_deploy_app)
1009}
1010
1011/// Retrieve an app by its global alias.
1012pub async fn get_app_by_alias(
1013    client: &WasmerClient,
1014    alias: String,
1015) -> Result<Option<types::DeployApp>, anyhow::Error> {
1016    client
1017        .run_graphql(types::GetDeployAppByAlias::build(
1018            types::GetDeployAppByAliasVars { alias },
1019        ))
1020        .await
1021        .map(|x| x.get_app_by_global_alias)
1022}
1023
1024/// Retrieve an app version.
1025pub async fn get_app_version(
1026    client: &WasmerClient,
1027    owner: String,
1028    name: String,
1029    version: String,
1030) -> Result<Option<types::DeployAppVersion>, anyhow::Error> {
1031    client
1032        .run_graphql(types::GetDeployAppVersion::build(
1033            types::GetDeployAppVersionVars {
1034                name,
1035                owner,
1036                version,
1037            },
1038        ))
1039        .await
1040        .map(|x| x.get_deploy_app_version)
1041}
1042
1043/// Retrieve an app together with a specific version.
1044pub async fn get_app_with_version(
1045    client: &WasmerClient,
1046    owner: String,
1047    name: String,
1048    version: String,
1049) -> Result<GetDeployAppAndVersion, anyhow::Error> {
1050    client
1051        .run_graphql(types::GetDeployAppAndVersion::build(
1052            types::GetDeployAppAndVersionVars {
1053                name,
1054                owner,
1055                version,
1056            },
1057        ))
1058        .await
1059}
1060
1061/// Retrieve an app together with a specific version.
1062pub async fn get_app_and_package_by_name(
1063    client: &WasmerClient,
1064    vars: types::GetPackageAndAppVars,
1065) -> Result<(Option<types::Package>, Option<types::DeployApp>), anyhow::Error> {
1066    let res = client
1067        .run_graphql(types::GetPackageAndApp::build(vars))
1068        .await?;
1069    Ok((res.get_package, res.get_deploy_app))
1070}
1071
1072/// Retrieve apps.
1073pub async fn get_deploy_apps(
1074    client: &WasmerClient,
1075    vars: types::GetDeployAppsVars,
1076) -> Result<DeployAppConnection, anyhow::Error> {
1077    let res = client
1078        .run_graphql(types::GetDeployApps::build(vars))
1079        .await?;
1080    res.get_deploy_apps.context("no apps returned")
1081}
1082
1083/// Retrieve apps as a stream that will automatically paginate.
1084pub fn get_deploy_apps_stream(
1085    client: &WasmerClient,
1086    vars: types::GetDeployAppsVars,
1087) -> impl futures::Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + '_ {
1088    futures::stream::try_unfold(
1089        Some(vars),
1090        move |vars: Option<types::GetDeployAppsVars>| async move {
1091            let vars = match vars {
1092                Some(vars) => vars,
1093                None => return Ok(None),
1094            };
1095
1096            let page = get_deploy_apps(client, vars.clone()).await?;
1097
1098            let end_cursor = page.page_info.end_cursor;
1099
1100            let items = page
1101                .edges
1102                .into_iter()
1103                .filter_map(|x| x.and_then(|x| x.node))
1104                .collect::<Vec<_>>();
1105
1106            let new_vars = end_cursor.map(|c| types::GetDeployAppsVars {
1107                after: Some(c),
1108                ..vars
1109            });
1110
1111            Ok(Some((items, new_vars)))
1112        },
1113    )
1114}
1115
1116/// Retrieve versions for an app.
1117pub async fn get_deploy_app_versions(
1118    client: &WasmerClient,
1119    vars: GetDeployAppVersionsVars,
1120) -> Result<DeployAppVersionConnection, anyhow::Error> {
1121    let res = client
1122        .run_graphql_strict(types::GetDeployAppVersions::build(vars))
1123        .await?;
1124    let versions = res.get_deploy_app.context("app not found")?.versions;
1125    Ok(versions)
1126}
1127
1128/// Get app deployments for an app.
1129pub async fn app_deployments(
1130    client: &WasmerClient,
1131    vars: types::GetAppDeploymentsVariables,
1132) -> Result<Vec<types::Deployment>, anyhow::Error> {
1133    let res = client
1134        .run_graphql_strict(types::GetAppDeployments::build(vars))
1135        .await?;
1136    let builds = res
1137        .get_deploy_app
1138        .and_then(|x| x.deployments)
1139        .context("no data returned")?
1140        .edges
1141        .into_iter()
1142        .flatten()
1143        .filter_map(|x| x.node)
1144        .collect();
1145
1146    Ok(builds)
1147}
1148
1149/// Get an app deployment by ID.
1150pub async fn app_deployment(
1151    client: &WasmerClient,
1152    id: String,
1153) -> Result<types::AutobuildRepository, anyhow::Error> {
1154    let node = get_node(client, id.clone())
1155        .await?
1156        .with_context(|| format!("app deployment with id '{id}' not found"))?;
1157    match node {
1158        types::Node::AutobuildRepository(x) => Ok(*x),
1159        _ => anyhow::bail!("invalid node type returned"),
1160    }
1161}
1162
1163/// Load all versions of an app.
1164///
1165/// Will paginate through all versions and return them in a single list.
1166pub async fn all_app_versions(
1167    client: &WasmerClient,
1168    owner: String,
1169    name: String,
1170) -> Result<Vec<DeployAppVersion>, anyhow::Error> {
1171    let mut vars = GetDeployAppVersionsVars {
1172        owner,
1173        name,
1174        offset: None,
1175        before: None,
1176        after: None,
1177        first: Some(10),
1178        last: None,
1179        sort_by: None,
1180    };
1181
1182    let mut all_versions = Vec::<DeployAppVersion>::new();
1183
1184    loop {
1185        let page = get_deploy_app_versions(client, vars.clone()).await?;
1186        if page.edges.is_empty() {
1187            break;
1188        }
1189
1190        for edge in page.edges {
1191            let edge = match edge {
1192                Some(edge) => edge,
1193                None => continue,
1194            };
1195            let version = match edge.node {
1196                Some(item) => item,
1197                None => continue,
1198            };
1199
1200            // Sanity check to avoid duplication.
1201            if all_versions.iter().any(|v| v.id == version.id) == false {
1202                all_versions.push(version);
1203            }
1204
1205            // Update pagination.
1206            vars.after = Some(edge.cursor);
1207        }
1208    }
1209
1210    Ok(all_versions)
1211}
1212
1213/// Retrieve versions for an app.
1214pub async fn get_deploy_app_versions_by_id(
1215    client: &WasmerClient,
1216    vars: types::GetDeployAppVersionsByIdVars,
1217) -> Result<DeployAppVersionConnection, anyhow::Error> {
1218    let res = client
1219        .run_graphql_strict(types::GetDeployAppVersionsById::build(vars))
1220        .await?;
1221    let versions = res
1222        .node
1223        .context("app not found")?
1224        .into_app()
1225        .context("invalid node type returned")?
1226        .versions;
1227    Ok(versions)
1228}
1229
1230/// Load all versions of an app id.
1231///
1232/// Will paginate through all versions and return them in a single list.
1233pub async fn all_app_versions_by_id(
1234    client: &WasmerClient,
1235    app_id: impl Into<String>,
1236) -> Result<Vec<DeployAppVersion>, anyhow::Error> {
1237    let mut vars = types::GetDeployAppVersionsByIdVars {
1238        id: cynic::Id::new(app_id),
1239        offset: None,
1240        before: None,
1241        after: None,
1242        first: Some(10),
1243        last: None,
1244        sort_by: None,
1245    };
1246
1247    let mut all_versions = Vec::<DeployAppVersion>::new();
1248
1249    loop {
1250        let page = get_deploy_app_versions_by_id(client, vars.clone()).await?;
1251        if page.edges.is_empty() {
1252            break;
1253        }
1254
1255        for edge in page.edges {
1256            let edge = match edge {
1257                Some(edge) => edge,
1258                None => continue,
1259            };
1260            let version = match edge.node {
1261                Some(item) => item,
1262                None => continue,
1263            };
1264
1265            // Sanity check to avoid duplication.
1266            if all_versions.iter().any(|v| v.id == version.id) == false {
1267                all_versions.push(version);
1268            }
1269
1270            // Update pagination.
1271            vars.after = Some(edge.cursor);
1272        }
1273    }
1274
1275    Ok(all_versions)
1276}
1277
1278/// Activate a particular version of an app.
1279pub async fn app_version_activate(
1280    client: &WasmerClient,
1281    version: String,
1282) -> Result<DeployApp, anyhow::Error> {
1283    let res = client
1284        .run_graphql_strict(types::MarkAppVersionAsActive::build(
1285            types::MarkAppVersionAsActiveVars {
1286                input: types::MarkAppVersionAsActiveInput {
1287                    app_version: version.into(),
1288                },
1289            },
1290        ))
1291        .await?;
1292    res.mark_app_version_as_active
1293        .context("app not found")
1294        .map(|x| x.app)
1295}
1296
1297/// Retrieve a node based on its global id.
1298pub async fn get_node(
1299    client: &WasmerClient,
1300    id: String,
1301) -> Result<Option<types::Node>, anyhow::Error> {
1302    client
1303        .run_graphql(types::GetNode::build(types::GetNodeVars { id: id.into() }))
1304        .await
1305        .map(|x| x.node)
1306}
1307
1308/// Retrieve an app by its global id.
1309pub async fn get_app_by_id(
1310    client: &WasmerClient,
1311    app_id: String,
1312) -> Result<DeployApp, anyhow::Error> {
1313    get_app_by_id_opt(client, app_id)
1314        .await?
1315        .context("app not found")
1316}
1317
1318/// Retrieve an app by its global id.
1319pub async fn get_app_by_id_opt(
1320    client: &WasmerClient,
1321    app_id: String,
1322) -> Result<Option<DeployApp>, anyhow::Error> {
1323    let app_opt = client
1324        .run_graphql(types::GetDeployAppById::build(
1325            types::GetDeployAppByIdVars {
1326                app_id: app_id.into(),
1327            },
1328        ))
1329        .await?
1330        .app;
1331
1332    if let Some(app) = app_opt {
1333        let app = app.into_deploy_app().context("app conversion failed")?;
1334        Ok(Some(app))
1335    } else {
1336        Ok(None)
1337    }
1338}
1339
1340/// Retrieve an app together with a specific version.
1341pub async fn get_app_with_version_by_id(
1342    client: &WasmerClient,
1343    app_id: String,
1344    version_id: String,
1345) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
1346    let res = client
1347        .run_graphql(types::GetDeployAppAndVersionById::build(
1348            types::GetDeployAppAndVersionByIdVars {
1349                app_id: app_id.into(),
1350                version_id: version_id.into(),
1351            },
1352        ))
1353        .await?;
1354
1355    let app = res
1356        .app
1357        .context("app not found")?
1358        .into_deploy_app()
1359        .context("app conversion failed")?;
1360    let version = res
1361        .version
1362        .context("version not found")?
1363        .into_deploy_app_version()
1364        .context("version conversion failed")?;
1365
1366    Ok((app, version))
1367}
1368
1369/// Retrieve an app version by its global id.
1370pub async fn get_app_version_by_id(
1371    client: &WasmerClient,
1372    version_id: String,
1373) -> Result<DeployAppVersion, anyhow::Error> {
1374    client
1375        .run_graphql(types::GetDeployAppVersionById::build(
1376            types::GetDeployAppVersionByIdVars {
1377                version_id: version_id.into(),
1378            },
1379        ))
1380        .await?
1381        .version
1382        .context("app not found")?
1383        .into_deploy_app_version()
1384        .context("app version conversion failed")
1385}
1386
1387pub async fn get_app_version_by_id_with_app(
1388    client: &WasmerClient,
1389    version_id: String,
1390) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
1391    let version = client
1392        .run_graphql(types::GetDeployAppVersionById::build(
1393            types::GetDeployAppVersionByIdVars {
1394                version_id: version_id.into(),
1395            },
1396        ))
1397        .await?
1398        .version
1399        .context("app not found")?
1400        .into_deploy_app_version()
1401        .context("app version conversion failed")?;
1402
1403    let app_id = version
1404        .app
1405        .as_ref()
1406        .context("could not load app for version")?
1407        .id
1408        .clone();
1409
1410    let app = get_app_by_id(client, app_id.into_inner()).await?;
1411
1412    Ok((app, version))
1413}
1414
1415pub async fn user_apps_page(
1416    client: &WasmerClient,
1417    sort: types::DeployAppsSortBy,
1418    cursor: Option<String>,
1419) -> Result<Paginated<types::DeployApp>, anyhow::Error> {
1420    let user = client
1421        .run_graphql(types::GetCurrentUserWithApps::build(
1422            GetCurrentUserWithAppsVars {
1423                after: cursor,
1424                first: Some(10),
1425                sort: Some(sort),
1426            },
1427        ))
1428        .await?
1429        .viewer
1430        .context("not logged in")?;
1431
1432    let apps: Vec<_> = user
1433        .apps
1434        .edges
1435        .into_iter()
1436        .flatten()
1437        .filter_map(|x| x.node)
1438        .collect();
1439
1440    let out = Paginated {
1441        items: apps,
1442        next_cursor: user.apps.page_info.end_cursor,
1443    };
1444
1445    Ok(out)
1446}
1447
1448/// List all apps that are accessible by the current user.
1449///
1450/// NOTE: this will only include the first pages and does not provide pagination.
1451pub async fn user_apps(
1452    client: &WasmerClient,
1453    sort: types::DeployAppsSortBy,
1454) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
1455    futures::stream::try_unfold(None, move |cursor| async move {
1456        let user = client
1457            .run_graphql(types::GetCurrentUserWithApps::build(
1458                GetCurrentUserWithAppsVars {
1459                    first: Some(10),
1460                    after: cursor,
1461                    sort: Some(sort),
1462                },
1463            ))
1464            .await?
1465            .viewer
1466            .context("not logged in")?;
1467
1468        let apps: Vec<_> = user
1469            .apps
1470            .edges
1471            .into_iter()
1472            .flatten()
1473            .filter_map(|x| x.node)
1474            .collect();
1475
1476        let cursor = user.apps.page_info.end_cursor;
1477
1478        if apps.is_empty() {
1479            Ok(None)
1480        } else {
1481            Ok(Some((apps, cursor)))
1482        }
1483    })
1484}
1485
1486/// List all apps that are accessible by the current user.
1487pub async fn user_accessible_apps(
1488    client: &WasmerClient,
1489    sort: types::DeployAppsSortBy,
1490) -> Result<
1491    impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_,
1492    anyhow::Error,
1493> {
1494    let user_apps = user_apps(client, sort).await;
1495
1496    // Get all aps in user-accessible namespaces.
1497    let namespace_res = client
1498        .run_graphql(types::GetCurrentUserWithNamespaces::build(
1499            types::GetCurrentUserWithNamespacesVars {
1500                namespace_role: None,
1501            },
1502        ))
1503        .await?;
1504    let active_user = namespace_res.viewer.context("not logged in")?;
1505    let namespace_names = active_user
1506        .namespaces
1507        .edges
1508        .iter()
1509        .filter_map(|edge| edge.as_ref())
1510        .filter_map(|edge| edge.node.as_ref())
1511        .map(|node| node.name.clone())
1512        .collect::<Vec<_>>();
1513
1514    let mut ns_apps = vec![];
1515    for ns in namespace_names {
1516        let apps = namespace_apps(client, ns, sort).await;
1517        ns_apps.push(apps);
1518    }
1519
1520    Ok((user_apps, ns_apps.merge()).merge())
1521}
1522
1523/// Get apps for a specific namespace.
1524///
1525/// NOTE: only retrieves the first page and does not do pagination.
1526pub async fn namespace_apps_page(
1527    client: &WasmerClient,
1528    namespace: String,
1529    sort: types::DeployAppsSortBy,
1530    cursor: Option<String>,
1531) -> Result<Paginated<types::DeployApp>, anyhow::Error> {
1532    let namespace = namespace.clone();
1533
1534    let res = client
1535        .run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
1536            name: namespace.to_string(),
1537            after: cursor,
1538            sort: Some(sort),
1539        }))
1540        .await?
1541        .get_namespace
1542        .context("namespace not found")?
1543        .apps;
1544
1545    let apps: Vec<_> = res
1546        .edges
1547        .into_iter()
1548        .flatten()
1549        .filter_map(|x| x.node)
1550        .collect();
1551
1552    let out = Paginated {
1553        items: apps,
1554        next_cursor: res.page_info.end_cursor,
1555    };
1556
1557    Ok(out)
1558}
1559
1560/// Get apps for a specific namespace.
1561///
1562/// NOTE: only retrieves the first page and does not do pagination.
1563pub async fn namespace_apps(
1564    client: &WasmerClient,
1565    namespace: String,
1566    sort: types::DeployAppsSortBy,
1567) -> impl futures::Stream<Item = Result<Vec<types::DeployApp>, anyhow::Error>> + '_ {
1568    let namespace = namespace.clone();
1569
1570    futures::stream::try_unfold((None, namespace), move |(cursor, namespace)| async move {
1571        let res = client
1572            .run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
1573                name: namespace.to_string(),
1574                after: cursor,
1575                sort: Some(sort),
1576            }))
1577            .await?;
1578
1579        let ns = res
1580            .get_namespace
1581            .with_context(|| format!("failed to get namespace '{namespace}'"))?;
1582
1583        let apps: Vec<_> = ns
1584            .apps
1585            .edges
1586            .into_iter()
1587            .flatten()
1588            .filter_map(|x| x.node)
1589            .collect();
1590
1591        let cursor = ns.apps.page_info.end_cursor;
1592
1593        if apps.is_empty() {
1594            Ok(None)
1595        } else {
1596            Ok(Some((apps, (cursor, namespace))))
1597        }
1598    })
1599}
1600
1601/// Publish a new app (version).
1602pub async fn publish_deploy_app(
1603    client: &WasmerClient,
1604    vars: PublishDeployAppVars,
1605) -> Result<DeployAppVersion, anyhow::Error> {
1606    let res = client
1607        .run_graphql_raw(types::PublishDeployApp::build(vars))
1608        .await?;
1609
1610    if let Some(app) = res
1611        .data
1612        .and_then(|d| d.publish_deploy_app)
1613        .map(|d| d.deploy_app_version)
1614    {
1615        Ok(app)
1616    } else {
1617        Err(GraphQLApiFailure::from_errors(
1618            "could not publish app",
1619            res.errors,
1620        ))
1621    }
1622}
1623
1624/// Delete an app.
1625pub async fn delete_app(client: &WasmerClient, app_id: String) -> Result<(), anyhow::Error> {
1626    let res = client
1627        .run_graphql_strict(types::DeleteApp::build(types::DeleteAppVars {
1628            app_id: app_id.into(),
1629        }))
1630        .await?
1631        .delete_app
1632        .context("API did not return data for the delete_app mutation")?;
1633
1634    if !res.success {
1635        bail!("App deletion failed for an unknown reason");
1636    }
1637
1638    Ok(())
1639}
1640
1641/// Get all namespaces accessible by the current user.
1642pub async fn user_namespaces(
1643    client: &WasmerClient,
1644) -> Result<Vec<types::Namespace>, anyhow::Error> {
1645    let user = client
1646        .run_graphql(types::GetCurrentUserWithNamespaces::build(
1647            types::GetCurrentUserWithNamespacesVars {
1648                namespace_role: None,
1649            },
1650        ))
1651        .await?
1652        .viewer
1653        .context("not logged in")?;
1654
1655    let ns = user
1656        .namespaces
1657        .edges
1658        .into_iter()
1659        .flatten()
1660        // .filter_map(|x| x)
1661        .filter_map(|x| x.node)
1662        .collect();
1663
1664    Ok(ns)
1665}
1666
1667/// Retrieve a namespace by its name.
1668pub async fn get_namespace(
1669    client: &WasmerClient,
1670    name: String,
1671) -> Result<Option<types::Namespace>, anyhow::Error> {
1672    client
1673        .run_graphql(types::GetNamespace::build(types::GetNamespaceVars { name }))
1674        .await
1675        .map(|x| x.get_namespace)
1676}
1677
1678/// Create a new namespace.
1679pub async fn create_namespace(
1680    client: &WasmerClient,
1681    vars: CreateNamespaceVars,
1682) -> Result<types::Namespace, anyhow::Error> {
1683    client
1684        .run_graphql(types::CreateNamespace::build(vars))
1685        .await?
1686        .create_namespace
1687        .map(|x| x.namespace)
1688        .context("no namespace returned")
1689}
1690
1691/// Retrieve a package by its name.
1692pub async fn get_package(
1693    client: &WasmerClient,
1694    name: String,
1695) -> Result<Option<types::Package>, anyhow::Error> {
1696    client
1697        .run_graphql_strict(types::GetPackage::build(types::GetPackageVars { name }))
1698        .await
1699        .map(|x| x.get_package)
1700}
1701
1702/// Retrieve a package version by its name.
1703pub async fn get_package_version(
1704    client: &WasmerClient,
1705    name: String,
1706    version: String,
1707) -> Result<Option<types::PackageVersionWithPackage>, anyhow::Error> {
1708    client
1709        .run_graphql_strict(types::GetPackageVersion::build(
1710            types::GetPackageVersionVars { name, version },
1711        ))
1712        .await
1713        .map(|x| x.get_package_version)
1714}
1715
1716/// Retrieve package versions for an app.
1717pub async fn get_package_versions(
1718    client: &WasmerClient,
1719    vars: types::AllPackageVersionsVars,
1720) -> Result<PackageVersionConnection, anyhow::Error> {
1721    let res = client
1722        .run_graphql(types::GetAllPackageVersions::build(vars))
1723        .await?;
1724    Ok(res.all_package_versions)
1725}
1726
1727/// Retrieve a package release by hash.
1728pub async fn get_package_release(
1729    client: &WasmerClient,
1730    hash: &str,
1731) -> Result<Option<types::PackageWebc>, anyhow::Error> {
1732    let hash = hash.trim_start_matches("sha256:");
1733    client
1734        .run_graphql_strict(types::GetPackageRelease::build(
1735            types::GetPackageReleaseVars {
1736                hash: hash.to_string(),
1737            },
1738        ))
1739        .await
1740        .map(|x| x.get_package_release)
1741}
1742
1743pub async fn get_package_releases(
1744    client: &WasmerClient,
1745    vars: types::AllPackageReleasesVars,
1746) -> Result<types::PackageWebcConnection, anyhow::Error> {
1747    let res = client
1748        .run_graphql(types::GetAllPackageReleases::build(vars))
1749        .await?;
1750    Ok(res.all_package_releases)
1751}
1752
1753/// Retrieve all versions of a package as a stream that auto-paginates.
1754pub fn get_package_versions_stream(
1755    client: &WasmerClient,
1756    vars: types::AllPackageVersionsVars,
1757) -> impl futures::Stream<Item = Result<Vec<types::PackageVersionWithPackage>, anyhow::Error>> + '_
1758{
1759    futures::stream::try_unfold(
1760        Some(vars),
1761        move |vars: Option<types::AllPackageVersionsVars>| async move {
1762            let vars = match vars {
1763                Some(vars) => vars,
1764                None => return Ok(None),
1765            };
1766
1767            let page = get_package_versions(client, vars.clone()).await?;
1768
1769            let end_cursor = page.page_info.end_cursor;
1770
1771            let items = page
1772                .edges
1773                .into_iter()
1774                .filter_map(|x| x.and_then(|x| x.node))
1775                .collect::<Vec<_>>();
1776
1777            let new_vars = end_cursor.map(|cursor| types::AllPackageVersionsVars {
1778                after: Some(cursor),
1779                ..vars
1780            });
1781
1782            Ok(Some((items, new_vars)))
1783        },
1784    )
1785}
1786
1787/// Retrieve all package releases as a stream.
1788pub fn get_package_releases_stream(
1789    client: &WasmerClient,
1790    vars: types::AllPackageReleasesVars,
1791) -> impl futures::Stream<Item = Result<Vec<types::PackageWebc>, anyhow::Error>> + '_ {
1792    futures::stream::try_unfold(
1793        Some(vars),
1794        move |vars: Option<types::AllPackageReleasesVars>| async move {
1795            let vars = match vars {
1796                Some(vars) => vars,
1797                None => return Ok(None),
1798            };
1799
1800            let page = get_package_releases(client, vars.clone()).await?;
1801
1802            let end_cursor = page.page_info.end_cursor;
1803
1804            let items = page
1805                .edges
1806                .into_iter()
1807                .filter_map(|x| x.and_then(|x| x.node))
1808                .collect::<Vec<_>>();
1809
1810            let new_vars = end_cursor.map(|cursor| types::AllPackageReleasesVars {
1811                after: Some(cursor),
1812                ..vars
1813            });
1814
1815            Ok(Some((items, new_vars)))
1816        },
1817    )
1818}
1819
1820#[derive(Debug, PartialEq)]
1821pub enum TokenKind {
1822    SSH,
1823}
1824
1825pub async fn generate_deploy_config_token_raw(
1826    client: &WasmerClient,
1827    token_kind: TokenKind,
1828) -> Result<String, anyhow::Error> {
1829    let res = client
1830        .run_graphql(types::GenerateDeployConfigToken::build(
1831            types::GenerateDeployConfigTokenVars {
1832                input: match token_kind {
1833                    TokenKind::SSH => "{}".to_string(),
1834                },
1835            },
1836        ))
1837        .await?;
1838
1839    res.generate_deploy_config_token
1840        .map(|x| x.token)
1841        .context("no token returned")
1842}
1843
1844/// Generate an SSH token for accesing Edge over SSH or SFTP.
1845///
1846/// If an app id is provided, the token will be scoped to that app,
1847/// and using the token will open an ssh context for that app.
1848pub async fn generate_ssh_token(
1849    client: &WasmerClient,
1850    app_id: Option<String>,
1851) -> Result<String, anyhow::Error> {
1852    let res = client
1853        .run_graphql_strict(types::GenerateSshToken::build(
1854            types::GenerateSshTokenVariables {
1855                app_id: app_id.map(cynic::Id::new),
1856            },
1857        ))
1858        .await?;
1859
1860    res.generate_ssh_token
1861        .map(|x| x.token)
1862        .context("no token returned")
1863}
1864
1865/// Get pages of logs associated with an application that lie within the
1866/// specified date range.
1867// NOTE: this is not public due to severe usability issues.
1868// The stream can loop forever due to re-fetching the same logs over and over.
1869#[tracing::instrument(skip_all, level = "debug")]
1870#[allow(clippy::let_with_type_underscore)]
1871#[allow(clippy::too_many_arguments)]
1872fn get_app_logs(
1873    client: &WasmerClient,
1874    name: String,
1875    owner: String,
1876    tag: Option<String>,
1877    start: OffsetDateTime,
1878    end: Option<OffsetDateTime>,
1879    watch: bool,
1880    streams: Option<Vec<LogStream>>,
1881    request_id: Option<String>,
1882    instance_ids: Option<Vec<String>>,
1883) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
1884    // Note: the backend will limit responses to a certain number of log
1885    // messages, so we use try_unfold() to keep calling it until we stop getting
1886    // new log messages.
1887    let span = tracing::Span::current();
1888
1889    futures::stream::try_unfold(start, move |start| {
1890        let variables = types::GetDeployAppLogsVars {
1891            name: name.clone(),
1892            owner: owner.clone(),
1893            version: tag.clone(),
1894            first: Some(100),
1895            starting_from: unix_timestamp(start),
1896            until: end.map(unix_timestamp),
1897            streams: streams.clone(),
1898            request_id: request_id.clone(),
1899            instance_ids: instance_ids.clone(),
1900        };
1901
1902        let fut = async move {
1903            loop {
1904                let deploy_app_version = client
1905                    .run_graphql(types::GetDeployAppLogs::build(variables.clone()))
1906                    .await?
1907                    .get_deploy_app_version
1908                    .context("app version not found")?;
1909
1910                let page: Vec<_> = deploy_app_version
1911                    .logs
1912                    .edges
1913                    .into_iter()
1914                    .flatten()
1915                    .filter_map(|edge| edge.node)
1916                    .collect();
1917
1918                if page.is_empty() {
1919                    if watch {
1920                        /*
1921                         * [TODO]: The resolution here should be configurable.
1922                         */
1923
1924                        #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
1925                        std::thread::sleep(Duration::from_secs(1));
1926
1927                        #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
1928                        tokio::time::sleep(Duration::from_secs(1)).await;
1929
1930                        continue;
1931                    }
1932
1933                    break Ok(None);
1934                } else {
1935                    let last_message = page.last().expect("The page is non-empty");
1936                    let timestamp = last_message.timestamp;
1937                    // NOTE: adding 1 microsecond to the timestamp to avoid fetching
1938                    // the last message again.
1939                    let timestamp = OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)
1940                        .with_context(|| {
1941                            format!("Unable to interpret {timestamp} as a unix timestamp")
1942                        })?;
1943
1944                    // FIXME: We need a better way to tell the backend "give me the
1945                    // next set of logs". Adding 1 nanosecond could theoretically
1946                    // mean we miss messages if multiple log messages arrived at
1947                    // the same nanosecond and the page ended midway.
1948
1949                    let next_timestamp = timestamp + Duration::from_nanos(1_000);
1950
1951                    break Ok(Some((page, next_timestamp)));
1952                }
1953            }
1954        };
1955
1956        fut.instrument(span.clone())
1957    })
1958}
1959
1960/// Get pages of logs associated with an application that lie within the
1961/// specified date range.
1962///
1963/// In contrast to `get_app_logs`, this function collects the stream into a
1964/// final vector.
1965#[tracing::instrument(skip_all, level = "debug")]
1966#[allow(clippy::let_with_type_underscore)]
1967#[allow(clippy::too_many_arguments)]
1968pub async fn get_app_logs_paginated(
1969    client: &WasmerClient,
1970    name: String,
1971    owner: String,
1972    tag: Option<String>,
1973    start: OffsetDateTime,
1974    end: Option<OffsetDateTime>,
1975    watch: bool,
1976    streams: Option<Vec<LogStream>>,
1977) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
1978    let stream = get_app_logs(
1979        client, name, owner, tag, start, end, watch, streams, None, None,
1980    );
1981
1982    stream.map(|res| {
1983        let mut logs = Vec::new();
1984        let mut hasher = HashSet::new();
1985        let mut page = res?;
1986
1987        // Prevent duplicates.
1988        // TODO: don't clone the message, just hash it.
1989        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
1990
1991        logs.extend(page);
1992
1993        Ok(logs)
1994    })
1995}
1996
1997/// Get pages of logs associated with an application that lie within the
1998/// specified date range with a specific instance identifier.
1999///
2000/// In contrast to `get_app_logs`, this function collects the stream into a
2001/// final vector.
2002#[tracing::instrument(skip_all, level = "debug")]
2003#[allow(clippy::let_with_type_underscore)]
2004#[allow(clippy::too_many_arguments)]
2005pub async fn get_app_logs_paginated_filter_instance(
2006    client: &WasmerClient,
2007    name: String,
2008    owner: String,
2009    tag: Option<String>,
2010    start: OffsetDateTime,
2011    end: Option<OffsetDateTime>,
2012    watch: bool,
2013    streams: Option<Vec<LogStream>>,
2014    instance_ids: Vec<String>,
2015) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2016    let stream = get_app_logs(
2017        client,
2018        name,
2019        owner,
2020        tag,
2021        start,
2022        end,
2023        watch,
2024        streams,
2025        None,
2026        Some(instance_ids),
2027    );
2028
2029    stream.map(|res| {
2030        let mut logs = Vec::new();
2031        let mut hasher = HashSet::new();
2032        let mut page = res?;
2033
2034        // Prevent duplicates.
2035        // TODO: don't clone the message, just hash it.
2036        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
2037
2038        logs.extend(page);
2039
2040        Ok(logs)
2041    })
2042}
2043
2044/// Get pages of logs associated with an specific request for application that lie within the
2045/// specified date range.
2046///
2047/// In contrast to `get_app_logs`, this function collects the stream into a
2048/// final vector.
2049#[tracing::instrument(skip_all, level = "debug")]
2050#[allow(clippy::let_with_type_underscore)]
2051#[allow(clippy::too_many_arguments)]
2052pub async fn get_app_logs_paginated_filter_request(
2053    client: &WasmerClient,
2054    name: String,
2055    owner: String,
2056    tag: Option<String>,
2057    start: OffsetDateTime,
2058    end: Option<OffsetDateTime>,
2059    watch: bool,
2060    streams: Option<Vec<LogStream>>,
2061    request_id: String,
2062) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
2063    let stream = get_app_logs(
2064        client,
2065        name,
2066        owner,
2067        tag,
2068        start,
2069        end,
2070        watch,
2071        streams,
2072        Some(request_id),
2073        None,
2074    );
2075
2076    stream.map(|res| {
2077        let mut logs = Vec::new();
2078        let mut hasher = HashSet::new();
2079        let mut page = res?;
2080
2081        // Prevent duplicates.
2082        // TODO: don't clone the message, just hash it.
2083        page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
2084
2085        logs.extend(page);
2086
2087        Ok(logs)
2088    })
2089}
2090
2091/// Retrieve a domain by its name.
2092///
2093/// Specify with_records to also retrieve all records for the domain.
2094pub async fn get_domain(
2095    client: &WasmerClient,
2096    domain: String,
2097) -> Result<Option<types::DnsDomain>, anyhow::Error> {
2098    let vars = types::GetDomainVars { domain };
2099
2100    let opt = client
2101        .run_graphql(types::GetDomain::build(vars))
2102        .await?
2103        .get_domain;
2104    Ok(opt)
2105}
2106
2107/// Retrieve a domain by its name.
2108///
2109/// Specify with_records to also retrieve all records for the domain.
2110pub async fn get_domain_zone_file(
2111    client: &WasmerClient,
2112    domain: String,
2113) -> Result<Option<types::DnsDomainWithZoneFile>, anyhow::Error> {
2114    let vars = types::GetDomainVars { domain };
2115
2116    let opt = client
2117        .run_graphql(types::GetDomainWithZoneFile::build(vars))
2118        .await?
2119        .get_domain;
2120    Ok(opt)
2121}
2122
2123/// Retrieve a domain by its name, along with all it's records.
2124pub async fn get_domain_with_records(
2125    client: &WasmerClient,
2126    domain: String,
2127) -> Result<Option<types::DnsDomainWithRecords>, anyhow::Error> {
2128    let vars = types::GetDomainVars { domain };
2129
2130    let opt = client
2131        .run_graphql(types::GetDomainWithRecords::build(vars))
2132        .await?
2133        .get_domain;
2134    Ok(opt)
2135}
2136
2137/// Register a new domain
2138pub async fn register_domain(
2139    client: &WasmerClient,
2140    name: String,
2141    namespace: Option<String>,
2142    import_records: Option<bool>,
2143) -> Result<types::DnsDomain, anyhow::Error> {
2144    let vars = types::RegisterDomainVars {
2145        name,
2146        namespace,
2147        import_records,
2148    };
2149    let opt = client
2150        .run_graphql_strict(types::RegisterDomain::build(vars))
2151        .await?
2152        .register_domain
2153        .context("Domain registration failed")?
2154        .domain
2155        .context("Domain registration failed, no associatede domain found.")?;
2156    Ok(opt)
2157}
2158
2159/// Retrieve all DNS records.
2160///
2161/// NOTE: this is a privileged operation that requires extra permissions.
2162pub async fn get_all_dns_records(
2163    client: &WasmerClient,
2164    vars: types::GetAllDnsRecordsVariables,
2165) -> Result<types::DnsRecordConnection, anyhow::Error> {
2166    client
2167        .run_graphql_strict(types::GetAllDnsRecords::build(vars))
2168        .await
2169        .map(|x| x.get_all_dnsrecords)
2170}
2171
2172/// Retrieve all DNS domains.
2173pub async fn get_all_domains(
2174    client: &WasmerClient,
2175    vars: types::GetAllDomainsVariables,
2176) -> Result<Vec<DnsDomain>, anyhow::Error> {
2177    let connection = client
2178        .run_graphql_strict(types::GetAllDomains::build(vars))
2179        .await
2180        .map(|x| x.get_all_domains)
2181        .context("no domains returned")?;
2182    Ok(connection
2183        .edges
2184        .into_iter()
2185        .flatten()
2186        .filter_map(|x| x.node)
2187        .collect())
2188}
2189
2190/// Retrieve a domain by its name.
2191///
2192/// Specify with_records to also retrieve all records for the domain.
2193pub fn get_all_dns_records_stream(
2194    client: &WasmerClient,
2195    vars: types::GetAllDnsRecordsVariables,
2196) -> impl futures::Stream<Item = Result<Vec<types::DnsRecord>, anyhow::Error>> + '_ {
2197    futures::stream::try_unfold(
2198        Some(vars),
2199        move |vars: Option<types::GetAllDnsRecordsVariables>| async move {
2200            let vars = match vars {
2201                Some(vars) => vars,
2202                None => return Ok(None),
2203            };
2204
2205            let page = get_all_dns_records(client, vars.clone()).await?;
2206
2207            let end_cursor = page.page_info.end_cursor;
2208
2209            let items = page
2210                .edges
2211                .into_iter()
2212                .filter_map(|x| x.and_then(|x| x.node))
2213                .collect::<Vec<_>>();
2214
2215            let new_vars = end_cursor.map(|c| types::GetAllDnsRecordsVariables {
2216                after: Some(c),
2217                ..vars
2218            });
2219
2220            Ok(Some((items, new_vars)))
2221        },
2222    )
2223}
2224
2225pub async fn purge_cache_for_app_version(
2226    client: &WasmerClient,
2227    vars: types::PurgeCacheForAppVersionVars,
2228) -> Result<(), anyhow::Error> {
2229    client
2230        .run_graphql_strict(types::PurgeCacheForAppVersion::build(vars))
2231        .await
2232        .map(|x| x.purge_cache_for_app_version)
2233        .context("backend did not return data")?;
2234
2235    Ok(())
2236}
2237
2238/// Convert a [`OffsetDateTime`] to a unix timestamp that the WAPM backend
2239/// understands.
2240fn unix_timestamp(ts: OffsetDateTime) -> f64 {
2241    let nanos_per_second = 1_000_000_000;
2242    let timestamp = ts.unix_timestamp_nanos();
2243    let nanos = timestamp % nanos_per_second;
2244    let secs = timestamp / nanos_per_second;
2245
2246    (secs as f64) + (nanos as f64 / nanos_per_second as f64)
2247}
2248
2249/// Publish a new app (version).
2250pub async fn upsert_domain_from_zone_file(
2251    client: &WasmerClient,
2252    zone_file_contents: String,
2253    delete_missing_records: bool,
2254) -> Result<DnsDomain, anyhow::Error> {
2255    let vars = UpsertDomainFromZoneFileVars {
2256        zone_file: zone_file_contents,
2257        delete_missing_records: Some(delete_missing_records),
2258    };
2259    let res = client
2260        .run_graphql_strict(types::UpsertDomainFromZoneFile::build(vars))
2261        .await?;
2262
2263    let domain = res
2264        .upsert_domain_from_zone_file
2265        .context("Upserting domain from zonefile failed")?
2266        .domain;
2267
2268    Ok(domain)
2269}