Skip to content

Commit 6ea29f5

Browse files
kamilkisielaKai Wetlesen
authored and
Kai Wetlesen
committed
all: GraphQL API Versioning (graphprotocol#3185)
1 parent fe00a2b commit 6ea29f5

File tree

27 files changed

+293
-75
lines changed

27 files changed

+293
-75
lines changed

core/tests/interfaces.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ async fn insert_and_query(
2424
insert_entities(&deployment, entities).await?;
2525

2626
let document = graphql_parser::parse_query(query).unwrap().into_static();
27-
let target = QueryTarget::Deployment(subgraph_id);
27+
let target = QueryTarget::Deployment(subgraph_id, Default::default());
2828
let query = Query::new(document, None);
2929
Ok(execute_subgraph_query(query, target)
3030
.await

graph/src/components/mod.rs

+3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ pub mod trigger_processor;
5757
/// Components dealing with collecting metrics
5858
pub mod metrics;
5959

60+
/// Components dealing with versioning
61+
pub mod versions;
62+
6063
/// A component that receives events of type `T`.
6164
pub trait EventConsumer<E> {
6265
/// Get the event sink.

graph/src/components/store/traits.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use super::*;
44
use crate::blockchain::block_stream::FirehoseCursor;
55
use crate::components::server::index_node::VersionInfo;
66
use crate::components::transaction_receipt;
7+
use crate::components::versions::ApiVersion;
78
use crate::data::query::Trace;
89
use crate::data::subgraph::status;
910
use crate::data::value::Word;
@@ -112,7 +113,11 @@ pub trait SubgraphStore: Send + Sync + 'static {
112113

113114
/// Return the GraphQL schema that was derived from the user's schema by
114115
/// adding a root query type etc. to it
115-
fn api_schema(&self, subgraph_id: &DeploymentHash) -> Result<Arc<ApiSchema>, StoreError>;
116+
fn api_schema(
117+
&self,
118+
subgraph_id: &DeploymentHash,
119+
api_version: &ApiVersion,
120+
) -> Result<Arc<ApiSchema>, StoreError>;
116121

117122
/// Return a `SubgraphFork`, derived from the user's `debug-fork` deployment argument,
118123
/// that is used for debugging purposes only.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#[derive(Clone, PartialEq, Eq, Debug, Ord, PartialOrd, Hash)]
2+
pub enum FeatureFlag {}

graph/src/components/versions/mod.rs

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod features;
2+
mod registry;
3+
4+
pub use features::FeatureFlag;
5+
pub use registry::{ApiVersion, VERSIONS};
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use crate::prelude::FeatureFlag;
2+
use itertools::Itertools;
3+
use lazy_static::lazy_static;
4+
use semver::{Version, VersionReq};
5+
use std::collections::HashMap;
6+
7+
lazy_static! {
8+
static ref VERSION_COLLECTION: HashMap<Version, Vec<FeatureFlag>> = {
9+
vec![
10+
// baseline version
11+
(Version::new(1, 0, 0), vec![]),
12+
].into_iter().collect()
13+
};
14+
15+
// Sorted vector of versions. From higher to lower.
16+
pub static ref VERSIONS: Vec<&'static Version> = {
17+
let mut versions = VERSION_COLLECTION.keys().collect_vec().clone();
18+
versions.sort_by(|a, b| b.partial_cmp(a).unwrap());
19+
versions
20+
};
21+
}
22+
23+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
24+
pub struct ApiVersion {
25+
pub version: Version,
26+
features: Vec<FeatureFlag>,
27+
}
28+
29+
impl ApiVersion {
30+
pub fn new(version_requirement: &VersionReq) -> Result<Self, String> {
31+
let version = Self::resolve(&version_requirement)?;
32+
33+
Ok(Self {
34+
version: version.clone(),
35+
features: VERSION_COLLECTION
36+
.get(&version)
37+
.expect(format!("Version {:?} is not supported", version).as_str())
38+
.to_vec(),
39+
})
40+
}
41+
42+
pub fn from_version(version: &Version) -> Result<ApiVersion, String> {
43+
ApiVersion::new(
44+
&VersionReq::parse(version.to_string().as_str())
45+
.map_err(|error| format!("Invalid version requirement: {}", error))?,
46+
)
47+
}
48+
49+
pub fn supports(&self, feature: FeatureFlag) -> bool {
50+
self.features.contains(&feature)
51+
}
52+
53+
fn resolve(version_requirement: &VersionReq) -> Result<&Version, String> {
54+
for version in VERSIONS.iter() {
55+
if version_requirement.matches(version) {
56+
return Ok(version.clone());
57+
}
58+
}
59+
60+
Err("Could not resolve the version".to_string())
61+
}
62+
}
63+
64+
impl Default for ApiVersion {
65+
fn default() -> Self {
66+
// Default to the latest version.
67+
// The `VersionReq::default()` returns `*` which means "any version".
68+
// The first matching version is the latest version.
69+
ApiVersion::new(&VersionReq::default()).unwrap()
70+
}
71+
}

graph/src/data/query/query.rs

+8-12
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::sync::Arc;
77

88
use crate::{
99
data::graphql::shape_hash::shape_hash,
10-
prelude::{q, r, DeploymentHash, SubgraphName, ENV_VARS},
10+
prelude::{q, r, ApiVersion, DeploymentHash, SubgraphName, ENV_VARS},
1111
};
1212

1313
fn deserialize_number<'de, D>(deserializer: D) -> Result<q::Number, D::Error>
@@ -112,19 +112,15 @@ impl serde::ser::Serialize for QueryVariables {
112112

113113
#[derive(Clone, Debug)]
114114
pub enum QueryTarget {
115-
Name(SubgraphName),
116-
Deployment(DeploymentHash),
115+
Name(SubgraphName, ApiVersion),
116+
Deployment(DeploymentHash, ApiVersion),
117117
}
118118

119-
impl From<DeploymentHash> for QueryTarget {
120-
fn from(id: DeploymentHash) -> Self {
121-
Self::Deployment(id)
122-
}
123-
}
124-
125-
impl From<SubgraphName> for QueryTarget {
126-
fn from(name: SubgraphName) -> Self {
127-
QueryTarget::Name(name)
119+
impl QueryTarget {
120+
pub fn get_version(&self) -> &ApiVersion {
121+
match self {
122+
Self::Deployment(_, version) | Self::Name(_, version) => version,
123+
}
128124
}
129125
}
130126

graph/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ pub mod prelude {
133133
SubgraphVersionSwitchingMode,
134134
};
135135
pub use crate::components::trigger_processor::TriggerProcessor;
136+
pub use crate::components::versions::{ApiVersion, FeatureFlag};
136137
pub use crate::components::{transaction_receipt, EventConsumer, EventProducer};
137138
pub use crate::env::ENV_VARS;
138139

@@ -141,7 +142,7 @@ pub mod prelude {
141142
shape_hash::shape_hash, SerializableValue, TryFromValue, ValueMap,
142143
};
143144
pub use crate::data::query::{
144-
Query, QueryError, QueryExecutionError, QueryResult, QueryVariables,
145+
Query, QueryError, QueryExecutionError, QueryResult, QueryTarget, QueryVariables,
145146
};
146147
pub use crate::data::schema::{ApiSchema, Schema};
147148
pub use crate::data::store::ethereum::*;

graphql/src/runner.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ where
108108
// while the query is running. `self.store` can not be used after this
109109
// point, and everything needs to go through the `store` we are
110110
// setting up here
111-
let store = self.store.query_store(target, false).await?;
111+
112+
let store = self.store.query_store(target.clone(), false).await?;
112113
let state = store.deployment_state().await?;
113114
let network = Some(store.network_name().to_string());
114115
let schema = store.api_schema()?;
@@ -227,7 +228,7 @@ where
227228
subscription: Subscription,
228229
target: QueryTarget,
229230
) -> Result<SubscriptionResult, SubscriptionError> {
230-
let store = self.store.query_store(target, true).await?;
231+
let store = self.store.query_store(target.clone(), true).await?;
231232
let schema = store.api_schema()?;
232233
let network = store.network_name().to_string();
233234

graphql/tests/query.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ async fn execute_query_document_with_variables(
263263
LOAD_MANAGER.clone(),
264264
METRICS_REGISTRY.clone(),
265265
));
266-
let target = QueryTarget::Deployment(id.clone());
266+
let target = QueryTarget::Deployment(id.clone(), Default::default());
267267
let query = Query::new(query, variables);
268268

269269
runner
@@ -364,7 +364,7 @@ where
364364
LOAD_MANAGER.clone(),
365365
METRICS_REGISTRY.clone(),
366366
));
367-
let target = QueryTarget::Deployment(id.clone());
367+
let target = QueryTarget::Deployment(id.clone(), Default::default());
368368
let query = Query::new(query, variables);
369369

370370
runner
@@ -388,7 +388,10 @@ async fn run_subscription(
388388
let deployment = setup_readonly(store.as_ref()).await;
389389
let logger = Logger::root(slog::Discard, o!());
390390
let query_store = store
391-
.query_store(deployment.hash.clone().into(), true)
391+
.query_store(
392+
QueryTarget::Deployment(deployment.hash.clone(), Default::default()),
393+
true,
394+
)
392395
.await
393396
.unwrap();
394397

@@ -407,7 +410,10 @@ async fn run_subscription(
407410
max_skip: std::u32::MAX,
408411
graphql_metrics: graphql_metrics(),
409412
};
410-
let schema = STORE.subgraph_store().api_schema(&deployment.hash).unwrap();
413+
let schema = STORE
414+
.subgraph_store()
415+
.api_schema(&deployment.hash, &Default::default())
416+
.unwrap();
411417

412418
execute_subscription(Subscription { query }, schema.clone(), options)
413419
}
@@ -931,7 +937,7 @@ fn instant_timeout() {
931937
match first_result(
932938
execute_subgraph_query_with_deadline(
933939
query,
934-
deployment.hash.into(),
940+
QueryTarget::Deployment(deployment.hash.into(), Default::default()),
935941
Some(Instant::now()),
936942
)
937943
.await,

node/src/config.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ impl Config {
189189
})
190190
}
191191

192-
/// Genrate a JSON representation of the config.
192+
/// Generate a JSON representation of the config.
193193
pub fn to_json(&self) -> Result<String> {
194194
// It would be nice to produce a TOML representation, but that runs
195195
// into this error: https://github.com/alexcrichton/toml-rs/issues/142

node/src/manager/commands/copy.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::{collections::HashMap, sync::Arc, time::SystemTime};
33

44
use graph::{
55
components::store::BlockStore as _,
6+
data::query::QueryTarget,
67
prelude::{
78
anyhow::{anyhow, bail, Error},
89
chrono::{DateTime, Duration, SecondsFormat, Utc},
@@ -90,7 +91,12 @@ pub async fn create(
9091
let block_offset = block_offset as i32;
9192
let subgraph_store = store.subgraph_store();
9293
let src = src.locate_unique(&primary)?;
93-
let query_store = store.query_store(src.hash.clone().into(), true).await?;
94+
let query_store = store
95+
.query_store(
96+
QueryTarget::Deployment(src.hash.clone(), Default::default()),
97+
true,
98+
)
99+
.await?;
94100
let network = query_store.network_name();
95101

96102
let src_ptr = query_store.block_ptr().await?.ok_or_else(|| anyhow!("subgraph {} has not indexed any blocks yet and can not be used as the source of a copy", src))?;

node/src/manager/commands/query.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ pub async fn run(
2929
let target = if target.starts_with("Qm") {
3030
let id =
3131
DeploymentHash::new(target).map_err(|id| anyhow!("illegal deployment id `{}`", id))?;
32-
QueryTarget::Deployment(id)
32+
QueryTarget::Deployment(id, Default::default())
3333
} else {
3434
let name = SubgraphName::new(target.clone())
3535
.map_err(|()| anyhow!("illegal subgraph name `{}`", target))?;
36-
QueryTarget::Name(name)
36+
QueryTarget::Name(name, Default::default())
3737
};
3838

3939
let document = graphql_parser::parse_query(&query)?.into_static();

server/http/src/request.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,10 @@ mod tests {
5959
use super::parse_graphql_request;
6060

6161
lazy_static! {
62-
static ref TARGET: QueryTarget =
63-
QueryTarget::Name(SubgraphName::new("test/request").unwrap());
62+
static ref TARGET: QueryTarget = QueryTarget::Name(
63+
SubgraphName::new("test/request").unwrap(),
64+
Default::default()
65+
);
6466
}
6567

6668
#[test]

server/http/src/service.rs

+43-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::task::Poll;
55
use std::time::Instant;
66

77
use graph::prelude::*;
8+
use graph::semver::VersionReq;
89
use graph::{components::server::query::GraphQLServerError, data::query::QueryTarget};
910
use http::header;
1011
use http::header::{
@@ -89,17 +90,49 @@ where
8990
self.serve_dynamic_file(self.graphiql_html())
9091
}
9192

93+
fn resolve_api_version(
94+
&self,
95+
request: &Request<Body>,
96+
) -> Result<ApiVersion, GraphQLServerError> {
97+
let mut version = ApiVersion::default();
98+
99+
if let Some(query) = request.uri().query() {
100+
let potential_version_requirement = query.split("&").find_map(|pair| {
101+
if pair.starts_with("api-version=") {
102+
if let Some(version_requirement) = pair.split("=").nth(1) {
103+
return Some(version_requirement);
104+
}
105+
}
106+
return None;
107+
});
108+
109+
if let Some(version_requirement) = potential_version_requirement {
110+
version = ApiVersion::new(
111+
&VersionReq::parse(version_requirement)
112+
.map_err(|error| GraphQLServerError::ClientError(error.to_string()))?,
113+
)
114+
.map_err(|error| GraphQLServerError::ClientError(error))?;
115+
}
116+
}
117+
118+
Ok(version)
119+
}
120+
92121
async fn handle_graphql_query_by_name(
93122
self,
94123
subgraph_name: String,
95124
request: Request<Body>,
96125
) -> GraphQLServiceResult {
126+
let version = self.resolve_api_version(&request)?;
97127
let subgraph_name = SubgraphName::new(subgraph_name.as_str()).map_err(|()| {
98128
GraphQLServerError::ClientError(format!("Invalid subgraph name {:?}", subgraph_name))
99129
})?;
100130

101-
self.handle_graphql_query(subgraph_name.into(), request.into_body())
102-
.await
131+
self.handle_graphql_query(
132+
QueryTarget::Name(subgraph_name, version),
133+
request.into_body(),
134+
)
135+
.await
103136
}
104137

105138
fn handle_graphql_query_by_id(
@@ -108,11 +141,16 @@ where
108141
request: Request<Body>,
109142
) -> GraphQLServiceResponse {
110143
let res = DeploymentHash::new(id)
111-
.map_err(|id| GraphQLServerError::ClientError(format!("Invalid subgraph id `{}`", id)));
144+
.map_err(|id| GraphQLServerError::ClientError(format!("Invalid subgraph id `{}`", id)))
145+
.and_then(|id| match self.resolve_api_version(&request) {
146+
Ok(version) => Ok((id, version)),
147+
Err(error) => Err(error),
148+
});
149+
112150
match res {
113151
Err(_) => self.handle_not_found(),
114-
Ok(id) => self
115-
.handle_graphql_query(id.into(), request.into_body())
152+
Ok((id, version)) => self
153+
.handle_graphql_query(QueryTarget::Deployment(id, version), request.into_body())
116154
.boxed(),
117155
}
118156
}

0 commit comments

Comments
 (0)