Skip to content
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

Closed
wants to merge 31 commits into from
Closed

GraphQL API Versioning #3155

wants to merge 31 commits into from

Conversation

kamilkisiela
Copy link
Contributor

@kamilkisiela kamilkisiela commented Jan 13, 2022

This is the initial implementation of versioning.

To do:

  • resolve version of WebSocket connections
  • switch from header-based approach to url-based
  • make sure we use default (latest) version correctly
  • remove the example implementation of a feature flag

I left few comments on the interesting parts of the code to make it easier to review.

}
&["subgraphs", "name", _] | &["subgraphs", "name", _, _] => Ok(state(
store,
target_from_name(path_segments[2..].join("/"), Default::default()), // TODO: version
Copy link
Contributor Author

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
Copy link
Contributor Author

@kamilkisiela kamilkisiela Jan 14, 2022

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
Copy link
Contributor Author

@kamilkisiela kamilkisiela Jan 14, 2022

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?
Copy link
Contributor Author

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
Copy link
Contributor Author

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())?;
Copy link
Contributor Author

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,
Copy link
Contributor Author

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.

Comment on lines +9 to +14
let supported_versions: Vec<(&str, Vec<FeatureFlag>)> = vec![
// baseline version
("1.0.0", vec![]),
// Versions with feature flags
("1.1.0", vec![FeatureFlag::BasicOrdering])
];
Copy link
Contributor Author

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.

Comment on lines +25 to +32
static ref LATEST_VERSION: String = {
let keys: Vec<VersionNumber> = VERSIONS.clone().into_keys().collect();

let last_version = keys.last().unwrap();

last_version.0.clone()
};
}
Copy link
Contributor Author

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.

Comment on lines +51 to +72
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(())
}
}
Copy link
Contributor Author

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.

Comment on lines +119 to -123
impl QueryTarget {
pub fn get_version_number(&self) -> VersionNumber {
match self {
Self::Name(_, version) => version.clone(),
Self::Deployment(_, version) => version.clone(),
}
}
}
Copy link
Contributor Author

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.

graphql/src/runner.rs Outdated Show resolved Hide resolved
Comment on lines +88 to +90
if version.supports(FeatureFlag::BasicOrdering) {
add_order_direction_enum(&mut schema);
}
Copy link
Contributor Author

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.

Comment on lines +169 to +191
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)
}
Copy link
Contributor Author

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!]!
Copy link
Contributor Author

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.

Comment on lines +100 to +105
type SubgraphVersion {
"""
Version number in SemVer format
"""
version: String!
}
Copy link
Contributor Author

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
Copy link
Contributor Author

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

@dotansimha dotansimha changed the title Versioning GraphQL API Versioning Jan 18, 2022
@lutter lutter force-pushed the lutter/gql branch 2 times, most recently from c98485d to 3727335 Compare January 20, 2022 00:58
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants