diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index a95994c99..24ca80052 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -24,7 +24,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.61 + toolchain: 1.63 - name: Configure cargo data directory # After this point, all cargo registry and crate data is stored in # $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 69e911f37..08d5bb497 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,7 +100,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.61 + toolchain: 1.63 target: ${{ matrix.target }} - uses: Swatinem/rust-cache@v1 if: matrix.use_sccache != true diff --git a/.github/workflows/cloudcompiler.yaml b/.github/workflows/cloudcompiler.yaml index a296b312f..4bd3f1f5c 100644 --- a/.github/workflows/cloudcompiler.yaml +++ b/.github/workflows/cloudcompiler.yaml @@ -21,7 +21,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.61 + toolchain: 1.63 target: ${{ matrix.target }} - name: Install wasm32-wasi target shell: bash diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 545696a35..98e07cccf 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -21,7 +21,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.61 + toolchain: 1.63 - name: Install LLVM (Linux) run: | curl --proto '=https' --tlsv1.2 -sSf https://github.com/llvm/llvm-project/releases/download/llvmorg-10.0.0/clang+llvm-10.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz -L -o llvm.tar.xz diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index 0096c315d..ae018dd31 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -16,7 +16,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.61 + toolchain: 1.63 - name: Install LLVM shell: bash run: | diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 9e3a58101..60bc17f1b 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -18,7 +18,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.61 + toolchain: 1.63 components: rustfmt, clippy - name: Install LLVM (Linux) run: | diff --git a/.github/workflows/test-js.yaml b/.github/workflows/test-js.yaml index f83064f48..75ba39ca1 100644 --- a/.github/workflows/test-js.yaml +++ b/.github/workflows/test-js.yaml @@ -33,7 +33,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.61 + toolchain: 1.63 - name: Install NodeJS uses: actions/setup-node@v2 diff --git a/.github/workflows/test-sys.yaml b/.github/workflows/test-sys.yaml index 46d4a7697..9808452ce 100644 --- a/.github/workflows/test-sys.yaml +++ b/.github/workflows/test-sys.yaml @@ -110,7 +110,7 @@ jobs: - name: Install Rust uses: dtolnay/rust-toolchain@stable with: - toolchain: 1.61 + toolchain: 1.63 target: ${{ matrix.target }} - uses: Swatinem/rust-cache@v1 if: matrix.use_sccache != true @@ -210,17 +210,19 @@ jobs: TARGET: ${{ matrix.target }} TARGET_DIR: target/${{ matrix.target }}/release CARGO_TARGET: --target ${{ matrix.target }} - - name: Test integration CLI - if: matrix.run_test && matrix.os == 'windows-2019' - shell: bash - run: | - make && make build-wasmer && make build-capi && make package-capi && make package - export WASMER_DIR=`pwd`/package - make test-integration-cli - env: - TARGET: x86_64-pc-windows-msvc - TARGET_DIR: target/x86_64-pc-windows-msvc/release - CARGO_TARGET: --target x86_64-pc-windows-msvc + WAPM_DEV_TOKEN: ${{ secrets.WAPM_DEV_TOKEN }} + + #- name: Test integration CLI + # if: matrix.run_test && matrix.os == 'windows-2019' + # shell: bash + # run: | + # make && make build-wasmer && make build-capi && make package-capi && make package + # export WASMER_DIR=`pwd`/package + # make test-integration-cli + # env: + # TARGET: x86_64-pc-windows-msvc + # TARGET_DIR: target/x86_64-pc-windows-msvc/release + # CARGO_TARGET: --target x86_64-pc-windows-msvc - name: Test if: matrix.run_test && matrix.os != 'windows-2019' run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 859f9fa1e..37230e092 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,12 @@ Looking for changes that affect our C API? See the [C API Changelog](lib/c-api/C ## Added +- [#3317](https://github.com/wasmerio/wasmer/pull/3317) Add a `wasmer add` command for adding bindings for a WAPM package to your project (only Python and JavaScript are supported for now) + ## Changed +- [#3318](https://github.com/wasmerio/wasmer/pull/3318) Bump the Minimum Supported Rust Version (MSRV) to 1.63 + ## Fixed ## 3.0.0-rc.2 - 2022/11/02 @@ -112,8 +116,8 @@ Looking for changes that affect our C API? See the [C API Changelog](lib/c-api/C - [#3035](https://github.com/wasmerio/wasmer/pull/3035) Added a simple "divide by zero" wast test, for #1899, as the trap information are correctly tracked on singlepass now - [#3021](https://github.com/wasmerio/wasmer/pull/3021) Add back missing Aarch64 relocations (needed for llvm compiler) - [#3008](https://github.com/wasmerio/wasmer/pull/3008) Add a new cargo public-api CI check -- [#2941](https://github.com/wasmerio/wasmer/pull/2941) Implementation of WASIX and a fully networking for Web Assembly -- [#2952](https://github.com/wasmerio/wasmer/pull/2952) CI: add make build-wasmer-wasm test +- [#2941](https://github.com/wasmerio/wasmer/pull/2941) Implementation of WASIX and a fully networking for Web Assembly +- [#2952](https://github.com/wasmerio/wasmer/pull/2952) CI: add make build-wasmer-wasm test - [#2982](https://github.com/wasmerio/wasmer/pull/2982) Add a rustfmt.toml file to the repository ### Changed @@ -133,13 +137,13 @@ Looking for changes that affect our C API? See the [C API Changelog](lib/c-api/C - [#2946](https://github.com/wasmerio/wasmer/pull/2946) Remove dylib,staticlib engines in favor of a single Universal engine - [#2949](https://github.com/wasmerio/wasmer/pull/2949) Switch back to using custom LLVM builds on CI - [#2892](https://github.com/wasmerio/wasmer/pull/2892) Renamed `get_native_function` to `get_typed_function`, marked former as deprecated. -- [#2976](https://github.com/wasmerio/wasmer/pull/2976) Upgrade enumset minimum version to one that compiles -- [#2974](https://github.com/wasmerio/wasmer/pull/2974) Context api tests -- [#2973](https://github.com/wasmerio/wasmer/pull/2973) Port C API to new Context API -- [#2969](https://github.com/wasmerio/wasmer/pull/2969) Port JS API to new Context API -- [#2966](https://github.com/wasmerio/wasmer/pull/2966) Singlepass nopanic #2966 -- [#2957](https://github.com/wasmerio/wasmer/pull/2957) Enable multi-value handling in Singlepass compiler -- [#2954](https://github.com/wasmerio/wasmer/pull/2954) Some fixes to x86_64 Singlepass compiler, when using atomics +- [#2976](https://github.com/wasmerio/wasmer/pull/2976) Upgrade enumset minimum version to one that compiles +- [#2974](https://github.com/wasmerio/wasmer/pull/2974) Context api tests +- [#2973](https://github.com/wasmerio/wasmer/pull/2973) Port C API to new Context API +- [#2969](https://github.com/wasmerio/wasmer/pull/2969) Port JS API to new Context API +- [#2966](https://github.com/wasmerio/wasmer/pull/2966) Singlepass nopanic #2966 +- [#2957](https://github.com/wasmerio/wasmer/pull/2957) Enable multi-value handling in Singlepass compiler +- [#2954](https://github.com/wasmerio/wasmer/pull/2954) Some fixes to x86_64 Singlepass compiler, when using atomics - [#2953](https://github.com/wasmerio/wasmer/pull/2953) Makefile: add check target - [#2950](https://github.com/wasmerio/wasmer/pull/2950) compiler-cranelift: Fix typo in enum variant - [#2947](https://github.com/wasmerio/wasmer/pull/2947) Converted the WASI js test into a generic stdio test that works for both sys and js versions of wasmer @@ -159,9 +163,9 @@ Looking for changes that affect our C API? See the [C API Changelog](lib/c-api/C - [#2943](https://github.com/wasmerio/wasmer/pull/2943) Fix build error on some archs by using c_char instead of i8 - [#2976](https://github.com/wasmerio/wasmer/pull/2976) Upgrade minimum enumset to one that compiles - [#2988](https://github.com/wasmerio/wasmer/pull/2988) Have make targets install-capi-lib,install-pkgconfig work without building the wasmer binary -- [#2967](https://github.com/wasmerio/wasmer/pull/2967) Fix singlepass on arm64 that was trying to emit a sub opcode with a constant as destination (for #2959) +- [#2967](https://github.com/wasmerio/wasmer/pull/2967) Fix singlepass on arm64 that was trying to emit a sub opcode with a constant as destination (for #2959) - [#2948](https://github.com/wasmerio/wasmer/pull/2948) Fix regression on gen_import_call_trampoline_arm64() -- [#2944](https://github.com/wasmerio/wasmer/pull/2944) Fix duplicate entries in the CHANGELOG +- [#2944](https://github.com/wasmerio/wasmer/pull/2944) Fix duplicate entries in the CHANGELOG ## 2.3.0 - 2022/06/06 diff --git a/Cargo.lock b/Cargo.lock index d43d2adc7..9ff2c7952 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,6 +467,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "console" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode 0.3.6", + "lazy_static", + "libc", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -830,6 +844,17 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +dependencies = [ + "console", + "tempfile", + "zeroize", +] + [[package]] name = "diff" version = "0.1.13" @@ -969,6 +994,12 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -2250,7 +2281,7 @@ checksum = "5f375cb74c23b51d23937ffdeb48b1fbf5b6409d4b9979c1418c1de58bc8f801" dependencies = [ "atty", "csv", - "encode_unicode", + "encode_unicode 1.0.0", "lazy_static", "term 0.7.0", "unicode-width", @@ -3214,6 +3245,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "termtree" version = "0.2.4" @@ -3937,6 +3978,7 @@ dependencies = [ "chrono", "clap 3.2.23", "colored 2.0.0", + "dialoguer", "dirs 4.0.0", "distance", "fern", @@ -4181,7 +4223,9 @@ dependencies = [ "dirs 4.0.0", "flate2", "graphql_client", + "log", "lzma-rs", + "rand 0.8.5", "reqwest", "semver 1.0.14", "serde", @@ -4858,3 +4902,9 @@ name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Makefile b/Makefile index 2241b1b33..69012a0b9 100644 --- a/Makefile +++ b/Makefile @@ -748,3 +748,6 @@ install-local: package test-minimal-versions: rm -f Cargo.lock cargo +nightly build --tests -Z minimal-versions --all-features + +update-graphql-schema: + curl -sSfL https://registry.wapm.io/graphql/schema.graphql > lib/registry/graphql/schema.graphql diff --git a/lib/api/src/js/mod.rs b/lib/api/src/js/mod.rs index e9e9e3295..a172dffcf 100644 --- a/lib/api/src/js/mod.rs +++ b/lib/api/src/js/mod.rs @@ -54,7 +54,7 @@ pub use crate::js::function_env::{FunctionEnv, FunctionEnvMut}; pub use crate::js::imports::Imports; pub use crate::js::instance::Instance; pub use crate::js::mem_access::{MemoryAccessError, WasmRef, WasmSlice, WasmSliceIter}; -pub use crate::js::module::{Module, ModuleTypeHints}; +pub use crate::js::module::{IoCompileError, Module, ModuleTypeHints}; pub use crate::js::native::TypedFunction; pub use crate::js::native_type::NativeWasmTypeInto; pub use crate::js::ptr::{Memory32, Memory64, MemorySize, WasmPtr, WasmPtr64}; diff --git a/lib/api/src/js/module.rs b/lib/api/src/js/module.rs index 47b751aaf..36c7f3553 100644 --- a/lib/api/src/js/module.rs +++ b/lib/api/src/js/module.rs @@ -22,6 +22,7 @@ use wasmer_types::{ Pages, TableType, Type, }; +/// IO Error on a Module Compilation #[derive(Debug)] #[cfg_attr(feature = "std", derive(Error))] pub enum IoCompileError { diff --git a/lib/api/src/sys/mod.rs b/lib/api/src/sys/mod.rs index 1c272a2a6..d24be112d 100644 --- a/lib/api/src/sys/mod.rs +++ b/lib/api/src/sys/mod.rs @@ -23,7 +23,7 @@ pub use crate::sys::function_env::{FunctionEnv, FunctionEnvMut}; pub use crate::sys::imports::Imports; pub use crate::sys::instance::{Instance, InstantiationError}; pub use crate::sys::mem_access::{MemoryAccessError, WasmRef, WasmSlice, WasmSliceIter}; -pub use crate::sys::module::Module; +pub use crate::sys::module::{IoCompileError, Module}; pub use crate::sys::native::TypedFunction; pub use crate::sys::native_type::NativeWasmTypeInto; pub use crate::sys::store::{AsStoreMut, AsStoreRef, StoreMut, StoreRef}; diff --git a/lib/api/src/sys/module.rs b/lib/api/src/sys/module.rs index 4b6807178..247530809 100644 --- a/lib/api/src/sys/module.rs +++ b/lib/api/src/sys/module.rs @@ -18,6 +18,7 @@ use wasmer_types::{ use wasmer_types::{ExportType, ImportType}; use wasmer_vm::InstanceHandle; +/// IO Error on a Module Compilation #[derive(Error, Debug)] pub enum IoCompileError { /// An IO error diff --git a/lib/cli/Cargo.toml b/lib/cli/Cargo.toml index 3c203a5a3..b9d43b3aa 100644 --- a/lib/cli/Cargo.toml +++ b/lib/cli/Cargo.toml @@ -44,7 +44,7 @@ atty = "0.2" colored = "2.0" anyhow = "1.0" spinner = "0.5.0" -clap = { version = "3.2.22", features = ["derive"] } +clap = { version = "3.2.22", features = ["derive", "env"] } # For the function names autosuggestion distance = "0.4" # For the inspect subcommand @@ -56,7 +56,7 @@ log = { version = "0.4", optional = true } tempfile = "3" tempdir = "0.3.7" http_req = { version="^0.8", default-features = false, features = ["rust-tls"], optional = true } -reqwest = { version = "^0.11", default-features = false, feature = ["rustls-tls", "json"], optional = true } +reqwest = { version = "^0.11", default-features = false, features = ["rustls-tls", "json"], optional = true } serde = { version = "1.0.147", features = ["derive"], optional = true } dirs = { version = "4.0", optional = true } serde_json = { version = "1.0", optional = true } @@ -72,6 +72,7 @@ nuke-dir = { version = "0.1.0", optional = true } webc = { version = "3.0.1", optional = true } isatty = "0.1.9" tar = "0.4" +dialoguer = "0.10.2" [build-dependencies] chrono = { version = "^0.4", default-features = false, features = [ "std", "clock" ] } diff --git a/lib/cli/src/cli.rs b/lib/cli/src/cli.rs index 6e1603f48..34c2c1835 100644 --- a/lib/cli/src/cli.rs +++ b/lib/cli/src/cli.rs @@ -10,10 +10,10 @@ use crate::commands::CreateExe; use crate::commands::CreateObj; #[cfg(feature = "wast")] use crate::commands::Wast; -use crate::commands::{Cache, Config, Inspect, List, Run, SelfUpdate, Validate}; +use crate::commands::{Add, Cache, Config, Inspect, List, Login, Run, SelfUpdate, Validate}; use crate::error::PrettyError; use clap::{CommandFactory, ErrorKind, Parser}; -use std::fmt; +use std::{fmt, str::FromStr}; #[derive(Parser, Debug)] #[cfg_attr( @@ -44,6 +44,10 @@ enum WasmerCLIOptions { #[clap(name = "run")] Run(Run), + /// Login into a wapm.io-like registry + #[clap(name = "login")] + Login(Login), + /// Wasmer cache #[clap(subcommand, name = "cache")] Cache(Cache), @@ -146,6 +150,9 @@ enum WasmerCLIOptions { #[cfg(target_os = "linux")] #[clap(name = "binfmt")] Binfmt(Binfmt), + + /// Add a WAPM package's bindings to your application. + Add(Add), } impl WasmerCLIOptions { @@ -164,10 +171,12 @@ impl WasmerCLIOptions { Self::Config(config) => config.execute(), Self::Inspect(inspect) => inspect.execute(), Self::List(list) => list.execute(), + Self::Login(login) => login.execute(), #[cfg(feature = "wast")] Self::Wast(wast) => wast.execute(), #[cfg(target_os = "linux")] Self::Binfmt(binfmt) => binfmt.execute(), + Self::Add(install) => install.execute(), } } } @@ -219,8 +228,10 @@ fn wasmer_main_inner() -> Result<(), anyhow::Error> { WasmerCLIOptions::Run(Run::from_binfmt_args()) } else { match command.unwrap_or(&"".to_string()).as_ref() { - "cache" | "compile" | "config" | "create-exe" | "help" | "inspect" | "run" - | "self-update" | "validate" | "wast" | "binfmt" | "list" => WasmerCLIOptions::parse(), + "add" | "cache" | "compile" | "config" | "create-exe" | "help" | "inspect" | "run" + | "self-update" | "validate" | "wast" | "binfmt" | "list" | "login" => { + WasmerCLIOptions::parse() + } _ => { WasmerCLIOptions::try_parse_from(args.iter()).unwrap_or_else(|e| { match e.kind() { @@ -271,7 +282,7 @@ impl fmt::Display for SplitVersion { #[test] fn test_split_version() { assert_eq!( - SplitVersion::new("registry.wapm.io/graphql/python/python").unwrap(), + SplitVersion::parse("registry.wapm.io/graphql/python/python").unwrap(), SplitVersion { original: "registry.wapm.io/graphql/python/python".to_string(), registry: Some("https://registry.wapm.io/graphql".to_string()), @@ -281,7 +292,7 @@ fn test_split_version() { } ); assert_eq!( - SplitVersion::new("registry.wapm.io/python/python").unwrap(), + SplitVersion::parse("registry.wapm.io/python/python").unwrap(), SplitVersion { original: "registry.wapm.io/python/python".to_string(), registry: Some("https://registry.wapm.io/graphql".to_string()), @@ -291,7 +302,7 @@ fn test_split_version() { } ); assert_eq!( - SplitVersion::new("namespace/name@version:command").unwrap(), + SplitVersion::parse("namespace/name@version:command").unwrap(), SplitVersion { original: "namespace/name@version:command".to_string(), registry: None, @@ -301,7 +312,7 @@ fn test_split_version() { } ); assert_eq!( - SplitVersion::new("namespace/name@version").unwrap(), + SplitVersion::parse("namespace/name@version").unwrap(), SplitVersion { original: "namespace/name@version".to_string(), registry: None, @@ -311,7 +322,7 @@ fn test_split_version() { } ); assert_eq!( - SplitVersion::new("namespace/name").unwrap(), + SplitVersion::parse("namespace/name").unwrap(), SplitVersion { original: "namespace/name".to_string(), registry: None, @@ -321,7 +332,7 @@ fn test_split_version() { } ); assert_eq!( - SplitVersion::new("registry.wapm.io/namespace/name").unwrap(), + SplitVersion::parse("registry.wapm.io/namespace/name").unwrap(), SplitVersion { original: "registry.wapm.io/namespace/name".to_string(), registry: Some("https://registry.wapm.io/graphql".to_string()), @@ -331,13 +342,21 @@ fn test_split_version() { } ); assert_eq!( - format!("{}", SplitVersion::new("namespace").unwrap_err()), + format!("{}", SplitVersion::parse("namespace").unwrap_err()), "Invalid package version: \"namespace\"".to_string(), ); } impl SplitVersion { - pub fn new(s: &str) -> Result { + pub fn parse(s: &str) -> Result { + s.parse() + } +} + +impl FromStr for SplitVersion { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { let command = WasmerCLIOptions::command(); let mut prohibited_package_names = command.get_subcommands().map(|s| s.get_name()); diff --git a/lib/cli/src/commands.rs b/lib/cli/src/commands.rs index ad5dc0135..964bde1ab 100644 --- a/lib/cli/src/commands.rs +++ b/lib/cli/src/commands.rs @@ -1,4 +1,5 @@ //! The commands available in the Wasmer binary. +mod add; #[cfg(target_os = "linux")] mod binfmt; mod cache; @@ -11,6 +12,7 @@ mod create_exe; mod create_obj; mod inspect; mod list; +mod login; mod run; mod self_update; mod validate; @@ -27,7 +29,9 @@ pub use create_exe::*; pub use create_obj::*; #[cfg(feature = "wast")] pub use wast::*; -pub use {cache::*, config::*, inspect::*, list::*, run::*, self_update::*, validate::*}; +pub use { + add::*, cache::*, config::*, inspect::*, list::*, login::*, run::*, self_update::*, validate::*, +}; /// The kind of object format to emit. #[derive(Debug, Copy, Clone, clap::Parser)] diff --git a/lib/cli/src/commands/add.rs b/lib/cli/src/commands/add.rs new file mode 100644 index 000000000..76c4ffb2c --- /dev/null +++ b/lib/cli/src/commands/add.rs @@ -0,0 +1,181 @@ +use std::process::{Command, Stdio}; + +use anyhow::{Context, Error}; +use clap::Parser; +use wasmer_registry::{Bindings, PartialWapmConfig, ProgrammingLanguage}; + +use crate::cli::SplitVersion; + +/// Add a WAPM package's bindings to your application. +#[derive(Debug, Parser)] +pub struct Add { + /// The registry to fetch bindings from. + #[clap(long, env = "WAPM_REGISTRY")] + registry: Option, + /// Add the JavaScript bindings using "npm install". + #[clap(long, groups = &["bindings", "js"])] + npm: bool, + /// Add the JavaScript bindings using "yarn add". + #[clap(long, groups = &["bindings", "js"])] + yarn: bool, + /// Add the package as a dev-dependency. + #[clap(long, requires = "js")] + dev: bool, + /// Add the Python bindings using "pip install". + #[clap(long, groups = &["bindings", "py"])] + pip: bool, + /// The packages to add (e.g. "wasmer/wasmer-pack@0.5.0" or "python/python") + #[clap(parse(try_from_str))] + packages: Vec, +} + +impl Add { + /// Execute [`Add`]. + pub fn execute(&self) -> Result<(), Error> { + anyhow::ensure!(!self.packages.is_empty(), "No packages specified"); + + let registry = self + .registry() + .context("Unable to determine which registry to use")?; + + let bindings = self.lookup_bindings(®istry)?; + + let mut cmd = self.target().command(&bindings); + + #[cfg(feature = "debug")] + log::debug!("Running {cmd:?}"); + + let status = cmd + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .status() + .with_context(|| { + format!( + "Unable to start \"{:?}\". Is it installed?", + cmd.get_program() + ) + })?; + + anyhow::ensure!(status.success(), "Command failed: {:?}", cmd); + + Ok(()) + } + + fn lookup_bindings(&self, registry: &str) -> Result, Error> { + #[cfg(feature = "debug")] + log::debug!("Querying WAPM for the bindings packages"); + + let mut bindings_to_add = Vec::new(); + let language = self.target().language(); + + for pkg in &self.packages { + let bindings = lookup_bindings_for_package(registry, pkg, &language) + .with_context(|| format!("Unable to find bindings for {pkg}"))?; + bindings_to_add.push(bindings); + } + + Ok(bindings_to_add) + } + + fn registry(&self) -> Result { + match &self.registry { + Some(r) => Ok(r.clone()), + None => { + let cfg = PartialWapmConfig::from_file() + .map_err(Error::msg) + .context("Unable to load WAPM's config file")?; + Ok(cfg.registry.get_current_registry()) + } + } + } + + fn target(&self) -> Target { + match (self.pip, self.npm, self.yarn) { + (true, false, false) => Target::Pip, + (false, true, false) => Target::Npm { dev: self.dev }, + (false, false, true) => Target::Yarn { dev: self.dev }, + _ => unreachable!( + "Clap should ensure at least one item in the \"bindings\" group is specified" + ), + } + } +} + +fn lookup_bindings_for_package( + registry: &str, + pkg: &SplitVersion, + language: &ProgrammingLanguage, +) -> Result { + let all_bindings = + wasmer_registry::list_bindings(registry, &pkg.package, pkg.version.as_deref())?; + + match all_bindings.iter().find(|b| b.language == *language) { + Some(b) => { + #[cfg(feature = "debug")] + { + let Bindings { url, generator, .. } = b; + log::debug!("Found {pkg} bindings generated by {generator} at {url}"); + } + + Ok(b.clone()) + } + None => { + if all_bindings.is_empty() { + anyhow::bail!("The package doesn't contain any bindings"); + } else { + todo!(); + } + } + } +} + +#[derive(Debug, Copy, Clone)] +enum Target { + Pip, + Yarn { dev: bool }, + Npm { dev: bool }, +} + +impl Target { + fn language(self) -> ProgrammingLanguage { + match self { + Target::Pip => ProgrammingLanguage::PYTHON, + Target::Yarn { .. } | Target::Npm { .. } => ProgrammingLanguage::JAVASCRIPT, + } + } + + /// Construct a command which we can run to add packages. + /// + /// This deliberately runs the command using the OS shell instead of + /// invoking the tool directly. That way we can handle when a version + /// manager (e.g. `nvm` or `asdf`) replaces the tool with a script (e.g. + /// `npm.cmd` or `yarn.ps1`). + /// + /// See for more. + fn command(self, packages: &[Bindings]) -> Command { + let command_line = match self { + Target::Pip => "pip install", + Target::Yarn { dev: true } => "yarn add --dev", + Target::Yarn { dev: false } => "yarn add", + Target::Npm { dev: true } => "npm install --dev", + Target::Npm { dev: false } => "npm install", + }; + let mut command_line = command_line.to_string(); + + for pkg in packages { + command_line.push(' '); + command_line.push_str(&pkg.url); + } + + if cfg!(windows) { + let mut cmd = Command::new("cmd"); + cmd.arg("/C").arg(command_line); + cmd + } else { + let mut cmd = Command::new("sh"); + cmd.arg("-c").arg(command_line); + cmd + } + } +} diff --git a/lib/cli/src/commands/create_exe.rs b/lib/cli/src/commands/create_exe.rs index 755bef43d..59a192105 100644 --- a/lib/cli/src/commands/create_exe.rs +++ b/lib/cli/src/commands/create_exe.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "http")] use std::collections::BTreeMap; use std::env; +use std::fmt::Write as _; use std::fs; use std::fs::File; use std::io::prelude::*; @@ -920,7 +921,8 @@ impl CreateExe { for atom_name in atom_names.iter() { let atom_name = Self::normalize_atom_name(atom_name); - c_code_to_instantiate.push_str(&format!( + write!( + c_code_to_instantiate, " wasm_module_t *atom_{atom_name} = wasmer_object_module_new(store, \"{atom_name}\"); @@ -931,11 +933,16 @@ impl CreateExe { return -1; }} " - )); - deallocate_module.push_str(&format!("wasm_module_delete(atom_{atom_name});")); + ) + .unwrap(); + write!(deallocate_module, "wasm_module_delete(atom_{atom_name});").unwrap(); } - c_code_to_instantiate.push_str(&format!("wasm_module_t *module = atom_{atom_to_run};")); + write!( + c_code_to_instantiate, + "wasm_module_t *module = atom_{atom_to_run};" + ) + .unwrap(); WASMER_STATIC_MAIN_C_SOURCE .replace("#define WASI", "#define WASI\r\n#define WASI_PIRITA") diff --git a/lib/cli/src/commands/list.rs b/lib/cli/src/commands/list.rs index 332129b39..0851d9947 100644 --- a/lib/cli/src/commands/list.rs +++ b/lib/cli/src/commands/list.rs @@ -36,7 +36,7 @@ impl List { if empty_table { table.add_empty_row(); } - let _ = table.printstd(); + table.printstd(); Ok(()) } diff --git a/lib/cli/src/commands/login.rs b/lib/cli/src/commands/login.rs new file mode 100644 index 000000000..290c1e73f --- /dev/null +++ b/lib/cli/src/commands/login.rs @@ -0,0 +1,51 @@ +use clap::Parser; +use dialoguer::Input; + +/// Subcommand for listing packages +#[derive(Debug, Clone, Parser)] +pub struct Login { + /// Registry to log into (default: wapm.io) + #[clap(long, default_value = "wapm.io")] + pub registry: String, + /// Login token + #[clap(name = "TOKEN")] + pub token: Option, +} + +impl Login { + fn get_token_or_ask_user(&self) -> Result { + match self.token.as_ref() { + Some(s) => Ok(s.clone()), + None => { + let registry_host = url::Url::parse(&wasmer_registry::format_graphql( + &self.registry, + )) + .map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Invalid registry for login {}: {e}", self.registry), + ) + })?; + let registry_host = registry_host.host_str().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::Other, + format!("Invalid registry for login {}: no host", self.registry), + ) + })?; + Input::new() + .with_prompt(&format!( + "Please paste the login token from https://{}/me:\"", + registry_host + )) + .interact_text() + } + } + } + + /// execute [List] + pub fn execute(&self) -> Result<(), anyhow::Error> { + let token = self.get_token_or_ask_user()?; + wasmer_registry::login::login_and_save_token(&self.registry, &token) + .map_err(|e| anyhow::anyhow!("{e}")) + } +} diff --git a/lib/cli/src/commands/run.rs b/lib/cli/src/commands/run.rs index 2b5dad57b..ab019ae6d 100644 --- a/lib/cli/src/commands/run.rs +++ b/lib/cli/src/commands/run.rs @@ -845,7 +845,7 @@ pub(crate) fn try_run_package_or_file( let package = format!("{}", r.path.display()); let mut is_fake_sv = false; - let mut sv = match SplitVersion::new(&package) { + let mut sv = match SplitVersion::parse(&package) { Ok(o) => o, Err(_) => { let mut fake_sv = SplitVersion { diff --git a/lib/compiler-cranelift/src/translator/func_state.rs b/lib/compiler-cranelift/src/translator/func_state.rs index fafa6fe0d..773ec7810 100644 --- a/lib/compiler-cranelift/src/translator/func_state.rs +++ b/lib/compiler-cranelift/src/translator/func_state.rs @@ -189,7 +189,7 @@ impl ControlStackFrame { /// Pop values from the value stack so that it is left at the /// input-parameters to an else-block. pub fn truncate_value_stack_to_else_params(&self, stack: &mut Vec) { - debug_assert!(matches!(self, &ControlStackFrame::If { .. })); + debug_assert!(matches!(self, &Self::If { .. })); stack.truncate(self.original_stack_size()); } @@ -202,7 +202,7 @@ impl ControlStackFrame { // block can see the same number of parameters as the consequent block. As a matter of // fact, we need to substract an extra number of parameter values for if blocks. let num_duplicated_params = match self { - &ControlStackFrame::If { + &Self::If { num_param_values, .. } => { debug_assert!(num_param_values <= self.original_stack_size()); diff --git a/lib/registry/Cargo.toml b/lib/registry/Cargo.toml index a354ae20b..97dc0e702 100644 --- a/lib/registry/Cargo.toml +++ b/lib/registry/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" license = "MIT" description = "Crate to interact with the wasmer registry (wapm.io), download packages, etc." +[dev-dependencies] +rand = "0.8.5" + [dependencies] dirs = "4.0.0" graphql_client = "0.11.0" @@ -21,4 +24,5 @@ tar = "0.4.38" flate2 = "1.0.24" semver = "1.0.14" lzma-rs = "0.2.0" -tempdir = "0.3.7" \ No newline at end of file +tempdir = "0.3.7" +log = "0.4.17" diff --git a/lib/registry/graphql/queries/get_bindings.graphql b/lib/registry/graphql/queries/get_bindings.graphql new file mode 100644 index 000000000..bb4d8f966 --- /dev/null +++ b/lib/registry/graphql/queries/get_bindings.graphql @@ -0,0 +1,22 @@ +query GetBindingsQuery ($name: String!, $version: String = "latest") { + packageVersion: getPackageVersion(name:$name, version:$version) { + bindings { + id + language + url + + generator { + packageVersion { + id + version + package { + name + } + } + commandName + } + + __typename + } + } +} diff --git a/lib/registry/graphql/queries/whoami.graphql b/lib/registry/graphql/queries/whoami.graphql new file mode 100644 index 000000000..0c42eec26 --- /dev/null +++ b/lib/registry/graphql/queries/whoami.graphql @@ -0,0 +1,5 @@ +query WhoAmIQuery { + viewer { + username + } +} \ No newline at end of file diff --git a/lib/registry/graphql/schema.graphql b/lib/registry/graphql/schema.graphql index 07687311d..c79035d96 100644 --- a/lib/registry/graphql/schema.graphql +++ b/lib/registry/graphql/schema.graphql @@ -1,318 +1,304 @@ -type APIToken { - createdAt: DateTime! +interface Node { + """The ID of the object""" id: ID! - identifier: String - lastUsedAt: DateTime +} + +type PublicKey implements Node { + """The ID of the object""" + id: ID! + owner: User! + keyId: String! + key: String! revokedAt: DateTime - user: User! + uploadedAt: DateTime! + verifyingSignature: Signature + revoked: Boolean! } -type APITokenConnection { - # Contains the nodes in this connection. - edges: [APITokenEdge]! +type User implements Node & PackageOwner { + """Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.""" + username: String! + firstName: String! + lastName: String! + email: String! + dateJoined: DateTime! + isEmailValidated: Boolean! + bio: String + location: String + websiteUrl: String - # Pagination data for this connection. + """The ID of the object""" + id: ID! + globalName: String! + avatar(size: Int = 80): String! + isViewer: Boolean! + hasUsablePassword: Boolean + fullName: String! + githubUrl: String + twitterUrl: String + publicActivity(before: String, after: String, first: Int, last: Int): ActivityEventConnection! + namespaces(before: String, after: String, first: Int, last: Int): NamespaceConnection! + packages(collaborating: Boolean = false, before: String, after: String, first: Int, last: Int): PackageConnection! + packageVersions(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! +} + +interface PackageOwner { + 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]! } -# A Relay edge containing a `APIToken` and its cursor. -type APITokenEdge { - # A cursor for use in pagination +""" +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! - - # The item at the end of the edge - node: APIToken -} - -input AcceptNamespaceCollaboratorInviteInput { - clientMutationId: String - inviteId: ID! -} - -type AcceptNamespaceCollaboratorInvitePayload { - clientMutationId: String - namespaceCollaboratorInvite: NamespaceCollaboratorInvite! -} - -input AcceptPackageCollaboratorInviteInput { - clientMutationId: String - inviteId: ID! -} - -type AcceptPackageCollaboratorInvitePayload { - clientMutationId: String - packageCollaboratorInvite: PackageCollaboratorInvite! -} - -input AcceptPackageTransferRequestInput { - clientMutationId: String - packageTransferRequestId: ID! -} - -type AcceptPackageTransferRequestPayload { - clientMutationId: String - package: Package! - packageTransferRequest: PackageTransferRequest! } type ActivityEvent implements Node { - actorIcon: String! - body: ActivityEventBody! - createdAt: DateTime! - - # The ID of the object + """The ID of the object""" id: ID! + body: ActivityEventBody! + actorIcon: String! + createdAt: DateTime! } type ActivityEventBody { - ranges: [NodeBodyRange!]! text: String! + ranges: [NodeBodyRange!]! } -type ActivityEventConnection { - # Contains the nodes in this connection. - edges: [ActivityEventEdge]! +type NodeBodyRange { + entity: Node! + offset: Int! + length: Int! +} - # Pagination data for this connection. +type NamespaceConnection { + """Pagination data for this connection.""" pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NamespaceEdge]! } -# A Relay edge containing a `ActivityEvent` and its cursor. -type ActivityEventEdge { - # A cursor for use in pagination +"""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! - - # The item at the end of the edge - node: ActivityEvent } -input ArchivePackageInput { - clientMutationId: String - packageId: ID! -} - -type ArchivePackagePayload { - clientMutationId: String - package: Package! -} - -input ChangePackageVersionArchivedStatusInput { - clientMutationId: String - isArchived: Boolean - packageVersionId: ID! -} - -type ChangePackageVersionArchivedStatusPayload { - clientMutationId: String - packageVersion: PackageVersion! -} - -input ChangeUserEmailInput { - clientMutationId: String - newEmail: String! -} - -type ChangeUserEmailPayload { - clientMutationId: String - user: User! -} - -input ChangeUserPasswordInput { - clientMutationId: String - password: String! - - # The token associated to change the password. If not existing it will use the request user by default - token: String -} - -type ChangeUserPasswordPayload { - clientMutationId: String - token: String -} - -input ChangeUserUsernameInput { - clientMutationId: String - - # The new user username - username: String! -} - -type ChangeUserUsernamePayload { - clientMutationId: String - token: String - user: User -} - -input CheckUserExistsInput { - clientMutationId: String - - # The user - user: String! -} - -type CheckUserExistsPayload { - clientMutationId: String - exists: Boolean! - - # The user is only returned if the user input was the username - user: User -} - -type Command { - command: String! - module: PackageVersionModule! - packageVersion: PackageVersion! -} - -input CreateNamespaceInput { - # The namespace avatar - avatar: String - clientMutationId: String - - # The namespace description - description: String - - # The namespace display name +type Namespace implements Node & PackageOwner { + """The ID of the object""" + id: ID! + name: String! displayName: String - name: String! -} - -type CreateNamespacePayload { - clientMutationId: String - namespace: Namespace! - user: User! -} - -# The `DateTime` scalar type represents a DateTime -# value as specified by -# [iso8601](https://en.wikipedia.org/wiki/ISO_8601). -scalar DateTime - -input DeleteNamespaceInput { - clientMutationId: String - namespaceId: ID! -} - -type DeleteNamespacePayload { - clientMutationId: String - success: Boolean! -} - -type ErrorType { - field: String! - messages: [String!]! -} - -input GenerateAPITokenInput { - clientMutationId: String - identifier: String -} - -type GenerateAPITokenPayload { - clientMutationId: String - token: APIToken - tokenRaw: String - user: User -} - -# The `GenericScalar` scalar type represents a generic -# GraphQL scalar value that could be: -# String, Boolean, Int, Float, List or Object. -scalar GenericScalar - -type GetPasswordResetToken { - user: User - valid: Boolean! -} - -union GlobalObject = Namespace | User - -input InputSignature { - data: String! - publicKeyKeyId: String! -} - -type Interface implements Node { - createdAt: DateTime! description: String! - displayName: String! - homepage: String - icon: String - - # The ID of the object - id: ID! - lastVersion: InterfaceVersion - name: String! - updatedAt: DateTime! - versions(after: String = null, before: String = null, first: Int = null, last: Int = null, offset: Int = null): InterfaceVersionConnection! -} - -type InterfaceVersion implements Node { - content: String! + avatar: String! + avatarUpdatedAt: DateTime createdAt: DateTime! - - # The ID of the object - id: ID! - interface: Interface! - packageVersions(after: String = null, before: String = null, first: Int = null, last: Int = null, offset: Int = null): PackageVersionConnection! - publishedBy: User! updatedAt: DateTime! - version: String! + 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(before: String, after: String, first: Int, last: Int): PackageConnection! + packageVersions(before: String, after: String, first: Int, last: Int): PackageVersionConnection! + collaborators(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: Role!): Boolean! + packageTransfersIncoming(before: String, after: String, first: Int, last: Int): PackageTransferRequestConnection! } -type InterfaceVersionConnection { - # Contains the nodes in this connection. - edges: [InterfaceVersionEdge]! - - # Pagination data for this connection. +type NamespaceCollaboratorInviteConnection { + """Pagination data for this connection.""" pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NamespaceCollaboratorInviteEdge]! } -# A Relay edge containing a `InterfaceVersion` and its cursor. -type InterfaceVersionEdge { - # A cursor for use in pagination +""" +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! - - # The item at the end of the edge - node: InterfaceVersion } -input InviteNamespaceCollaboratorInput { - clientMutationId: String - email: String - namespaceId: ID! - role: Role! - username: String -} - -type InviteNamespaceCollaboratorPayload { - clientMutationId: String - invite: NamespaceCollaboratorInvite! +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 } -input InvitePackageCollaboratorInput { - clientMutationId: String - email: String +"""An enumeration.""" +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 +} + +"""An enumeration.""" +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]! +} + +"""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]! +} + +"""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! + namespace: 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 + displayName: String! + + """The name of the package without the owner""" packageName: String! - role: Role! - username: String -} -type InvitePackageCollaboratorPayload { - clientMutationId: String - invite: PackageCollaboratorInvite! - package: Package! -} + """The app icon. It should be formatted in the same way as Apple icons""" + appIcon: String! @deprecated(reason: "Please use icon instead") -input LikePackageInput { - clientMutationId: String - packageId: ID! -} + """The total number of downloads of the package""" + downloadsCount: Int -type LikePackagePayload { - clientMutationId: String - package: Package! + """The public keys for all the published versions""" + publicKeys: [PublicKey!]! + collaborators(before: String, after: String, first: Int, last: Int): PackageCollaboratorConnection! + pendingInvites(before: String, after: String, first: Int, last: Int): PackageCollaboratorInviteConnection! + viewerHasRole(role: Role!): Boolean! + owner: PackageOwner! + isTransferring: Boolean! + activeTransferRequest: PackageTransferRequest + isArchived: Boolean! + viewerIsWatching: Boolean! } interface Likeable { @@ -321,726 +307,424 @@ interface Likeable { viewerHasLiked: Boolean! } -type Mutation { - acceptNamespaceCollaboratorInvite(input: AcceptNamespaceCollaboratorInviteInput!): AcceptNamespaceCollaboratorInvitePayload - acceptPackageCollaboratorInvite(input: AcceptPackageCollaboratorInviteInput!): AcceptPackageCollaboratorInvitePayload - acceptPackageTransferRequest(input: AcceptPackageTransferRequestInput!): AcceptPackageTransferRequestPayload - archivePackage(input: ArchivePackageInput!): ArchivePackagePayload - changePackageVersionArchivedStatus(input: ChangePackageVersionArchivedStatusInput!): ChangePackageVersionArchivedStatusPayload - changeUserEmail(input: ChangeUserEmailInput!): ChangeUserEmailPayload - changeUserPassword(input: ChangeUserPasswordInput!): ChangeUserPasswordPayload - changeUserUsername(input: ChangeUserUsernameInput!): ChangeUserUsernamePayload - checkUserExists(input: CheckUserExistsInput!): CheckUserExistsPayload - createNamespace(input: CreateNamespaceInput!): CreateNamespacePayload - deleteNamespace(input: DeleteNamespaceInput!): DeleteNamespacePayload - generateApiToken(input: GenerateAPITokenInput!): GenerateAPITokenPayload - inviteNamespaceCollaborator(input: InviteNamespaceCollaboratorInput!): InviteNamespaceCollaboratorPayload - invitePackageCollaborator(input: InvitePackageCollaboratorInput!): InvitePackageCollaboratorPayload - likePackage(input: LikePackageInput!): LikePackagePayload - publishPackage(input: PublishPackageInput!): PublishPackagePayload - publishPublicKey(input: PublishPublicKeyInput!): PublishPublicKeyPayload - readNotification(input: ReadNotificationInput!): ReadNotificationPayload - refreshToken(input: RefreshInput!): RefreshPayload - registerUser(input: RegisterUserInput!): RegisterUserPayload - removeNamespaceCollaborator(input: RemoveNamespaceCollaboratorInput!): RemoveNamespaceCollaboratorPayload - removeNamespaceCollaboratorInvite(input: RemoveNamespaceCollaboratorInviteInput!): RemoveNamespaceCollaboratorInvitePayload - removePackageCollaborator(input: RemovePackageCollaboratorInput!): RemovePackageCollaboratorPayload - removePackageCollaboratorInvite(input: RemovePackageCollaboratorInviteInput!): RemovePackageCollaboratorInvitePayload - removePackageTransferRequest(input: RemovePackageTransferRequestInput!): RemovePackageTransferRequestPayload - requestPackageTransfer(input: RequestPackageTransferInput!): RequestPackageTransferPayload - requestPasswordReset(input: RequestPasswordResetInput!): RequestPasswordResetPayload - requestValidationEmail(input: RequestValidationEmailInput!): RequestValidationEmailPayload - revokeApiToken(input: RevokeAPITokenInput!): RevokeAPITokenPayload - seePendingNotifications(input: SeePendingNotificationsInput!): SeePendingNotificationsPayload - - # Social Auth for JSON Web Token (JWT) - socialAuth(input: SocialAuthJWTInput!): SocialAuthJWTPayload - - # Obtain JSON Web Token mutation - tokenAuth(input: ObtainJSONWebTokenInput!): ObtainJSONWebTokenPayload - unlikePackage(input: UnlikePackageInput!): UnlikePackagePayload - unwatchPackage(input: UnwatchPackageInput!): UnwatchPackagePayload - updateNamespace(input: UpdateNamespaceInput!): UpdateNamespacePayload - updateNamespaceCollaboratorRole(input: UpdateNamespaceCollaboratorRoleInput!): UpdateNamespaceCollaboratorRolePayload - updatePackage(input: UpdatePackageInput!): UpdatePackagePayload - updatePackageCollaboratorRole(input: UpdatePackageCollaboratorRoleInput!): UpdatePackageCollaboratorRolePayload - updateUserInfo(input: UpdateUserInfoInput!): UpdateUserInfoPayload - validateUserEmail(input: ValidateUserEmailInput!): ValidateUserEmailPayload - validateUserPassword(input: ValidateUserPasswordInput!): ValidateUserPasswordPayload - verifyToken(input: VerifyInput!): VerifyPayload - watchPackage(input: WatchPackageInput!): WatchPackagePayload -} - -type Namespace implements Node & PackageOwner { - avatar: String! - avatarUpdatedAt: DateTime - collaborators(after: String = null, before: String = null, first: Int = null, last: Int = null): NamespaceCollaboratorConnection - createdAt: DateTime! +type PackageVersion implements Node { + """The ID of the object""" + id: ID! + package: Package! + version: String! description: String! - displayName: String - globalName: 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! - # The ID of the object - id: ID! - maintainerInvites: [NamespaceCollaboratorInvite!]! - maintainersWithRoles(after: String = null, before: String = null, first: Int = null, last: Int = null, offset: Int = null): NamespaceMaintainerConnection! + """""" + fileSize: BigInt! + piritaFile: String + + """""" + piritaFileSize: BigInt! + piritaManifest: JSONString + piritaVolumes: JSONString + totalDownloads: Int! + hasBindings: Boolean! + 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 +} + +""" +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 + +type Command { + command: String! + packageVersion: PackageVersion! + module: PackageVersionModule! +} + +type PackageVersionModule { name: String! - packageVersions(after: String = null, before: String = null, first: Int = null, last: Int = null): PackageVersionConnection - packages(after: String = null, before: String = null, first: Int = null, last: Int = null): PackageConnection - pendingInvites(after: String = null, before: String = null, first: Int = null, last: Int = null): NamespaceCollaboratorInviteConnection - publicActivity(after: String = null, before: String = null, first: Int = null, last: Int = null): ActivityEventConnection! - updatedAt: DateTime! - userSet(after: String = null, before: String = null, first: Int = null, last: Int = null, offset: Int = null): UserConnection! - viewerHasRole(role: Role!): Boolean! + source: String! + abi: String + publicUrl: String! } -type NamespaceCollaborator { - createdAt: DateTime! +type NativeExecutableConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NativeExecutableEdge]! +} + +"""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! - invite: NamespaceCollaboratorInvite - namespace: Namespace! - role: RegistryNamespaceMaintainerRoleChoices! - updatedAt: DateTime! - user: User! + module: String! @deprecated(reason: "Use filename instead") + filename: String! + targetTriple: String! + downloadUrl: String! } -type NamespaceCollaboratorConnection { - # Contains the nodes in this connection. - edges: [NamespaceCollaboratorEdge]! - - # Pagination data for this connection. +type BindingsGeneratorConnection { + """Pagination data for this connection.""" pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [BindingsGeneratorEdge]! } -# A Relay edge containing a `NamespaceCollaborator` and its cursor. -type NamespaceCollaboratorEdge { - # A cursor for use in pagination +"""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! - - # The item at the end of the edge - node: NamespaceCollaborator } -type NamespaceCollaboratorInvite { - accepted: NamespaceMaintainer - approvedBy: User - closedAt: DateTime - createdAt: DateTime! - declinedBy: User - expiresAt: DateTime! +type BindingsGenerator implements Node { + """The ID of the object""" id: ID! - inviteEmail: String - namespace: Namespace! - requestedBy: User! - role: RegistryNamespaceMaintainerInviteRoleChoices! - user: User + packageVersion: PackageVersion! + 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 NamespaceCollaboratorInviteConnection { - # Contains the nodes in this connection. - edges: [NamespaceCollaboratorInviteEdge]! - - # Pagination data for this connection. +type PackageVersionNPMBindingConnection { + """Pagination data for this connection.""" pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageVersionNPMBindingEdge]! } -# A Relay edge containing a `NamespaceCollaboratorInvite` and its cursor. -type NamespaceCollaboratorInviteEdge { - # A cursor for use in pagination +"""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! - - # The item at the end of the edge - node: NamespaceCollaboratorInvite } -type NamespaceConnection { - # Contains the nodes in this connection. - edges: [NamespaceEdge]! +type PackageVersionNPMBinding implements PackageVersionLanguageBinding & Node { + """The ID of the object""" + id: ID! + language: ProgrammingLanguage! - # Pagination data for this connection. - pageInfo: PageInfo! -} + """The URL of the generated artifacts on WAPM's CDN.""" + url: String! -# A Relay edge containing a `Namespace` and its cursor. -type NamespaceEdge { - # A cursor for use in pagination - cursor: String! - - # The item at the end of the edge - node: Namespace -} - -type NamespaceMaintainer implements Node { + """When the binding was generated""" createdAt: DateTime! - # The ID of the object - id: ID! - invite: NamespaceCollaboratorInvite - namespace: Namespace! - role: RegistryNamespaceMaintainerRoleChoices! - updatedAt: DateTime! - user: User! -} + """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.") -type NamespaceMaintainerConnection { - # Contains the nodes in this connection. - edges: [NamespaceMaintainerEdge]! - - # Pagination data for this connection. - pageInfo: PageInfo! -} - -# A Relay edge containing a `NamespaceMaintainer` and its cursor. -type NamespaceMaintainerEdge { - # A cursor for use in pagination - cursor: String! - - # The item at the end of the edge - node: NamespaceMaintainer -} - -# An object with an ID -interface Node { - # The ID of the object - id: ID! -} - -type NodeBodyRange { - entity: Node! - length: Int! - offset: Int! -} - -input ObtainJSONWebTokenInput { - clientMutationId: String - password: String! - username: String! -} - -# Obtain JSON Web Token mutation -type ObtainJSONWebTokenPayload { - clientMutationId: String - payload: GenericScalar! - refreshExpiresIn: Int! - refreshToken: String! - token: String! -} - -type Package implements Likeable & Node & PackageOwner { - alias: String - - # The app icon. It should be formatted in the same way as Apple icons - appIcon: String! @deprecated(reason: "Please use icon instead") - collaborators(after: String = null, before: String = null, first: Int = null, last: Int = null): PackageCollaboratorConnection - createdAt: DateTime! - curated: Boolean! - displayName: String! - - # The total number of downloads of the package - downloadsCount: Int - globalName: String! - - # The app icon. It should be formatted in the same way as Apple icons - icon: String! - iconUpdatedAt: DateTime - - # The ID of the object - id: ID! - isTransferring: Boolean! - lastVersion: PackageVersion - likeCount: Int! - likersCount: Int! - maintainers: [User]! @deprecated(reason: "Please use collaborators instead") - name: String! - namespace: String - owner: PackageOwner - ownerObjectId: Int! - - # The name of the package without the owner + """Name of package source""" packageName: String! - pendingInvites(after: String = null, before: String = null, first: Int = null, last: Int = null): PackageCollaboratorInviteConnection - private: Boolean! - - # The public keys for all the published versions - publicKeys: [PublicKey!]! - updatedAt: DateTime! - versions: [PackageVersion] - viewerHasLiked: Boolean! - viewerHasRole(role: Role!): Boolean! - viewerIsWatching: Boolean! - watchCount: Int! + 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") } -type PackageCollaborator implements Node { +interface PackageVersionLanguageBinding { + id: ID! + language: ProgrammingLanguage! + + """The URL of the generated artifacts on WAPM's CDN.""" + url: String! + + """When the binding was generated""" createdAt: DateTime! - # The ID of the object - id: ID! - invite: PackageCollaboratorInvite - package: Package! - role: RegistryPackageMaintainerRoleChoices! - updatedAt: DateTime! - user: User! + """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.") } -type PackageCollaboratorConnection { - # Contains the nodes in this connection. - edges: [PackageCollaboratorEdge]! +enum ProgrammingLanguage { + PYTHON + JAVASCRIPT +} - # Pagination data for this connection. +type PackageVersionPythonBindingConnection { + """Pagination data for this connection.""" pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageVersionPythonBindingEdge]! } -# A Relay edge containing a `PackageCollaborator` and its cursor. -type PackageCollaboratorEdge { - # A cursor for use in pagination +""" +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! - - # The item at the end of the edge - node: PackageCollaborator } -type PackageCollaboratorInvite implements Node { - accepted: PackageCollaborator - approvedBy: User - closedAt: DateTime +type PackageVersionPythonBinding implements PackageVersionLanguageBinding & Node { + """The ID of the object""" + id: ID! + language: ProgrammingLanguage! + + """The URL of the generated artifacts on WAPM's CDN.""" + url: String! + + """When the binding was generated""" createdAt: DateTime! - declinedBy: User - expiresAt: DateTime! - # The ID of the object - id: ID! - inviteEmail: String - package: Package! - requestedBy: User! - role: RegistryPackageMaintainerInviteRoleChoices! - user: User -} + """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.") -type PackageCollaboratorInviteConnection { - # Contains the nodes in this connection. - edges: [PackageCollaboratorInviteEdge]! - - # Pagination data for this connection. - pageInfo: PageInfo! -} - -# A Relay edge containing a `PackageCollaboratorInvite` and its cursor. -type PackageCollaboratorInviteEdge { - # A cursor for use in pagination - cursor: String! - - # The item at the end of the edge - node: PackageCollaboratorInvite -} - -type PackageConnection { - # Contains the nodes in this connection. - edges: [PackageEdge]! - - # Pagination data for this connection. - pageInfo: PageInfo! + """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! -} - -# A Relay edge containing a `Package` and its cursor. -type PackageEdge { - # A cursor for use in pagination - cursor: String! - - # The item at the end of the edge - node: Package -} - -interface PackageOwner { - globalName: String! -} - -type PackageTransferRequest implements Node { - approvedBy: User - closedAt: DateTime - createdAt: DateTime! - declinedBy: User - expiresAt: DateTime! - - # The ID of the object - id: ID! - newOwnerObjectId: Int! - package: Package! - previousOwnerObjectId: Int! - requestedBy: User! -} - -type PackageTransferRequestConnection { - # Contains the nodes in this connection. - edges: [PackageTransferRequestEdge]! - - # Pagination data for this connection. - pageInfo: PageInfo! -} - -# A Relay edge containing a `PackageTransferRequest` and its cursor. -type PackageTransferRequestEdge { - # A cursor for use in pagination - cursor: String! - - # The item at the end of the edge - node: PackageTransferRequest -} - -type PackageVersion implements Node { - bindings: [PackageVersionBinding]! - commands: [Command!]! - createdAt: DateTime! - description: String! - distribution: PackageDistribution! - file: String! - fileSize: Int! - filesystem: [PackageVersionFilesystem]! - homepage: String - - # The ID of the object - id: ID! - isArchived: Boolean! - isLastVersion: Boolean! - isSigned: Boolean! - license: String - licenseFile: String - manifest: String! - moduleInterfaces: [InterfaceVersion!]! - modules: [PackageVersionModule!]! - package: Package! - publishedBy: User! - readme: String - repository: String - signature: Signature - updatedAt: DateTime! - version: String! -} - -interface PackageVersionBinding { - # The module these bindings are associated with. - module: String! -} - -type PackageVersionNPMBinding implements PackageVersionBinding { - npmDefaultInstallPackageName: String! -} - -type PackageVersionPythonBinding implements PackageVersionBinding { - pythonDefaultInstallPackageName: String! -} - -type PackageVersionConnection { - # Contains the nodes in this connection. - edges: [PackageVersionEdge]! - - # Pagination data for this connection. - pageInfo: PageInfo! -} - -# A Relay edge containing a `PackageVersion` and its cursor. -type PackageVersionEdge { - # A cursor for use in pagination - cursor: String! - - # The item at the end of the edge - node: PackageVersion + piritaDownloadUrl: String + piritaSize: Int! } type PackageVersionFilesystem { - host: String! wasm: String! + host: String! } -type PackageVersionModule { - abi: String - name: String! - publicUrl: String! - source: String! -} - -# The Relay compliant `PageInfo` type, containing data necessary to paginate this connection. -type PageInfo { - # When paginating forwards, the cursor to continue. - endCursor: String - - # 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 -} - -type PublicKey implements Node { - # The ID of the object +type InterfaceVersion implements Node { + """The ID of the object""" id: ID! - key: String! - keyId: String! - owner: User! - revoked: Boolean! - revokedAt: DateTime - uploadedAt: DateTime! - verifyingSignature: Signature -} - -input PublishPackageInput { - clientMutationId: String - description: String! - file: String - homepage: String - - # The package icon - icon: String - license: String - licenseFile: String - manifest: String! - name: String! - readme: String - repository: String - signature: InputSignature + 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 PublishPackagePayload { - clientMutationId: String - packageVersion: PackageVersion! - success: Boolean! +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 } -input PublishPublicKeyInput { - clientMutationId: String - key: String! - keyId: String! - verifyingSignatureId: String +type InterfaceVersionConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [InterfaceVersionEdge]! } -type PublishPublicKeyPayload { - clientMutationId: String - publicKey: PublicKey! - success: Boolean! +"""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! } -type SignedUrl { - url: String! +type PackageVersionConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageVersionEdge]! } -type Query { - getCommand(name: String!): Command - getCommands(names: [String!]!): [Command] - getContract(name: String!): Interface @deprecated(reason: "Please use getInterface instead") - getContractVersion(name: String!, version: String = null): InterfaceVersion @deprecated(reason: "Please use getInterfaceVersion instead") - getContracts(names: [String!]!): [Interface]! @deprecated(reason: "Please use getInterfaces instead") - getGlobalObject(slug: String!): GlobalObject - getInterface(name: String!): Interface - getInterfaceVersion(name: String!, version: String = "latest"): InterfaceVersion - getInterfaces(names: [String!]!): [Interface]! - getNamespace(name: String!): Namespace - getPackage(name: String!): Package - getPackageVersion(name: String!, version: String = "latest"): PackageVersion - getPackageVersions(names: [String!]!): [PackageVersion] - getPackages(names: [String!]!): [Package]! - getPasswordResetToken(token: String!): GetPasswordResetToken - getSignedUrlForPackageUpload(name:String!,version:String!): SignedUrl - getUser(username: String!): User - node( - # The ID of the object - id: ID! - ): Node - packages(after: String = null, before: String = null, first: Int = null, last: Int = null): PackageConnection - recentPackageVersions(after: String = null, before: String = null, curated: Boolean = null, first: Int = null, last: Int = null, offset: Int = null): PackageVersionConnection - search(after: String = null, before: String = null, curated: Boolean = null, first: Int = null, hasBindings: Boolean = null, isStandalone: Boolean = null, kind: [SearchKind!] = null, last: Int = null, orderBy: SearchOrderBy = null, publishDate: SearchPublishDate = null, query: String!, sort: SearchOrderSort = null, withInterfaces: [String!] = null): SearchConnection! - searchAutocomplete(after: String = null, before: String = null, first: Int = null, kind: [SearchKind!] = null, last: Int = null, query: String!): SearchConnection! - viewer: User +"""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! } -input ReadNotificationInput { - clientMutationId: String - notificationId: ID! +union PiritaFilesystemItem = PiritaFilesystemFile | PiritaFilesystemDir + +type PiritaFilesystemFile { + name(display: PiritaFilesystemNameDisplay): String! + size: Int! + offset: Int! } -type ReadNotificationPayload { - clientMutationId: String - notification: UserNotification +enum PiritaFilesystemNameDisplay { + RELATIVE + ABSOLUTE } -input RefreshInput { - clientMutationId: String - refreshToken: String +type PiritaFilesystemDir { + name(display: PiritaFilesystemNameDisplay): String! } -type RefreshPayload { - clientMutationId: String - payload: GenericScalar! - refreshExpiresIn: Int! - refreshToken: String! - token: String! +type Collection { + slug: String! + displayName: String! + description: String! + createdAt: DateTime! + banner: String! + packages(before: String, after: String, first: Int, last: Int): PackageConnection! } -input RegisterUserInput { - clientMutationId: String - email: String! - fullName: String! - password: String! - username: String! +type PackageCollaboratorConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [PackageCollaboratorEdge]! } -type RegisterUserPayload { - clientMutationId: String - token: String +"""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! } -# An enumeration. -enum RegistryNamespaceMaintainerInviteRoleChoices { - # Admin - ADMIN - - # Editor - EDITOR - - # Viewer - VIEWER +type PackageCollaborator implements Node { + """The ID of the object""" + id: ID! + user: User! + role: RegistryPackageMaintainerRoleChoices! + package: Package! + createdAt: DateTime! + updatedAt: DateTime! + invite: PackageCollaboratorInvite } -# An enumeration. -enum RegistryNamespaceMaintainerRoleChoices { - # Admin - ADMIN - - # Editor - EDITOR - - # Viewer - VIEWER -} - -# An enumeration. -enum RegistryPackageMaintainerInviteRoleChoices { - # Admin - ADMIN - - # Editor - EDITOR - - # Viewer - VIEWER -} - -# An enumeration. +"""An enumeration.""" enum RegistryPackageMaintainerRoleChoices { - # Admin + """Admin""" ADMIN - # Editor + """Editor""" EDITOR - # Viewer + """Viewer""" VIEWER } -input RemoveNamespaceCollaboratorInput { - clientMutationId: String - namespaceCollaboratorId: ID! -} - -input RemoveNamespaceCollaboratorInviteInput { - clientMutationId: String - inviteId: ID! -} - -type RemoveNamespaceCollaboratorInvitePayload { - clientMutationId: String - namespace: Namespace! -} - -type RemoveNamespaceCollaboratorPayload { - clientMutationId: String - namespace: Namespace! -} - -input RemovePackageCollaboratorInput { - clientMutationId: String - packageCollaboratorId: ID! -} - -input RemovePackageCollaboratorInviteInput { - clientMutationId: String - inviteId: ID! -} - -type RemovePackageCollaboratorInvitePayload { - clientMutationId: String - package: Package! -} - -type RemovePackageCollaboratorPayload { - clientMutationId: String - package: Package! -} - -input RemovePackageTransferRequestInput { - clientMutationId: String - packageTransferRequestId: ID! -} - -type RemovePackageTransferRequestPayload { - clientMutationId: String - package: Package! -} - -input RequestPackageTransferInput { - clientMutationId: String - newOwnerId: ID! - packageId: ID! -} - -type RequestPackageTransferPayload { - clientMutationId: String - package: Package! -} - -input RequestPasswordResetInput { - clientMutationId: String - email: String! -} - -type RequestPasswordResetPayload { - clientMutationId: String - email: String! - errors: [ErrorType] -} - -input RequestValidationEmailInput { - clientMutationId: String - - # The user id - userId: ID -} - -type RequestValidationEmailPayload { - clientMutationId: String - success: Boolean! +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 } -input RevokeAPITokenInput { - clientMutationId: String +"""An enumeration.""" +enum RegistryPackageMaintainerInviteRoleChoices { + """Admin""" + ADMIN - # The API token ID - tokenId: ID! + """Editor""" + EDITOR + + """Viewer""" + VIEWER } -type RevokeAPITokenPayload { - clientMutationId: String - success: Boolean - token: APIToken +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 Role { @@ -1049,34 +733,226 @@ enum Role { VIEWER } -type SearchConnection { - # Contains the nodes in this connection. - edges: [SearchEdge]! +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! +} - # Pagination data for this connection. +type NamespaceCollaboratorConnection { + """Pagination data for this connection.""" pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [NamespaceCollaboratorEdge]! } -# A Relay edge containing a `Search` and its cursor. -type SearchEdge { - # A cursor for use in pagination +"""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! +} - # The item at the end of the edge +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 +} + +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! +} + +type Signature { + id: ID! + publicKey: PublicKey! + data: String! + createdAt: DateTime! +} + +type UserNotificationKindIncomingPackageTransfer { + packageTransferRequest: PackageTransferRequest! +} + +type UserNotificationKindIncomingPackageInvite { + packageInvite: PackageCollaboratorInvite! +} + +type Query { + viewer: User + getUser(username: String!): User + getPasswordResetToken(token: String!): GetPasswordResetToken + packages(before: String, after: String, first: Int, last: Int): PackageConnection + recentPackageVersions(curated: Boolean, 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 + search(query: String!, curated: Boolean, orderBy: SearchOrderBy, sort: SearchOrderSort, kind: [SearchKind!], publishDate: SearchPublishDate, hasBindings: Boolean, isStandalone: Boolean, withInterfaces: [String!], 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 +} + +type GetPasswordResetToken { + valid: Boolean! + user: User +} + +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 SearchConnection { + """Pagination data for this connection.""" + pageInfo: PageInfo! + + """Contains the nodes in this connection.""" + edges: [SearchEdge]! +} + +"""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! } -enum SearchKind { - NAMESPACE - PACKAGE - USER -} +union SearchResult = PackageVersion | User | Namespace enum SearchOrderBy { ALPHABETICALLY - PUBLISHED_DATE SIZE TOTAL_DOWNLOADS + PUBLISHED_DATE } enum SearchOrderSort { @@ -1084,318 +960,643 @@ enum SearchOrderSort { DESC } +enum SearchKind { + PACKAGE + NAMESPACE + USER +} + enum SearchPublishDate { LAST_DAY - LAST_MONTH LAST_WEEK + LAST_MONTH LAST_YEAR } -union SearchResult = Namespace | PackageVersion | User +union GlobalObject = User | Namespace + +type Mutation { + tokenAuth(input: ObtainJSONWebTokenInput!): ObtainJSONWebTokenPayload + 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 + 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 +} + +type ObtainJSONWebTokenPayload { + payload: GenericScalar! + refreshExpiresIn: Int! + 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 + +input ObtainJSONWebTokenInput { + clientMutationId: String + username: String! + password: String! +} + +type RegisterUserPayload { + token: String + clientMutationId: String +} + +input RegisterUserInput { + fullName: String! + email: String! + username: String! + 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! + 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: String! + 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 SeePendingNotificationsPayload { - clientMutationId: String - success: Boolean -} - -type Signature { - createdAt: DateTime! - data: String! - id: ID! +type PublishPublicKeyPayload { + success: Boolean! publicKey: PublicKey! -} - -input SocialAuthJWTInput { - accessToken: String! clientMutationId: String - provider: String! } -# Social Auth for JSON Web Token (JWT) -type SocialAuthJWTPayload { +input PublishPublicKeyInput { + keyId: String! + key: String! + verifyingSignatureId: String clientMutationId: String - social: SocialNode - token: String } -scalar SocialCamelJSON - -type SocialNode implements Node { - created: DateTime! - extraData: SocialCamelJSON - - # The ID of the object - id: ID! - modified: DateTime! - provider: String! - uid: String! - user: User! -} - -type Subscription { - packageVersionCreated(ownerId: ID = null, publishedBy: ID = null): PackageVersion! - userNotificationCreated(userId: ID!): UserNotificationCreated! -} - -input UnlikePackageInput { +type PublishPackagePayload { + success: Boolean! + packageVersion: PackageVersion! clientMutationId: String - packageId: ID! } -type UnlikePackagePayload { - clientMutationId: String - package: Package! -} +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 -input UnwatchPackageInput { - clientMutationId: String - packageId: ID! -} - -type UnwatchPackagePayload { - clientMutationId: String - package: Package! -} - -input UpdateNamespaceCollaboratorRoleInput { - clientMutationId: String - namespaceCollaboratorId: ID! - role: Role! -} - -type UpdateNamespaceCollaboratorRolePayload { - clientMutationId: String - collaborator: NamespaceCollaborator! -} - -input UpdateNamespaceInput { - # The namespace avatar - avatar: String - clientMutationId: String - - # The namespace description - description: String - - # The namespace display name - displayName: String - - # The namespace slug name - name: String - namespaceId: ID! -} - -type UpdateNamespacePayload { - clientMutationId: String - namespace: Namespace! -} - -input UpdatePackageCollaboratorRoleInput { - clientMutationId: String - packageCollaboratorId: ID! - role: Role! -} - -type UpdatePackageCollaboratorRolePayload { - clientMutationId: String - collaborator: PackageCollaborator! -} - -input UpdatePackageInput { - clientMutationId: String - - # The package icon + """The package icon""" icon: String - packageId: ID! + clientMutationId: String +} + +input InputSignature { + publicKeyKeyId: String! + data: String! } type UpdatePackagePayload { - clientMutationId: String package: Package! -} - -input UpdateUserInfoInput { - # The user avatar - avatar: String - - # The user bio - bio: String clientMutationId: String - - # The user full name - fullName: String - - # The user Github (it can be the url, or the handle with or without the @) - github: String - - # The user location - location: String - - # The user Twitter (it can be the url, or the handle with or without the @) - twitter: String - - # The user id - userId: ID - - # The user website (it must be a valid url) - websiteUrl: String } -type UpdateUserInfoPayload { - clientMutationId: String - user: User -} +input UpdatePackageInput { + packageId: ID! -type User implements Node & PackageOwner { - apiTokens(after: String = null, before: String = null, first: Int = null, last: Int = null): APITokenConnection - avatar(size: Int = 80): String! - bio: String - dateJoined: DateTime! - email: String! - firstName: String! - fullName: String! - githubUrl: String - globalName: String! - - # The ID of the object - id: ID! - isEmailValidated: Boolean! - isViewer: Boolean! - lastName: String! - location: String - namespaceInvitesIncoming(after: String = null, before: String = null, first: Int = null, last: Int = null): NamespaceCollaboratorInviteConnection - namespaces(after: String = null, before: String = null, first: Int = null, last: Int = null): NamespaceConnection - notifications(after: String = null, before: String = null, first: Int = null, last: Int = null): UserNotificationConnection - packageInvitesIncoming(after: String = null, before: String = null, first: Int = null, last: Int = null): PackageCollaboratorInviteConnection - packageTransfersIncoming(after: String = null, before: String = null, first: Int = null, last: Int = null): PackageTransferRequestConnection - packageVersions(after: String = null, before: String = null, first: Int = null, last: Int = null): PackageVersionConnection - packages(after: String = null, before: String = null, collaborating: Boolean = null, first: Int = null, last: Int = null): PackageConnection - publicActivity(after: String = null, before: String = null, first: Int = null, last: Int = null): ActivityEventConnection! - twitterUrl: String - - # Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only. - username: String! - websiteUrl: String -} - -type UserConnection { - # Contains the nodes in this connection. - edges: [UserEdge]! - - # Pagination data for this connection. - pageInfo: PageInfo! -} - -# A Relay edge containing a `User` and its cursor. -type UserEdge { - # A cursor for use in pagination - cursor: String! - - # The item at the end of the edge - node: User -} - -type UserNotification implements Node { - body: UserNotificationBody! - createdAt: DateTime! + """The package icon""" icon: String - - # The ID of the object - id: ID! - kind: UserNotificationKind - seenState: UserNotificationSeenState! + clientMutationId: String } -type UserNotificationBody { - ranges: [NodeBodyRange]! - text: String! +type LikePackagePayload { + package: Package! + clientMutationId: String } -type UserNotificationConnection { - # Contains the nodes in this connection. - edges: [UserNotificationEdge]! - hasPendingNotifications: Boolean! +input LikePackageInput { + packageId: ID! + clientMutationId: String +} - # Pagination data for this connection. - pageInfo: PageInfo! +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: Role! + 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: Role! + clientMutationId: String +} + +type UpdateNamespaceCollaboratorInviteRolePayload { + collaboratorInvite: NamespaceCollaboratorInvite! + clientMutationId: String +} + +input UpdateNamespaceCollaboratorInviteRoleInput { + namespaceCollaboratorInviteId: ID! + role: Role! + clientMutationId: String +} + +type InvitePackageCollaboratorPayload { + invite: PackageCollaboratorInvite! + package: Package! + clientMutationId: String +} + +input InvitePackageCollaboratorInput { + packageName: String! + role: Role! + 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: Role! + clientMutationId: String +} + +type UpdatePackageCollaboratorInviteRolePayload { + collaboratorInvite: PackageCollaboratorInvite! + clientMutationId: String +} + +input UpdatePackageCollaboratorInviteRoleInput { + packageCollaboratorInviteId: ID! + role: Role! + 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 Subscription { + packageVersionCreated(publishedBy: ID, ownerId: ID): PackageVersion! + userNotificationCreated(userId: ID!): UserNotificationCreated! } type UserNotificationCreated { notification: UserNotification notificationDeletedId: ID } - -# A Relay edge containing a `UserNotification` and its cursor. -type UserNotificationEdge { - # A cursor for use in pagination - cursor: String! - - # The item at the end of the edge - node: UserNotification -} - -union UserNotificationKind = UserNotificationKindIncomingPackageInvite | UserNotificationKindIncomingPackageTransfer | UserNotificationKindPublishedPackageVersion - -type UserNotificationKindIncomingPackageInvite { - packageInvite: PackageCollaboratorInvite! -} - -type UserNotificationKindIncomingPackageTransfer { - packageTransferRequest: PackageTransferRequest! -} - -type UserNotificationKindPublishedPackageVersion { - packageVersion: PackageVersion! -} - -enum UserNotificationSeenState { - SEEN - SEEN_AND_READ - UNSEEN -} - -input ValidateUserEmailInput { - challenge: String! - clientMutationId: String - - # The user id - userId: ID -} - -type ValidateUserEmailPayload { - clientMutationId: String - user: User -} - -input ValidateUserPasswordInput { - clientMutationId: String - password: String! -} - -type ValidateUserPasswordPayload { - clientMutationId: String - success: Boolean -} - -input VerifyInput { - clientMutationId: String - token: String -} - -type VerifyPayload { - clientMutationId: String - payload: GenericScalar! -} - -input WatchPackageInput { - clientMutationId: String - packageId: ID! -} - -type WatchPackagePayload { - clientMutationId: String - package: Package! -} diff --git a/lib/registry/src/config.rs b/lib/registry/src/config.rs new file mode 100644 index 000000000..ae606d124 --- /dev/null +++ b/lib/registry/src/config.rs @@ -0,0 +1,325 @@ +use graphql_client::GraphQLQuery; +use serde::Deserialize; +use serde::Serialize; +use std::collections::BTreeMap; +#[cfg(not(test))] +use std::env; +use std::path::{Path, PathBuf}; + +pub static GLOBAL_CONFIG_FILE_NAME: &str = if cfg!(target_os = "wasi") { + "/.private/wapm.toml" +} else { + "wapm.toml" +}; + +#[derive(Deserialize, Default, Serialize, Debug, PartialEq, Eq)] +pub struct PartialWapmConfig { + /// The number of seconds to wait before checking the registry for a new + /// version of the package. + #[serde(default = "wax_default_cooldown")] + pub wax_cooldown: i32, + + /// The registry that wapm will connect to. + pub registry: Registries, + + /// Whether or not telemetry is enabled. + #[cfg(feature = "telemetry")] + #[serde(default)] + pub telemetry: Telemetry, + + /// Whether or not updated notifications are enabled. + #[cfg(feature = "update-notifications")] + #[serde(default)] + pub update_notifications: UpdateNotifications, + + /// The proxy to use when connecting to the Internet. + #[serde(default)] + pub proxy: Proxy, +} + +pub const fn wax_default_cooldown() -> i32 { + 5 * 60 +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default)] +pub struct Proxy { + pub url: Option, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default)] +pub struct UpdateNotifications { + pub enabled: String, +} + +#[cfg(feature = "telemetry")] +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct Telemetry { + pub enabled: String, +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] +#[serde(untagged)] +pub enum Registries { + Single(Registry), + Multi(MultiRegistry), +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] +pub struct MultiRegistry { + /// Currently active registry + pub current: String, + /// Map from "RegistryUrl" to "LoginToken", in order to + /// be able to be able to easily switch between registries + pub tokens: BTreeMap, +} + +impl Default for Registries { + fn default() -> Self { + Registries::Single(Registry { + url: format_graphql("https://registry.wapm.io"), + token: None, + }) + } +} + +#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] +pub struct Registry { + pub url: String, + pub token: Option, +} + +pub fn format_graphql(registry: &str) -> String { + let mut registry = registry.to_string(); + if registry.contains("wapm.dev") { + registry = "https://registry.wapm.dev/graphql".to_string(); + } else if registry.contains("wapm.io") { + registry = "https://registry.wapm.io/graphql".to_string(); + } + if !registry.starts_with("https://") { + registry = format!("https://{registry}"); + } + if registry.ends_with("/graphql") { + registry + } else if registry.ends_with('/') { + format!("{}graphql", registry) + } else { + format!("{}/graphql", registry) + } +} + +fn test_if_registry_present(registry: &str) -> Result<(), String> { + crate::utils::get_username_registry_token(registry, "") + .map(|_| ()) + .map_err(|e| format!("{e}")) +} + +#[derive(PartialEq, Eq, Copy, Clone)] +pub enum UpdateRegistry { + Update, + LeaveAsIs, +} + +#[test] +fn test_registries_switch_token() { + let mut registries = Registries::default(); + + registries.set_current_registry("https://registry.wapm.dev"); + assert_eq!( + registries.get_current_registry(), + "https://registry.wapm.dev/graphql".to_string() + ); + registries.set_login_token_for_registry( + "https://registry.wapm.io", + "token1", + UpdateRegistry::LeaveAsIs, + ); + assert_eq!( + registries.get_current_registry(), + "https://registry.wapm.dev/graphql".to_string() + ); + assert_eq!( + registries.get_login_token_for_registry(®istries.get_current_registry()), + None + ); + registries.set_current_registry("https://registry.wapm.io"); + assert_eq!( + registries.get_login_token_for_registry(®istries.get_current_registry()), + Some("token1".to_string()) + ); + registries.clear_current_registry_token(); + assert_eq!( + registries.get_login_token_for_registry(®istries.get_current_registry()), + None + ); +} + +impl Registries { + /// Gets the current (active) registry URL + pub fn clear_current_registry_token(&mut self) { + match self { + Registries::Single(s) => { + s.token = None; + } + Registries::Multi(m) => { + m.tokens.remove(&m.current); + m.tokens.remove(&format_graphql(&m.current)); + } + } + } + + pub fn get_graphql_url(&self) -> String { + let registry = self.get_current_registry(); + format_graphql(®istry) + } + + /// Gets the current (active) registry URL + pub fn get_current_registry(&self) -> String { + match self { + Registries::Single(s) => format_graphql(&s.url), + Registries::Multi(m) => format_graphql(&m.current), + } + } + + /// Sets the current (active) registry URL + pub fn set_current_registry(&mut self, registry: &str) { + let registry = format_graphql(registry); + if let Err(e) = test_if_registry_present(®istry) { + println!("Error when trying to ping registry {registry:?}: {e}"); + println!("WARNING: Registry {registry:?} will be used, but commands may not succeed."); + } + match self { + Registries::Single(s) => s.url = registry, + Registries::Multi(m) => m.current = registry, + } + } + + /// Returns the login token for the registry + pub fn get_login_token_for_registry(&self, registry: &str) -> Option { + match self { + Registries::Single(s) if s.url == registry || format_graphql(registry) == s.url => { + s.token.clone() + } + Registries::Multi(m) => m + .tokens + .get(registry) + .or_else(|| m.tokens.get(&format_graphql(registry))) + .cloned(), + _ => None, + } + } + + /// Sets the login token for the registry URL + pub fn set_login_token_for_registry( + &mut self, + registry: &str, + token: &str, + update_current_registry: UpdateRegistry, + ) { + let new_map = match self { + Registries::Single(s) => { + if s.url == registry { + Registries::Single(Registry { + url: format_graphql(registry), + token: Some(token.to_string()), + }) + } else { + let mut map = BTreeMap::new(); + if let Some(token) = s.token.clone() { + map.insert(format_graphql(&s.url), token); + } + map.insert(format_graphql(registry), token.to_string()); + Registries::Multi(MultiRegistry { + current: format_graphql(&s.url), + tokens: map, + }) + } + } + Registries::Multi(m) => { + m.tokens.insert(format_graphql(registry), token.to_string()); + if update_current_registry == UpdateRegistry::Update { + m.current = format_graphql(registry); + } + Registries::Multi(m.clone()) + } + }; + *self = new_map; + } +} + +impl PartialWapmConfig { + /// Save the config to a file + pub fn save>(&self, to: P) -> anyhow::Result<()> { + use std::{fs::File, io::Write}; + let config_serialized = toml::to_string(&self)?; + let mut file = File::create(to)?; + file.write_all(config_serialized.as_bytes())?; + Ok(()) + } + + pub fn from_file(#[cfg(test)] test_name: &str) -> Result { + #[cfg(test)] + let path = Self::get_file_location(test_name)?; + #[cfg(not(test))] + let path = Self::get_file_location()?; + + match std::fs::read_to_string(&path) { + Ok(config_toml) => { + toml::from_str(&config_toml).map_err(|e| format!("could not parse {path:?}: {e}")) + } + Err(_e) => Ok(Self::default()), + } + } + + pub fn get_current_dir() -> std::io::Result { + std::env::current_dir() + } + + #[cfg(test)] + pub fn get_folder(test_name: &str) -> Result { + let test_dir = std::env::temp_dir().join("test_wasmer").join(test_name); + let _ = std::fs::create_dir_all(&test_dir); + Ok(test_dir) + } + + #[cfg(not(test))] + pub fn get_folder() -> Result { + Ok( + if let Some(folder_str) = env::var("WASMER_DIR").ok().filter(|s| !s.is_empty()) { + let folder = PathBuf::from(folder_str); + std::fs::create_dir_all(folder.clone()) + .map_err(|e| format!("cannot create config directory: {e}"))?; + folder + } else { + #[allow(unused_variables)] + let default_dir = Self::get_current_dir() + .ok() + .unwrap_or_else(|| PathBuf::from("/".to_string())); + let home_dir = + dirs::home_dir().ok_or_else(|| "cannot find home directory".to_string())?; + let mut folder = home_dir; + folder.push(".wasmer"); + std::fs::create_dir_all(folder.clone()) + .map_err(|e| format!("cannot create config directory: {e}"))?; + folder + }, + ) + } + + #[cfg(test)] + pub fn get_file_location(test_name: &str) -> Result { + Ok(Self::get_folder(test_name)?.join(GLOBAL_CONFIG_FILE_NAME)) + } + + #[cfg(not(test))] + pub fn get_file_location() -> Result { + Ok(Self::get_folder()?.join(GLOBAL_CONFIG_FILE_NAME)) + } +} + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "graphql/schema.graphql", + query_path = "graphql/queries/test_if_registry_present.graphql", + response_derives = "Debug" +)] +struct TestIfRegistryPresent; diff --git a/lib/registry/src/graphql.rs b/lib/registry/src/graphql.rs new file mode 100644 index 000000000..730c7599a --- /dev/null +++ b/lib/registry/src/graphql.rs @@ -0,0 +1,267 @@ +use graphql_client::*; +#[cfg(not(target_os = "wasi"))] +use reqwest::{ + blocking::{multipart::Form, Client}, + header::USER_AGENT, +}; +use std::env; +use std::time::Duration; +#[cfg(target_os = "wasi")] +use {wasm_bus_reqwest::prelude::header::*, wasm_bus_reqwest::prelude::*}; + +mod proxy { + //! Code for dealing with setting things up to proxy network requests + use thiserror::Error; + + #[derive(Debug, Error)] + pub enum ProxyError { + #[error("Failed to parse URL from {}: {}", url_location, error_message)] + UrlParseError { + url_location: String, + error_message: String, + }, + + #[error("Could not connect to proxy: {0}")] + ConnectionError(String), + } + + /// Tries to set up a proxy + /// + /// This function reads from wapm config's `proxy.url` first, then checks + /// `ALL_PROXY`, `HTTPS_PROXY`, and `HTTP_PROXY` environment variables, in both + /// upper case and lower case, in that order. + /// + /// If a proxy is specified in wapm config's `proxy.url`, it is assumed + /// to be a general proxy + /// + /// A return value of `Ok(None)` means that there was no attempt to set up a proxy, + /// `Ok(Some(proxy))` means that the proxy was set up successfully, and `Err(e)` that + /// there was a failure while attempting to set up the proxy. + pub fn maybe_set_up_proxy() -> anyhow::Result> { + use std::env; + let proxy = if let Ok(proxy_url) = env::var("ALL_PROXY").or_else(|_| env::var("all_proxy")) + { + reqwest::Proxy::all(&proxy_url).map(|proxy| (proxy_url, proxy, "ALL_PROXY")) + } else if let Ok(https_proxy_url) = + env::var("HTTPS_PROXY").or_else(|_| env::var("https_proxy")) + { + reqwest::Proxy::https(&https_proxy_url) + .map(|proxy| (https_proxy_url, proxy, "HTTPS_PROXY")) + } else if let Ok(http_proxy_url) = + env::var("HTTP_PROXY").or_else(|_| env::var("http_proxy")) + { + reqwest::Proxy::http(&http_proxy_url).map(|proxy| (http_proxy_url, proxy, "http_proxy")) + } else { + return Ok(None); + } + .map_err(|e| ProxyError::ConnectionError(e.to_string())) + .and_then( + |(proxy_url_str, proxy, url_location): (String, _, &'static str)| { + url::Url::parse(&proxy_url_str) + .map_err(|e| ProxyError::UrlParseError { + url_location: url_location.to_string(), + error_message: e.to_string(), + }) + .map(|url| { + if !(url.username().is_empty()) && url.password().is_some() { + proxy.basic_auth(url.username(), url.password().unwrap_or_default()) + } else { + proxy + } + }) + }, + )?; + + Ok(Some(proxy)) + } +} + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "graphql/schema.graphql", + query_path = "graphql/queries/get_package_version.graphql", + response_derives = "Debug" +)] +pub(crate) struct GetPackageVersionQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "graphql/schema.graphql", + query_path = "graphql/queries/get_package_by_command.graphql", + response_derives = "Debug" +)] +pub(crate) struct GetPackageByCommandQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "graphql/schema.graphql", + query_path = "graphql/queries/test_if_registry_present.graphql", + response_derives = "Debug" +)] +pub(crate) struct TestIfRegistryPresent; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "graphql/schema.graphql", + query_path = "graphql/queries/get_bindings.graphql", + response_derives = "Debug,Clone,PartialEq,Eq" +)] +pub(crate) struct GetBindingsQuery; + +#[cfg(target_os = "wasi")] +pub fn whoami_distro() -> String { + whoami::os().to_lowercase() +} + +#[cfg(not(target_os = "wasi"))] +pub fn whoami_distro() -> String { + whoami::distro().to_lowercase() +} + +fn setup_client() -> Result { + let builder = Client::builder(); + + let builder = if let Some(proxy) = proxy::maybe_set_up_proxy()? { + builder.proxy(proxy) + } else { + builder + }; + + builder.build().map_err(|e| e.into()) +} + +/// This function is being used to "ping" the registry +/// (see test_if_registry_present and see whether the response +/// is valid JSON, it doesn't check the response itself, +/// since the response format might change +pub fn execute_query_modifier_inner_check_json( + registry_url: &str, + login_token: &str, + query: &QueryBody, + timeout: Option, + form_modifier: F, +) -> anyhow::Result<()> +where + V: serde::Serialize, + F: FnOnce(Form) -> Form, +{ + let client = setup_client()?; + + let vars = serde_json::to_string(&query.variables).unwrap(); + + let form = Form::new() + .text("query", query.query.to_string()) + .text("operationName", query.operation_name.to_string()) + .text("variables", vars); + + let form = form_modifier(form); + + let user_agent = format!( + "wasmer/{} {} {}", + env!("CARGO_PKG_VERSION"), + whoami::platform(), + whoami_distro(), + ); + + let mut res = client + .post(registry_url) + .multipart(form) + .bearer_auth( + env::var("WASMER_TOKEN") + .ok() + .or_else(|| env::var("WAPM_REGISTRY_TOKEN").ok()) + .unwrap_or_else(|| login_token.to_string()), + ) + .header(USER_AGENT, user_agent); + + if let Some(t) = timeout { + res = res.timeout(t); + } + + let res = res.send()?; + + let _: Response = res.json()?; + + Ok(()) +} + +pub fn execute_query_modifier_inner( + registry_url: &str, + login_token: &str, + query: &QueryBody, + timeout: Option, + form_modifier: F, +) -> anyhow::Result +where + for<'de> R: serde::Deserialize<'de>, + V: serde::Serialize, + F: FnOnce(Form) -> Form, +{ + let client = setup_client()?; + + let vars = serde_json::to_string(&query.variables).unwrap(); + + let form = Form::new() + .text("query", query.query.to_string()) + .text("operationName", query.operation_name.to_string()) + .text("variables", vars); + + let form = form_modifier(form); + + let user_agent = format!( + "wasmer/{} {} {}", + env!("CARGO_PKG_VERSION"), + whoami::platform(), + whoami_distro(), + ); + + let mut res = client + .post(registry_url) + .multipart(form) + .bearer_auth( + env::var("WASMER_TOKEN") + .ok() + .or_else(|| env::var("WAPM_REGISTRY_TOKEN").ok()) + .unwrap_or_else(|| login_token.to_string()), + ) + .header(USER_AGENT, user_agent); + + if let Some(t) = timeout { + res = res.timeout(t); + } + + let res = res.send()?; + let response_body: Response = res.json()?; + if let Some(errors) = response_body.errors { + let error_messages: Vec = errors.into_iter().map(|err| err.message).collect(); + return Err(anyhow::anyhow!("{}", error_messages.join(", "))); + } + response_body + .data + .ok_or_else(|| anyhow::anyhow!("missing response data")) +} + +pub fn execute_query( + registry_url: &str, + login_token: &str, + query: &QueryBody, +) -> anyhow::Result +where + for<'de> R: serde::Deserialize<'de>, + V: serde::Serialize, +{ + execute_query_modifier_inner(registry_url, login_token, query, None, |f| f) +} + +pub fn execute_query_with_timeout( + registry_url: &str, + login_token: &str, + timeout: Duration, + query: &QueryBody, +) -> anyhow::Result +where + for<'de> R: serde::Deserialize<'de>, + V: serde::Serialize, +{ + execute_query_modifier_inner(registry_url, login_token, query, Some(timeout), |f| f) +} diff --git a/lib/registry/src/lib.rs b/lib/registry/src/lib.rs index f79d89e13..00d9b1bed 100644 --- a/lib/registry/src/lib.rs +++ b/lib/registry/src/lib.rs @@ -1,273 +1,36 @@ -use std::collections::BTreeMap; -use std::env; +//! High-level interactions with the WAPM backend. +//! +//! The GraphQL schema can be updated by running `make` in the Wasmer repo's +//! root directory. +//! +//! ```console +//! $ make update-graphql-schema +//! curl -sSfL https://registry.wapm.io/graphql/schema.graphql > lib/registry/graphql/schema.graphql +//! ``` + use std::fmt; use std::path::{Path, PathBuf}; use std::time::Duration; - +use std::{ + collections::BTreeMap, + fmt::{Display, Formatter}, +}; use anyhow::Context; use serde::Deserialize; use serde::Serialize; -pub mod graphql { +pub mod config; +pub mod graphql; +pub mod login; +pub mod utils; - use graphql_client::*; - #[cfg(not(target_os = "wasi"))] - use reqwest::{ - blocking::{multipart::Form, Client}, - header::USER_AGENT, - }; - use std::env; - use std::time::Duration; - #[cfg(target_os = "wasi")] - use {wasm_bus_reqwest::prelude::header::*, wasm_bus_reqwest::prelude::*}; +pub use crate::{ + config::{format_graphql, PartialWapmConfig}, + graphql::get_bindings_query::ProgrammingLanguage, +}; - mod proxy { - //! Code for dealing with setting things up to proxy network requests - use thiserror::Error; - - #[derive(Debug, Error)] - pub enum ProxyError { - #[error("Failed to parse URL from {}: {}", url_location, error_message)] - UrlParseError { - url_location: String, - error_message: String, - }, - - #[error("Could not connect to proxy: {0}")] - ConnectionError(String), - } - - /// Tries to set up a proxy - /// - /// This function reads from wapm config's `proxy.url` first, then checks - /// `ALL_PROXY`, `HTTPS_PROXY`, and `HTTP_PROXY` environment variables, in both - /// upper case and lower case, in that order. - /// - /// If a proxy is specified in wapm config's `proxy.url`, it is assumed - /// to be a general proxy - /// - /// A return value of `Ok(None)` means that there was no attempt to set up a proxy, - /// `Ok(Some(proxy))` means that the proxy was set up successfully, and `Err(e)` that - /// there was a failure while attempting to set up the proxy. - pub fn maybe_set_up_proxy() -> anyhow::Result> { - use std::env; - let proxy = if let Ok(proxy_url) = - env::var("ALL_PROXY").or_else(|_| env::var("all_proxy")) - { - reqwest::Proxy::all(&proxy_url).map(|proxy| (proxy_url, proxy, "ALL_PROXY")) - } else if let Ok(https_proxy_url) = - env::var("HTTPS_PROXY").or_else(|_| env::var("https_proxy")) - { - reqwest::Proxy::https(&https_proxy_url) - .map(|proxy| (https_proxy_url, proxy, "HTTPS_PROXY")) - } else if let Ok(http_proxy_url) = - env::var("HTTP_PROXY").or_else(|_| env::var("http_proxy")) - { - reqwest::Proxy::http(&http_proxy_url) - .map(|proxy| (http_proxy_url, proxy, "http_proxy")) - } else { - return Ok(None); - } - .map_err(|e| ProxyError::ConnectionError(e.to_string())) - .and_then( - |(proxy_url_str, proxy, url_location): (String, _, &'static str)| { - url::Url::parse(&proxy_url_str) - .map_err(|e| ProxyError::UrlParseError { - url_location: url_location.to_string(), - error_message: e.to_string(), - }) - .map(|url| { - if !(url.username().is_empty()) && url.password().is_some() { - proxy.basic_auth(url.username(), url.password().unwrap_or_default()) - } else { - proxy - } - }) - }, - )?; - - Ok(Some(proxy)) - } - } - - #[derive(GraphQLQuery)] - #[graphql( - schema_path = "graphql/schema.graphql", - query_path = "graphql/queries/get_package_version.graphql", - response_derives = "Debug" - )] - pub(crate) struct GetPackageVersionQuery; - - #[derive(GraphQLQuery)] - #[graphql( - schema_path = "graphql/schema.graphql", - query_path = "graphql/queries/get_package_by_command.graphql", - response_derives = "Debug" - )] - pub(crate) struct GetPackageByCommandQuery; - - #[derive(GraphQLQuery)] - #[graphql( - schema_path = "graphql/schema.graphql", - query_path = "graphql/queries/test_if_registry_present.graphql", - response_derives = "Debug" - )] - pub(crate) struct TestIfRegistryPresent; - - #[cfg(target_os = "wasi")] - pub fn whoami_distro() -> String { - whoami::os().to_lowercase() - } - - #[cfg(not(target_os = "wasi"))] - pub fn whoami_distro() -> String { - whoami::distro().to_lowercase() - } - - pub fn execute_query_modifier_inner_check_json( - registry_url: &str, - login_token: &str, - query: &QueryBody, - timeout: Option, - form_modifier: F, - ) -> anyhow::Result<()> - where - V: serde::Serialize, - F: FnOnce(Form) -> Form, - { - let client = { - let builder = Client::builder(); - - #[cfg(not(target_os = "wasi"))] - let builder = if let Some(proxy) = proxy::maybe_set_up_proxy()? { - builder.proxy(proxy) - } else { - builder - }; - builder.build()? - }; - - let vars = serde_json::to_string(&query.variables).unwrap(); - - let form = Form::new() - .text("query", query.query.to_string()) - .text("operationName", query.operation_name.to_string()) - .text("variables", vars); - - let form = form_modifier(form); - - let user_agent = format!( - "wapm/{} {} {}", - env!("CARGO_PKG_VERSION"), - whoami::platform(), - whoami_distro(), - ); - - let mut res = client - .post(registry_url) - .multipart(form) - .bearer_auth( - env::var("WAPM_REGISTRY_TOKEN").unwrap_or_else(|_| login_token.to_string()), - ) - .header(USER_AGENT, user_agent); - - if let Some(t) = timeout { - res = res.timeout(t); - } - - let res = res.send()?; - - let _: Response = res.json()?; - - Ok(()) - } - - pub fn execute_query_modifier_inner( - registry_url: &str, - login_token: &str, - query: &QueryBody, - timeout: Option, - form_modifier: F, - ) -> anyhow::Result - where - for<'de> R: serde::Deserialize<'de>, - V: serde::Serialize, - F: FnOnce(Form) -> Form, - { - let client = { - let builder = Client::builder(); - - #[cfg(not(target_os = "wasi"))] - let builder = if let Some(proxy) = proxy::maybe_set_up_proxy()? { - builder.proxy(proxy) - } else { - builder - }; - builder.build()? - }; - - let vars = serde_json::to_string(&query.variables).unwrap(); - - let form = Form::new() - .text("query", query.query.to_string()) - .text("operationName", query.operation_name.to_string()) - .text("variables", vars); - - let form = form_modifier(form); - - let user_agent = format!( - "wapm/{} {} {}", - env!("CARGO_PKG_VERSION"), - whoami::platform(), - whoami_distro(), - ); - - let mut res = client - .post(registry_url) - .multipart(form) - .bearer_auth( - env::var("WAPM_REGISTRY_TOKEN").unwrap_or_else(|_| login_token.to_string()), - ) - .header(USER_AGENT, user_agent); - - if let Some(t) = timeout { - res = res.timeout(t); - } - - let res = res.send()?; - let response_body: Response = res.json()?; - if let Some(errors) = response_body.errors { - let error_messages: Vec = errors.into_iter().map(|err| err.message).collect(); - return Err(anyhow::anyhow!("{}", error_messages.join(", "))); - } - Ok(response_body.data.expect("missing response data")) - } - - pub fn execute_query( - registry_url: &str, - login_token: &str, - query: &QueryBody, - ) -> anyhow::Result - where - for<'de> R: serde::Deserialize<'de>, - V: serde::Serialize, - { - execute_query_modifier_inner(registry_url, login_token, query, None, |f| f) - } - - pub fn execute_query_with_timeout( - registry_url: &str, - login_token: &str, - timeout: Duration, - query: &QueryBody, - ) -> anyhow::Result - where - for<'de> R: serde::Deserialize<'de>, - V: serde::Serialize, - { - execute_query_modifier_inner(registry_url, login_token, query, Some(timeout), |f| f) - } -} +use crate::config::Registries; +use anyhow::Context; pub static GLOBAL_CONFIG_FILE_NAME: &str = if cfg!(target_os = "wasi") { "/.private/wapm.toml" @@ -275,145 +38,6 @@ pub static GLOBAL_CONFIG_FILE_NAME: &str = if cfg!(target_os = "wasi") { "wapm.toml" }; -#[derive(Deserialize, Default, Serialize, Debug, PartialEq)] -pub struct PartialWapmConfig { - /// The number of seconds to wait before checking the registry for a new - /// version of the package. - #[serde(default = "wax_default_cooldown")] - pub wax_cooldown: i32, - - /// The registry that wapm will connect to. - pub registry: Registries, - - /// Whether or not telemetry is enabled. - #[cfg(feature = "telemetry")] - #[serde(default)] - pub telemetry: Telemetry, - - /// Whether or not updated notifications are enabled. - #[cfg(feature = "update-notifications")] - #[serde(default)] - pub update_notifications: UpdateNotifications, - - /// The proxy to use when connecting to the Internet. - #[serde(default)] - pub proxy: Proxy, -} - -pub const fn wax_default_cooldown() -> i32 { - 5 * 60 -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Default)] -pub struct Proxy { - pub url: Option, -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Default)] -pub struct UpdateNotifications { - pub enabled: String, -} - -#[cfg(feature = "telemetry")] -#[derive(Deserialize, Serialize, Debug, PartialEq)] -pub struct Telemetry { - pub enabled: String, -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] -#[serde(untagged)] -pub enum Registries { - Single(Registry), - Multi(MultiRegistry), -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] -pub struct MultiRegistry { - /// Currently active registry - pub current: String, - /// Map from "RegistryUrl" to "LoginToken", in order to - /// be able to be able to easily switch between registries - pub tokens: BTreeMap, -} - -impl Default for Registries { - fn default() -> Self { - Registries::Single(Registry { - url: format_graphql("https://registry.wapm.io"), - token: None, - }) - } -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] -pub struct Registry { - pub url: String, - pub token: Option, -} - -fn format_graphql(registry: &str) -> String { - if registry.ends_with("/graphql") { - registry.to_string() - } else if registry.ends_with('/') { - format!("{}graphql", registry) - } else { - format!("{}/graphql", registry) - } -} - -impl PartialWapmConfig { - pub fn from_file() -> Result { - let path = Self::get_file_location()?; - - match std::fs::read_to_string(&path) { - Ok(config_toml) => { - toml::from_str(&config_toml).map_err(|e| format!("could not parse {path:?}: {e}")) - } - Err(_e) => Ok(Self::default()), - } - } - - pub fn get_current_dir() -> std::io::Result { - #[cfg(target_os = "wasi")] - if let Some(pwd) = std::env::var("PWD").ok() { - return Ok(PathBuf::from(pwd)); - } - std::env::current_dir() - } - - pub fn get_folder() -> Result { - Ok( - if let Some(folder_str) = env::var("WASMER_DIR").ok().filter(|s| !s.is_empty()) { - let folder = PathBuf::from(folder_str); - std::fs::create_dir_all(folder.clone()) - .map_err(|e| format!("cannot create config directory: {e}"))?; - folder - } else { - #[allow(unused_variables)] - let default_dir = Self::get_current_dir() - .ok() - .unwrap_or_else(|| PathBuf::from("/".to_string())); - #[cfg(feature = "dirs")] - let home_dir = - dirs::home_dir().ok_or(GlobalConfigError::CannotFindHomeDirectory)?; - #[cfg(not(feature = "dirs"))] - let home_dir = std::env::var("HOME") - .ok() - .unwrap_or_else(|| default_dir.to_string_lossy().to_string()); - let mut folder = PathBuf::from(home_dir); - folder.push(".wasmer"); - std::fs::create_dir_all(folder.clone()) - .map_err(|e| format!("cannot create config directory: {e}"))?; - folder - }, - ) - } - - fn get_file_location() -> Result { - Ok(Self::get_folder()?.join(GLOBAL_CONFIG_FILE_NAME)) - } -} - #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)] pub struct PackageDownloadInfo { pub registry: String, @@ -426,6 +50,7 @@ pub struct PackageDownloadInfo { } pub fn get_package_local_dir( + #[cfg(test)] test_name: &str, registry_host: &str, name: &str, version: &str, @@ -438,18 +63,26 @@ pub fn get_package_local_dir( let (namespace, name) = name .split_once('/') .ok_or_else(|| format!("missing namespace / name for {name:?}"))?; - let install_dir = get_global_install_dir(registry_host) - .ok_or_else(|| format!("no install dir for {name:?}"))?; + #[cfg(test)] + let global_install_dir = get_global_install_dir(test_name, registry_host); + #[cfg(not(test))] + let global_install_dir = get_global_install_dir(registry_host); + let install_dir = global_install_dir.ok_or_else(|| format!("no install dir for {name:?}"))?; Ok(install_dir.join(namespace).join(name).join(version)) } -pub fn try_finding_local_command(cmd: &str) -> Option { - for p in get_all_local_packages(None) { - if p.get_commands() - .unwrap_or_default() - .iter() - .any(|c| c == cmd) - { +pub fn try_finding_local_command(#[cfg(test)] test_name: &str, cmd: &str) -> Option { + #[cfg(test)] + let local_packages = get_all_local_packages(test_name, None); + #[cfg(not(test))] + let local_packages = get_all_local_packages(None); + for p in local_packages { + #[cfg(not(test))] + let commands = p.get_commands(); + #[cfg(test)] + let commands = p.get_commands(test_name); + + if commands.unwrap_or_default().iter().any(|c| c == cmd) { return Some(p); } } @@ -464,16 +97,28 @@ pub struct LocalPackage { } impl LocalPackage { - pub fn get_path(&self) -> Result { + pub fn get_path(&self, #[cfg(test)] test_name: &str) -> Result { let host = url::Url::parse(&self.registry) .ok() .and_then(|o| o.host_str().map(|s| s.to_string())) .unwrap_or_else(|| self.registry.clone()); - get_package_local_dir(&host, &self.name, &self.version) + #[cfg(test)] + { + get_package_local_dir(test_name, &host, &self.name, &self.version) + } + + #[cfg(not(test))] + { + get_package_local_dir(&host, &self.name, &self.version) + } } - pub fn get_commands(&self) -> Result, String> { - let toml_path = self.get_path()?.join("wapm.toml"); + pub fn get_commands(&self, #[cfg(test)] test_name: &str) -> Result, String> { + #[cfg(not(test))] + let path = self.get_path()?; + #[cfg(test)] + let path = self.get_path(test_name)?; + let toml_path = path.join("wapm.toml"); let toml = std::fs::read_to_string(&toml_path) .map_err(|e| format!("error reading {}: {e}", toml_path.display()))?; let toml_parsed = toml::from_str::(&toml) @@ -553,11 +198,23 @@ fn get_all_names_in_dir(dir: &PathBuf) -> Vec<(PathBuf, String)> { } /// Returns a list of all locally installed packages -pub fn get_all_local_packages(registry: Option<&str>) -> Vec { +pub fn get_all_local_packages( + #[cfg(test)] test_name: &str, + registry: Option<&str>, +) -> Vec { let mut packages = Vec::new(); let registries = match registry { Some(s) => vec![s.to_string()], - None => get_all_available_registries().unwrap_or_default(), + None => { + #[cfg(test)] + { + get_all_available_registries(test_name).unwrap_or_default() + } + #[cfg(not(test))] + { + get_all_available_registries().unwrap_or_default() + } + } }; let mut registry_hosts = registries @@ -565,7 +222,12 @@ pub fn get_all_local_packages(registry: Option<&str>) -> Vec { .filter_map(|s| url::Url::parse(&s).ok()?.host_str().map(|s| s.to_string())) .collect::>(); - let mut registries_in_root_dir = get_checkouts_dir() + #[cfg(not(test))] + let checkouts_dir = get_checkouts_dir(); + #[cfg(test)] + let checkouts_dir = get_checkouts_dir(test_name); + + let mut registries_in_root_dir = checkouts_dir .as_ref() .map(get_all_names_in_dir) .unwrap_or_default() @@ -578,7 +240,11 @@ pub fn get_all_local_packages(registry: Option<&str>) -> Vec { registry_hosts.dedup(); for host in registry_hosts { - let root_dir = match get_global_install_dir(&host) { + #[cfg(not(test))] + let global_install_dir = get_global_install_dir(&host); + #[cfg(test)] + let global_install_dir = get_global_install_dir(test_name, &host); + let root_dir = match global_install_dir { Some(o) => o, None => continue, }; @@ -604,11 +270,17 @@ pub fn get_all_local_packages(registry: Option<&str>) -> Vec { } pub fn get_local_package( + #[cfg(test)] test_name: &str, registry: Option<&str>, name: &str, version: Option<&str>, ) -> Option { - get_all_local_packages(registry) + #[cfg(not(test))] + let local_packages = get_all_local_packages(registry); + #[cfg(test)] + let local_packages = get_all_local_packages(test_name, registry); + + local_packages .iter() .find(|p| { if p.name != name { @@ -657,7 +329,7 @@ pub fn query_command_from_registry( }) } -#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] pub enum QueryPackageError { ErrorSendingQuery(String), NoPackageFound { @@ -667,7 +339,7 @@ pub enum QueryPackageError { } impl fmt::Display for QueryPackageError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { QueryPackageError::ErrorSendingQuery(q) => write!(f, "error sending query: {q}"), QueryPackageError::NoPackageFound { name, version } => { @@ -677,7 +349,7 @@ impl fmt::Display for QueryPackageError { } } -#[derive(Debug, Clone, PartialEq, PartialOrd)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd)] pub enum GetIfPackageHasNewVersionResult { // if version = Some(...) and the ~/.wasmer/checkouts/.../{version} exists, the package is already installed UseLocalAlreadyInstalled { @@ -707,15 +379,17 @@ pub enum GetIfPackageHasNewVersionResult { #[test] fn test_get_if_package_has_new_version() { + const TEST_NAME: &str = "test_get_if_package_has_new_version"; let fake_registry = "https://h0.com"; let fake_name = "namespace0/project1"; let fake_version = "1.0.0"; - let package_path = get_package_local_dir("h0.com", fake_name, fake_version).unwrap(); + let package_path = get_package_local_dir(TEST_NAME, "h0.com", fake_name, fake_version).unwrap(); let _ = std::fs::remove_file(&package_path.join("wapm.toml")); let _ = std::fs::remove_file(&package_path.join("wapm.toml")); let r1 = get_if_package_has_new_version( + TEST_NAME, fake_registry, "namespace0/project1", Some(fake_version.to_string()), @@ -732,11 +406,12 @@ fn test_get_if_package_has_new_version() { } ); - let package_path = get_package_local_dir("h0.com", fake_name, fake_version).unwrap(); + let package_path = get_package_local_dir(TEST_NAME, "h0.com", fake_name, fake_version).unwrap(); std::fs::create_dir_all(&package_path).unwrap(); std::fs::write(&package_path.join("wapm.toml"), b"").unwrap(); let r1 = get_if_package_has_new_version( + TEST_NAME, fake_registry, "namespace0/project1", Some(fake_version.to_string()), @@ -759,6 +434,7 @@ fn test_get_if_package_has_new_version() { /// /// Also returns true if the package is not installed yet. pub fn get_if_package_has_new_version( + #[cfg(test)] test_name: &str, registry_url: &str, name: &str, version: Option, @@ -776,7 +452,12 @@ pub fn get_if_package_has_new_version( .split_once('/') .ok_or_else(|| format!("missing namespace / name for {name:?}"))?; - let package_dir = get_global_install_dir(&host).map(|path| path.join(namespace).join(name)); + #[cfg(not(test))] + let global_install_dir = get_global_install_dir(&host); + #[cfg(test)] + let global_install_dir = get_global_install_dir(test_name, &host); + + let package_dir = global_install_dir.map(|path| path.join(namespace).join(name)); let package_dir = match package_dir { Some(s) => s, @@ -934,16 +615,35 @@ pub fn query_package_from_registry( }) } -pub fn get_wasmer_root_dir() -> Option { - PartialWapmConfig::get_folder().ok() +pub fn get_wasmer_root_dir(#[cfg(test)] test_name: &str) -> Option { + #[cfg(test)] + { + PartialWapmConfig::get_folder(test_name).ok() + } + #[cfg(not(test))] + { + PartialWapmConfig::get_folder().ok() + } } -pub fn get_checkouts_dir() -> Option { - Some(get_wasmer_root_dir()?.join("checkouts")) + +pub fn get_checkouts_dir(#[cfg(test)] test_name: &str) -> Option { + #[cfg(test)] + let root_dir = get_wasmer_root_dir(test_name)?; + #[cfg(not(test))] + let root_dir = get_wasmer_root_dir()?; + Some(root_dir.join("checkouts")) } /// Returs the path to the directory where all packages on this computer are being stored -pub fn get_global_install_dir(registry_host: &str) -> Option { - Some(get_checkouts_dir()?.join(registry_host)) +pub fn get_global_install_dir( + #[cfg(test)] test_name: &str, + registry_host: &str, +) -> Option { + #[cfg(test)] + let root_dir = get_checkouts_dir(test_name)?; + #[cfg(not(test))] + let root_dir = get_checkouts_dir()?; + Some(root_dir.join(registry_host)) } /// Convenience function that will unpack .tar.gz files and .tar.bz @@ -1060,6 +760,7 @@ where /// Given a triple of [registry, name, version], downloads and installs the /// .tar.gz if it doesn't yet exist, returns the (package dir, entrypoint .wasm file path) pub fn install_package( + #[cfg(test)] test_name: &str, registry: Option<&str>, name: &str, version: Option<&str>, @@ -1071,7 +772,16 @@ pub fn install_package( None => { let registries = match registry { Some(s) => vec![s.to_string()], - None => get_all_available_registries()?, + None => { + #[cfg(test)] + { + get_all_available_registries(test_name)? + } + #[cfg(not(test))] + { + get_all_available_registries()? + } + } }; let mut url_of_package = None; @@ -1090,12 +800,21 @@ pub fn install_package( for r in registries.iter() { if !force_install { + #[cfg(not(test))] let package_has_new_version = get_if_package_has_new_version( r, name, version.map(|s| s.to_string()), Duration::from_secs(60 * 5), )?; + #[cfg(test)] + let package_has_new_version = get_if_package_has_new_version( + test_name, + r, + name, + version.map(|s| s.to_string()), + Duration::from_secs(60 * 5), + )?; if let GetIfPackageHasNewVersionResult::UseLocalAlreadyInstalled { registry_host, namespace, @@ -1146,6 +865,14 @@ pub fn install_package( .ok_or_else(|| format!("invalid url: {}", package_info.registry))? .to_string(); + #[cfg(test)] + let dir = get_package_local_dir( + test_name, + &host, + &package_info.package, + &package_info.version, + )?; + #[cfg(not(test))] let dir = get_package_local_dir(&host, &package_info.package, &package_info.version)?; let version = package_info.version; @@ -1170,7 +897,7 @@ pub fn test_if_registry_present(registry: &str) -> Result { use graphql_client::GraphQLQuery; let q = TestIfRegistryPresent::build_query(test_if_registry_present::Variables {}); - let _ = crate::graphql::execute_query_modifier_inner_check_json( + crate::graphql::execute_query_modifier_inner_check_json( registry, "", &q, @@ -1182,8 +909,12 @@ pub fn test_if_registry_present(registry: &str) -> Result { Ok(true) } -pub fn get_all_available_registries() -> Result, String> { +pub fn get_all_available_registries(#[cfg(test)] test_name: &str) -> Result, String> { + #[cfg(test)] + let config = PartialWapmConfig::from_file(test_name)?; + #[cfg(not(test))] let config = PartialWapmConfig::from_file()?; + let mut registries = Vec::new(); match config.registry { Registries::Single(s) => { @@ -1203,6 +934,8 @@ pub fn get_all_available_registries() -> Result, String> { #[cfg(not(target_env = "musl"))] #[test] fn test_install_package() { + const TEST_NAME: &str = "test_install_package"; + println!("test install package..."); let registry = "https://registry.wapm.io/graphql"; if !test_if_registry_present(registry).unwrap_or(false) { @@ -1226,21 +959,28 @@ fn test_install_package() { "https://registry-cdn.wapm.io/packages/wasmer/wabt/wabt-1.0.29.tar.gz".to_string() ); - let (package, _) = - install_package(Some(registry), "wasmer/wabt", Some("1.0.29"), None, true).unwrap(); + let (package, _) = install_package( + TEST_NAME, + Some(registry), + "wasmer/wabt", + Some("1.0.29"), + None, + true, + ) + .unwrap(); println!("package installed: {package:#?}"); assert_eq!( - package.get_path().unwrap(), - get_global_install_dir("registry.wapm.io") + package.get_path(TEST_NAME).unwrap(), + get_global_install_dir(TEST_NAME, "registry.wapm.io") .unwrap() .join("wasmer") .join("wabt") .join("1.0.29") ); - let all_installed_packages = get_all_local_packages(Some(registry)); + let all_installed_packages = get_all_local_packages(TEST_NAME, Some(registry)); println!("all_installed_packages: {all_installed_packages:#?}"); @@ -1251,7 +991,7 @@ fn test_install_package() { println!("is_installed: {is_installed:#?}"); if !is_installed { - let panic_str = get_all_local_packages(Some(registry)) + let panic_str = get_all_local_packages(TEST_NAME, Some(registry)) .iter() .map(|p| format!("{} {} {}", p.registry, p.name, p.version)) .collect::>() @@ -1261,3 +1001,90 @@ fn test_install_package() { println!("ok, done"); } + +/// A library that exposes bindings to a WAPM package. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bindings { + /// A unique ID specifying this set of bindings. + pub id: String, + /// The URL which can be used to download the files that were generated + /// (typically as a `*.tar.gz` file). + pub url: String, + /// The programming language these bindings are written in. + pub language: graphql::get_bindings_query::ProgrammingLanguage, + /// The generator used to generate these bindings. + pub generator: BindingsGenerator, +} + +/// The generator used to create [`Bindings`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BindingsGenerator { + /// A unique ID specifying this generator. + pub id: String, + /// The generator package's name (e.g. `wasmer/wasmer-pack`). + pub package_name: String, + /// The exact package version. + pub version: String, + /// The name of the command that was used for generating bindings. + pub command: String, +} + +impl Display for BindingsGenerator { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let BindingsGenerator { + package_name, + version, + command, + .. + } = self; + + write!(f, "{package_name}@{version}:{command}")?; + + Ok(()) + } +} + +/// List all bindings associated with a particular package. +/// +/// If a version number isn't provided, this will default to the most recently +/// published version. +pub fn list_bindings( + registry: &str, + name: &str, + version: Option<&str>, +) -> Result, anyhow::Error> { + use crate::graphql::{ + get_bindings_query::{ResponseData, Variables}, + GetBindingsQuery, + }; + use graphql_client::GraphQLQuery; + + let variables = Variables { + name: name.to_string(), + version: version.map(String::from), + }; + + let q = GetBindingsQuery::build_query(variables); + let response: ResponseData = crate::graphql::execute_query(registry, "", &q)?; + + let package_version = response.package_version.context("Package not found")?; + + let mut bindings_packages = Vec::new(); + + for b in package_version.bindings.into_iter().flatten() { + let pkg = Bindings { + id: b.id, + url: b.url, + language: b.language, + generator: BindingsGenerator { + id: b.generator.package_version.id, + package_name: b.generator.package_version.package.name, + version: b.generator.package_version.version, + command: b.generator.command_name, + }, + }; + bindings_packages.push(pkg); + } + + Ok(bindings_packages) +} diff --git a/lib/registry/src/login.rs b/lib/registry/src/login.rs new file mode 100644 index 000000000..53bd86286 --- /dev/null +++ b/lib/registry/src/login.rs @@ -0,0 +1,36 @@ +use crate::config::{format_graphql, UpdateRegistry}; +use crate::PartialWapmConfig; + +/// Login to a registry and save the token associated with it. +/// +/// Also sets the registry as the currently active registry to provide a better UX. +pub fn login_and_save_token( + #[cfg(test)] test_name: &str, + registry: &str, + token: &str, +) -> Result<(), anyhow::Error> { + let registry = format_graphql(registry); + #[cfg(test)] + let mut config = PartialWapmConfig::from_file(test_name).map_err(|e| anyhow::anyhow!("{e}"))?; + #[cfg(not(test))] + let mut config = PartialWapmConfig::from_file().map_err(|e| anyhow::anyhow!("{e}"))?; + config.registry.set_current_registry(®istry); + config.registry.set_login_token_for_registry( + &config.registry.get_current_registry(), + token, + UpdateRegistry::Update, + ); + #[cfg(test)] + let path = + PartialWapmConfig::get_file_location(test_name).map_err(|e| anyhow::anyhow!("{e}"))?; + #[cfg(not(test))] + let path = PartialWapmConfig::get_file_location().map_err(|e| anyhow::anyhow!("{e}"))?; + config.save(&path)?; + let username = crate::utils::get_username_registry_token(®istry, token); + if let Some(s) = username.ok().and_then(|o| o) { + println!("Login for WAPM user {:?} saved", s); + } else { + println!("Login for WAPM user saved"); + } + Ok(()) +} diff --git a/lib/registry/src/utils.rs b/lib/registry/src/utils.rs new file mode 100644 index 000000000..e6185a326 --- /dev/null +++ b/lib/registry/src/utils.rs @@ -0,0 +1,27 @@ +use crate::{graphql::execute_query, PartialWapmConfig}; +use graphql_client::GraphQLQuery; + +#[derive(GraphQLQuery)] +#[graphql( + schema_path = "graphql/schema.graphql", + query_path = "graphql/queries/whoami.graphql", + response_derives = "Debug" +)] +struct WhoAmIQuery; + +pub fn get_username(#[cfg(test)] test_name: &str) -> anyhow::Result> { + #[cfg(test)] + let config = PartialWapmConfig::from_file(test_name).map_err(|e| anyhow::anyhow!("{e}"))?; + #[cfg(not(test))] + let config = PartialWapmConfig::from_file().map_err(|e| anyhow::anyhow!("{e}"))?; + let registry = &config.registry.get_current_registry(); + let q = WhoAmIQuery::build_query(who_am_i_query::Variables {}); + let response: who_am_i_query::ResponseData = execute_query(registry, "", &q)?; + Ok(response.viewer.map(|viewer| viewer.username)) +} + +pub fn get_username_registry_token(registry: &str, token: &str) -> anyhow::Result> { + let q = WhoAmIQuery::build_query(who_am_i_query::Variables {}); + let response: who_am_i_query::ResponseData = execute_query(registry, token, &q)?; + Ok(response.viewer.map(|viewer| viewer.username)) +} diff --git a/lib/vfs/src/webc_fs.rs b/lib/vfs/src/webc_fs.rs index 8111157b1..344023563 100644 --- a/lib/vfs/src/webc_fs.rs +++ b/lib/vfs/src/webc_fs.rs @@ -23,7 +23,7 @@ where pub memory: Arc, } -impl<'a, T> WebcFileSystem +impl WebcFileSystem where T: std::fmt::Debug + Send + Sync + 'static, T: Deref>, @@ -54,7 +54,7 @@ where pub memory: Arc, } -impl<'a, T> FileOpener for WebCFileOpener +impl FileOpener for WebCFileOpener where T: std::fmt::Debug + Send + Sync + 'static, T: Deref>, diff --git a/lib/vm/src/store.rs b/lib/vm/src/store.rs index 57630f15c..ea11d0582 100644 --- a/lib/vm/src/store.rs +++ b/lib/vm/src/store.rs @@ -261,8 +261,8 @@ impl MaybeInstanceOwned { /// Returns underlying pointer to the VM data. pub fn as_ptr(&self) -> NonNull { match self { - MaybeInstanceOwned::Host(p) => unsafe { NonNull::new_unchecked(p.get()) }, - MaybeInstanceOwned::Instance(p) => *p, + Self::Host(p) => unsafe { NonNull::new_unchecked(p.get()) }, + Self::Instance(p) => *p, } } } @@ -273,12 +273,12 @@ where { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - MaybeInstanceOwned::Host(p) => { + Self::Host(p) => { write!(f, "host(")?; p.as_ref().fmt(f)?; write!(f, ")") } - MaybeInstanceOwned::Instance(p) => { + Self::Instance(p) => { write!(f, "instance(")?; unsafe { p.as_ref().fmt(f)? }; write!(f, ")") diff --git a/lib/vm/src/trap/traphandlers.rs b/lib/vm/src/trap/traphandlers.rs index 2899b5d18..c065efc54 100644 --- a/lib/vm/src/trap/traphandlers.rs +++ b/lib/vm/src/trap/traphandlers.rs @@ -829,14 +829,14 @@ enum UnwindReason { impl UnwindReason { fn into_trap(self) -> Trap { match self { - UnwindReason::UserTrap(data) => Trap::User(data), - UnwindReason::LibTrap(trap) => trap, - UnwindReason::WasmTrap { + Self::UserTrap(data) => Trap::User(data), + Self::LibTrap(trap) => trap, + Self::WasmTrap { backtrace, pc, signal_trap, } => Trap::wasm(pc, backtrace, signal_trap), - UnwindReason::Panic(panic) => std::panic::resume_unwind(panic), + Self::Panic(panic) => std::panic::resume_unwind(panic), } } } diff --git a/lib/wasi-types/src/lib.rs b/lib/wasi-types/src/lib.rs index 2e54b95e5..037263546 100644 --- a/lib/wasi-types/src/lib.rs +++ b/lib/wasi-types/src/lib.rs @@ -1,3 +1,6 @@ +#![doc(html_favicon_url = "https://wasmer.io/images/icons/favicon-32x32.png")] +#![doc(html_logo_url = "https://github.com/wasmerio.png?size=200")] + pub mod types; pub mod wasi; diff --git a/lib/wasi-types/src/types.rs b/lib/wasi-types/src/types.rs index 9eee82f3e..157ad333a 100644 --- a/lib/wasi-types/src/types.rs +++ b/lib/wasi-types/src/types.rs @@ -1,6 +1,4 @@ #![deny(unused_mut)] -#![doc(html_favicon_url = "https://wasmer.io/images/icons/favicon-32x32.png")] -#![doc(html_logo_url = "https://github.com/wasmerio.png?size=200")] #![allow(non_camel_case_types, clippy::identity_op)] //! Wasmer's WASI types implementation. diff --git a/rust-toolchain b/rust-toolchain index 4213d88dc..58e4eb6b2 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.61 +1.63 diff --git a/tests/compilers/config.rs b/tests/compilers/config.rs index 4a3e30d40..59f90b918 100644 --- a/tests/compilers/config.rs +++ b/tests/compilers/config.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use wasmer::{CompilerConfig, Features, ModuleMiddleware, Store}; use wasmer_compiler::Engine; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Compiler { LLVM, Cranelift, diff --git a/tests/compilers/traps.rs b/tests/compilers/traps.rs index 5c0e6a20d..d3a244b3e 100644 --- a/tests/compilers/traps.rs +++ b/tests/compilers/traps.rs @@ -34,8 +34,7 @@ fn test_trap_return(config: crate::Config) -> Result<()> { let e = run_func .call(&mut store, &[]) - .err() - .expect("error calling function"); + .expect_err("error calling function"); assert_eq!(e.message(), "test 123"); @@ -62,8 +61,7 @@ fn test_trap_trace(config: crate::Config) -> Result<()> { let e = run_func .call(&mut store, &[]) - .err() - .expect("error calling function"); + .expect_err("error calling function"); let trace = e.trace(); assert_eq!(trace.len(), 2); @@ -113,8 +111,7 @@ fn test_trap_trace_cb(config: crate::Config) -> Result<()> { let e = run_func .call(&mut store, &[]) - .err() - .expect("error calling function"); + .expect_err("error calling function"); let trace = e.trace(); println!("Trace {:?}", trace); @@ -148,8 +145,7 @@ fn test_trap_stack_overflow(config: crate::Config) -> Result<()> { let e = run_func .call(&mut store, &[]) - .err() - .expect("error calling function"); + .expect_err("error calling function"); // We specifically don't check the stack trace here: stack traces after // stack overflows are not generally possible due to unreliable unwinding @@ -181,8 +177,7 @@ fn trap_display_pretty(config: crate::Config) -> Result<()> { let e = run_func .call(&mut store, &[]) - .err() - .expect("error calling function"); + .expect_err("error calling function"); assert_eq!( e.to_string(), "\ @@ -236,8 +231,7 @@ fn trap_display_multi_module(config: crate::Config) -> Result<()> { let e = bar2 .call(&mut store, &[]) - .err() - .expect("error calling function"); + .expect_err("error calling function"); assert_eq!( e.to_string(), "\ @@ -433,9 +427,7 @@ fn call_signature_mismatch(config: crate::Config) -> Result<()> { "#; let module = Module::new(&store, &binary)?; - let err = Instance::new(&mut store, &module, &imports! {}) - .err() - .expect("expected error"); + let err = Instance::new(&mut store, &module, &imports! {}).expect_err("expected error"); assert_eq!( format!("{}", err), "\ @@ -461,9 +453,7 @@ fn start_trap_pretty(config: crate::Config) -> Result<()> { "#; let module = Module::new(&store, wat)?; - let err = Instance::new(&mut store, &module, &imports! {}) - .err() - .expect("expected error"); + let err = Instance::new(&mut store, &module, &imports! {}).expect_err("expected error"); assert_eq!( format!("{}", err), diff --git a/tests/compilers/typed_functions.rs b/tests/compilers/typed_functions.rs index c6c09b072..93fdd6db4 100644 --- a/tests/compilers/typed_functions.rs +++ b/tests/compilers/typed_functions.rs @@ -1,3 +1,5 @@ +#![allow(clippy::unnecessary_operation)] // We use x1 multiplies for clarity + use anyhow::Result; use std::convert::Infallible; use std::sync::{Arc, Mutex}; diff --git a/tests/integration/cli/tests/login.rs b/tests/integration/cli/tests/login.rs new file mode 100644 index 000000000..7a3cdb9ad --- /dev/null +++ b/tests/integration/cli/tests/login.rs @@ -0,0 +1,30 @@ +use anyhow::bail; +use std::path::PathBuf; +use std::process::Command; +use wasmer_integration_tests_cli::{get_repo_root_path, get_wasmer_path, ASSET_PATH, C_ASSET_PATH}; + +#[test] +fn login_works() -> anyhow::Result<()> { + let wapm_dev_token = std::env::var("WAPM_DEV_TOKEN").expect("WAPM_DEV_TOKEN env var not set"); + let output = Command::new(get_wasmer_path()) + .arg("login") + .arg("--registry") + .arg("wapm.dev") + .arg(wapm_dev_token) + .output()?; + + if !output.status.success() { + bail!( + "wasmer login failed with: stdout: {}\n\nstderr: {}", + std::str::from_utf8(&output.stdout) + .expect("stdout is not utf8! need to handle arbitrary bytes"), + std::str::from_utf8(&output.stderr) + .expect("stderr is not utf8! need to handle arbitrary bytes") + ); + } + + let stdout_output = std::str::from_utf8(&output.stdout).unwrap(); + assert_eq!(stdout_output, "Login for WAPM user \"ciuser\" saved\n"); + + Ok(()) +}