diff --git a/graph/src/data/subgraph/mod.rs b/graph/src/data/subgraph/mod.rs index 10c4e471e38..723ba8dc8c4 100644 --- a/graph/src/data/subgraph/mod.rs +++ b/graph/src/data/subgraph/mod.rs @@ -597,6 +597,7 @@ impl IntoValue for DeploymentFeatures { fn into_value(self) -> r::Value { object! { __typename: "SubgraphFeatures", + subgraph: self.id, specVersion: self.spec_version, apiVersion: self.api_version, features: self.features, diff --git a/server/index-node/src/resolver.rs b/server/index-node/src/resolver.rs index 13379aa6959..5e887f3e719 100644 --- a/server/index-node/src/resolver.rs +++ b/server/index-node/src/resolver.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use std::convert::TryInto; use graph::data::query::Trace; @@ -565,37 +565,54 @@ impl IndexNodeResolver { field: &a::Field, ) -> Result { // We can safely unwrap because the argument is non-nullable and has been validated. - let subgraph_id = field.get_required::("subgraphId").unwrap(); + let subgraph_ids: HashSet = field + .get_required::>("subgraphs") + .unwrap() + .into_iter() + .collect(); - // Try to build a deployment hash with the input string - let deployment_hash = DeploymentHash::new(subgraph_id).map_err(|invalid_qm_hash| { - QueryExecutionError::SubgraphDeploymentIdError(invalid_qm_hash) - })?; + if subgraph_ids.is_empty() { + return Ok(r::Value::List(Vec::new())); + } let subgraph_store = self.store.subgraph_store(); - let features = match subgraph_store.subgraph_features(&deployment_hash).await? { - Some(features) => { - let mut deployment_features = features.clone(); - let features = &mut deployment_features.features; + let mut all_features = vec![]; - if deployment_features.has_declared_calls { - features.push("declaredEthCalls".to_string()); - } - if deployment_features.has_aggregations { - features.push("aggregations".to_string()); + for subgraph_id in subgraph_ids { + let deployment_hash = match DeploymentHash::new(subgraph_id) { + Ok(hash) => hash, + Err(_) => { + continue; } - if !deployment_features.immutable_entities.is_empty() { - features.push("immutableEntities".to_string()); - } - if deployment_features.has_bytes_as_ids { - features.push("bytesAsIds".to_string()); + }; + + // Fetch features from store or IPFS + let features = match subgraph_store.subgraph_features(&deployment_hash).await? { + Some(features) => { + let mut deployment_features = features.clone(); + let features = &mut deployment_features.features; + + if deployment_features.has_declared_calls { + features.push("declaredEthCalls".to_string()); + } + if deployment_features.has_aggregations { + features.push("aggregations".to_string()); + } + if !deployment_features.immutable_entities.is_empty() { + features.push("immutableEntities".to_string()); + } + if deployment_features.has_bytes_as_ids { + features.push("bytesAsIds".to_string()); + } + deployment_features } - deployment_features - } - None => self.get_features_from_ipfs(&deployment_hash).await?, - }; + None => self.get_features_from_ipfs(&deployment_hash).await?, + }; - Ok(features.into_value()) + all_features.push(features.into_value()); + } + + Ok(r::Value::List(all_features)) } fn resolve_api_versions(&self, _field: &a::Field) -> Result { @@ -817,6 +834,11 @@ impl Resolver for IndexNodeResolver { self.resolve_public_proofs_of_indexing(field).await } + // The top-level `subgraphFeatures` field + (None, "SubgraphFeatures", "subgraphFeatures") => { + self.resolve_subgraph_features(field).await + } + // Resolve fields of `Object` values (e.g. the `chains` field of `ChainIndexingStatus`) (value, _, _) => Ok(value.unwrap_or(r::Value::Null)), } @@ -837,7 +859,6 @@ impl Resolver for IndexNodeResolver { (None, "indexingStatusForPendingVersion") => { self.resolve_indexing_status_for_version(field, false) } - (None, "subgraphFeatures") => self.resolve_subgraph_features(field).await, (None, "entityChangesInBlock") => self.resolve_entity_changes_in_block(field), // The top-level `subgraphVersions` field (None, "apiVersions") => self.resolve_api_versions(field), diff --git a/server/index-node/src/schema.graphql b/server/index-node/src/schema.graphql index 4179cabad8c..2672990d454 100644 --- a/server/index-node/src/schema.graphql +++ b/server/index-node/src/schema.graphql @@ -36,7 +36,7 @@ type Query { publicProofsOfIndexing( requests: [PublicProofOfIndexingRequest!]! ): [PublicProofOfIndexingResult!]! - subgraphFeatures(subgraphId: String!): SubgraphFeatures! + subgraphFeatures(subgraphs: [String!]!): [SubgraphFeatures!]! entityChangesInBlock(subgraphId: String!, blockNumber: Int!): EntityChanges! blockData(network: String!, blockHash: Bytes!): JSONObject blockHashFromNumber(network: String!, blockNumber: Int!): Bytes @@ -148,6 +148,7 @@ type CachedEthereumCall { } type SubgraphFeatures { + subgraph: String! apiVersion: String specVersion: String! features: [Feature!]! diff --git a/tests/tests/integration_tests.rs b/tests/tests/integration_tests.rs index ebec6fd8fc4..30a549e264b 100644 --- a/tests/tests/integration_tests.rs +++ b/tests/tests/integration_tests.rs @@ -534,7 +534,8 @@ pub async fn test_block_handlers(ctx: TestContext) -> anyhow::Result<()> { // test subgraphFeatures endpoint returns handlers correctly let subgraph_features = Subgraph::query_with_vars( "query GetSubgraphFeatures($deployment: String!) { - subgraphFeatures(subgraphId: $deployment) { + subgraphFeatures(subgraphs: [$deployment]) { + subgraph specVersion apiVersion features @@ -546,7 +547,23 @@ pub async fn test_block_handlers(ctx: TestContext) -> anyhow::Result<()> { json!({ "deployment": subgraph.deployment }), ) .await?; - let handlers = &subgraph_features["data"]["subgraphFeatures"]["handlers"]; + // The response is now an array, get the first element + let features_array = &subgraph_features["data"]["subgraphFeatures"]; + assert!( + features_array.is_array(), + "subgraphFeatures must return an array" + ); + assert_eq!( + features_array.as_array().unwrap().len(), + 1, + "Expected exactly one subgraph feature set" + ); + let subgraph_feature = &features_array[0]; + assert_eq!( + subgraph_feature["subgraph"], subgraph.deployment, + "Subgraph ID should match the deployment" + ); + let handlers = &subgraph_feature["handlers"]; assert!( handlers.is_array(), "subgraphFeatures.handlers must be an array" @@ -762,7 +779,8 @@ async fn test_non_fatal_errors(ctx: TestContext) -> anyhow::Result<()> { assert!(!subgraph.healthy); let query = "query GetSubgraphFeatures($deployment: String!) { - subgraphFeatures(subgraphId: $deployment) { + subgraphFeatures(subgraphs: [$deployment]) { + subgraph specVersion apiVersion features @@ -774,8 +792,27 @@ async fn test_non_fatal_errors(ctx: TestContext) -> anyhow::Result<()> { let resp = Subgraph::query_with_vars(query, json!({ "deployment" : subgraph.deployment })).await?; - let subgraph_features = &resp["data"]["subgraphFeatures"]; + + // The response is now an array, get the first element + let features_array = &resp["data"]["subgraphFeatures"]; + assert!( + features_array.is_array(), + "subgraphFeatures must return an array" + ); + assert_eq!( + features_array.as_array().unwrap().len(), + 1, + "Expected exactly one subgraph feature set" + ); + + let subgraph_feature = &features_array[0]; + assert_eq!( + subgraph_feature["subgraph"], subgraph.deployment, + "Subgraph ID should match the deployment" + ); + let exp = json!({ + "subgraph": subgraph.deployment, "specVersion": "0.0.4", "apiVersion": "0.0.6", "features": ["nonFatalErrors"], @@ -783,7 +820,7 @@ async fn test_non_fatal_errors(ctx: TestContext) -> anyhow::Result<()> { "handlers": ["block"], "network": "test", }); - assert_eq!(&exp, subgraph_features); + assert_eq!(&exp, subgraph_feature); let resp = subgraph .query("{ foos(orderBy: id, subgraphError: allow) { id } }")