Skip to main content

wasmer_backend_api/
query.rs

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