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