-
Notifications
You must be signed in to change notification settings - Fork 275
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
persisted queries have client names #6198
Changes from all commits
c4a24d3
d40678f
6bfdef9
e3fb191
09f7715
2b0befa
9506cd6
f0d53cd
3b4e9ca
92172c9
54e79ca
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
### Client name support for Persisted Query Lists ([PR #6198](https://github.com/apollographql/router/pull/6198)) | ||
|
||
The persisted query manifest fetched from Uplink can now contain a `clientName` field in each operation. Two operations with the same `id` but different `clientName` are considered to be distinct operations (and may have distinct bodies). | ||
|
||
Router resolves the client name by taking the first of these which exists: | ||
- Reading the `apollo_persisted_queries::client_name` context key (which may be set by a `router_service` plugin) | ||
- Reading the HTTP header named by `telemetry.apollo.client_name_header` (which defaults to `apollographql-client-name`) | ||
|
||
If a client name can be resolved for a request, Router first tries to find a persisted query with the specified ID and the resolved client name. | ||
|
||
If there is no operation with that ID and client name, or if a client name cannot be resolved, Router tries to find a persisted query with the specified ID and no client name specified. (This means that existing PQ lists that do not contain client names will continue to work.) | ||
|
||
By [@glasser](https://github.com/glasser) in https://github.com/apollographql/router/pull/6198 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,8 +21,18 @@ use crate::uplink::stream_from_uplink_transforming_new_response; | |
use crate::uplink::UplinkConfig; | ||
use crate::Configuration; | ||
|
||
/// The full identifier for an operation in a PQ list consists of an operation | ||
/// ID and an optional client name. | ||
#[derive(Debug, Clone, Eq, Hash, PartialEq)] | ||
pub struct FullPersistedQueryOperationId { | ||
/// The operation ID (usually a hash). | ||
pub operation_id: String, | ||
/// The client name associated with the operation; if None, can be any client. | ||
pub client_name: Option<String>, | ||
} | ||
|
||
/// An in memory cache of persisted queries. | ||
pub(crate) type PersistedQueryManifest = HashMap<String, String>; | ||
pub type PersistedQueryManifest = HashMap<FullPersistedQueryOperationId, String>; | ||
|
||
/// How the router should respond to requests that are not resolved as the IDs | ||
/// of an operation in the manifest. (For the most part this means "requests | ||
|
@@ -212,7 +222,7 @@ impl PersistedQueryManifestPoller { | |
if manifest_files.is_empty() { | ||
return Err("no local persisted query list files specified".into()); | ||
} | ||
let mut manifest: HashMap<String, String> = PersistedQueryManifest::new(); | ||
let mut manifest = PersistedQueryManifest::new(); | ||
|
||
for local_pq_list in manifest_files { | ||
tracing::info!( | ||
|
@@ -250,7 +260,13 @@ impl PersistedQueryManifestPoller { | |
} | ||
|
||
for operation in manifest_file.operations { | ||
manifest.insert(operation.id, operation.body); | ||
manifest.insert( | ||
FullPersistedQueryOperationId { | ||
operation_id: operation.id, | ||
client_name: operation.client_name, | ||
}, | ||
operation.body, | ||
); | ||
} | ||
} | ||
|
||
|
@@ -343,15 +359,35 @@ impl PersistedQueryManifestPoller { | |
} | ||
} | ||
|
||
pub(crate) fn get_operation_body(&self, persisted_query_id: &str) -> Option<String> { | ||
pub(crate) fn get_operation_body( | ||
&self, | ||
persisted_query_id: &str, | ||
client_name: Option<String>, | ||
) -> Option<String> { | ||
let state = self | ||
.state | ||
.read() | ||
.expect("could not acquire read lock on persisted query manifest state"); | ||
state | ||
if let Some(body) = state | ||
.persisted_query_manifest | ||
.get(persisted_query_id) | ||
.get(&FullPersistedQueryOperationId { | ||
operation_id: persisted_query_id.to_string(), | ||
client_name: client_name.clone(), | ||
}) | ||
.cloned() | ||
{ | ||
Some(body) | ||
} else if client_name.is_some() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this else if? It looks like we would be returning the null operation for the a request for an operation with a client_name. If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What this is saying is — if we don't have an operation that is specifically designated for this client name, try one that was published without a client name. (If we didn't have this, all PQs would suddenly stop working for any clients that send a client name as soon as you upgraded to this version.) The reason it's a conditional at all instead of just an "else try I should add comments to this function though! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooohhhh riiight! that totally makes sense! comment will definitely help, but logic is definitely good! thanks :) |
||
state | ||
.persisted_query_manifest | ||
.get(&FullPersistedQueryOperationId { | ||
operation_id: persisted_query_id.to_string(), | ||
client_name: None, | ||
}) | ||
.cloned() | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
pub(crate) fn get_all_operations(&self) -> Vec<String> { | ||
|
@@ -588,7 +624,13 @@ async fn add_chunk_to_operations( | |
match fetch_chunk(http_client.clone(), chunk_url).await { | ||
Ok(chunk) => { | ||
for operation in chunk.operations { | ||
operations.insert(operation.id, operation.body); | ||
operations.insert( | ||
FullPersistedQueryOperationId { | ||
operation_id: operation.id, | ||
client_name: operation.client_name, | ||
}, | ||
operation.body, | ||
); | ||
} | ||
return Ok(()); | ||
} | ||
|
@@ -674,9 +716,11 @@ pub(crate) struct SignedUrlChunk { | |
|
||
/// A single operation containing an ID and a body, | ||
#[derive(Debug, Clone, Deserialize, Serialize)] | ||
#[serde(rename_all = "camelCase")] | ||
pub(crate) struct Operation { | ||
pub(crate) id: String, | ||
pub(crate) body: String, | ||
pub(crate) client_name: Option<String>, | ||
} | ||
|
||
#[cfg(test)] | ||
|
@@ -701,7 +745,7 @@ mod tests { | |
) | ||
.await | ||
.unwrap(); | ||
assert_eq!(manifest_manager.get_operation_body(&id), Some(body)) | ||
assert_eq!(manifest_manager.get_operation_body(&id, None), Some(body)) | ||
} | ||
|
||
#[tokio::test(flavor = "multi_thread")] | ||
|
@@ -734,18 +778,26 @@ mod tests { | |
) | ||
.await | ||
.unwrap(); | ||
assert_eq!(manifest_manager.get_operation_body(&id), Some(body)) | ||
assert_eq!(manifest_manager.get_operation_body(&id, None), Some(body)) | ||
} | ||
|
||
#[test] | ||
fn safelist_body_normalization() { | ||
let safelist = FreeformGraphQLSafelist::new(&PersistedQueryManifest::from([( | ||
"valid-syntax".to_string(), | ||
"fragment A on T { a } query SomeOp { ...A ...B } fragment,,, B on U{b c } # yeah" | ||
.to_string(), | ||
), ( | ||
"invalid-syntax".to_string(), | ||
"}}}".to_string()), | ||
let safelist = FreeformGraphQLSafelist::new(&PersistedQueryManifest::from([ | ||
( | ||
FullPersistedQueryOperationId { | ||
operation_id: "valid-syntax".to_string(), | ||
client_name: None, | ||
}, | ||
"fragment A on T { a } query SomeOp { ...A ...B } fragment,,, B on U{b c } # yeah".to_string(), | ||
), | ||
( | ||
FullPersistedQueryOperationId { | ||
operation_id: "invalid-syntax".to_string(), | ||
client_name: None, | ||
}, | ||
"}}}".to_string(), | ||
), | ||
])); | ||
|
||
let is_allowed = |body: &str| -> bool { | ||
|
@@ -795,6 +847,6 @@ mod tests { | |
) | ||
.await | ||
.unwrap(); | ||
assert_eq!(manifest_manager.get_operation_body(&id), Some(body)) | ||
assert_eq!(manifest_manager.get_operation_body(&id, None), Some(body)) | ||
BrynCooke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
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 PR involves various pub and pub(crate) changes; this one is to make the actual logic work, and others are to make the integration tests work. I don't know if that's appropriate, just that it makes it build.