-
Notifications
You must be signed in to change notification settings - Fork 982
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
GraphQL API Versioning #3155
GraphQL API Versioning #3155
Conversation
c08d3e5
to
6273c09
Compare
f2b6739
to
11971fa
Compare
} | ||
&["subgraphs", "name", _] | &["subgraphs", "name", _, _] => Ok(state( | ||
store, | ||
target_from_name(path_segments[2..].join("/"), Default::default()), // TODO: version |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- resolve version of WebSocket connections
@@ -275,7 +276,9 @@ impl DeploymentStore { | |||
// if that's Fred the Dog, Fred the Cat or both. | |||
// | |||
// This assumes that there are no concurrent writes to a subgraph. | |||
let schema = self.subgraph_info_with_conn(&conn, &layout.site)?.api; | |||
let schema = self | |||
.subgraph_info_with_conn(&conn, &layout.site, &Default::default())? // TODO: ask if we need a non-default version here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Do we need a version here? Is default (latest) enough?
@@ -923,7 +931,7 @@ impl DeploymentStore { | |||
) -> Result<StoreEvent, StoreError> { | |||
let event = conn.transaction(|| -> Result<_, StoreError> { | |||
// Don't revert past a graft point | |||
let info = self.subgraph_info_with_conn(&conn, site.as_ref())?; | |||
let info = self.subgraph_info_with_conn(&conn, site.as_ref(), &Default::default())?; // TODO: ask if we need a non-default version here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Do we need a version here? Is default (latest) enough?
}; | ||
|
||
let (store, site) = self.store(&id)?; | ||
let (store, site) = self.store(&id)?; // TODO: maybe use a tuple (id, version) here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- not sure if we want to get the store based on
id
or(id, version)
.
@@ -858,7 +858,7 @@ impl SubgraphStoreInner { | |||
.ok_or_else(|| constraint_violation!("no chain info for {}", deployment_id))?; | |||
let latest_ethereum_block_number = | |||
chain.latest_block.as_ref().map(|ref block| block.number()); | |||
let subgraph_info = store.subgraph_info(site.as_ref())?; | |||
let subgraph_info = store.subgraph_info(site.as_ref(), &Default::default())?; // TODO: ask if we need to use non-default version here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Do we need a version here? Is default (latest) enough?
@@ -1022,13 +1022,18 @@ impl SubgraphStoreTrait for SubgraphStore { | |||
|
|||
fn input_schema(&self, id: &DeploymentHash) -> Result<Arc<Schema>, StoreError> { | |||
let (store, site) = self.store(&id)?; | |||
let info = store.subgraph_info(site.as_ref())?; | |||
// TODO: ask if we need to use non-default version here, I think we don't because it's schema provided by user | |||
let info = store.subgraph_info(site.as_ref(), &Default::default())?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Do we need a version here? Is default (latest) enough?
I think we don't because it's schema provided by user
#[derive(Clone, PartialEq)] | ||
pub enum FeatureFlag { | ||
// A description of the feature. Give it a little context. | ||
BasicOrdering, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example flag, just to demo the versioning.
let supported_versions: Vec<(&str, Vec<FeatureFlag>)> = vec![ | ||
// baseline version | ||
("1.0.0", vec![]), | ||
// Versions with feature flags | ||
("1.1.0", vec![FeatureFlag::BasicOrdering]) | ||
]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is something we need to maintain. It's the single source of truth.
static ref LATEST_VERSION: String = { | ||
let keys: Vec<VersionNumber> = VERSIONS.clone().into_keys().collect(); | ||
|
||
let last_version = keys.last().unwrap(); | ||
|
||
last_version.0.clone() | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latest version is the last key of VERSIONS
.
pub fn validate(version: String) -> Result<(), String> { | ||
let chunks: Vec<&str> = version.split(".").collect(); | ||
|
||
if chunks.len() != 3 { | ||
return Err(format!("Invalid version number: {}", version)); | ||
} | ||
|
||
let major = chunks[0].parse::<u32>(); | ||
let minor = chunks[1].parse::<u32>(); | ||
let patch = chunks[2].parse::<u32>(); | ||
|
||
if major.is_err() || minor.is_err() || patch.is_err() { | ||
return Err(format!("Invalid version number: {}", version)); | ||
} | ||
|
||
if !VERSIONS.contains_key(&VersionNumber::from(version)) { | ||
return Err("No versions found".to_string()); | ||
} | ||
|
||
Ok(()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation happens in http and websocket part of the server.
impl QueryTarget { | ||
pub fn get_version_number(&self) -> VersionNumber { | ||
match self { | ||
Self::Name(_, version) => version.clone(), | ||
Self::Deployment(_, version) => version.clone(), | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to look for the best place to put the requested version and QueryTarget
won the contest.
if version.supports(FeatureFlag::BasicOrdering) { | ||
add_order_direction_enum(&mut schema); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An example implementation of a feature flag.
fn resolve_version_number( | ||
&self, | ||
request: &Request<Body>, | ||
) -> Result<VersionNumber, GraphQLServerError> { | ||
let version_header = request.headers().get("api-version"); | ||
|
||
let mut version = VersionNumber::default(); | ||
|
||
if let Some(header_value) = version_header { | ||
let version_value = header_value.to_str().map_err(|_| { | ||
GraphQLServerError::ClientError(format!( | ||
"Invalid api-version header value {:?}", | ||
header_value | ||
)) | ||
})?; | ||
VersionNumber::validate(version_value.to_string()) | ||
.map_err(|error| GraphQLServerError::ClientError(error))?; | ||
|
||
version = version_value.into(); | ||
} | ||
|
||
Ok(version) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used api-version
header because it was the easiest to implement quickly but before merging the Pull Request we should switch to url-based approach (this way we could use GraphiQL - it does not support headers).
@@ -19,6 +19,7 @@ type Query { | |||
indexer: Bytes | |||
): Bytes | |||
subgraphFeatures(subgraphId: String!): SubgraphFeatures! | |||
subgraphVersions(subgraphId: String!): [SubgraphVersion!]! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is for the Gateway.
type SubgraphVersion { | ||
""" | ||
Version number in SemVer format | ||
""" | ||
version: String! | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why an object instead of a list? It's extendable and harder to break backwards-compatibility.
@@ -313,7 +313,7 @@ where | |||
}; | |||
|
|||
// Construct a subscription | |||
let target = QueryTarget::Deployment(deployment.clone()); | |||
let target = QueryTarget::Deployment(deployment.clone(), Default::default()); // TODO: resolve and validate a version number from WebSocket connection |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- resolve version of WebSocket connections
c98485d
to
3727335
Compare
Query preprocessing eliminated all variable references
That makes it possible to use the same schema for data nad for introspection queries
The old name wasn't matching what we use it for any more
No cows were harmed in making this change
That is demanded by the GraphQL spec. We need to have a predictable order to ensure attestations remain stable, though this change breaks attestations since it changes the order in which fields appear in the output from alphabetical (however a `BTreeMap` orders string keys) to the order in which fields appear in a query. It also allows us to replace `BTreeMaps`, which are fairly memory intensive, with cheaper `Vec`. The test changes all reflect the changed output behavior; they only reorder fields in the expected output but do not otherwise alter the tests. Fixes #2943
Rather than use a string name, use the actual object type to identify types. It's not possible to do this with plain references, for example, because we pass a reference to a SelectionSet to graph::spawn_blocking, so we do the next best thing and use an Arc. Unfortunately, because `graphql_parser` doesn't wrap its object types in an Arc, that means we need to keep a copy of all of them in ApiSchema.
This just shuffles some code around, but doesn't change anything else, in preparation for representing the schema in a way that's more useful to us.
Set ENABLE_GRAPHQL_VALIDATIONS to any value in the environment to enable validations, rather than enabling them by default and disabling them on demand
Make sure that we handle queries that have a selection from a scalar field by ignoring the selection or that have no selection for a non-scalar field by ignoring that field. The latter differs from the previous behavior where the result would contain an entry for such a field, but the data for the field would be an empty object or a list of empty objects.
Instead of immediately reporting an error, treat missing variables as nulls and let the rest of the execution logic deal with nulls
558ad33
to
291a675
Compare
291a675
to
306a886
Compare
This is the initial implementation of versioning.
To do:
I left few comments on the interesting parts of the code to make it easier to review.