feat: Add wasmer-api crate

Provides bindings to the Wasmer GraphQL api.
This commit is contained in:
Christoph Herzog
2024-01-24 22:31:17 +01:00
parent b1689b825e
commit 8c74b2cd8c
14 changed files with 5242 additions and 1 deletions

25
Cargo.lock generated
View File

@@ -5841,6 +5841,29 @@ dependencies = [
"winapi",
]
[[package]]
name = "wasmer-api"
version = "0.0.23"
dependencies = [
"anyhow",
"base64 0.13.1",
"cynic",
"edge-schema 0.0.2",
"futures",
"harsh",
"pin-project-lite",
"reqwest",
"serde",
"serde_json",
"serde_path_to_error",
"time",
"tokio",
"tracing",
"url",
"uuid",
"webc",
]
[[package]]
name = "wasmer-api"
version = "0.0.23"
@@ -6173,7 +6196,7 @@ dependencies = [
"uuid",
"virtual-mio 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"virtual-net 0.6.1",
"wasmer-api",
"wasmer-api 0.0.23 (registry+https://github.com/rust-lang/crates.io-index)",
"wasmer-registry 5.10.0",
"wasmer-toml",
"webc",

View File

@@ -39,6 +39,7 @@ members = [
"fuzz",
"lib/api",
"lib/api/macro-wasmer-universal-test",
"lib/backend-api",
"lib/c-api",
"lib/c-api/examples/wasmer-capi-examples-runner",
"lib/c-api/tests/wasmer-c-api-test-runner",

View File

@@ -0,0 +1,94 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 0.0.1 (2023-04-27)
* Removed legacy API implementation in favour of the new cynic client
* Fixed log querying
* Added methods for retrieving DeployApp/Version by unique id
## 0.1.0-alpha.1 (2023-03-29)
<csr-id-7195702c618c0fb0937244034ee4600531b97034/>
<csr-id-b8b9af7429a8c9b0880be9bf72f1b8a732d05d75/>
<csr-id-a40f5127796316069aa2ac646ea5c3817f7a29fa/>
<csr-id-065513b5f6ee350b9f2607d5aa7003df352f5cbc/>
<csr-id-38336ebe12a85b142871a4c0fa50541df815a441/>
<csr-id-8febcde88d1d7df8a7a86c79afcf960c2cb868a3/>
<csr-id-33f822d9701e87c0f0564574a76adef83ccf72a5/>
<csr-id-d968694ade1191afff80e6d141598b9c10b2f3c7/>
<csr-id-b1245bfa194e05d49809973b716e2565689bfccf/>
### Refactor (BREAKING)
- <csr-id-7195702c618c0fb0937244034ee4600531b97034/> Rename wasmer-deploy-core to wasmer-deploy-schema
-schema is a more sensible / expressive name for the crate,
since it just holds type definitions.
Done in preparation for publishing the crate, since it will need to be
used by downstream consumers like the Wasmer repo
### Chore
- <csr-id-bbc3105d5f04bc4c35dad794443f53980106981e/> Add description to wasmer-api Cargo.toml
Required for releasing.
### Other
- <csr-id-b8b9af7429a8c9b0880be9bf72f1b8a732d05d75/> Dependency cleanup
* Lift some dependencies to workspace.dependencies to avoid duplication
* Remove a bunch of unused dependencies
- <csr-id-a40f5127796316069aa2ac646ea5c3817f7a29fa/> Add crate metadata and prepare for first CLI release
- <csr-id-065513b5f6ee350b9f2607d5aa7003df352f5cbc/> "app list" filters
Extend the "app list" command to query either a namespace, a users apps,
or all apps accessible by a user.
(--namepsace X, --all)
- <csr-id-38336ebe12a85b142871a4c0fa50541df815a441/> Add a webc app config fetch tests
- <csr-id-8febcde88d1d7df8a7a86c79afcf960c2cb868a3/> Make serde_json a workspace dependency
To avoid duplication...
- <csr-id-33f822d9701e87c0f0564574a76adef83ccf72a5/> Lift serde to be a workspace dependency
Easier version management...
- <csr-id-d968694ade1191afff80e6d141598b9c10b2f3c7/> Lift anyhow, time and clap to workspace dependnecies
Less version management...
### Bug Fixes
- <csr-id-07a199f0bcccd3178e8f773ae96d100febfb88d0/> Use token for webc fetching
If the api is configured with a token, use the token for fetching webcs.
Previously it just used anonymous access.
- <csr-id-c6ad494a45968bb4a69455f3c292b6fcf9631770/> Update deployment config generation to backend changes
The generateDeployConfig GraphQL API has changed
* Takes a DeployConfigVersion id instead of DeployConfig id
* Returns a DeployConfigVersion
### New Features
- <csr-id-f96944cd8097aff11d51e8e0c3f6fa1efc6b1ec6/> Add generate_deploy_token to new cynic GQL api client
Will be needed for various commands
- <csr-id-33e469b921256a241866a5a972278665905dcdf4/> Add getPackage GQL query
- <csr-id-4783d6a5c53875724bbea3ed5a65b13e5056c001/> Add query for DeployAppVersion
- <csr-id-b4aa770fb388970c837f2e5429caa5803eef64bf/> Add new namespace and app commands
- <csr-id-63ec5f98dca19c417df0e42a5bfbbd963c9b19c2/> Add a CapabilityLoggingV1 config
Allows to configure the logging behaviour of workloads.
Will be used very soon to implement instance log forwarding.
### Documentation
- <csr-id-98b9313b34e93c7973b778ef0867f042bf7aed57/> add some changelogs
- <csr-id-845a8b3ebece96d5fff941110a425ab19a3a2eed/> Add REAMDE to Cargo.toml of to-be-published crates
### Chore
- <csr-id-b1245bfa194e05d49809973b716e2565689bfccf/> Remove download_url from WebcPackageIdentifierV1
Not needed anymore, since now we have a deployment config registry.
<csr-unknown>
Needed to update relevant callsites<csr-unknown/>
<csr-unknown/>

View File

@@ -0,0 +1,39 @@
[package]
name = "wasmer-api"
version = "0.0.23"
description = "Wasmer API client library."
readme = "README.md"
homepage = "https://wasmer.io"
documentation = "https://docs.rs/wasmer-api"
# NOTE: Using a distinct license for the CLI, since it might be different from
# other crates.
license = "MIT"
edition.workspace = true
authors.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# Wasmer dependencies.
edge-schema = "0.0.2"
webc = "5"
# crates.io dependencies.
anyhow = "1"
serde = { version = "1", features = ["derive"] }
time = { version = "0.3", features = ["formatting", "parsing"] }
tokio = { version = "1.23.0" }
serde_json = "1"
url = "2"
futures = "0.3"
tracing = "0.1"
cynic = { version = "3.2.2", features = ["http-reqwest"] }
pin-project-lite = "0.2.10"
serde_path_to_error = "0.1.14"
harsh = "0.2.2"
reqwest = { version = "0.11.13", default-features = false, features = ["json"] }
[dev-dependencies]
base64 = "0.13.1"
tokio = { version = "1.3", features = ["macros", "rt"] }
uuid = { version = "1", features = ["v4"] }

46
lib/backend-api/README.md Normal file
View File

@@ -0,0 +1,46 @@
# wasmer-api
GraphQL API client for the [Wasmer](https://wasmer.io) backend.
## Development
This client is built on the [cynic][cynic-api-docs] crate,
a GraphQL client library that allows tight integration between Rust and
GraphQL types.
It was chosen over other implementations like `graphql-client` because it
significantly reduces boilerplate and improves the development experience.
The downside is that the underlying GraphQL queries are much less obvious when
looking at the code. This can be remedied with some strategies mentioned below.
Consult the Cynic docs at [cynic-rs.dev][cynic-website] for more
information.
### Backend GraphQL Schema
The GraphQL schema for the backend is stored in `./schema.graphql`.
To update the schema, simply download the latest version and replace the local
file.
It can be retrieved from
https://github.com/wasmerio/wapm.io-backend/blob/master/backend/graphql/schema.graphql.
### Writing/Updating Queries
You can use the [Cynic web UI][cynic-web-ui] to easily create the types for new
queries.
Simply upload the local schema from `./schema.graphql` and use the UI to build
your desired query.
NOTE: Where possible, do not duplicate types that are already defined,
and instead reuse/extend them where possible.
This is not always sensible though, depending on which nested data you want to
fetch.
[cynic-api-docs]: https://docs.rs/cynic/latest/cynic/
[cynic-web-ui]: https://docs.rs/cynic/latest/cynic/
[cynic-website]: https://cynic-rs.dev

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,209 @@
use std::time::Duration;
use anyhow::{bail, Context as _};
use cynic::{http::CynicReqwestError, GraphQlResponse, Operation};
use url::Url;
use crate::GraphQLApiFailure;
/// API client for the Wasmer API.
///
/// Use the queries in [`crate::queries`] to interact with the API.
#[derive(Clone, Debug)]
pub struct WasmerClient {
auth_token: Option<String>,
graphql_endpoint: Url,
pub(crate) client: reqwest::Client,
pub(crate) user_agent: reqwest::header::HeaderValue,
#[allow(unused)]
extra_debugging: bool,
}
impl WasmerClient {
pub fn graphql_endpoint(&self) -> &Url {
&self.graphql_endpoint
}
pub fn auth_token(&self) -> Option<&str> {
self.auth_token.as_deref()
}
fn parse_user_agent(user_agent: &str) -> Result<reqwest::header::HeaderValue, anyhow::Error> {
if user_agent.is_empty() {
bail!("user agent must not be empty");
}
user_agent
.parse()
.with_context(|| format!("invalid user agent: '{}'", user_agent))
}
pub fn new_with_client(
client: reqwest::Client,
graphql_endpoint: Url,
user_agent: &str,
) -> Result<Self, anyhow::Error> {
Ok(Self {
client,
auth_token: None,
user_agent: Self::parse_user_agent(user_agent)?,
graphql_endpoint,
extra_debugging: false,
})
}
pub fn new(graphql_endpoint: Url, user_agent: &str) -> Result<Self, anyhow::Error> {
let client = reqwest::Client::builder()
.connect_timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(90))
.build()
.context("could not construct http client")?;
Self::new_with_client(client, graphql_endpoint, user_agent)
}
pub fn with_auth_token(mut self, auth_token: String) -> Self {
self.auth_token = Some(auth_token);
self
}
pub(crate) async fn run_graphql_raw<ResponseData, Vars>(
&self,
operation: Operation<ResponseData, Vars>,
) -> Result<cynic::GraphQlResponse<ResponseData>, anyhow::Error>
where
Vars: serde::Serialize + std::fmt::Debug,
ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
{
let req = self
.client
.post(self.graphql_endpoint.as_str())
.header(reqwest::header::USER_AGENT, &self.user_agent);
let req = if let Some(token) = &self.auth_token {
req.bearer_auth(token)
} else {
req
};
if self.extra_debugging {
tracing::trace!(
query=%operation.query,
vars=?operation.variables,
"running GraphQL query"
);
}
let query = operation.query.clone();
tracing::trace!(
endpoint=%self.graphql_endpoint,
query=serde_json::to_string(&operation).unwrap_or_default(),
"sending graphql query"
);
let res = req.json(&operation).send().await;
let res = match res {
Ok(response) => {
let status = response.status();
if !status.is_success() {
let body_string = match response.text().await {
Ok(b) => b,
Err(err) => {
tracing::error!("could not load response body: {err}");
"<could not retrieve body>".to_string()
}
};
match serde_json::from_str::<GraphQlResponse<ResponseData>>(&body_string) {
Ok(response) => Ok(response),
Err(_) => Err(CynicReqwestError::ErrorResponse(status, body_string)),
}
} else {
let body = response.bytes().await?;
let jd = &mut serde_json::Deserializer::from_slice(&body);
let data: Result<GraphQlResponse<ResponseData>, _> =
serde_path_to_error::deserialize(jd).map_err(|err| {
let body_txt = String::from_utf8_lossy(&body);
CynicReqwestError::ErrorResponse(
reqwest::StatusCode::INTERNAL_SERVER_ERROR,
format!("Could not decode JSON response: {err} -- '{body_txt}'"),
)
});
data
}
}
Err(e) => Err(CynicReqwestError::ReqwestError(e)),
};
let res = match res {
Ok(res) => {
tracing::trace!(?res, "GraphQL query succeeded");
res
}
Err(err) => {
tracing::error!(?err, "GraphQL query failed");
return Err(err.into());
}
};
if let Some(errors) = &res.errors {
if !errors.is_empty() {
tracing::warn!(
?errors,
data=?res.data,
%query,
endpoint=%self.graphql_endpoint,
"GraphQL query succeeded, but returned errors",
);
}
}
Ok(res)
}
pub(crate) async fn run_graphql<ResponseData, Vars>(
&self,
operation: Operation<ResponseData, Vars>,
) -> Result<ResponseData, anyhow::Error>
where
Vars: serde::Serialize + std::fmt::Debug,
ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
{
let res = self.run_graphql_raw(operation).await?;
if let Some(data) = res.data {
Ok(data)
} else if let Some(errs) = res.errors {
let errs = GraphQLApiFailure { errors: errs };
Err(errs).context("GraphQL query failed")
} else {
Err(anyhow::anyhow!("Query did not return any data"))
}
}
/// Run a GraphQL query, but fail (return an Error) if any error is returned
/// in the response.
pub(crate) async fn run_graphql_strict<ResponseData, Vars>(
&self,
operation: Operation<ResponseData, Vars>,
) -> Result<ResponseData, anyhow::Error>
where
Vars: serde::Serialize + std::fmt::Debug,
ResponseData: serde::de::DeserializeOwned + std::fmt::Debug + 'static,
{
let res = self.run_graphql_raw(operation).await?;
if let Some(errs) = res.errors {
if !errs.is_empty() {
let errs = GraphQLApiFailure { errors: errs };
return Err(errs).context("GraphQL query failed");
}
}
if let Some(data) = res.data {
Ok(data)
} else {
Err(anyhow::anyhow!("Query did not return any data"))
}
}
}

View File

@@ -0,0 +1,36 @@
/// One or multiple errors returned by the GraphQL API.
// Mainly exists to implement [`std::error::Error`].
#[derive(Debug)]
pub struct GraphQLApiFailure {
pub errors: Vec<cynic::GraphQlError>,
}
impl GraphQLApiFailure {
pub fn from_errors(
msg: impl Into<String>,
errors: Option<Vec<cynic::GraphQlError>>,
) -> anyhow::Error {
let msg = msg.into();
if let Some(errs) = errors {
if !errs.is_empty() {
let err = GraphQLApiFailure { errors: errs };
return anyhow::Error::new(err).context(msg);
}
}
anyhow::anyhow!("{msg} - query did not return any data")
}
}
impl std::fmt::Display for GraphQLApiFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let errs = self
.errors
.iter()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join(", ");
write!(f, "GraphQL API failure: {}", errs)
}
}
impl std::error::Error for GraphQLApiFailure {}

View File

@@ -0,0 +1,483 @@
//! [`GlobalId`]s are used by the backend to identify a specific object.
//!
//! This module provides a parser/encoder and related type defintions
//! for global ids.
use std::fmt::Display;
#[repr(u16)]
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NodeKind {
User = 0,
SocialAuth = 1,
Namespace = 2,
Package = 3,
PackageVersion = 4,
PackageCollaborator = 5,
PackageCollaboratorInvite = 6,
NativeExecutable = 7,
PackageVersionNPMBinding = 8,
PackageVersionPythonBinding = 9,
PackageTransferRequest = 10,
Interface = 11,
InterfaceVersion = 12,
PublicKey = 13,
UserNotification = 14,
ActivityEvent = 15,
NamespaceCollaborator = 16,
NamespaceCollaboratorInvite = 17,
BindingsGenerator = 18,
DeployConfigVersion = 19,
DeployConfigInfo = 20,
DeployApp = 21,
DeployAppVersion = 22,
Waitlist = 23,
WaitlistMember = 24,
CardPaymentMethod = 25,
PaymentIntent = 26,
AppAlias = 27,
Nonce = 28,
TermsOfService = 29,
}
impl NodeKind {
pub fn from_num(x: u64) -> Option<Self> {
match x {
0 => Some(Self::User),
1 => Some(Self::SocialAuth),
2 => Some(Self::Namespace),
3 => Some(Self::Package),
4 => Some(Self::PackageVersion),
5 => Some(Self::PackageCollaborator),
6 => Some(Self::PackageCollaboratorInvite),
7 => Some(Self::NativeExecutable),
8 => Some(Self::PackageVersionNPMBinding),
9 => Some(Self::PackageVersionPythonBinding),
10 => Some(Self::PackageTransferRequest),
11 => Some(Self::Interface),
12 => Some(Self::InterfaceVersion),
13 => Some(Self::PublicKey),
14 => Some(Self::UserNotification),
15 => Some(Self::ActivityEvent),
16 => Some(Self::NamespaceCollaborator),
17 => Some(Self::NamespaceCollaboratorInvite),
18 => Some(Self::BindingsGenerator),
19 => Some(Self::DeployConfigVersion),
20 => Some(Self::DeployConfigInfo),
21 => Some(Self::DeployApp),
22 => Some(Self::DeployAppVersion),
23 => Some(Self::Waitlist),
24 => Some(Self::WaitlistMember),
25 => Some(Self::CardPaymentMethod),
26 => Some(Self::PaymentIntent),
27 => Some(Self::AppAlias),
28 => Some(Self::Nonce),
29 => Some(Self::TermsOfService),
_ => None,
}
}
pub fn parse_prefix(s: &str) -> Option<NodeKind> {
match s {
"u" => Some(Self::User),
"su" => Some(Self::SocialAuth),
"ns" => Some(Self::Namespace),
"pk" => Some(Self::Package),
"pkv" => Some(Self::PackageVersion),
"pc" => Some(Self::PackageCollaborator),
"pci" => Some(Self::PackageCollaboratorInvite),
"ne" => Some(Self::NativeExecutable),
"pkvbjs" => Some(Self::PackageVersionNPMBinding),
"pkvbpy" => Some(Self::PackageVersionPythonBinding),
"pt" => Some(Self::PackageTransferRequest),
"in" => Some(Self::Interface),
"inv" => Some(Self::InterfaceVersion),
"pub" => Some(Self::PublicKey),
"nt" => Some(Self::UserNotification),
"ae" => Some(Self::ActivityEvent),
"nsc" => Some(Self::NamespaceCollaborator),
"nsci" => Some(Self::NamespaceCollaboratorInvite),
"bg" => Some(Self::BindingsGenerator),
"dcv" => Some(Self::DeployConfigVersion),
"dci" => Some(Self::DeployConfigInfo),
"da" => Some(Self::DeployApp),
"dav" => Some(Self::DeployAppVersion),
"wl" => Some(Self::Waitlist),
"wlm" => Some(Self::WaitlistMember),
"cpm" => Some(Self::CardPaymentMethod),
"pi" => Some(Self::PaymentIntent),
"daa" => Some(Self::AppAlias),
"nnc" => Some(Self::Nonce),
"tos" => Some(Self::TermsOfService),
_ => None,
}
}
fn as_prefix(&self) -> &'static str {
match self {
Self::User => "u",
Self::SocialAuth => "su",
Self::Namespace => "ns",
Self::Package => "pk",
Self::PackageVersion => "pkv",
Self::PackageCollaborator => "pc",
Self::PackageCollaboratorInvite => "pci",
Self::NativeExecutable => "ne",
Self::PackageVersionNPMBinding => "pkvbjs",
Self::PackageVersionPythonBinding => "pkvbpy",
Self::PackageTransferRequest => "pt",
Self::Interface => "in",
Self::InterfaceVersion => "inv",
Self::PublicKey => "pub",
Self::UserNotification => "nt",
Self::ActivityEvent => "ae",
Self::NamespaceCollaborator => "nsc",
Self::NamespaceCollaboratorInvite => "nsci",
Self::BindingsGenerator => "bg",
Self::DeployConfigVersion => "dcv",
Self::DeployConfigInfo => "dci",
Self::DeployApp => "da",
Self::DeployAppVersion => "dav",
Self::Waitlist => "wl",
Self::WaitlistMember => "wlm",
Self::CardPaymentMethod => "cpm",
Self::PaymentIntent => "pi",
Self::AppAlias => "daa",
Self::Nonce => "nnc",
Self::TermsOfService => "tos",
}
}
}
impl Display for NodeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::User => "User",
Self::SocialAuth => "SocialAuth",
Self::Namespace => "Namespace",
Self::Package => "Package",
Self::PackageVersion => "PackageVersion",
Self::PackageCollaborator => "PackageCollaborator",
Self::PackageCollaboratorInvite => "PackageCollaboratorInvite",
Self::NativeExecutable => "NativeExecutable",
Self::PackageVersionNPMBinding => "PackageVersionNPMBinding",
Self::PackageVersionPythonBinding => "PackageVersionPythonBinding",
Self::PackageTransferRequest => "PackageTransferRequest",
Self::Interface => "Interface",
Self::InterfaceVersion => "InterfaceVersion",
Self::PublicKey => "PublicKey",
Self::UserNotification => "UserNotification",
Self::ActivityEvent => "ActivityEvent",
Self::NamespaceCollaborator => "NamespaceCollaborator",
Self::NamespaceCollaboratorInvite => "NamespaceCollaboratorInvite",
Self::BindingsGenerator => "BindingsGenerator",
Self::DeployConfigVersion => "DeployConfigVersion",
Self::DeployConfigInfo => "DeployConfigInfo",
Self::DeployApp => "DeployApp",
Self::DeployAppVersion => "DeployAppVersion",
Self::Waitlist => "Waitlist",
Self::WaitlistMember => "WaitlistMember",
Self::CardPaymentMethod => "CardPaymentMethod",
Self::PaymentIntent => "PaymentIntent",
Self::AppAlias => "AppAlias",
Self::Nonce => "Nonce",
Self::TermsOfService => "TermsOfService",
};
write!(f, "{name}")
}
}
/// Global id of backend nodes.
///
/// IDs are encoded using the "hashid" scheme, which uses a given alphabet and
/// a salt to encode u64 numbers into a string hash.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct GlobalId {
/// The node type of the ID.
kind: NodeKind,
/// The database ID of the node.
database_id: u64,
}
impl GlobalId {
/// Salt used by the backend to encode hashes.
const SALT: &'static str = "wasmer salt hashid";
/// Minimum length of the encoded hashes.
const MIN_LENGTH: usize = 12;
/// Hash alphabet used for the prefix id variant.
const ALPHABET_PREFIXED: &'static str =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
/// Hash alphabet used for the non-prefixed id variant.
const ALPHABET_URL: &'static str = "abcdefghijklmnopqrstuvwxyz0123456789";
pub fn new(kind: NodeKind, database_id: u64) -> Self {
Self { kind, database_id }
}
fn build_harsh(alphabet: &str, salt: &[u8]) -> harsh::Harsh {
harsh::HarshBuilder::new()
.alphabet(alphabet.as_bytes())
.salt(salt)
.length(GlobalId::MIN_LENGTH)
.build()
.unwrap()
}
fn build_harsh_prefixed() -> harsh::Harsh {
Self::build_harsh(Self::ALPHABET_PREFIXED, Self::SALT.as_bytes())
}
fn build_harsh_url() -> harsh::Harsh {
Self::build_harsh(Self::ALPHABET_URL, Self::SALT.as_bytes())
}
pub fn kind(&self) -> NodeKind {
self.kind
}
pub fn database_id(&self) -> u64 {
self.database_id
}
/// Encode a prefixed global id.
pub fn encode_prefixed(&self) -> String {
let hash = Self::build_harsh_prefixed().encode(&[
// scope
1,
// version
2,
self.kind as u64,
self.database_id,
]);
format!("{}_{}", self.kind.as_prefix(), hash)
}
fn parse_values(values: &[u64]) -> Result<Self, ErrorKind> {
let scope = values.first().cloned().ok_or(ErrorKind::MissingScope)?;
if scope != 1 {
return Err(ErrorKind::UnknownScope(scope));
}
let version = values.get(1).cloned().ok_or(ErrorKind::MissingVersion)?;
if version != 2 {
return Err(ErrorKind::UnknownVersion(version));
}
let ty_raw = values.get(2).cloned().ok_or(ErrorKind::MissingNodeType)?;
let ty_parsed = NodeKind::from_num(ty_raw).ok_or(ErrorKind::UnknownNodeType(ty_raw))?;
let db_id = values.get(3).cloned().ok_or(ErrorKind::MissingDatabaseId)?;
Ok(Self {
kind: ty_parsed,
database_id: db_id,
})
}
/// Parse a prefixed global id.
pub fn parse_prefixed(hash: &str) -> Result<Self, GlobalIdParseError> {
let (prefix, value) = hash
.split_once('_')
.ok_or_else(|| GlobalIdParseError::new(hash, ErrorKind::MissingPrefix))?;
if prefix.is_empty() {
return Err(GlobalIdParseError::new(hash, ErrorKind::MissingPrefix));
}
let ty_prefix = NodeKind::parse_prefix(prefix).ok_or_else(|| {
GlobalIdParseError::new(hash, ErrorKind::UnknownPrefix(prefix.to_string()))
})?;
let values = Self::build_harsh_prefixed()
.decode(value)
.map_err(|err| GlobalIdParseError::new(hash, ErrorKind::Decode(err.to_string())))?;
let s = Self::parse_values(&values).map_err(|kind| GlobalIdParseError::new(hash, kind))?;
if ty_prefix != s.kind {
return Err(GlobalIdParseError::new(hash, ErrorKind::PrefixTypeMismatch));
}
Ok(s)
}
/// Encode a non-prefixed global id.
///
/// Note: URL ids use a different alphabet than prefixed ids.
pub fn encode_url(&self) -> String {
Self::build_harsh_url().encode(&[
// scope
1,
// version
2,
self.kind as u64,
self.database_id,
])
}
/// Parse a non-prefixed URL global id variant.
///
/// Note: URL ids use a different alphabet than prefixed ids.
pub fn parse_url(hash: &str) -> Result<Self, GlobalIdParseError> {
let values = Self::build_harsh_url()
.decode(hash)
.map_err(|err| GlobalIdParseError::new(hash, ErrorKind::Decode(err.to_string())))?;
Self::parse_values(&values).map_err(|kind| GlobalIdParseError::new(hash, kind))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GlobalIdParseError {
id: String,
kind: ErrorKind,
}
impl GlobalIdParseError {
fn new(id: impl Into<String>, kind: ErrorKind) -> Self {
Self {
id: id.into(),
kind,
}
}
}
/// Error type for parsing of [`GlobalId`]s.
// Note: kept private on purpose, not useful to export.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
enum ErrorKind {
MissingPrefix,
UnknownPrefix(String),
PrefixTypeMismatch,
MissingScope,
UnknownScope(u64),
MissingVersion,
UnknownVersion(u64),
MissingNodeType,
UnknownNodeType(u64),
MissingDatabaseId,
Decode(String),
}
impl std::fmt::Display for GlobalIdParseError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "could not parse global id '{}': ", self.id)?;
match &self.kind {
ErrorKind::UnknownPrefix(p) => {
write!(f, "unknown type prefix '{}'", p)
}
ErrorKind::Decode(s) => {
write!(f, "decode error: {}", s)
}
ErrorKind::MissingScope => {
write!(f, "missing scope value")
}
ErrorKind::UnknownScope(x) => {
write!(f, "unknown scope value {}", x)
}
ErrorKind::MissingVersion => {
write!(f, "missing version value")
}
ErrorKind::UnknownVersion(v) => {
write!(f, "unknown version value {}", v)
}
ErrorKind::UnknownNodeType(t) => {
write!(f, "unknown node type '{}'", t)
}
ErrorKind::MissingPrefix => write!(f, "missing prefix"),
ErrorKind::PrefixTypeMismatch => write!(f, "prefix type mismatch"),
ErrorKind::MissingNodeType => write!(f, "missing node type"),
ErrorKind::MissingDatabaseId => write!(f, "missing database id"),
}
}
}
impl std::error::Error for GlobalIdParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_global_id() {
// Roundtrip.
let x1 = GlobalId {
kind: NodeKind::DeployApp,
database_id: 123,
};
assert_eq!(Ok(x1), GlobalId::parse_prefixed(&x1.encode_prefixed()),);
assert_eq!(Ok(x1), GlobalId::parse_url(&x1.encode_url()));
assert_eq!(
GlobalId::parse_prefixed("da_MRrWI0t5U582"),
Ok(GlobalId {
kind: NodeKind::DeployApp,
database_id: 273,
})
);
// Error conditions.
assert_eq!(
GlobalId::parse_prefixed("oOtQIDI7q").err().unwrap().kind,
ErrorKind::MissingPrefix,
);
assert_eq!(
GlobalId::parse_prefixed("oOtQIDI7q").err().unwrap().kind,
ErrorKind::MissingPrefix,
);
assert_eq!(
GlobalId::parse_prefixed("_oOtQIDI7q").err().unwrap().kind,
ErrorKind::MissingPrefix,
);
assert_eq!(
GlobalId::parse_prefixed("lala_oOtQIDI7q")
.err()
.unwrap()
.kind,
ErrorKind::UnknownPrefix("lala".to_string()),
);
let kind = GlobalId::parse_prefixed("da_xxx").err().unwrap().kind;
assert!(matches!(kind, ErrorKind::Decode(_)));
}
#[test]
fn test_global_id_parse_values() {
assert_eq!(GlobalId::parse_values(&[]), Err(ErrorKind::MissingScope),);
assert_eq!(
GlobalId::parse_values(&[2]),
Err(ErrorKind::UnknownScope(2)),
);
assert_eq!(GlobalId::parse_values(&[1]), Err(ErrorKind::MissingVersion),);
assert_eq!(
GlobalId::parse_values(&[1, 999]),
Err(ErrorKind::UnknownVersion(999)),
);
assert_eq!(
GlobalId::parse_values(&[1, 2]),
Err(ErrorKind::MissingNodeType),
);
assert_eq!(
GlobalId::parse_values(&[1, 2, 99999]),
Err(ErrorKind::UnknownNodeType(99999)),
);
assert_eq!(
GlobalId::parse_values(&[1, 2, 1]),
Err(ErrorKind::MissingDatabaseId),
);
assert_eq!(
GlobalId::parse_values(&[1, 2, 1, 1]),
Ok(GlobalId {
kind: NodeKind::SocialAuth,
database_id: 1,
}),
);
}
}

View File

@@ -0,0 +1,2 @@
types

View File

@@ -0,0 +1,29 @@
// Allowed because it makes code more readable.
#![allow(clippy::bool_comparison, clippy::match_like_matches_macro)]
mod client;
mod error;
pub mod global_id;
pub mod query;
pub mod stream;
pub mod types;
use url::Url;
pub use self::{client::WasmerClient, error::GraphQLApiFailure};
/// Api endpoint for the dev environment.
pub const ENDPOINT_DEV: &str = "https://registry.wasmer.wtf/graphql";
/// Api endpoint for the prod environment.
pub const ENDPOINT_PROD: &str = "https://registry.wasmer.io/graphql";
/// API endpoint for the dev environment.
pub fn endpoint_dev() -> Url {
Url::parse(ENDPOINT_DEV).unwrap()
}
/// API endpoint for the prod environment.
pub fn endpoint_prod() -> Url {
Url::parse(ENDPOINT_PROD).unwrap()
}

View File

@@ -0,0 +1,796 @@
use std::{collections::HashSet, time::Duration};
use anyhow::{bail, Context};
use cynic::{MutationBuilder, QueryBuilder};
use edge_schema::schema::{NetworkTokenV1, WebcIdent};
use futures::StreamExt;
use time::OffsetDateTime;
use tracing::Instrument;
use url::Url;
use crate::{
types::{
self, CreateNamespaceVars, DeployApp, DeployAppConnection, DeployAppVersion,
DeployAppVersionConnection, GetDeployAppAndVersion, GetDeployAppVersionsVars,
GetNamespaceAppsVars, Log, PackageVersionConnection, PublishDeployAppVars,
},
GraphQLApiFailure, WasmerClient,
};
/// Load a webc package from the registry.
///
/// NOTE: this uses the public URL instead of the download URL available through
/// the API, and should not be used where possible.
pub async fn fetch_webc_package(
client: &WasmerClient,
ident: &WebcIdent,
default_registry: &Url,
) -> Result<webc::compat::Container, anyhow::Error> {
let url = ident.build_download_url_with_default_registry(default_registry);
let data = client
.client
.get(url)
.header(reqwest::header::USER_AGENT, &client.user_agent)
.header(reqwest::header::ACCEPT, "application/webc")
.send()
.await?
.error_for_status()?
.bytes()
.await?;
webc::compat::Container::from_bytes(data).context("failed to parse webc package")
}
/// Get the currently logged in used, together with all accessible namespaces.
///
/// You can optionally filter the namespaces by the user role.
pub async fn current_user_with_namespaces(
client: &WasmerClient,
namespace_role: Option<types::GrapheneRole>,
) -> Result<types::UserWithNamespaces, anyhow::Error> {
client
.run_graphql(types::GetCurrentUser::build(types::GetCurrentUserVars {
namespace_role,
}))
.await?
.viewer
.context("not logged in")
}
/// Retrieve an app.
pub async fn get_app(
client: &WasmerClient,
owner: String,
name: String,
) -> Result<Option<types::DeployApp>, anyhow::Error> {
client
.run_graphql(types::GetDeployApp::build(types::GetDeployAppVars {
name,
owner,
}))
.await
.map(|x| x.get_deploy_app)
}
/// Retrieve an app by its global alias.
pub async fn get_app_by_alias(
client: &WasmerClient,
alias: String,
) -> Result<Option<types::DeployApp>, anyhow::Error> {
client
.run_graphql(types::GetDeployAppByAlias::build(
types::GetDeployAppByAliasVars { alias },
))
.await
.map(|x| x.get_app_by_global_alias)
}
/// Retrieve an app version.
pub async fn get_app_version(
client: &WasmerClient,
owner: String,
name: String,
version: String,
) -> Result<Option<types::DeployAppVersion>, anyhow::Error> {
client
.run_graphql(types::GetDeployAppVersion::build(
types::GetDeployAppVersionVars {
name,
owner,
version,
},
))
.await
.map(|x| x.get_deploy_app_version)
}
/// Retrieve an app together with a specific version.
pub async fn get_app_with_version(
client: &WasmerClient,
owner: String,
name: String,
version: String,
) -> Result<GetDeployAppAndVersion, anyhow::Error> {
client
.run_graphql(types::GetDeployAppAndVersion::build(
types::GetDeployAppAndVersionVars {
name,
owner,
version,
},
))
.await
}
/// Retrieve an app together with a specific version.
pub async fn get_app_and_package_by_name(
client: &WasmerClient,
vars: types::GetPackageAndAppVars,
) -> Result<(Option<types::Package>, Option<types::DeployApp>), anyhow::Error> {
let res = client
.run_graphql(types::GetPackageAndApp::build(vars))
.await?;
Ok((res.get_package, res.get_deploy_app))
}
/// Retrieve apps.
pub async fn get_deploy_apps(
client: &WasmerClient,
vars: types::GetDeployAppsVars,
) -> Result<DeployAppConnection, anyhow::Error> {
let res = client
.run_graphql(types::GetDeployApps::build(vars))
.await?;
res.get_deploy_apps.context("no apps returned")
}
/// Retrieve apps as a stream that will automatically paginate.
pub fn get_deploy_apps_stream(
client: &WasmerClient,
vars: types::GetDeployAppsVars,
) -> impl futures::Stream<Item = Result<Vec<DeployApp>, anyhow::Error>> + '_ {
futures::stream::try_unfold(
Some(vars),
move |vars: Option<types::GetDeployAppsVars>| async move {
let vars = match vars {
Some(vars) => vars,
None => return Ok(None),
};
let page = get_deploy_apps(client, vars.clone()).await?;
let end_cursor = page.page_info.end_cursor;
let items = page
.edges
.into_iter()
.filter_map(|x| x.and_then(|x| x.node))
.collect::<Vec<_>>();
let new_vars = end_cursor.map(|c| types::GetDeployAppsVars {
after: Some(c),
..vars
});
Ok(Some((items, new_vars)))
},
)
}
/// Retrieve versions for an app.
pub async fn get_deploy_app_versions(
client: &WasmerClient,
vars: GetDeployAppVersionsVars,
) -> Result<DeployAppVersionConnection, anyhow::Error> {
let res = client
.run_graphql_strict(types::GetDeployAppVersions::build(vars))
.await?;
let versions = res.get_deploy_app.context("app not found")?.versions;
Ok(versions)
}
/// Load all versions of an app.
///
/// Will paginate through all versions and return them in a single list.
pub async fn all_app_versions(
client: &WasmerClient,
owner: String,
name: String,
) -> Result<Vec<DeployAppVersion>, anyhow::Error> {
let mut vars = GetDeployAppVersionsVars {
owner,
name,
offset: None,
before: None,
after: None,
first: Some(10),
last: None,
sort_by: None,
};
let mut all_versions = Vec::<DeployAppVersion>::new();
loop {
let page = get_deploy_app_versions(client, vars.clone()).await?;
dbg!(&page);
if page.edges.is_empty() {
break;
}
for edge in page.edges {
let edge = match edge {
Some(edge) => edge,
None => continue,
};
let version = match edge.node {
Some(item) => item,
None => continue,
};
// Sanity check to avoid duplication.
if all_versions.iter().any(|v| v.id == version.id) == false {
all_versions.push(version);
}
// Update pagination.
vars.after = Some(edge.cursor);
}
}
Ok(all_versions)
}
/// Activate a particular version of an app.
pub async fn app_version_activate(
client: &WasmerClient,
version: String,
) -> Result<DeployApp, anyhow::Error> {
let res = client
.run_graphql_strict(types::MarkAppVersionAsActive::build(
types::MarkAppVersionAsActiveVars {
input: types::MarkAppVersionAsActiveInput {
app_version: version.into(),
},
},
))
.await?;
res.mark_app_version_as_active
.context("app not found")
.map(|x| x.app)
}
/// Retrieve a node based on its global id.
pub async fn get_node(
client: &WasmerClient,
id: String,
) -> Result<Option<types::Node>, anyhow::Error> {
client
.run_graphql(types::GetNode::build(types::GetNodeVars { id: id.into() }))
.await
.map(|x| x.node)
}
/// Retrieve an app by its global id.
pub async fn get_app_by_id(
client: &WasmerClient,
app_id: String,
) -> Result<DeployApp, anyhow::Error> {
client
.run_graphql(types::GetDeployAppById::build(
types::GetDeployAppByIdVars {
app_id: app_id.into(),
},
))
.await?
.app
.context("app not found")?
.into_deploy_app()
.context("app conversion failed")
}
/// Retrieve an app together with a specific version.
pub async fn get_app_with_version_by_id(
client: &WasmerClient,
app_id: String,
version_id: String,
) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
let res = client
.run_graphql(types::GetDeployAppAndVersionById::build(
types::GetDeployAppAndVersionByIdVars {
app_id: app_id.into(),
version_id: version_id.into(),
},
))
.await?;
let app = res
.app
.context("app not found")?
.into_deploy_app()
.context("app conversion failed")?;
let version = res
.version
.context("version not found")?
.into_deploy_app_version()
.context("version conversion failed")?;
Ok((app, version))
}
/// Retrieve an app version by its global id.
pub async fn get_app_version_by_id(
client: &WasmerClient,
version_id: String,
) -> Result<DeployAppVersion, anyhow::Error> {
client
.run_graphql(types::GetDeployAppVersionById::build(
types::GetDeployAppVersionByIdVars {
version_id: version_id.into(),
},
))
.await?
.version
.context("app not found")?
.into_deploy_app_version()
.context("app version conversion failed")
}
pub async fn get_app_version_by_id_with_app(
client: &WasmerClient,
version_id: String,
) -> Result<(DeployApp, DeployAppVersion), anyhow::Error> {
let version = client
.run_graphql(types::GetDeployAppVersionById::build(
types::GetDeployAppVersionByIdVars {
version_id: version_id.into(),
},
))
.await?
.version
.context("app not found")?
.into_deploy_app_version()
.context("app version conversion failed")?;
let app_id = version
.app
.as_ref()
.context("could not load app for version")?
.id
.clone();
let app = get_app_by_id(client, app_id.into_inner()).await?;
Ok((app, version))
}
/// List all apps that are accessible by the current user.
///
/// NOTE: this will only include the first pages and does not provide pagination.
pub async fn user_apps(client: &WasmerClient) -> Result<Vec<types::DeployApp>, anyhow::Error> {
let user = client
.run_graphql(types::GetCurrentUserWithApps::build(()))
.await?
.viewer
.context("not logged in")?;
let apps = user
.apps
.edges
.into_iter()
.flatten()
.filter_map(|x| x.node)
.collect();
Ok(apps)
}
/// List all apps that are accessible by the current user.
///
/// NOTE: this does not currently do full pagination properly.
// TODO(theduke): fix pagination
pub async fn user_accessible_apps(
client: &WasmerClient,
) -> Result<Vec<types::DeployApp>, anyhow::Error> {
let mut apps = Vec::new();
// Get user apps.
let user_apps = user_apps(client).await?;
apps.extend(user_apps);
// Get all aps in user-accessible namespaces.
let namespace_res = client
.run_graphql(types::GetCurrentUser::build(types::GetCurrentUserVars {
namespace_role: None,
}))
.await?;
let active_user = namespace_res.viewer.context("not logged in")?;
let namespace_names = active_user
.namespaces
.edges
.iter()
.filter_map(|edge| edge.as_ref())
.filter_map(|edge| edge.node.as_ref())
.map(|node| node.name.clone())
.collect::<Vec<_>>();
for namespace in namespace_names {
let out = client
.run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
name: namespace.to_string(),
}))
.await?;
if let Some(ns) = out.get_namespace {
let ns_apps = ns.apps.edges.into_iter().flatten().filter_map(|x| x.node);
apps.extend(ns_apps);
}
}
Ok(apps)
}
/// Get apps for a specific namespace.
///
/// NOTE: only retrieves the first page and does not do pagination.
pub async fn namespace_apps(
client: &WasmerClient,
namespace: &str,
) -> Result<Vec<types::DeployApp>, anyhow::Error> {
let res = client
.run_graphql(types::GetNamespaceApps::build(GetNamespaceAppsVars {
name: namespace.to_string(),
}))
.await?;
let ns = res
.get_namespace
.with_context(|| format!("failed to get namespace '{}'", namespace))?;
let apps = ns
.apps
.edges
.into_iter()
.flatten()
.filter_map(|x| x.node)
.collect();
Ok(apps)
}
/// Publish a new app (version).
pub async fn publish_deploy_app(
client: &WasmerClient,
vars: PublishDeployAppVars,
) -> Result<DeployAppVersion, anyhow::Error> {
let res = client
.run_graphql_raw(types::PublishDeployApp::build(vars))
.await?;
if let Some(app) = res
.data
.and_then(|d| d.publish_deploy_app)
.map(|d| d.deploy_app_version)
{
Ok(app)
} else {
Err(GraphQLApiFailure::from_errors(
"could not publish app",
res.errors,
))
}
}
/// Delete an app.
pub async fn delete_app(client: &WasmerClient, app_id: String) -> Result<(), anyhow::Error> {
let res = client
.run_graphql_strict(types::DeleteApp::build(types::DeleteAppVars {
app_id: app_id.into(),
}))
.await?
.delete_app
.context("API did not return data for the delete_app mutation")?;
if !res.success {
bail!("App deletion failed for an unknown reason");
}
Ok(())
}
/// Get all namespaces accessible by the current user.
pub async fn user_namespaces(
client: &WasmerClient,
) -> Result<Vec<types::Namespace>, anyhow::Error> {
let user = client
.run_graphql(types::GetCurrentUser::build(types::GetCurrentUserVars {
namespace_role: None,
}))
.await?
.viewer
.context("not logged in")?;
let ns = user
.namespaces
.edges
.into_iter()
.flatten()
// .filter_map(|x| x)
.filter_map(|x| x.node)
.collect();
Ok(ns)
}
/// Retrieve a namespace by its name.
pub async fn get_namespace(
client: &WasmerClient,
name: String,
) -> Result<Option<types::Namespace>, anyhow::Error> {
client
.run_graphql(types::GetNamespace::build(types::GetNamespaceVars { name }))
.await
.map(|x| x.get_namespace)
}
/// Create a new namespace.
pub async fn create_namespace(
client: &WasmerClient,
vars: CreateNamespaceVars,
) -> Result<types::Namespace, anyhow::Error> {
client
.run_graphql(types::CreateNamespace::build(vars))
.await?
.create_namespace
.map(|x| x.namespace)
.context("no namespace returned")
}
/// Retrieve a package by its name.
pub async fn get_package(
client: &WasmerClient,
name: String,
) -> Result<Option<types::Package>, anyhow::Error> {
client
.run_graphql_strict(types::GetPackage::build(types::GetPackageVars { name }))
.await
.map(|x| x.get_package)
}
/// Retrieve a package version by its name.
pub async fn get_package_version(
client: &WasmerClient,
name: String,
version: String,
) -> Result<Option<types::PackageVersionWithPackage>, anyhow::Error> {
client
.run_graphql_strict(types::GetPackageVersion::build(
types::GetPackageVersionVars { name, version },
))
.await
.map(|x| x.get_package_version)
}
/// Retrieve package versions for an app.
pub async fn get_package_versions(
client: &WasmerClient,
vars: types::AllPackageVersionsVars,
) -> Result<PackageVersionConnection, anyhow::Error> {
let res = client
.run_graphql(types::GetAllPackageVersions::build(vars))
.await?;
Ok(res.all_package_versions)
}
/// Retrieve all versions of a package as a stream that auto-paginates.
pub fn get_package_versions_stream(
client: &WasmerClient,
vars: types::AllPackageVersionsVars,
) -> impl futures::Stream<Item = Result<Vec<types::PackageVersionWithPackage>, anyhow::Error>> + '_
{
futures::stream::try_unfold(
Some(vars),
move |vars: Option<types::AllPackageVersionsVars>| async move {
let vars = match vars {
Some(vars) => vars,
None => return Ok(None),
};
let page = get_package_versions(client, vars.clone()).await?;
let end_cursor = page.page_info.end_cursor;
let items = page
.edges
.into_iter()
.filter_map(|x| x.and_then(|x| x.node))
.collect::<Vec<_>>();
let new_vars = end_cursor.map(|cursor| types::AllPackageVersionsVars {
after: Some(cursor),
..vars
});
Ok(Some((items, new_vars)))
},
)
}
/// Generate a new Edge token.
pub async fn generate_deploy_token_raw(
client: &WasmerClient,
app_version_id: String,
) -> Result<String, anyhow::Error> {
let res = client
.run_graphql(types::GenerateDeployToken::build(
types::GenerateDeployTokenVars { app_version_id },
))
.await?;
res.generate_deploy_token
.map(|x| x.token)
.context("no token returned")
}
#[derive(Debug, PartialEq)]
pub enum GenerateTokenBy {
Id(NetworkTokenV1),
}
#[derive(Debug, PartialEq)]
pub enum TokenKind {
SSH,
Network(GenerateTokenBy),
}
pub async fn generate_deploy_config_token_raw(
client: &WasmerClient,
token_kind: TokenKind,
) -> Result<String, anyhow::Error> {
let res = client
.run_graphql(types::GenerateDeployConfigToken::build(
types::GenerateDeployConfigTokenVars {
input: match token_kind {
TokenKind::SSH => "{}".to_string(),
TokenKind::Network(by) => match by {
GenerateTokenBy::Id(token) => serde_json::to_string(&token)?,
},
},
},
))
.await?;
res.generate_deploy_config_token
.map(|x| x.token)
.context("no token returned")
}
/// Get pages of logs associated with an application that lie within the
/// specified date range.
// NOTE: this is not public due to severe usability issues.
// The stream can loop forever due to re-fetching the same logs over and over.
#[tracing::instrument(skip_all, level = "debug")]
#[allow(clippy::let_with_type_underscore)]
fn get_app_logs(
client: &WasmerClient,
name: String,
owner: String,
tag: Option<String>,
start: OffsetDateTime,
end: Option<OffsetDateTime>,
watch: bool,
) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
// Note: the backend will limit responses to a certain number of log
// messages, so we use try_unfold() to keep calling it until we stop getting
// new log messages.
let span = tracing::Span::current();
futures::stream::try_unfold(start, move |start| {
let variables = types::GetDeployAppLogsVars {
name: name.clone(),
owner: owner.clone(),
version: tag.clone(),
// TODO: increase pagination size
// See https://github.com/wasmerio/edge/issues/460
// first: Some(500),
first: Some(10),
starting_from: unix_timestamp(start),
until: end.map(unix_timestamp),
};
let fut = async move {
loop {
let deploy_app_version = client
.run_graphql(types::GetDeployAppLogs::build(variables.clone()))
.await?
.get_deploy_app_version
.context("unknown package version")?;
let page: Vec<_> = deploy_app_version
.logs
.edges
.into_iter()
.flatten()
.filter_map(|edge| edge.node)
.collect();
if page.is_empty() {
if watch {
/*
TODO: the resolution of watch should be configurable
TODO: should this be async?
*/
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
break Ok(None);
} else {
let last_message = page.last().expect("The page is non-empty");
let timestamp = last_message.timestamp;
// NOTE: adding 1 microsecond to the timestamp to avoid fetching
// the last message again.
let timestamp = OffsetDateTime::from_unix_timestamp_nanos(timestamp as i128)
.with_context(|| {
format!("Unable to interpret {timestamp} as a unix timestamp")
})?;
// FIXME: We need a better way to tell the backend "give me the
// next set of logs". Adding 1 nanosecond could theoretically
// mean we miss messages if multiple log messages arrived at
// the same nanosecond and the page ended midway.
let next_timestamp = timestamp + Duration::from_nanos(1_000);
break Ok(Some((page, next_timestamp)));
}
}
};
fut.instrument(span.clone())
})
}
/// Get pages of logs associated with an application that lie within the
/// specified date range.
///
/// In contrast to [`get_app_logs`], this function collects the stream into a
/// final vector.
#[tracing::instrument(skip_all, level = "debug")]
#[allow(clippy::let_with_type_underscore)]
pub async fn get_app_logs_paginated(
client: &WasmerClient,
name: String,
owner: String,
tag: Option<String>,
start: OffsetDateTime,
end: Option<OffsetDateTime>,
watch: bool,
) -> impl futures::Stream<Item = Result<Vec<Log>, anyhow::Error>> + '_ {
let stream = get_app_logs(client, name, owner, tag, start, end, watch);
stream.map(|res| {
let mut logs = Vec::new();
let mut hasher = HashSet::new();
let mut page = res?;
// Prevent duplicates.
// TODO: don't clone the message, just hash it.
page.retain(|log| hasher.insert((log.message.clone(), log.timestamp.round() as i128)));
logs.extend(page);
Ok(logs)
})
}
/// Convert a [`OffsetDateTime`] to a unix timestamp that the WAPM backend
/// understands.
fn unix_timestamp(ts: OffsetDateTime) -> f64 {
let nanos_per_second = 1_000_000_000;
let timestamp = ts.unix_timestamp_nanos();
let nanos = timestamp % nanos_per_second;
let secs = timestamp / nanos_per_second;
(secs as f64) + (nanos as f64 / nanos_per_second as f64)
}

View File

@@ -0,0 +1,119 @@
use std::{collections::VecDeque, task::Poll};
use futures::{
future::{BoxFuture, OptionFuture},
Future,
};
use super::WasmerClient;
type PaginationFuture<I, P> = BoxFuture<'static, Result<(Vec<I>, Option<P>), anyhow::Error>>;
pub trait PaginatedQuery {
type Vars;
type Paginator;
type Item;
fn query(
&self,
client: WasmerClient,
paginator: Option<Self::Paginator>,
) -> PaginationFuture<Self::Item, Self::Paginator>;
}
pin_project_lite::pin_project! {
pub struct QueryStream<Q: PaginatedQuery> {
query: Q,
client: WasmerClient,
page: usize,
paginator: Option<Q::Paginator>,
finished: bool,
items: VecDeque<Q::Item>,
#[pin]
fut: OptionFuture<PaginationFuture<Q::Item, Q::Paginator>>,
}
}
impl<Q: PaginatedQuery> QueryStream<Q> {
pub fn new(query: Q, client: WasmerClient) -> Self {
Self {
query,
client,
page: 0,
finished: false,
paginator: None,
items: VecDeque::new(),
fut: None.into(),
}
}
}
impl<Q: PaginatedQuery> futures::Stream for QueryStream<Q> {
type Item = Result<Q::Item, anyhow::Error>;
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
let mut this = self.project();
if let Some(item) = this.items.pop_front() {
return Poll::Ready(Some(Ok(item)));
}
match this.fut.as_mut().poll(cx) {
Poll::Ready(None) => {}
Poll::Ready(Some(Ok((items, paginator)))) => {
*this.paginator = paginator;
*this.page += 1;
// *this.fut = None.into();
this.items.extend(items);
this.fut.set(None.into());
if let Some(item) = this.items.pop_front() {
return Poll::Ready(Some(Ok(item)));
}
}
Poll::Ready(Some(Err(err))) => {
return Poll::Ready(Some(Err(err)));
}
Poll::Pending => {
return Poll::Pending;
}
};
let pager = match this.paginator.take() {
Some(p) => Some(p),
None if *this.page == 0 => None,
None => {
return Poll::Ready(None);
}
};
let f = this.query.query(this.client.clone(), pager);
this.fut.set(Some(f).into());
match this.fut.as_mut().poll(cx) {
Poll::Ready(None) => {
unreachable!()
}
Poll::Ready(Some(Ok((items, paginator)))) => {
*this.paginator = paginator;
*this.page += 1;
// *this.fut = None.into();
this.items.extend(items);
this.fut.set(None.into());
if let Some(item) = this.items.pop_front() {
Poll::Ready(Some(Ok(item)))
} else {
Poll::Ready(None)
}
}
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
Poll::Pending => Poll::Pending,
}
}
}

View File

@@ -0,0 +1,740 @@
pub use queries::*;
pub use cynic::Id;
#[cynic::schema_for_derives(file = r#"schema.graphql"#, module = "schema")]
mod queries {
use serde::Serialize;
use time::OffsetDateTime;
use super::schema;
#[derive(cynic::Scalar, Debug, Clone)]
pub struct DateTime(pub String);
impl TryFrom<OffsetDateTime> for DateTime {
type Error = time::error::Format;
fn try_from(value: OffsetDateTime) -> Result<Self, Self::Error> {
value
.format(&time::format_description::well_known::Rfc3339)
.map(Self)
}
}
impl TryFrom<DateTime> for OffsetDateTime {
type Error = time::error::Parse;
fn try_from(value: DateTime) -> Result<Self, Self::Error> {
OffsetDateTime::parse(&value.0, &time::format_description::well_known::Rfc3339)
}
}
#[derive(cynic::Scalar, Debug, Clone)]
pub struct JSONString(pub String);
#[derive(cynic::Enum, Clone, Copy, Debug)]
pub enum GrapheneRole {
Admin,
Editor,
Viewer,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetCurrentUserVars {
pub namespace_role: Option<GrapheneRole>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetCurrentUserVars")]
pub struct GetCurrentUser {
pub viewer: Option<UserWithNamespaces>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct User {
pub id: cynic::Id,
pub username: String,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
pub struct Package {
pub id: cynic::Id,
pub package_name: String,
pub namespace: Option<String>,
pub last_version: Option<PackageVersion>,
pub private: bool,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
pub struct PackageDistribution {
pub pirita_sha256_hash: Option<String>,
pub pirita_download_url: Option<String>,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
pub struct PackageVersion {
pub id: cynic::Id,
pub version: String,
pub created_at: DateTime,
pub distribution: PackageDistribution,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
#[cynic(graphql_type = "PackageVersion")]
pub struct PackageVersionWithPackage {
pub id: cynic::Id,
pub version: String,
pub created_at: DateTime,
pub pirita_manifest: Option<JSONString>,
pub distribution: PackageDistribution,
pub package: Package,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetPackageVars {
pub name: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetPackageVars")]
pub struct GetPackage {
#[arguments(name: $name)]
pub get_package: Option<Package>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetPackageVersionVars {
pub name: String,
pub version: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetPackageVersionVars")]
pub struct GetPackageVersion {
#[arguments(name: $name, version: $version)]
pub get_package_version: Option<PackageVersionWithPackage>,
}
#[derive(cynic::Enum, Clone, Copy, Debug)]
pub enum PackageVersionSortBy {
Newest,
Oldest,
}
#[derive(cynic::QueryVariables, Debug, Clone, Default)]
pub struct AllPackageVersionsVars {
pub offset: Option<i32>,
pub before: Option<String>,
pub after: Option<String>,
pub first: Option<i32>,
pub last: Option<i32>,
pub created_after: Option<DateTime>,
pub updated_after: Option<DateTime>,
pub sort_by: Option<PackageVersionSortBy>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "AllPackageVersionsVars")]
pub struct GetAllPackageVersions {
#[arguments(
first: $first,
last: $last,
after: $after,
before: $before,
offset: $offset,
updatedAfter: $updated_after,
createdAfter: $created_after,
sortBy: $sort_by,
)]
pub all_package_versions: PackageVersionConnection,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct PackageVersionConnection {
pub page_info: PageInfo,
pub edges: Vec<Option<PackageVersionEdge>>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct PackageVersionEdge {
pub node: Option<PackageVersionWithPackage>,
pub cursor: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetPackageAndAppVars {
pub package: String,
pub app_owner: String,
pub app_name: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetPackageAndAppVars")]
pub struct GetPackageAndApp {
#[arguments(name: $package)]
pub get_package: Option<Package>,
#[arguments(owner: $app_owner, name: $app_name)]
pub get_deploy_app: Option<DeployApp>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query")]
pub struct GetCurrentUserWithApps {
pub viewer: Option<UserWithApps>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "User")]
pub struct UserWithApps {
pub id: cynic::Id,
pub username: String,
pub apps: DeployAppConnection,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct Owner {
pub global_name: String,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
#[cynic(graphql_type = "User", variables = "GetCurrentUserVars")]
pub struct UserWithNamespaces {
pub id: cynic::Id,
pub username: String,
#[arguments(role: $namespace_role)]
pub namespaces: NamespaceConnection,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetUserAppsVars {
pub username: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetUserAppsVars")]
pub struct GetUserApps {
#[arguments(username: $username)]
pub get_user: Option<User>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetDeployAppVars {
pub name: String,
pub owner: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppVars")]
pub struct GetDeployApp {
#[arguments(owner: $owner, name: $name)]
pub get_deploy_app: Option<DeployApp>,
}
#[derive(cynic::QueryVariables, Debug, Clone)]
pub struct PaginationVars {
pub offset: Option<i32>,
pub before: Option<String>,
pub after: Option<String>,
pub first: Option<i32>,
pub last: Option<i32>,
}
#[derive(cynic::Enum, Clone, Copy, Debug)]
pub enum DeployAppsSortBy {
Newest,
Oldest,
MostActive,
}
#[derive(cynic::QueryVariables, Debug, Clone, Default)]
pub struct GetDeployAppsVars {
pub offset: Option<i32>,
pub before: Option<String>,
pub after: Option<String>,
pub first: Option<i32>,
pub last: Option<i32>,
pub updated_after: Option<DateTime>,
pub sort_by: Option<DeployAppsSortBy>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppsVars")]
pub struct GetDeployApps {
#[arguments(
first: $first,
last: $last,
after: $after,
before: $before,
offset: $offset,
updatedAfter: $updated_after,
sortBy: $sort_by,
)]
pub get_deploy_apps: Option<DeployAppConnection>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetDeployAppByAliasVars {
pub alias: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppByAliasVars")]
pub struct GetDeployAppByAlias {
#[arguments(alias: $alias)]
pub get_app_by_global_alias: Option<DeployApp>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetDeployAppAndVersionVars {
pub name: String,
pub owner: String,
pub version: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppAndVersionVars")]
pub struct GetDeployAppAndVersion {
#[arguments(owner: $owner, name: $name)]
pub get_deploy_app: Option<DeployApp>,
#[arguments(owner: $owner, name: $name, version: $version)]
pub get_deploy_app_version: Option<DeployAppVersion>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetDeployAppVersionVars {
pub name: String,
pub owner: String,
pub version: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppVersionVars")]
pub struct GetDeployAppVersion {
#[arguments(owner: $owner, name: $name, version: $version)]
pub get_deploy_app_version: Option<DeployAppVersion>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct CreateNamespaceVars {
pub name: String,
pub description: Option<String>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "CreateNamespaceVars")]
pub struct CreateNamespace {
#[arguments(input: {name: $name, description: $description})]
pub create_namespace: Option<CreateNamespacePayload>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct CreateNamespacePayload {
pub namespace: Namespace,
}
#[derive(cynic::InputObject, Debug)]
pub struct CreateNamespaceInput {
pub name: String,
pub display_name: Option<String>,
pub description: Option<String>,
pub avatar: Option<String>,
pub client_mutation_id: Option<String>,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
pub struct NamespaceEdge {
pub node: Option<Namespace>,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
pub struct NamespaceConnection {
pub edges: Vec<Option<NamespaceEdge>>,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct Namespace {
pub id: cynic::Id,
pub name: String,
pub global_name: String,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct DeployApp {
pub id: cynic::Id,
pub name: String,
pub created_at: DateTime,
pub description: Option<String>,
pub active_version: DeployAppVersion,
pub admin_url: String,
pub owner: Owner,
pub url: String,
pub deleted: bool,
pub aliases: AppAliasConnection,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct AppAliasConnection {
pub page_info: PageInfo,
pub edges: Vec<Option<AppAliasEdge>>,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct AppAliasEdge {
pub node: Option<AppAlias>,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct AppAlias {
pub name: String,
}
#[derive(cynic::QueryVariables, Debug, Clone)]
pub struct DeleteAppVars {
pub app_id: cynic::Id,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct DeleteAppPayload {
pub success: bool,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "DeleteAppVars")]
pub struct DeleteApp {
#[arguments(input: { id: $app_id })]
pub delete_app: Option<DeleteAppPayload>,
}
#[derive(cynic::Enum, Clone, Copy, Debug)]
pub enum DeployAppVersionsSortBy {
Newest,
Oldest,
}
#[derive(cynic::QueryVariables, Debug, Clone)]
pub struct GetDeployAppVersionsVars {
pub owner: String,
pub name: String,
pub offset: Option<i32>,
pub before: Option<String>,
pub after: Option<String>,
pub first: Option<i32>,
pub last: Option<i32>,
pub sort_by: Option<DeployAppVersionsSortBy>,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppVersionsVars")]
pub struct GetDeployAppVersions {
#[arguments(owner: $owner, name: $name)]
pub get_deploy_app: Option<DeployAppVersions>,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
#[cynic(graphql_type = "DeployApp", variables = "GetDeployAppVersionsVars")]
pub struct DeployAppVersions {
#[arguments(
first: $first,
last: $last,
before: $before,
after: $after,
offset: $offset,
sortBy: $sort_by
)]
pub versions: DeployAppVersionConnection,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
#[cynic(graphql_type = "DeployApp")]
pub struct SparseDeployApp {
pub id: cynic::Id,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct DeployAppVersion {
pub id: cynic::Id,
pub created_at: DateTime,
pub version: String,
pub description: Option<String>,
pub yaml_config: String,
pub user_yaml_config: String,
pub config: String,
pub json_config: String,
pub url: String,
pub app: Option<SparseDeployApp>,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
pub struct DeployAppVersionConnection {
pub page_info: PageInfo,
pub edges: Vec<Option<DeployAppVersionEdge>>,
}
#[derive(cynic::QueryFragment, Debug, Clone)]
pub struct DeployAppVersionEdge {
pub node: Option<DeployAppVersion>,
pub cursor: String,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct DeployAppConnection {
pub page_info: PageInfo,
pub edges: Vec<Option<DeployAppEdge>>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct DeployAppEdge {
pub node: Option<DeployApp>,
pub cursor: String,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct PageInfo {
pub has_next_page: bool,
pub end_cursor: Option<String>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetNamespaceVars {
pub name: String,
}
#[derive(cynic::QueryFragment, Serialize, Debug, Clone)]
pub struct MarkAppVersionAsActivePayload {
pub app: DeployApp,
}
#[derive(cynic::InputObject, Debug)]
pub struct MarkAppVersionAsActiveInput {
pub app_version: cynic::Id,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct MarkAppVersionAsActiveVars {
pub input: MarkAppVersionAsActiveInput,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "MarkAppVersionAsActiveVars")]
pub struct MarkAppVersionAsActive {
#[arguments(input: $input)]
pub mark_app_version_as_active: Option<MarkAppVersionAsActivePayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetNamespaceVars")]
pub struct GetNamespace {
#[arguments(name: $name)]
pub get_namespace: Option<Namespace>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetNamespaceAppsVars {
pub name: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetNamespaceAppsVars")]
pub struct GetNamespaceApps {
#[arguments(name: $name)]
pub get_namespace: Option<NamespaceWithApps>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Namespace")]
pub struct NamespaceWithApps {
pub id: cynic::Id,
pub name: String,
pub apps: DeployAppConnection,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct PublishDeployAppVars {
pub config: String,
pub name: cynic::Id,
pub owner: Option<cynic::Id>,
pub make_default: Option<bool>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "PublishDeployAppVars")]
pub struct PublishDeployApp {
#[arguments(input: { config: { yamlConfig: $config }, name: $name, owner: $owner, makeDefault: $make_default })]
pub publish_deploy_app: Option<PublishDeployAppPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct PublishDeployAppPayload {
pub deploy_app_version: DeployAppVersion,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GenerateDeployTokenVars {
pub app_version_id: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "GenerateDeployTokenVars")]
pub struct GenerateDeployToken {
#[arguments(input: { deployConfigVersionId: $app_version_id })]
pub generate_deploy_token: Option<GenerateDeployTokenPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct GenerateDeployTokenPayload {
pub token: String,
}
#[derive(cynic::QueryVariables, Debug, Clone)]
pub struct GetDeployAppLogsVars {
pub name: String,
pub owner: String,
/// The tag associated with a particular app version. Uses the active
/// version if not provided.
pub version: Option<String>,
/// The lower bound for log messages, in nanoseconds since the Unix
/// epoch.
pub starting_from: f64,
/// The upper bound for log messages, in nanoseconds since the Unix
/// epoch.
pub until: Option<f64>,
pub first: Option<i32>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppLogsVars")]
pub struct GetDeployAppLogs {
#[arguments(name: $name, owner: $owner, version: $version)]
pub get_deploy_app_version: Option<DeployAppVersionLogs>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "DeployAppVersion", variables = "GetDeployAppLogsVars")]
pub struct DeployAppVersionLogs {
#[arguments(startingFrom: $starting_from, until: $until, first: $first)]
pub logs: LogConnection,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct LogConnection {
pub edges: Vec<Option<LogEdge>>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct LogEdge {
pub node: Option<Log>,
}
#[derive(cynic::QueryFragment, Debug, serde::Serialize, PartialEq)]
pub struct Log {
pub message: String,
/// When the message was recorded, in nanoseconds since the Unix epoch.
pub timestamp: f64,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GenerateDeployConfigTokenVars {
pub input: String,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "GenerateDeployConfigTokenVars")]
pub struct GenerateDeployConfigToken {
#[arguments(input: { config: $input })]
pub generate_deploy_config_token: Option<GenerateDeployConfigTokenPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct GenerateDeployConfigTokenPayload {
pub token: String,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetNodeVars {
pub id: cynic::Id,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetNodeVars")]
pub struct GetNode {
#[arguments(id: $id)]
pub node: Option<Node>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetDeployAppByIdVars {
pub app_id: cynic::Id,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppByIdVars")]
pub struct GetDeployAppById {
#[arguments(id: $app_id)]
#[cynic(rename = "node")]
pub app: Option<Node>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetDeployAppAndVersionByIdVars {
pub app_id: cynic::Id,
pub version_id: cynic::Id,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppAndVersionByIdVars")]
pub struct GetDeployAppAndVersionById {
#[arguments(id: $app_id)]
#[cynic(rename = "node")]
pub app: Option<Node>,
#[arguments(id: $version_id)]
#[cynic(rename = "node")]
pub version: Option<Node>,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct GetDeployAppVersionByIdVars {
pub version_id: cynic::Id,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "GetDeployAppVersionByIdVars")]
pub struct GetDeployAppVersionById {
#[arguments(id: $version_id)]
#[cynic(rename = "node")]
pub version: Option<Node>,
}
#[derive(cynic::InlineFragments, Debug)]
pub enum Node {
DeployApp(Box<DeployApp>),
DeployAppVersion(Box<DeployAppVersion>),
#[cynic(fallback)]
Unknown,
}
impl Node {
pub fn into_deploy_app(self) -> Option<DeployApp> {
match self {
Node::DeployApp(app) => Some(*app),
_ => None,
}
}
pub fn into_deploy_app_version(self) -> Option<DeployAppVersion> {
match self {
Node::DeployAppVersion(version) => Some(*version),
_ => None,
}
}
}
}
#[allow(non_snake_case, non_camel_case_types)]
mod schema {
cynic::use_schema!(r#"schema.graphql"#);
}