diff --git a/Cargo.lock b/Cargo.lock index c5fc8a0ac..4e3c42c53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index b01512e29..85c024b70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/lib/backend-api/CHANGELOG.md b/lib/backend-api/CHANGELOG.md new file mode 100644 index 000000000..5d313e1be --- /dev/null +++ b/lib/backend-api/CHANGELOG.md @@ -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) + + + + + + + + + + + +### Refactor (BREAKING) + + - 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 + + - Add description to wasmer-api Cargo.toml + Required for releasing. + +### Other + + - Dependency cleanup + * Lift some dependencies to workspace.dependencies to avoid duplication + * Remove a bunch of unused dependencies + - Add crate metadata and prepare for first CLI release + - "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) + - Add a webc app config fetch tests + - Make serde_json a workspace dependency + To avoid duplication... + - Lift serde to be a workspace dependency + Easier version management... + - Lift anyhow, time and clap to workspace dependnecies + Less version management... + +### Bug Fixes + + - 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. + - 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 + + - Add generate_deploy_token to new cynic GQL api client + Will be needed for various commands + - Add getPackage GQL query + - Add query for DeployAppVersion + - Add new namespace and app commands + - Add a CapabilityLoggingV1 config + Allows to configure the logging behaviour of workloads. + + Will be used very soon to implement instance log forwarding. + +### Documentation + + - add some changelogs + - Add REAMDE to Cargo.toml of to-be-published crates + +### Chore + + - Remove download_url from WebcPackageIdentifierV1 + Not needed anymore, since now we have a deployment config registry. + + +Needed to update relevant callsites + + diff --git a/lib/backend-api/Cargo.toml b/lib/backend-api/Cargo.toml new file mode 100644 index 000000000..658075af2 --- /dev/null +++ b/lib/backend-api/Cargo.toml @@ -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"] } diff --git a/lib/backend-api/README.md b/lib/backend-api/README.md new file mode 100644 index 000000000..7aa1360c9 --- /dev/null +++ b/lib/backend-api/README.md @@ -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 diff --git a/lib/backend-api/schema.graphql b/lib/backend-api/schema.graphql new file mode 100644 index 000000000..081e28db4 --- /dev/null +++ b/lib/backend-api/schema.graphql @@ -0,0 +1,2624 @@ +interface Node { + """The ID of the object""" + id: ID! +} + +type PublicKey implements Node { + """The ID of the object""" + id: ID! + owner: User! + keyId: String! + key: String! + revokedAt: DateTime + uploadedAt: DateTime! + verifyingSignature: Signature + revoked: Boolean! +} + +type User implements Node & PackageOwner & Owner { + firstName: String! + lastName: String! + email: String! + dateJoined: DateTime! + + """Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.""" + username: String! + isEmailValidated: Boolean! + bio: String + location: String + websiteUrl: String + + """The ID of the object""" + id: ID! + globalName: String! + avatar(size: Int = 80): String! + isViewer: Boolean! + hasUsablePassword: Boolean + fullName: String! + githubUrl: String + twitterUrl: String + companyRole: String + companyDescription: String + publicActivity(before: String, after: String, first: Int, last: Int): ActivityEventConnection! + billing: Billing + waitlist(name: String!): WaitlistMember + namespaces(role: GrapheneRole, offset: Int, before: String, after: String, first: Int, last: Int): NamespaceConnection! + packages(collaborating: Boolean = false, offset: Int, before: String, after: String, first: Int, last: Int): PackageConnection! + apps(collaborating: Boolean = false, sortBy: DeployAppsSortBy, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppConnection! + usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! + isStaff: Boolean + packageVersions(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! + packageTransfersIncoming(before: String, after: String, first: Int, last: Int): PackageTransferRequestConnection! + packageInvitesIncoming(before: String, after: String, first: Int, last: Int): PackageCollaboratorInviteConnection! + namespaceInvitesIncoming(before: String, after: String, first: Int, last: Int): NamespaceCollaboratorInviteConnection! + apiTokens(before: String, after: String, first: Int, last: Int): APITokenConnection! + notifications(before: String, after: String, first: Int, last: Int): UserNotificationConnection! + loginMethods: [LoginMethod!]! +} + +"""Setup for backwards compatibility with existing frontends.""" +interface PackageOwner { + globalName: String! +} + +interface Owner { + globalName: String! +} + +""" +The `DateTime` scalar type represents a DateTime +value as specified by +[iso8601](https://en.wikipedia.org/wiki/ISO_8601). +""" +scalar DateTime + +type ActivityEventConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [ActivityEventEdge]! +} + +""" +The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. +""" +type PageInfo { + """When paginating forwards, are there more items?""" + hasNextPage: Boolean! + + """When paginating backwards, are there more items?""" + hasPreviousPage: Boolean! + + """When paginating backwards, the cursor to continue.""" + startCursor: String + + """When paginating forwards, the cursor to continue.""" + endCursor: String +} + +"""A Relay edge containing a `ActivityEvent` and its cursor.""" +type ActivityEventEdge { + """The item at the end of the edge""" + node: ActivityEvent + + """A cursor for use in pagination""" + cursor: String! +} + +type ActivityEvent implements Node { + """The ID of the object""" + id: ID! + body: ActivityEventBody! + actorIcon: String! + createdAt: DateTime! +} + +type ActivityEventBody { + text: String! + ranges: [NodeBodyRange!]! +} + +type NodeBodyRange { + entity: Node! + offset: Int! + length: Int! +} + +type WaitlistMember implements Node { + waitlist: Waitlist! + joinedAt: DateTime! + approvedAt: DateTime + + """The ID of the object""" + id: ID! + member: Owner! + approved: Boolean! +} + +type Waitlist implements Node { + name: String! + createdAt: DateTime! + updatedAt: DateTime! + + """The ID of the object""" + id: ID! +} + +type NamespaceConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NamespaceEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `Namespace` and its cursor.""" +type NamespaceEdge { + """The item at the end of the edge""" + node: Namespace + + """A cursor for use in pagination""" + cursor: String! +} + +type Namespace implements Node & PackageOwner & Owner { + """The ID of the object""" + id: ID! + name: String! + displayName: String + description: String! + avatar: String! + avatarUpdatedAt: DateTime + createdAt: DateTime! + updatedAt: DateTime! + maintainerInvites(offset: Int, before: String, after: String, first: Int, last: Int): NamespaceCollaboratorInviteConnection! + userSet(offset: Int, before: String, after: String, first: Int, last: Int): UserConnection! + globalName: String! + packages(offset: Int, before: String, after: String, first: Int, last: Int): PackageConnection! + apps(sortBy: DeployAppsSortBy, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppConnection! + packageVersions(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! + collaborators(offset: Int, before: String, after: String, first: Int, last: Int): NamespaceCollaboratorConnection! + publicActivity(before: String, after: String, first: Int, last: Int): ActivityEventConnection! + pendingInvites(before: String, after: String, first: Int, last: Int): NamespaceCollaboratorInviteConnection! + viewerHasRole(role: GrapheneRole!): Boolean! + packageTransfersIncoming(before: String, after: String, first: Int, last: Int): PackageTransferRequestConnection! + usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! +} + +type NamespaceCollaboratorInviteConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NamespaceCollaboratorInviteEdge]! +} + +""" +A Relay edge containing a `NamespaceCollaboratorInvite` and its cursor. +""" +type NamespaceCollaboratorInviteEdge { + """The item at the end of the edge""" + node: NamespaceCollaboratorInvite + + """A cursor for use in pagination""" + cursor: String! +} + +type NamespaceCollaboratorInvite implements Node { + """The ID of the object""" + id: ID! + requestedBy: User! + user: User + inviteEmail: String + namespace: Namespace! + role: RegistryNamespaceMaintainerInviteRoleChoices! + accepted: NamespaceCollaborator + approvedBy: User + declinedBy: User + createdAt: DateTime! + expiresAt: DateTime! + closedAt: DateTime +} + +enum RegistryNamespaceMaintainerInviteRoleChoices { + """Admin""" + ADMIN + + """Editor""" + EDITOR + + """Viewer""" + VIEWER +} + +type NamespaceCollaborator implements Node { + """The ID of the object""" + id: ID! + user: User! + role: RegistryNamespaceMaintainerRoleChoices! + namespace: Namespace! + createdAt: DateTime! + updatedAt: DateTime! + invite: NamespaceCollaboratorInvite +} + +enum RegistryNamespaceMaintainerRoleChoices { + """Admin""" + ADMIN + + """Editor""" + EDITOR + + """Viewer""" + VIEWER +} + +type UserConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [UserEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `User` and its cursor.""" +type UserEdge { + """The item at the end of the edge""" + node: User + + """A cursor for use in pagination""" + cursor: String! +} + +type PackageConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `Package` and its cursor.""" +type PackageEdge { + """The item at the end of the edge""" + node: Package + + """A cursor for use in pagination""" + cursor: String! +} + +type Package implements Likeable & Node & PackageOwner { + """The ID of the object""" + id: ID! + name: String! + private: Boolean! + createdAt: DateTime! + updatedAt: DateTime! + maintainers: [User]! @deprecated(reason: "Please use collaborators instead") + curated: Boolean! + ownerObjectId: Int! + lastVersion: PackageVersion + + """The app icon. It should be formatted in the same way as Apple icons""" + icon: String! + totalDownloads: Int! + iconUpdatedAt: DateTime + watchersCount: Int! + versions: [PackageVersion]! + collectionSet: [Collection!]! + likersCount: Int! + viewerHasLiked: Boolean! + globalName: String! + alias: String + namespace: String! + displayName: String! + + """The name of the package without the owner""" + packageName: String! + + """The app icon. It should be formatted in the same way as Apple icons""" + appIcon: String! @deprecated(reason: "Please use icon instead") + + """The total number of downloads of the package""" + downloadsCount: Int + + """The public keys for all the published versions""" + publicKeys: [PublicKey!]! + collaborators(offset: Int, before: String, after: String, first: Int, last: Int): PackageCollaboratorConnection! + pendingInvites(before: String, after: String, first: Int, last: Int): PackageCollaboratorInviteConnection! + viewerHasRole(role: GrapheneRole!): Boolean! + owner: PackageOwner! + isTransferring: Boolean! + activeTransferRequest: PackageTransferRequest + isArchived: Boolean! + viewerIsWatching: Boolean! + similarPackageVersions(before: String, after: String, first: Int, last: Int): PackageSearchConnection! +} + +interface Likeable { + id: ID! + likersCount: Int! + viewerHasLiked: Boolean! +} + +type PackageVersion implements Node { + """The ID of the object""" + id: ID! + package: Package! + version: String! + description: String! + manifest: String! + license: String + licenseFile: String + readme: String + witMd: String + repository: String + homepage: String + createdAt: DateTime! + updatedAt: DateTime! + staticObjectsCompiled: Boolean! + nativeExecutablesCompiled: Boolean! + publishedBy: User! + signature: Signature + isArchived: Boolean! + file: String! + + """""" + fileSize: BigInt! + piritaFile: String + + """""" + piritaFileSize: BigInt! + piritaManifest: JSONString + piritaVolumes: JSONString + totalDownloads: Int! + pirita256hash: String @deprecated(reason: "Please use distribution.piritaSha256Hash instead.") + bindingsState: RegistryPackageVersionBindingsStateChoices! + nativeExecutablesState: RegistryPackageVersionNativeExecutablesStateChoices! + + """List of direct dependencies of this package version""" + dependencies(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! + deployappversionSet(offset: Int, before: String, after: String, first: Int, last: Int): DeployAppVersionConnection! + lastversionPackage(offset: Int, before: String, after: String, first: Int, last: Int): PackageConnection! + commands: [Command!]! + nativeexecutableSet(offset: Int, before: String, after: String, first: Int, last: Int): NativeExecutableConnection! + bindingsgeneratorSet(offset: Int, before: String, after: String, first: Int, last: Int): BindingsGeneratorConnection! + javascriptlanguagebindingSet(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionNPMBindingConnection! + pythonlanguagebindingSet(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionPythonBindingConnection! + distribution: PackageDistribution! + filesystem: [PackageVersionFilesystem]! + isLastVersion: Boolean! + witFile: String + isSigned: Boolean! + moduleInterfaces: [InterfaceVersion!]! + modules: [PackageVersionModule!]! + getPiritaContents(volume: String! = "atom", root: String! = ""): [PiritaFilesystemItem!]! + nativeExecutables(triple: String, wasmerCompilerVersion: String): [NativeExecutable] + bindings: [PackageVersionLanguageBinding]! + npmBindings: PackageVersionNPMBinding + pythonBindings: PackageVersionPythonBinding + hasBindings: Boolean! + hasCommands: Boolean! +} + +""" +The `BigInt` scalar type represents non-fractional whole numeric values. +`BigInt` is not constrained to 32-bit like the `Int` type and thus is a less +compatible type. +""" +scalar BigInt + +""" +Allows use of a JSON String for input / output from the GraphQL schema. + +Use of this type is *not recommended* as you lose the benefits of having a defined, static +schema (one of the key benefits of GraphQL). +""" +scalar JSONString + +enum RegistryPackageVersionBindingsStateChoices { + """Bindings are not detected""" + NOT_PRESENT + + """Bindings are being built""" + GENERATING + + """Bindings generation has failed""" + ERROR + + """Bindings are built and present""" + GENERATED_AND_PRESENT +} + +enum RegistryPackageVersionNativeExecutablesStateChoices { + """Native Executables are not detected""" + NOT_PRESENT + + """Native Executables are being built""" + GENERATING + + """Native Executables generation has failed""" + ERROR + + """Native Executables are built and present""" + GENERATED_AND_PRESENT +} + +type PackageVersionConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageVersionEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `PackageVersion` and its cursor.""" +type PackageVersionEdge { + """The item at the end of the edge""" + node: PackageVersion + + """A cursor for use in pagination""" + cursor: String! +} + +type DeployAppVersionConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [DeployAppVersionEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `DeployAppVersion` and its cursor.""" +type DeployAppVersionEdge { + """The item at the end of the edge""" + node: DeployAppVersion + + """A cursor for use in pagination""" + cursor: String! +} + +type DeployAppVersion implements Node { + """The ID of the object""" + id: ID! + app: DeployApp! + yamlConfig: String! + userYamlConfig: String! + signature: String + description: String + publishedBy: User! + createdAt: DateTime! + updatedAt: DateTime! + configWebc: String @deprecated(reason: "webc support has been deprecated for apps") + config: String! @deprecated(reason: "Please use jsonConfig instead") + jsonConfig: String! + url: String! + permalink: String! + urls: [String]! + version: String! + isActive: Boolean! + manifest: String! + logs( + """ + Get logs starting from this timestamp. Takes EPOCH timestamp in seconds. + """ + startingFrom: Float! + + """Fetch logs until this timestamp. Takes EPOCH timestamp in seconds.""" + until: Float + before: String + after: String + first: Int + last: Int + ): LogConnection! + usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! + sourcePackageVersion: PackageVersion! + aggregateMetrics: AggregateMetrics! +} + +type DeployApp implements Node & Owner { + """The ID of the object""" + id: ID! + createdBy: User! + createdAt: DateTime! + updatedAt: DateTime! + activeVersion: DeployAppVersion! + globalName: String! + url: String! + adminUrl: String! + permalink: String! + urls: [String]! + description: String + name: String! + owner: Owner! + versions(sortBy: DeployAppVersionsSortBy, createdAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppVersionConnection! + aggregateMetrics: AggregateMetrics! + aliases(offset: Int, before: String, after: String, first: Int, last: Int): AppAliasConnection! + usageMetrics(forRange: MetricRange!, variant: MetricType!): [UsageMetric]! + deleted: Boolean! +} + +enum DeployAppVersionsSortBy { + NEWEST + OLDEST +} + +type AggregateMetrics { + cpuTime: String! + memoryTime: String! + ingress: String! + egress: String! + noRequests: String! + monthlyCost: String! +} + +type AppAliasConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [AppAliasEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `AppAlias` and its cursor.""" +type AppAliasEdge { + """The item at the end of the edge""" + node: AppAlias + + """A cursor for use in pagination""" + cursor: String! +} + +type AppAlias implements Node { + name: String! + app: DeployApp! + isDefault: Boolean! + + """The ID of the object""" + id: ID! + url: String! +} + +type UsageMetric { + variant: MetricType! + value: Float! + unit: MetricUnit! + timestamp: DateTime! +} + +enum MetricType { + cpu_time + memory_time + network_egress + network_ingress + no_of_requests + cost +} + +"""Units for metrics""" +enum MetricUnit { + """represents the unit of "seconds".""" + SEC + + """represents the unit of "kilobytes".""" + KB + + """represents the unit of "kilobytes per second".""" + KBS + + """represents the unit of "number of requests".""" + NO_REQUESTS + + """represents the unit of "cost" in USD.""" + DOLLARS +} + +enum MetricRange { + LAST_24_HOURS + LAST_30_DAYS +} + +type LogConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [LogEdge]! +} + +"""A Relay edge containing a `Log` and its cursor.""" +type LogEdge { + """The item at the end of the edge""" + node: Log + + """A cursor for use in pagination""" + cursor: String! +} + +type Command { + command: String! + packageVersion: PackageVersion! + module: PackageVersionModule! +} + +type PackageVersionModule { + name: String! + source: String! + abi: String + publicUrl: String! +} + +type NativeExecutableConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NativeExecutableEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `NativeExecutable` and its cursor.""" +type NativeExecutableEdge { + """The item at the end of the edge""" + node: NativeExecutable + + """A cursor for use in pagination""" + cursor: String! +} + +type NativeExecutable implements Node { + """The ID of the object""" + id: ID! + module: String! @deprecated(reason: "Use filename instead") + filename: String! + filesize: Int! + targetTriple: String! + downloadUrl: String! +} + +type BindingsGeneratorConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [BindingsGeneratorEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `BindingsGenerator` and its cursor.""" +type BindingsGeneratorEdge { + """The item at the end of the edge""" + node: BindingsGenerator + + """A cursor for use in pagination""" + cursor: String! +} + +type BindingsGenerator implements Node { + """The ID of the object""" + id: ID! + packageVersion: PackageVersion! + active: Boolean! + commandName: String! + registryJavascriptlanguagebindings(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionNPMBindingConnection! + registryPythonlanguagebindings(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionPythonBindingConnection! +} + +type PackageVersionNPMBindingConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageVersionNPMBindingEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `PackageVersionNPMBinding` and its cursor.""" +type PackageVersionNPMBindingEdge { + """The item at the end of the edge""" + node: PackageVersionNPMBinding + + """A cursor for use in pagination""" + cursor: String! +} + +type PackageVersionNPMBinding implements PackageVersionLanguageBinding & Node { + """The ID of the object""" + id: ID! + language: ProgrammingLanguage! + + """The URL of the generated artifacts on Wasmer CDN.""" + url: String! + + """When the binding was generated""" + createdAt: DateTime! + + """Package version used to generate this binding""" + generator: BindingsGenerator! + name: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") + kind: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") + + """Name of package source""" + packageName: String! + module: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") + npmDefaultInstallPackageName(url: String): String! @deprecated(reason: "Please use packageName instead") +} + +interface PackageVersionLanguageBinding { + id: ID! + language: ProgrammingLanguage! + + """The URL of the generated artifacts on Wasmer CDN.""" + url: String! + + """When the binding was generated""" + createdAt: DateTime! + + """Package version used to generate this binding""" + generator: BindingsGenerator! + name: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") + kind: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") + + """Name of package source""" + packageName: String! + module: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") +} + +enum ProgrammingLanguage { + PYTHON + JAVASCRIPT +} + +type PackageVersionPythonBindingConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageVersionPythonBindingEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +""" +A Relay edge containing a `PackageVersionPythonBinding` and its cursor. +""" +type PackageVersionPythonBindingEdge { + """The item at the end of the edge""" + node: PackageVersionPythonBinding + + """A cursor for use in pagination""" + cursor: String! +} + +type PackageVersionPythonBinding implements PackageVersionLanguageBinding & Node { + """The ID of the object""" + id: ID! + language: ProgrammingLanguage! + + """The URL of the generated artifacts on Wasmer CDN.""" + url: String! + + """When the binding was generated""" + createdAt: DateTime! + + """Package version used to generate this binding""" + generator: BindingsGenerator! + name: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") + kind: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") + + """Name of package source""" + packageName: String! + module: String! @deprecated(reason: "Do not use this field, since bindings for all modules are generated at once now.") + pythonDefaultInstallPackageName(url: String): String! +} + +type PackageDistribution { + downloadUrl: String! + size: Int! + piritaDownloadUrl: String + piritaSize: Int! + piritaSha256Hash: String +} + +type PackageVersionFilesystem { + wasm: String! + host: String! +} + +type InterfaceVersion implements Node { + """The ID of the object""" + id: ID! + interface: Interface! + version: String! + content: String! + createdAt: DateTime! + updatedAt: DateTime! + publishedBy: User! + packageVersions(offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! +} + +type Interface implements Node { + """The ID of the object""" + id: ID! + name: String! + displayName: String! + description: String! + homepage: String + icon: String + createdAt: DateTime! + updatedAt: DateTime! + versions(offset: Int, before: String, after: String, first: Int, last: Int): InterfaceVersionConnection! + lastVersion: InterfaceVersion +} + +type InterfaceVersionConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [InterfaceVersionEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `InterfaceVersion` and its cursor.""" +type InterfaceVersionEdge { + """The item at the end of the edge""" + node: InterfaceVersion + + """A cursor for use in pagination""" + cursor: String! +} + +union PiritaFilesystemItem = PiritaFilesystemFile | PiritaFilesystemDir + +type PiritaFilesystemFile { + name(display: PiritaFilesystemNameDisplay): String! + size: Int! + offset: Int! +} + +enum PiritaFilesystemNameDisplay { + RELATIVE + ABSOLUTE +} + +type PiritaFilesystemDir { + name(display: PiritaFilesystemNameDisplay): String! +} + +type Collection { + slug: String! + displayName: String! + description: String! + createdAt: DateTime! + banner: String! + packages(before: String, after: String, first: Int, last: Int): PackageConnection! +} + +type PackageCollaboratorConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageCollaboratorEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `PackageCollaborator` and its cursor.""" +type PackageCollaboratorEdge { + """The item at the end of the edge""" + node: PackageCollaborator + + """A cursor for use in pagination""" + cursor: String! +} + +type PackageCollaborator implements Node { + """The ID of the object""" + id: ID! + user: User! + role: RegistryPackageMaintainerRoleChoices! + package: Package! + createdAt: DateTime! + updatedAt: DateTime! + invite: PackageCollaboratorInvite +} + +enum RegistryPackageMaintainerRoleChoices { + """Admin""" + ADMIN + + """Editor""" + EDITOR + + """Viewer""" + VIEWER +} + +type PackageCollaboratorInvite implements Node { + """The ID of the object""" + id: ID! + requestedBy: User! + user: User + inviteEmail: String + package: Package! + role: RegistryPackageMaintainerInviteRoleChoices! + accepted: PackageCollaborator + approvedBy: User + declinedBy: User + createdAt: DateTime! + expiresAt: DateTime! + closedAt: DateTime +} + +enum RegistryPackageMaintainerInviteRoleChoices { + """Admin""" + ADMIN + + """Editor""" + EDITOR + + """Viewer""" + VIEWER +} + +type PackageCollaboratorInviteConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageCollaboratorInviteEdge]! +} + +"""A Relay edge containing a `PackageCollaboratorInvite` and its cursor.""" +type PackageCollaboratorInviteEdge { + """The item at the end of the edge""" + node: PackageCollaboratorInvite + + """A cursor for use in pagination""" + cursor: String! +} + +enum GrapheneRole { + ADMIN + EDITOR + VIEWER +} + +type PackageTransferRequest implements Node { + """The ID of the object""" + id: ID! + requestedBy: User! + previousOwnerObjectId: Int! + newOwnerObjectId: Int! + package: Package! + approvedBy: User + declinedBy: User + createdAt: DateTime! + expiresAt: DateTime! + closedAt: DateTime + previousOwner: PackageOwner! + newOwner: PackageOwner! +} + +type PackageSearchConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageSearchEdge]! + totalCount: Int +} + +"""A Relay edge containing a `PackageSearch` and its cursor.""" +type PackageSearchEdge { + """The item at the end of the edge""" + node: PackageVersion + + """A cursor for use in pagination""" + cursor: String! +} + +type DeployAppConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [DeployAppEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `DeployApp` and its cursor.""" +type DeployAppEdge { + """The item at the end of the edge""" + node: DeployApp + + """A cursor for use in pagination""" + cursor: String! +} + +enum DeployAppsSortBy { + NEWEST + OLDEST + MOST_ACTIVE +} + +type NamespaceCollaboratorConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NamespaceCollaboratorEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `NamespaceCollaborator` and its cursor.""" +type NamespaceCollaboratorEdge { + """The item at the end of the edge""" + node: NamespaceCollaborator + + """A cursor for use in pagination""" + cursor: String! +} + +type PackageTransferRequestConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageTransferRequestEdge]! +} + +"""A Relay edge containing a `PackageTransferRequest` and its cursor.""" +type PackageTransferRequestEdge { + """The item at the end of the edge""" + node: PackageTransferRequest + + """A cursor for use in pagination""" + cursor: String! +} + +type APITokenConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [APITokenEdge]! +} + +"""A Relay edge containing a `APIToken` and its cursor.""" +type APITokenEdge { + """The item at the end of the edge""" + node: APIToken + + """A cursor for use in pagination""" + cursor: String! +} + +type APIToken { + id: ID! + user: User! + identifier: String + createdAt: DateTime! + revokedAt: DateTime + lastUsedAt: DateTime + nonceSet(offset: Int, before: String, after: String, first: Int, last: Int): NonceConnection! +} + +type NonceConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NonceEdge]! + + """Total number of items in the connection.""" + totalCount: Int +} + +"""A Relay edge containing a `Nonce` and its cursor.""" +type NonceEdge { + """The item at the end of the edge""" + node: Nonce + + """A cursor for use in pagination""" + cursor: String! +} + +type Nonce implements Node { + """The ID of the object""" + id: ID! + name: String! + callbackUrl: String! + createdAt: DateTime! + isValidated: Boolean! + secret: String! + token: String! + expired: Boolean! + authUrl: String! +} + +type UserNotificationConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [UserNotificationEdge]! + hasPendingNotifications: Boolean! +} + +"""A Relay edge containing a `UserNotification` and its cursor.""" +type UserNotificationEdge { + """The item at the end of the edge""" + node: UserNotification + + """A cursor for use in pagination""" + cursor: String! +} + +type UserNotification implements Node { + """The ID of the object""" + id: ID! + icon: String + body: UserNotificationBody! + seenState: UserNotificationSeenState! + kind: UserNotificationKind + createdAt: DateTime! +} + +type UserNotificationBody { + text: String! + ranges: [NodeBodyRange]! +} + +enum UserNotificationSeenState { + UNSEEN + SEEN + SEEN_AND_READ +} + +union UserNotificationKind = UserNotificationKindPublishedPackageVersion | UserNotificationKindIncomingPackageTransfer | UserNotificationKindIncomingPackageInvite | UserNotificationKindIncomingNamespaceInvite + +type UserNotificationKindPublishedPackageVersion { + packageVersion: PackageVersion! +} + +type UserNotificationKindIncomingNamespaceInvite { + namespaceInvite: NamespaceCollaboratorInvite! +} + +""" + + Enum of ways a user can login. One user can have many login methods + associated with their account. + +""" +enum LoginMethod { + GOOGLE + GITHUB + PASSWORD +} + +type Signature { + id: ID! + publicKey: PublicKey! + data: String! + createdAt: DateTime! +} + +type UserNotificationKindIncomingPackageTransfer { + packageTransferRequest: PackageTransferRequest! +} + +type UserNotificationKindIncomingPackageInvite { + packageInvite: PackageCollaboratorInvite! +} + +input DeploymentV1 { + name: String! + workload: WorkloadV1! +} + +input WorkloadV1 { + capability: CapabilityMapV1 + name: String = null + runner: WorkloadRunnerV1! +} + +input AppV1Spec { + aliases: [String] = [] + workload: WorkloadV2! +} + +input WorkloadV2 { + source: String! +} + +input CapabilityCpuV1 { + maximumThreads: Int + maximumUsage: Int +} + +input FileSystemPermissionsV1 { + delete: Boolean + read: Boolean + write: Boolean +} + +input FileSystemVolumeMountV1 { + path: String! + permissions: [FileSystemPermissionsV1] +} + +input FileSystemVolumeSourceLocalV1 { + maximumSize: String! +} + +input FileSystemVolumeSourceV1 { + local: FileSystemVolumeSourceLocalV1! +} + +input FileSystemVolumeConfigV1 { + mounts: [FileSystemVolumeMountV1]! + name: String! + source: FileSystemVolumeSourceV1! +} + +input CapabilityFileSystemV1 { + volumes: [FileSystemVolumeConfigV1]! +} + +input CapabilityPersistentMemoryV1 { + volumes: [String] +} + +input CapabilityMemorySwapV1 { + maximumSize: String + memoryId: String +} + +input CapabilityNetworkV1 { + egress: NetworkEgressV1 +} + +input NetworkEgressV1 { + enabled: Boolean +} + +input CapabilityNetworkDnsV1 { + enabled: Boolean + servers: [String] + allowedHosts: NetworkDnsAllowedHostsV1 +} + +input NetworkDnsAllowedHostsV1 { + allowAllHosts: Boolean + hosts: [String] + regexPatterns: [String] + wildcardPatterns: [String] +} + +input CapabilityNetworkGatewayV1 { + domains: [String] + enforceHttps: Boolean +} + +input CapabilityMapV1 { + memorySwap: CapabilityCpuV1 +} + +input WebcSourceV1 { + name: String! + namespace: String! + repository: String! = "https://registry.wasmer.wtf" + tag: String + authToken: String +} + +input WorkloadRunnerV1 { + webProxy: RunnerWebProxyV1 + wcgi: RunnerWCGIV1 +} + +"""Run a webassembly file.""" +input RunnerWCGIV1 { + source: WorkloadRunnerWasmSourceV1! + dialect: String +} + +input RunnerWebProxyV1 { + source: WorkloadRunnerWasmSourceV1! +} + +input WorkloadRunnerWasmSourceV1 { + webc: WebcSourceV1! +} + +type StripeCustomer { + id: ID! +} + +type Billing { + stripeCustomer: StripeCustomer! + payments: [PaymentIntent]! + paymentMethods: [PaymentMethod]! +} + +type PaymentIntent implements Node { + """Three-letter ISO currency code""" + currency: String! + + """ + Status of this PaymentIntent, one of requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, or succeeded. You can read more about PaymentIntent statuses here. + """ + status: DjstripePaymentIntentStatusChoices! + + """The ID of the object""" + id: ID! + amount: String! +} + +enum DjstripePaymentIntentStatusChoices { + """ + Cancellation invalidates the intent for future confirmation and cannot be undone. + """ + CANCELED + + """Required actions have been handled.""" + PROCESSING + + """Payment Method require additional action, such as 3D secure.""" + REQUIRES_ACTION + + """Capture the funds on the cards which have been put on holds.""" + REQUIRES_CAPTURE + + """Intent is ready to be confirmed.""" + REQUIRES_CONFIRMATION + + """Intent created and requires a Payment Method to be attached.""" + REQUIRES_PAYMENT_METHOD + + """The funds are in your account.""" + SUCCEEDED +} + +union PaymentMethod = CardPaymentMethod + +type CardPaymentMethod implements Node { + """The ID of the object""" + id: ID! + brand: CardBrand! + country: String! + expMonth: Int! + expYear: Int! + funding: CardFunding! + last4: String! + isDefault: Boolean! +} + +""" +Card brand. + +Can be amex, diners, discover, jcb, mastercard, unionpay, visa, or unknown. +""" +enum CardBrand { + AMEX + DINERS + DISCOVER + JCB + MASTERCARD + UNIONPAY + VISA + UNKNOWN +} + +""" +Card funding type. + +Can be credit, debit, prepaid, or unknown. +""" +enum CardFunding { + CREDIT + DEBIT + PREPAID + UNKNOWN +} + +type Payment { + id: ID + amount: String + paidOn: DateTime +} + +"""Log entry for deploy app.""" +type Log { + timestamp: Float! + message: String! +} + +type Query { + latestTOS: TermsOfService! + getDeployAppVersion(name: String!, owner: String!, version: String): DeployAppVersion + getDeployApp(name: String!, owner: String!): DeployApp + getAppByGlobalAlias(alias: String!): DeployApp + getDeployApps(sortBy: DeployAppsSortBy, updatedAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppConnection! + getAppVersions(sortBy: DeployAppVersionsSortBy, updatedAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): DeployAppVersionConnection! + viewer: User + getUser(username: String!): User + getPasswordResetToken(token: String!): GetPasswordResetToken + getAuthNonce(name: String!): Nonce + packages(before: String, after: String, first: Int, last: Int): PackageConnection + recentPackageVersions(curated: Boolean, offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! + allPackageVersions(sortBy: PackageVersionSortBy, createdAfter: DateTime, updatedAfter: DateTime, offset: Int, before: String, after: String, first: Int, last: Int): PackageVersionConnection! + getNamespace(name: String!): Namespace + getPackage(name: String!): Package + getPackages(names: [String!]!): [Package]! + getPackageVersion(name: String!, version: String = "latest"): PackageVersion + getPackageVersions(names: [String!]!): [PackageVersion] + getInterface(name: String!): Interface + getInterfaces(names: [String!]!): [Interface]! + getInterfaceVersion(name: String!, version: String = "latest"): InterfaceVersion + getContract(name: String!): Interface @deprecated(reason: "Please use getInterface instead") + getContracts(names: [String!]!): [Interface]! @deprecated(reason: "Please use getInterfaces instead") + getContractVersion(name: String!, version: String): InterfaceVersion @deprecated(reason: "Please use getInterfaceVersion instead") + getCommand(name: String!): Command + getCommands(names: [String!]!): [Command] + getCollections(before: String, after: String, first: Int, last: Int): CollectionConnection + getSignedUrlForPackageUpload(name: String!, version: String = "latest", expiresAfterSeconds: Int = 60): SignedUrl + blogposts(tags: [String!], before: String, after: String, first: Int, last: Int): BlogPostConnection! + getBlogpost(slug: String, featured: Boolean): BlogPost + search(query: String!, packages: PackagesFilter, namespaces: NamespacesFilter, users: UsersFilter, apps: AppFilter, before: String, after: String, first: Int, last: Int): SearchConnection! + searchAutocomplete(kind: [SearchKind!], query: String!, before: String, after: String, first: Int, last: Int): SearchConnection! + getGlobalObject(slug: String!): GlobalObject + node( + """The ID of the object""" + id: ID! + ): Node + nodes(ids: [ID!]!): [Node] + info: RegistryInfo +} + +type TermsOfService implements Node { + """The ID of the object""" + id: ID! + content: String! + createdAt: DateTime! + viewerHasAccepted: Boolean! +} + +type GetPasswordResetToken { + valid: Boolean! + user: User +} + +enum PackageVersionSortBy { + NEWEST + OLDEST +} + +type CollectionConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [CollectionEdge]! +} + +"""A Relay edge containing a `Collection` and its cursor.""" +type CollectionEdge { + """The item at the end of the edge""" + node: Collection + + """A cursor for use in pagination""" + cursor: String! +} + +type SignedUrl { + url: String! +} + +type BlogPostConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [BlogPostEdge]! +} + +"""A Relay edge containing a `BlogPost` and its cursor.""" +type BlogPostEdge { + """The item at the end of the edge""" + node: BlogPost + + """A cursor for use in pagination""" + cursor: String! +} + +type BlogPost implements Node { + """The ID of the object""" + id: ID! + live: Boolean! + + """The page title as you'd like it to be seen by the public""" + title: String! + + """ + The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/ + """ + slug: String! + owner: User + body: String! + publishDate: DateTime + url: String! + coverImageUrl: String + tagline: String! + relatedArticles: [BlogPost!] + updatedAt: DateTime! + tags: [BlogPostTag!] + editUrl: String +} + +type BlogPostTag implements Node { + """The ID of the object""" + id: ID! + name: String! + slug: String! +} + +type SearchConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [SearchEdge]! + totalCount: Int +} + +"""A Relay edge containing a `Search` and its cursor.""" +type SearchEdge { + """The item at the end of the edge""" + node: SearchResult + + """A cursor for use in pagination""" + cursor: String! +} + +union SearchResult = PackageVersion | User | Namespace | DeployApp + +input PackagesFilter { + count: Int = 1000 + sortBy: SearchOrderSort = ASC + curated: Boolean + publishDate: SearchPublishDate + hasBindings: Boolean = false + isStandalone: Boolean = false + hasCommands: Boolean = false + withInterfaces: [String] + license: String + size: CountFilter + downloads: CountFilter + likes: CountFilter + owner: String + publishedBy: String + orderBy: PackageOrderBy = PUBLISHED_DATE +} + +enum SearchOrderSort { + ASC + DESC +} + +enum SearchPublishDate { + LAST_DAY + LAST_WEEK + LAST_MONTH + LAST_YEAR +} + +input CountFilter { + count: Int = 0 + comparison: CountComparison = GREATER_THAN_OR_EQUAL +} + +enum CountComparison { + EQUAL + GREATER_THAN + LESS_THAN + GREATER_THAN_OR_EQUAL + LESS_THAN_OR_EQUAL +} + +enum PackageOrderBy { + ALPHABETICALLY + SIZE + TOTAL_DOWNLOADS + PUBLISHED_DATE +} + +input NamespacesFilter { + count: Int = 1000 + sortBy: SearchOrderSort = ASC + packageCount: CountFilter + userCount: CountFilter + collaborator: String + orderBy: NamespaceOrderBy = CREATED_DATE +} + +enum NamespaceOrderBy { + PACKAGE_COUNT + COLLABORATOR_COUNT + APP_COUNT + CREATED_DATE +} + +input UsersFilter { + count: Int = 1000 + sortBy: SearchOrderSort = ASC + packageCount: CountFilter + namespaceCount: CountFilter + orderBy: UserOrderBy = CREATED_DATE +} + +enum UserOrderBy { + PACKAGE_COUNT + APP_COUNT + CREATED_DATE +} + +input AppFilter { + count: Int = 1000 + sortBy: SearchOrderSort = ASC + deployedBy: String + owner: String +} + +enum SearchKind { + PACKAGE + NAMESPACE + USER +} + +union GlobalObject = User | Namespace + +type RegistryInfo { + """Base URL for this registry""" + baseUrl: String! + + """Base URL for the default frontend""" + defaultFrontend: String! + + """URL to the graphql endpoint""" + graphqlUrl: String! + + """URL to the graphql endpoint""" + createBlogpostUrl: String + + """Public metadata about packages""" + packages: PackageInfo! + + """Public metadata about the graphql schema""" + schema: SchemaInfo! +} + +type PackageInfo { + """Number of package versions published this month""" + versionsPublishedThisMonth: Int! + + """Number of new packages published this month""" + newPackagesThisMonth: Int! + + """Number of package downloads this month""" + packageDownloadsThisMonth: Int! +} + +type SchemaInfo { + """Download link for graphql schema""" + downloadUrl: String! + + """SHA256 hash of the schema data""" + SHA256Hash: String! + + """Timestamp when the schema was last updated""" + lastUpdated: DateTime! +} + +type Mutation { + """Viewer accepts the latest ToS.""" + acceptTOS(input: AcceptTOSInput!): AcceptTOSPayload + publishDeployApp(input: PublishDeployAppInput!): PublishDeployAppPayload + deleteApp(input: DeleteAppInput!): DeleteAppPayload + + """Add current user to the waitlist.""" + joinWaitlist(input: JoinWaitlistInput!): JoinWaitlistPayload + + """Add stripe payment to the user""" + addPayment(input: AddPaymentInput!): AddPaymentPayload + + """ + Mutation to change the active version of a DeployApp to another DeployAppVersion. + """ + markAppVersionAsActive(input: MarkAppVersionAsActiveInput!): MarkAppVersionAsActivePayload + + """Set a payment method as default for the user.""" + makePaymentDefault(input: SetDefaultPaymentMethodInput!): SetDefaultPaymentMethodPayload + + """ + Try to detach a payment method from customer. + Fails if trying to detach a default method, + or if it's the only payment method. + """ + detachPaymentMethod(input: DetachPaymentMethodInput!): DetachPaymentMethodPayload + generateDeployConfigToken(input: GenerateDeployConfigTokenInput!): GenerateDeployConfigTokenPayload + tokenAuth(input: ObtainJSONWebTokenInput!): ObtainJSONWebTokenPayload + generateDeployToken(input: GenerateDeployTokenInput!): GenerateDeployTokenPayload + verifyAccessToken(token: String): Verify + refreshAccessToken(refreshToken: String): Refresh + revokeAccessToken(refreshToken: String): Revoke + registerUser(input: RegisterUserInput!): RegisterUserPayload + socialAuth(input: SocialAuthJWTInput!): SocialAuthJWTPayload + validateUserEmail(input: ValidateUserEmailInput!): ValidateUserEmailPayload + requestPasswordReset(input: RequestPasswordResetInput!): RequestPasswordResetPayload + requestValidationEmail(input: RequestValidationEmailInput!): RequestValidationEmailPayload + changeUserPassword(input: ChangeUserPasswordInput!): ChangeUserPasswordPayload + changeUserUsername(input: ChangeUserUsernameInput!): ChangeUserUsernamePayload + changeUserEmail(input: ChangeUserEmailInput!): ChangeUserEmailPayload + updateUserInfo(input: UpdateUserInfoInput!): UpdateUserInfoPayload + validateUserPassword(input: ValidateUserPasswordInput!): ValidateUserPasswordPayload + generateApiToken(input: GenerateAPITokenInput!): GenerateAPITokenPayload + revokeApiToken(input: RevokeAPITokenInput!): RevokeAPITokenPayload + checkUserExists(input: CheckUserExistsInput!): CheckUserExistsPayload + readNotification(input: ReadNotificationInput!): ReadNotificationPayload + seePendingNotifications(input: SeePendingNotificationsInput!): SeePendingNotificationsPayload + newNonce(input: NewNonceInput!): NewNoncePayload + validateNonce(input: ValidateNonceInput!): ValidateNoncePayload + publishPublicKey(input: PublishPublicKeyInput!): PublishPublicKeyPayload + publishPackage(input: PublishPackageInput!): PublishPackagePayload + updatePackage(input: UpdatePackageInput!): UpdatePackagePayload + likePackage(input: LikePackageInput!): LikePackagePayload + unlikePackage(input: UnlikePackageInput!): UnlikePackagePayload + watchPackage(input: WatchPackageInput!): WatchPackagePayload + unwatchPackage(input: UnwatchPackageInput!): UnwatchPackagePayload + archivePackage(input: ArchivePackageInput!): ArchivePackagePayload + changePackageVersionArchivedStatus(input: ChangePackageVersionArchivedStatusInput!): ChangePackageVersionArchivedStatusPayload + createNamespace(input: CreateNamespaceInput!): CreateNamespacePayload + updateNamespace(input: UpdateNamespaceInput!): UpdateNamespacePayload + deleteNamespace(input: DeleteNamespaceInput!): DeleteNamespacePayload + inviteNamespaceCollaborator(input: InviteNamespaceCollaboratorInput!): InviteNamespaceCollaboratorPayload + acceptNamespaceCollaboratorInvite(input: AcceptNamespaceCollaboratorInviteInput!): AcceptNamespaceCollaboratorInvitePayload + removeNamespaceCollaboratorInvite(input: RemoveNamespaceCollaboratorInviteInput!): RemoveNamespaceCollaboratorInvitePayload + removeNamespaceCollaborator(input: RemoveNamespaceCollaboratorInput!): RemoveNamespaceCollaboratorPayload + updateNamespaceCollaboratorRole(input: UpdateNamespaceCollaboratorRoleInput!): UpdateNamespaceCollaboratorRolePayload + updateNamespaceCollaboratorInviteRole(input: UpdateNamespaceCollaboratorInviteRoleInput!): UpdateNamespaceCollaboratorInviteRolePayload + invitePackageCollaborator(input: InvitePackageCollaboratorInput!): InvitePackageCollaboratorPayload + acceptPackageCollaboratorInvite(input: AcceptPackageCollaboratorInviteInput!): AcceptPackageCollaboratorInvitePayload + removePackageCollaboratorInvite(input: RemovePackageCollaboratorInviteInput!): RemovePackageCollaboratorInvitePayload + updatePackageCollaboratorRole(input: UpdatePackageCollaboratorRoleInput!): UpdatePackageCollaboratorRolePayload + updatePackageCollaboratorInviteRole(input: UpdatePackageCollaboratorInviteRoleInput!): UpdatePackageCollaboratorInviteRolePayload + removePackageCollaborator(input: RemovePackageCollaboratorInput!): RemovePackageCollaboratorPayload + requestPackageTransfer(input: RequestPackageTransferInput!): RequestPackageTransferPayload + acceptPackageTransferRequest(input: AcceptPackageTransferRequestInput!): AcceptPackageTransferRequestPayload + removePackageTransferRequest(input: RemovePackageTransferRequestInput!): RemovePackageTransferRequestPayload + generateBindingsForAllPackages(input: GenerateBindingsForAllPackagesInput!): GenerateBindingsForAllPackagesPayload +} + +"""Viewer accepts the latest ToS.""" +type AcceptTOSPayload { + TOS: TermsOfService! + clientMutationId: String +} + +input AcceptTOSInput { + clientMutationId: String +} + +type PublishDeployAppPayload { + deployAppVersion: DeployAppVersion! + clientMutationId: String +} + +input PublishDeployAppInput { + """The configuration of the app.""" + config: Configuration! + + """The name of the app.""" + name: ID + + """The owner of the app.""" + owner: ID + + """The description of the app.""" + description: String + + """If true, the new version will be set as the default version.""" + makeDefault: Boolean = true + + """ + If true, Publishing will fail if the source package does not have a valid webc. + """ + strict: Boolean = false + clientMutationId: String +} + +input Configuration { + deployment: AppV0 + yamlConfig: String +} + +input AppV0 { + kind: String = "wasmer.io/App.v0" + appId: ID + name: String! + description: String + package: String! +} + +type DeleteAppPayload { + success: Boolean! + clientMutationId: String +} + +input DeleteAppInput { + """App ID to delete.""" + id: ID! + clientMutationId: String +} + +"""Add current user to the waitlist.""" +type JoinWaitlistPayload { + waitlistMember: WaitlistMember! + clientMutationId: String +} + +input JoinWaitlistInput { + name: String! + clientMutationId: String +} + +"""Add stripe payment to the user""" +type AddPaymentPayload { + customerSecret: String! + clientMutationId: String +} + +input AddPaymentInput { + clientMutationId: String +} + +""" +Mutation to change the active version of a DeployApp to another DeployAppVersion. +""" +type MarkAppVersionAsActivePayload { + app: DeployApp! + clientMutationId: String +} + +input MarkAppVersionAsActiveInput { + """The ID of the DeployAppVersion to set as the new active version.""" + appVersion: ID! + clientMutationId: String +} + +"""Set a payment method as default for the user.""" +type SetDefaultPaymentMethodPayload { + success: Boolean! + billing: Billing! + clientMutationId: String +} + +input SetDefaultPaymentMethodInput { + paymentMethod: ID! + clientMutationId: String +} + +""" +Try to detach a payment method from customer. +Fails if trying to detach a default method, +or if it's the only payment method. +""" +type DetachPaymentMethodPayload { + success: Boolean! + billing: Billing! + clientMutationId: String +} + +input DetachPaymentMethodInput { + paymentMethod: ID! + clientMutationId: String +} + +type GenerateDeployConfigTokenPayload { + token: String! + config: String! + clientMutationId: String +} + +input GenerateDeployConfigTokenInput { + config: String! + clientMutationId: String +} + +type ObtainJSONWebTokenPayload { + payload: GenericScalar! + refreshExpiresIn: Int! + username: CaseInsensitiveString! + clientMutationId: String + token: String! + refreshToken: String! +} + +""" +The `GenericScalar` scalar type represents a generic +GraphQL scalar value that could be: +String, Boolean, Int, Float, List or Object. +""" +scalar GenericScalar + +""" +The `CaseInsensitiveString` scalar type represents textual data, represented as UTF-8 +character sequences. The String type is most often used by GraphQL to +represent free-form human-readable text. +""" +scalar CaseInsensitiveString + +input ObtainJSONWebTokenInput { + clientMutationId: String + username: String! + password: String! +} + +type GenerateDeployTokenPayload { + token: String! + deployConfigVersion: DeployAppVersion! + clientMutationId: String +} + +input GenerateDeployTokenInput { + deployConfigVersionId: String! + clientMutationId: String +} + +type Verify { + payload: GenericScalar! +} + +type Refresh { + payload: GenericScalar! + refreshExpiresIn: Int! + token: String! + refreshToken: String! +} + +type Revoke { + revoked: Int! +} + +type RegisterUserPayload { + token: String + clientMutationId: String +} + +input RegisterUserInput { + fullName: String! + email: String! + username: CaseInsensitiveString! + password: String! + clientMutationId: String +} + +type SocialAuthJWTPayload { + social: SocialAuth + token: String + clientMutationId: String +} + +type SocialAuth implements Node { + """The ID of the object""" + id: ID! + user: User! + provider: String! + uid: String! + extraData: String! + created: DateTime! + modified: DateTime! +} + +input SocialAuthJWTInput { + provider: String! + accessToken: String! + register: Boolean = false + clientMutationId: String +} + +type ValidateUserEmailPayload { + user: User + clientMutationId: String +} + +input ValidateUserEmailInput { + """The user id""" + userId: ID + challenge: String! + clientMutationId: String +} + +type RequestPasswordResetPayload { + email: String! + errors: [ErrorType] + clientMutationId: String +} + +type ErrorType { + field: String! + messages: [String!]! +} + +input RequestPasswordResetInput { + email: String! + clientMutationId: String +} + +type RequestValidationEmailPayload { + user: User + success: Boolean! + clientMutationId: String +} + +input RequestValidationEmailInput { + """The user id""" + userId: ID + clientMutationId: String +} + +type ChangeUserPasswordPayload { + token: String + clientMutationId: String +} + +input ChangeUserPasswordInput { + """ + The token associated to change the password. If not existing it will use the request user by default + """ + token: String + oldPassword: String + password: String! + clientMutationId: String +} + +type ChangeUserUsernamePayload { + user: User + token: String + clientMutationId: String +} + +input ChangeUserUsernameInput { + """The new user username""" + username: CaseInsensitiveString! + clientMutationId: String +} + +type ChangeUserEmailPayload { + user: User! + clientMutationId: String +} + +input ChangeUserEmailInput { + newEmail: String! + clientMutationId: String +} + +type UpdateUserInfoPayload { + user: User + clientMutationId: String +} + +input UpdateUserInfoInput { + """The user id""" + userId: ID + + """The user full name""" + fullName: String + + """The user bio""" + bio: String + + """The user avatar""" + avatar: String + + """ + The user Twitter (it can be the url, or the handle with or without the @) + """ + twitter: String + + """ + The user Github (it can be the url, or the handle with or without the @) + """ + github: String + + """The user website (it must be a valid url)""" + websiteUrl: String + + """The user location""" + location: String + clientMutationId: String +} + +type ValidateUserPasswordPayload { + success: Boolean + clientMutationId: String +} + +input ValidateUserPasswordInput { + password: String! + clientMutationId: String +} + +type GenerateAPITokenPayload { + token: APIToken + tokenRaw: String + user: User + clientMutationId: String +} + +input GenerateAPITokenInput { + identifier: String + clientMutationId: String +} + +type RevokeAPITokenPayload { + token: APIToken + success: Boolean + clientMutationId: String +} + +input RevokeAPITokenInput { + """The API token ID""" + tokenId: ID! + clientMutationId: String +} + +type CheckUserExistsPayload { + exists: Boolean! + + """The user is only returned if the user input was the username""" + user: User + clientMutationId: String +} + +input CheckUserExistsInput { + """The user""" + user: String! + clientMutationId: String +} + +type ReadNotificationPayload { + notification: UserNotification + clientMutationId: String +} + +input ReadNotificationInput { + notificationId: ID! + clientMutationId: String +} + +type SeePendingNotificationsPayload { + success: Boolean + clientMutationId: String +} + +input SeePendingNotificationsInput { + clientMutationId: String +} + +type NewNoncePayload { + nonce: Nonce! + clientMutationId: String +} + +input NewNonceInput { + name: String! + callbackUrl: String! + clientMutationId: String +} + +type ValidateNoncePayload { + nonce: Nonce! + clientMutationId: String +} + +input ValidateNonceInput { + id: ID! + secret: String! + clientMutationId: String +} + +type PublishPublicKeyPayload { + success: Boolean! + publicKey: PublicKey! + clientMutationId: String +} + +input PublishPublicKeyInput { + keyId: String! + key: String! + verifyingSignatureId: String + clientMutationId: String +} + +type PublishPackagePayload { + success: Boolean! + packageVersion: PackageVersion! + clientMutationId: String +} + +input PublishPackageInput { + name: String! + version: String! + description: String! + manifest: String! + license: String + licenseFile: String + readme: String + repository: String + homepage: String + file: String + signedUrl: String + signature: InputSignature + + """The package icon""" + icon: String + + """Whether the package is private""" + private: Boolean = false + clientMutationId: String +} + +input InputSignature { + publicKeyKeyId: String! + data: String! +} + +type UpdatePackagePayload { + package: Package! + clientMutationId: String +} + +input UpdatePackageInput { + packageId: ID! + + """The package icon""" + icon: String + clientMutationId: String +} + +type LikePackagePayload { + package: Package! + clientMutationId: String +} + +input LikePackageInput { + packageId: ID! + clientMutationId: String +} + +type UnlikePackagePayload { + package: Package! + clientMutationId: String +} + +input UnlikePackageInput { + packageId: ID! + clientMutationId: String +} + +type WatchPackagePayload { + package: Package! + clientMutationId: String +} + +input WatchPackageInput { + packageId: ID! + clientMutationId: String +} + +type UnwatchPackagePayload { + package: Package! + clientMutationId: String +} + +input UnwatchPackageInput { + packageId: ID! + clientMutationId: String +} + +type ArchivePackagePayload { + package: Package! + clientMutationId: String +} + +input ArchivePackageInput { + packageId: ID! + clientMutationId: String +} + +type ChangePackageVersionArchivedStatusPayload { + packageVersion: PackageVersion! + clientMutationId: String +} + +input ChangePackageVersionArchivedStatusInput { + packageVersionId: ID! + isArchived: Boolean + clientMutationId: String +} + +type CreateNamespacePayload { + namespace: Namespace! + user: User! + clientMutationId: String +} + +input CreateNamespaceInput { + name: String! + + """The namespace display name""" + displayName: String + + """The namespace description""" + description: String + + """The namespace avatar""" + avatar: String + clientMutationId: String +} + +type UpdateNamespacePayload { + namespace: Namespace! + clientMutationId: String +} + +input UpdateNamespaceInput { + namespaceId: ID! + + """The namespace slug name""" + name: String + + """The namespace display name""" + displayName: String + + """The namespace description""" + description: String + + """The namespace avatar""" + avatar: String + clientMutationId: String +} + +type DeleteNamespacePayload { + success: Boolean! + clientMutationId: String +} + +input DeleteNamespaceInput { + namespaceId: ID! + clientMutationId: String +} + +type InviteNamespaceCollaboratorPayload { + invite: NamespaceCollaboratorInvite! + namespace: Namespace! + clientMutationId: String +} + +input InviteNamespaceCollaboratorInput { + namespaceId: ID! + role: GrapheneRole! + username: String + email: String + clientMutationId: String +} + +type AcceptNamespaceCollaboratorInvitePayload { + namespaceCollaboratorInvite: NamespaceCollaboratorInvite! + clientMutationId: String +} + +input AcceptNamespaceCollaboratorInviteInput { + inviteId: ID! + clientMutationId: String +} + +type RemoveNamespaceCollaboratorInvitePayload { + namespace: Namespace! + clientMutationId: String +} + +input RemoveNamespaceCollaboratorInviteInput { + inviteId: ID! + clientMutationId: String +} + +type RemoveNamespaceCollaboratorPayload { + namespace: Namespace! + clientMutationId: String +} + +input RemoveNamespaceCollaboratorInput { + namespaceCollaboratorId: ID! + clientMutationId: String +} + +type UpdateNamespaceCollaboratorRolePayload { + collaborator: NamespaceCollaborator! + clientMutationId: String +} + +input UpdateNamespaceCollaboratorRoleInput { + namespaceCollaboratorId: ID! + role: GrapheneRole! + clientMutationId: String +} + +type UpdateNamespaceCollaboratorInviteRolePayload { + collaboratorInvite: NamespaceCollaboratorInvite! + clientMutationId: String +} + +input UpdateNamespaceCollaboratorInviteRoleInput { + namespaceCollaboratorInviteId: ID! + role: GrapheneRole! + clientMutationId: String +} + +type InvitePackageCollaboratorPayload { + invite: PackageCollaboratorInvite! + package: Package! + clientMutationId: String +} + +input InvitePackageCollaboratorInput { + packageName: String! + role: GrapheneRole! + username: String + email: String + clientMutationId: String +} + +type AcceptPackageCollaboratorInvitePayload { + packageCollaboratorInvite: PackageCollaboratorInvite! + clientMutationId: String +} + +input AcceptPackageCollaboratorInviteInput { + inviteId: ID! + clientMutationId: String +} + +type RemovePackageCollaboratorInvitePayload { + package: Package! + clientMutationId: String +} + +input RemovePackageCollaboratorInviteInput { + inviteId: ID! + clientMutationId: String +} + +type UpdatePackageCollaboratorRolePayload { + collaborator: PackageCollaborator! + clientMutationId: String +} + +input UpdatePackageCollaboratorRoleInput { + packageCollaboratorId: ID! + role: GrapheneRole! + clientMutationId: String +} + +type UpdatePackageCollaboratorInviteRolePayload { + collaboratorInvite: PackageCollaboratorInvite! + clientMutationId: String +} + +input UpdatePackageCollaboratorInviteRoleInput { + packageCollaboratorInviteId: ID! + role: GrapheneRole! + clientMutationId: String +} + +type RemovePackageCollaboratorPayload { + package: Package! + clientMutationId: String +} + +input RemovePackageCollaboratorInput { + packageCollaboratorId: ID! + clientMutationId: String +} + +type RequestPackageTransferPayload { + package: Package! + clientMutationId: String +} + +input RequestPackageTransferInput { + packageId: ID! + newOwnerId: ID! + clientMutationId: String +} + +type AcceptPackageTransferRequestPayload { + package: Package! + packageTransferRequest: PackageTransferRequest! + clientMutationId: String +} + +input AcceptPackageTransferRequestInput { + packageTransferRequestId: ID! + clientMutationId: String +} + +type RemovePackageTransferRequestPayload { + package: Package! + clientMutationId: String +} + +input RemovePackageTransferRequestInput { + packageTransferRequestId: ID! + clientMutationId: String +} + +type GenerateBindingsForAllPackagesPayload { + message: String! + clientMutationId: String +} + +input GenerateBindingsForAllPackagesInput { + bindingsGeneratorId: ID + bindingsGeneratorCommand: String + clientMutationId: String +} + +type Subscription { + packageVersionCreated(publishedBy: ID, ownerId: ID): PackageVersion! + userNotificationCreated(userId: ID!): UserNotificationCreated! +} + +type UserNotificationCreated { + notification: UserNotification + notificationDeletedId: ID +} diff --git a/lib/backend-api/src/client.rs b/lib/backend-api/src/client.rs new file mode 100644 index 000000000..66270ed9a --- /dev/null +++ b/lib/backend-api/src/client.rs @@ -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, + 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 { + 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 { + 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 { + 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( + &self, + operation: Operation, + ) -> Result, 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}"); + "".to_string() + } + }; + + match serde_json::from_str::>(&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, _> = + 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( + &self, + operation: Operation, + ) -> Result + 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( + &self, + operation: Operation, + ) -> Result + 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")) + } + } +} diff --git a/lib/backend-api/src/error.rs b/lib/backend-api/src/error.rs new file mode 100644 index 000000000..76f5fa90c --- /dev/null +++ b/lib/backend-api/src/error.rs @@ -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, +} + +impl GraphQLApiFailure { + pub fn from_errors( + msg: impl Into, + errors: Option>, + ) -> 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::>() + .join(", "); + write!(f, "GraphQL API failure: {}", errs) + } +} + +impl std::error::Error for GraphQLApiFailure {} diff --git a/lib/backend-api/src/global_id.rs b/lib/backend-api/src/global_id.rs new file mode 100644 index 000000000..e7481cd0b --- /dev/null +++ b/lib/backend-api/src/global_id.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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, 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, + }), + ); + } +} diff --git a/lib/backend-api/src/gql.rs b/lib/backend-api/src/gql.rs new file mode 100644 index 000000000..d9657ffbb --- /dev/null +++ b/lib/backend-api/src/gql.rs @@ -0,0 +1,2 @@ + +types diff --git a/lib/backend-api/src/lib.rs b/lib/backend-api/src/lib.rs new file mode 100644 index 000000000..ff6c2d1fa --- /dev/null +++ b/lib/backend-api/src/lib.rs @@ -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() +} diff --git a/lib/backend-api/src/query.rs b/lib/backend-api/src/query.rs new file mode 100644 index 000000000..52d9ec530 --- /dev/null +++ b/lib/backend-api/src/query.rs @@ -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 { + 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, +) -> Result { + 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, 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, 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, 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 { + 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, Option), 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 { + 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, anyhow::Error>> + '_ { + futures::stream::try_unfold( + Some(vars), + move |vars: Option| 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::>(); + + 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 { + 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, 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::::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 { + 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, 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 { + 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 { + 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, 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, 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::>(); + + 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, 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 { + 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, 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, 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 { + 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, 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, 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 { + 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, anyhow::Error>> + '_ +{ + futures::stream::try_unfold( + Some(vars), + move |vars: Option| 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::>(); + + 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 { + 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 { + 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, + start: OffsetDateTime, + end: Option, + watch: bool, +) -> impl futures::Stream, 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, + start: OffsetDateTime, + end: Option, + watch: bool, +) -> impl futures::Stream, 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) +} diff --git a/lib/backend-api/src/stream.rs b/lib/backend-api/src/stream.rs new file mode 100644 index 000000000..c2a8f8eb1 --- /dev/null +++ b/lib/backend-api/src/stream.rs @@ -0,0 +1,119 @@ +use std::{collections::VecDeque, task::Poll}; + +use futures::{ + future::{BoxFuture, OptionFuture}, + Future, +}; + +use super::WasmerClient; + +type PaginationFuture = BoxFuture<'static, Result<(Vec, Option

), anyhow::Error>>; + +pub trait PaginatedQuery { + type Vars; + type Paginator; + type Item; + + fn query( + &self, + client: WasmerClient, + paginator: Option, + ) -> PaginationFuture; +} + +pin_project_lite::pin_project! { + pub struct QueryStream { + query: Q, + + client: WasmerClient, + page: usize, + paginator: Option, + finished: bool, + items: VecDeque, + + #[pin] + fut: OptionFuture>, + } +} + +impl QueryStream { + pub fn new(query: Q, client: WasmerClient) -> Self { + Self { + query, + client, + page: 0, + finished: false, + paginator: None, + items: VecDeque::new(), + fut: None.into(), + } + } +} + +impl futures::Stream for QueryStream { + type Item = Result; + + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll> { + 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, + } + } +} diff --git a/lib/backend-api/src/types.rs b/lib/backend-api/src/types.rs new file mode 100644 index 000000000..62995a15f --- /dev/null +++ b/lib/backend-api/src/types.rs @@ -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 for DateTime { + type Error = time::error::Format; + + fn try_from(value: OffsetDateTime) -> Result { + value + .format(&time::format_description::well_known::Rfc3339) + .map(Self) + } + } + + impl TryFrom for OffsetDateTime { + type Error = time::error::Parse; + + fn try_from(value: DateTime) -> Result { + 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, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetCurrentUserVars")] + pub struct GetCurrentUser { + pub viewer: Option, + } + + #[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, + pub last_version: Option, + pub private: bool, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + pub struct PackageDistribution { + pub pirita_sha256_hash: Option, + pub pirita_download_url: Option, + } + + #[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, + 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, + } + + #[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, + } + + #[derive(cynic::Enum, Clone, Copy, Debug)] + pub enum PackageVersionSortBy { + Newest, + Oldest, + } + + #[derive(cynic::QueryVariables, Debug, Clone, Default)] + pub struct AllPackageVersionsVars { + pub offset: Option, + pub before: Option, + pub after: Option, + pub first: Option, + pub last: Option, + + pub created_after: Option, + pub updated_after: Option, + pub sort_by: Option, + } + + #[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>, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct PackageVersionEdge { + pub node: Option, + 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, + #[arguments(owner: $app_owner, name: $app_name)] + pub get_deploy_app: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query")] + pub struct GetCurrentUserWithApps { + pub viewer: Option, + } + + #[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, + } + + #[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, + } + + #[derive(cynic::QueryVariables, Debug, Clone)] + pub struct PaginationVars { + pub offset: Option, + pub before: Option, + pub after: Option, + pub first: Option, + pub last: Option, + } + + #[derive(cynic::Enum, Clone, Copy, Debug)] + pub enum DeployAppsSortBy { + Newest, + Oldest, + MostActive, + } + + #[derive(cynic::QueryVariables, Debug, Clone, Default)] + pub struct GetDeployAppsVars { + pub offset: Option, + pub before: Option, + pub after: Option, + pub first: Option, + pub last: Option, + + pub updated_after: Option, + pub sort_by: Option, + } + + #[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, + } + + #[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, + } + + #[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, + #[arguments(owner: $owner, name: $name, version: $version)] + pub get_deploy_app_version: Option, + } + + #[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, + } + + #[derive(cynic::QueryVariables, Debug)] + pub struct CreateNamespaceVars { + pub name: String, + pub description: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Mutation", variables = "CreateNamespaceVars")] + pub struct CreateNamespace { + #[arguments(input: {name: $name, description: $description})] + pub create_namespace: Option, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct CreateNamespacePayload { + pub namespace: Namespace, + } + + #[derive(cynic::InputObject, Debug)] + pub struct CreateNamespaceInput { + pub name: String, + pub display_name: Option, + pub description: Option, + pub avatar: Option, + pub client_mutation_id: Option, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + pub struct NamespaceEdge { + pub node: Option, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + pub struct NamespaceConnection { + pub edges: Vec>, + } + + #[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, + 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>, + } + + #[derive(cynic::QueryFragment, Serialize, Debug, Clone)] + pub struct AppAliasEdge { + pub node: Option, + } + + #[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, + } + + #[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, + pub before: Option, + pub after: Option, + pub first: Option, + pub last: Option, + pub sort_by: Option, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + #[cynic(graphql_type = "Query", variables = "GetDeployAppVersionsVars")] + pub struct GetDeployAppVersions { + #[arguments(owner: $owner, name: $name)] + pub get_deploy_app: Option, + } + + #[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, + pub yaml_config: String, + pub user_yaml_config: String, + pub config: String, + pub json_config: String, + pub url: String, + + pub app: Option, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + pub struct DeployAppVersionConnection { + pub page_info: PageInfo, + pub edges: Vec>, + } + + #[derive(cynic::QueryFragment, Debug, Clone)] + pub struct DeployAppVersionEdge { + pub node: Option, + pub cursor: String, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct DeployAppConnection { + pub page_info: PageInfo, + pub edges: Vec>, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct DeployAppEdge { + pub node: Option, + pub cursor: String, + } + + #[derive(cynic::QueryFragment, Serialize, Debug, Clone)] + pub struct PageInfo { + pub has_next_page: bool, + pub end_cursor: Option, + } + + #[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, + } + + #[derive(cynic::QueryFragment, Debug)] + #[cynic(graphql_type = "Query", variables = "GetNamespaceVars")] + pub struct GetNamespace { + #[arguments(name: $name)] + pub get_namespace: Option, + } + + #[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, + } + + #[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, + pub make_default: Option, + } + + #[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, + } + + #[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, + } + + #[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, + /// 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, + pub first: Option, + } + + #[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, + } + + #[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>, + } + + #[derive(cynic::QueryFragment, Debug)] + pub struct LogEdge { + pub node: Option, + } + + #[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, + } + + #[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, + } + + #[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, + } + + #[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, + #[arguments(id: $version_id)] + #[cynic(rename = "node")] + pub version: Option, + } + + #[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, + } + + #[derive(cynic::InlineFragments, Debug)] + pub enum Node { + DeployApp(Box), + DeployAppVersion(Box), + #[cynic(fallback)] + Unknown, + } + + impl Node { + pub fn into_deploy_app(self) -> Option { + match self { + Node::DeployApp(app) => Some(*app), + _ => None, + } + } + + pub fn into_deploy_app_version(self) -> Option { + match self { + Node::DeployAppVersion(version) => Some(*version), + _ => None, + } + } + } +} + +#[allow(non_snake_case, non_camel_case_types)] +mod schema { + cynic::use_schema!(r#"schema.graphql"#); +}