diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 20077b8..f064eb2 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -11,6 +11,8 @@ jobs: if: ${{ startsWith(github.ref, 'refs/tags/v') }} steps: + - uses: actions/checkout@v4 + - name: Get version from tag id: get-version run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 60e04d1..30dbae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,16 @@ This changelog documents the changes between release versions. Changes to be included in the next upcoming release +**Breaking changes** ([#38](https://github.com/hasura/ndc-sdk-rs/pull/38)): + +- Updated to support [v0.2.0 of the NDC Spec](https://hasura.github.io/ndc-spec/specification/changelog.html#020). This is a very large update which adds new features and some breaking changes. +- If the [`X-Hasura-NDC-Version`](https://hasura.github.io/ndc-spec/specification/versioning.html) header is sent, the SDK will validate that the connector supports the incoming request's version and reject it if it does not. If no header is sent, no action is taken. + ## [0.5.0] - 2024-10-29 -- add utilities to [implement PrintSchemaAndCapabilities](https://github.com/hasura/ndc-sdk-rs/pull/34). This splits the sdk into multiple crates to avoid bringing in openssl +- A default request size limit of 100MB was added. This can be overridden with the `HASURA_MAX_REQUEST_SIZE` environment variable ([#29](https://github.com/hasura/ndc-sdk-rs/pull/29)). +- Connector state is now only initialized on the first request that actually uses it. This means `/capabilities`, `/schema` and `/health` can be used even if state initialization would otherwise fail ([#31](https://github.com/hasura/ndc-sdk-rs/pull/31)). +- Add utilities to [implement PrintSchemaAndCapabilities](https://github.com/hasura/ndc-sdk-rs/pull/34). This splits the sdk into multiple crates to avoid bringing in openssl ## [0.4.0] - 2024-08-30 diff --git a/Cargo.lock b/Cargo.lock index 955460a..fb1ed07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,9 +152,9 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", "itoa", "matchit", "memchr", @@ -166,7 +166,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "tokio", "tower", "tower-layer", @@ -182,8 +182,8 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "mime", "rustversion", "tower-layer", @@ -200,8 +200,8 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "mime", "pin-project-lite", "serde", @@ -622,7 +622,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", + "http 0.2.12", "indexmap 2.4.0", "slab", "tokio", @@ -671,6 +671,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -678,7 +689,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", "pin-project-lite", ] @@ -711,8 +745,8 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -724,13 +758,32 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + [[package]] name = "hyper-timeout" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ - "hyper", + "hyper 0.14.30", "pin-project-lite", "tokio", "tokio-io-timeout", @@ -743,12 +796,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.30", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.5.0", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.60" @@ -966,8 +1054,8 @@ dependencies = [ [[package]] name = "ndc-models" -version = "0.1.6" -source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.6#d1be19e9cdd86ac7b6ad003ff82b7e5b4e96b84f" +version = "0.2.0" +source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.2.0-rc.1#1bcec88a5f20b005cf3883d289e7b18c1a6111b2" dependencies = [ "indexmap 2.4.0", "ref-cast", @@ -986,7 +1074,7 @@ dependencies = [ "axum", "axum-extra", "clap", - "http", + "http 0.2.12", "ndc-models", "ndc-sdk-core", "ndc-test", @@ -997,7 +1085,8 @@ dependencies = [ "opentelemetry-zipkin", "opentelemetry_sdk", "prometheus", - "reqwest", + "reqwest 0.11.27", + "semver", "serde_json", "thiserror", "tokio", @@ -1016,12 +1105,12 @@ dependencies = [ "async-trait", "axum", "bytes", - "http", + "http 0.2.12", "mime", "ndc-models", "ndc-test", "prometheus", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "thiserror", @@ -1032,8 +1121,8 @@ dependencies = [ [[package]] name = "ndc-test" -version = "0.1.6" -source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.1.6#d1be19e9cdd86ac7b6ad003ff82b7e5b4e96b84f" +version = "0.2.0" +source = "git+http://github.com/hasura/ndc-spec.git?tag=v0.2.0-rc.1#1bcec88a5f20b005cf3883d289e7b18c1a6111b2" dependencies = [ "async-trait", "clap", @@ -1041,14 +1130,12 @@ dependencies = [ "indexmap 2.4.0", "ndc-models", "rand", - "reqwest", + "reqwest 0.12.9", "semver", "serde", "serde_json", - "smol_str", "thiserror", "tokio", - "url", ] [[package]] @@ -1158,9 +1245,9 @@ checksum = "7690dc77bf776713848c4faa6501157469017eaf332baccd4eb1cea928743d94" dependencies = [ "async-trait", "bytes", - "http", + "http 0.2.12", "opentelemetry", - "reqwest", + "reqwest 0.11.27", ] [[package]] @@ -1171,14 +1258,14 @@ checksum = "1a016b8d9495c639af2145ac22387dcb88e44118e45320d9238fbf4e7889abcb" dependencies = [ "async-trait", "futures-core", - "http", + "http 0.2.12", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry-semantic-conventions", "opentelemetry_sdk", "prost", - "reqwest", + "reqwest 0.11.27", "thiserror", "tokio", "tonic", @@ -1210,13 +1297,13 @@ checksum = "d6943c09b1b7c17b403ae842b00f23e6d5fc6f5ec06cccb3f39aca97094a899a" dependencies = [ "async-trait", "futures-core", - "http", + "http 0.2.12", "once_cell", "opentelemetry", "opentelemetry-http", "opentelemetry-semantic-conventions", "opentelemetry_sdk", - "reqwest", + "reqwest 0.11.27", "serde", "serde_json", "thiserror", @@ -1519,15 +1606,14 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", - "http-body", - "hyper", - "hyper-tls", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", "mime", - "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -1537,7 +1623,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration", "tokio", "tokio-native-tls", @@ -1549,6 +1635,46 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest" +version = "0.12.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.0", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.1.3", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + [[package]] name = "ring" version = "0.17.8" @@ -1950,6 +2076,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -2170,9 +2305,9 @@ dependencies = [ "bytes", "flate2", "h2", - "http", - "http-body", - "hyper", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", "hyper-timeout", "percent-encoding", "pin-project", @@ -2219,8 +2354,8 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", - "http-body", + "http 0.2.12", + "http-body 0.4.6", "http-range-header", "mime", "pin-project-lite", @@ -2568,6 +2703,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 6d28ce4..6249660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,8 @@ members = ["crates/*"] [workspace.dependencies] ndc-sdk-core = { path = "../sdk-core" } -ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.6" } -ndc-test = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.1.6" } +ndc-models = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.2.0-rc.1" } +ndc-test = { git = "http://github.com/hasura/ndc-spec.git", tag = "v0.2.0-rc.1" } anyhow = "1" async-trait = "0.1" @@ -34,6 +34,7 @@ opentelemetry_sdk = { version = "0.22", features = ["rt-tokio"] } opentelemetry-zipkin = "0.20" prometheus = "0.13" reqwest = "0.11" +semver = "1" serde = { version = "1", features = ["derive"] } serde_json = { version = "1", features = ["raw_value"] } thiserror = "1" diff --git a/crates/sdk-core/src/connector/example.rs b/crates/sdk-core/src/connector/example.rs index e5ffe66..1cfa780 100644 --- a/crates/sdk-core/src/connector/example.rs +++ b/crates/sdk-core/src/connector/example.rs @@ -49,9 +49,13 @@ impl Connector for Example { filter_by: None, order_by: None, aggregates: None, + nested_collections: None, }, exists: models::ExistsCapabilities { nested_collections: None, + unrelated: None, + named_scopes: None, + nested_scalar_collections: None, }, }, mutation: models::MutationCapabilities { @@ -76,6 +80,7 @@ impl Connector for Example { procedures: vec![], object_types: BTreeMap::new(), scalar_types: BTreeMap::new(), + capabilities: None, } .into()) } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 9db9b22..e4c9c3a 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -41,6 +41,7 @@ opentelemetry_sdk = { workspace = true, features = ["rt-tokio"] } opentelemetry-zipkin = { workspace = true } prometheus = { workspace = true } reqwest = { workspace = true } +semver = { workspace = true } serde_json = { workspace = true, features = ["raw_value"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread", "signal"] } diff --git a/crates/sdk/src/default_main.rs b/crates/sdk/src/default_main.rs index c6bbb20..d296b65 100644 --- a/crates/sdk/src/default_main.rs +++ b/crates/sdk/src/default_main.rs @@ -12,6 +12,7 @@ use axum::{ use axum_extra::extract::WithRejection; use clap::{Parser, Subcommand}; use ndc_sdk_core::schema::{get_capabilities, print_schema_and_capabilities}; +use serde_json::json; use tower_http::{ limit::RequestBodyLimitLayer, trace::TraceLayer, validate_request::ValidateRequestHeaderLayer, }; @@ -295,6 +296,7 @@ where .layer(ValidateRequestHeaderLayer::custom(auth_handler( service_token_secret, ))) + .layer(ValidateRequestHeaderLayer::custom(check_version_header)) .route("/health", get(get_health_readiness::)) // health checks are not authenticated .with_state(state) .layer( @@ -354,6 +356,58 @@ fn auth_handler( } } +fn check_version_header( + request: &mut Request, +) -> std::result::Result<(), axum::response::Response> { + if let Some(version) = request.headers().get(ndc_models::VERSION_HEADER_NAME) { + let Ok(version) = version.to_str() else { + return Err(ErrorResponse::new( + StatusCode::BAD_REQUEST, + format!( + "Invalid {} header, expected a semver version string", + ndc_models::VERSION_HEADER_NAME + ), + serde_json::Value::Null, + ) + .into_response()); + }; + + let Ok(wanted_version) = semver::Version::parse(version) else { + return Err(ErrorResponse::new( + StatusCode::BAD_REQUEST, + format!( + "Invalid {} header, expected a semver version string", + ndc_models::VERSION_HEADER_NAME + ), + serde_json::Value::Null, + ) + .into_response()); + }; + + let comparator = semver::Comparator { + op: semver::Op::Caret, + major: wanted_version.major, + minor: Some(wanted_version.minor), + patch: Some(wanted_version.patch), + pre: wanted_version.pre, + }; + + if !comparator.matches(&semver::Version::parse(ndc_models::VERSION).unwrap()) { + return Err(ErrorResponse::new( + StatusCode::BAD_REQUEST, + "The connector does not support the requested NDC version".to_owned(), + json!({ + "connectorVersion": ndc_models::VERSION_HEADER_NAME, + "requestedVersionRange": format!("^{}", version) + }), + ) + .into_response()); + } + } + + Ok(()) +} + async fn get_metrics(State(state): State>) -> Result { fetch_metrics::(state.configuration(), state.state().await?, state.metrics()) }