From a418f681977272fd45224271f7eb23a91117869b Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 12 Dec 2024 12:59:00 -0800 Subject: [PATCH] [FOUN-575] persisted queries with client names (#2239) This depends on unreleased functionality of GraphOS Router. --- crates/rover-client/.schema/hash.id | 2 +- crates/rover-client/.schema/schema.graphql | 137 +++++++++++++++++- .../persisted_queries/publish/types.rs | 15 +- src/command/persisted_queries/publish.rs | 15 +- 4 files changed, 160 insertions(+), 9 deletions(-) diff --git a/crates/rover-client/.schema/hash.id b/crates/rover-client/.schema/hash.id index 665e68950..14e544097 100644 --- a/crates/rover-client/.schema/hash.id +++ b/crates/rover-client/.schema/hash.id @@ -1 +1 @@ -ca60ef1126f6772805a6a13e89abda7e694e51302e2e083bca3d5209d709793c \ No newline at end of file +66c854c2531a914cbabfba197cf96c7c67ba8a98b0e1f5d45df27a515c7f0317 \ No newline at end of file diff --git a/crates/rover-client/.schema/schema.graphql b/crates/rover-client/.schema/schema.graphql index 4e04f9d97..4f7ea569d 100644 --- a/crates/rover-client/.schema/schema.graphql +++ b/crates/rover-client/.schema/schema.graphql @@ -937,10 +937,16 @@ input CustomCheckCallbackInput { Sets the status of your check task. Setting this status to FAILURE will cause the entire check workflow to fail. """ status: CheckStepStatus! + + """The ID of the custom check task, provided in the webhook payload.""" taskId: ID! """The violations found by your check task. Max length is 1000""" violations: [CheckViolationInput!] + + """ + The ID of the workflow that the custom check task is a member of, provided in the webhook payload. + """ workflowId: ID! } @@ -1612,6 +1618,11 @@ type GraphVariant { """Retrieve a launch for this variant by ID.""" launch(id: ID!): Launch + """ + A list of launches metadata ordered by date, asc or desc depending on orderBy. The maximum limit is 100. + """ + launchSummaries(limit: Int! = 100, offset: Int! = 0, orderBy: LaunchHistoryOrder! = CREATED_DESC): [LaunchSummary!] + """ Which permissions the current user has for interacting with this variant """ @@ -1670,6 +1681,14 @@ Modifies a variant of a graph, also called a schema tag in parts of our product. type GraphVariantMutation { """Gets the router attached to a graph variant""" router: RouterMutation + + """ + Callback mutation for submitting custom check results once your validation has run. + Results are returned with the SUCCESS or FAILURE of your validations, the task and workflow ids + to associate results, with and an optional list of violations to provide more details to users. + The Schema Check will wait for this response for 10 minutes and not complete until the results are returned. + After 10 minutes have passed without a callback request being received, the task will be marked as timed out. + """ customCheckCallback(input: CustomCheckCallbackInput!): CustomCheckCallbackResult! """ @@ -2117,9 +2136,12 @@ type Launch { latestSequenceStep: LaunchSequenceStep """ - The launch right before this one. Null if this is the first on this variant. + The launch immediately prior to this one. If successOnly is true, returns the most recent successful launch; if false, returns the most recent launch, regardless of success. If no such previous launch exists, returns null. """ - previousLaunch: Launch + previousLaunch( + """Controls if only successful launches are returned. Defaults to false.""" + successOnly: Boolean + ): Launch """A specific publication of a graph variant pertaining to this launch.""" publication: SchemaPublication @@ -2169,6 +2191,11 @@ type Launch { proposalRevision: ProposalRevision } +enum LaunchHistoryOrder { + CREATED_ASC + CREATED_DESC +} + """Types of results that can be associated with a `Launch`""" union LaunchResult = ChangelogLaunchResult @@ -2223,6 +2250,51 @@ enum LaunchStatus { LAUNCH_INITIATED } +""" +Key information representing the complete process of making a set of updates to a deployed graph variant. +""" +type LaunchSummary { + """The timestamp when the launch was approved.""" + approvedAt: Timestamp + + """ + Identifier of the associated build for this launch. This value is null until the build is initiated. + """ + buildID: ID + + """ + The inputs provided to this launch's associated build, including subgraph schemas and contract filters. + """ + buildInput: BuildInput! + + """ + The timestamp when the launch completed. This value is null until the launch completes. + """ + completedAt: Timestamp + + """The timestamp when the launch was initiated.""" + createdAt: Timestamp! + + """The ID of the launch's associated graph.""" + graphId: String! + + """The name of the launch's associated variant.""" + graphVariant: String! + + """The unique identifier for this launch.""" + id: ID! + + """ + A list of results from the completed launch. The items included in this list vary depending on whether the launch succeeded, failed, or was superseded. + """ + results: [LaunchResult!]! + + """ + The launch's status. If a launch is superseded, its status remains `LAUNCH_INITIATED`. To check for a superseded launch, use `supersededAt`. + """ + status: LaunchStatus! +} + type LintCheckTask implements CheckWorkflowTask { completedAt: Timestamp createdAt: Timestamp! @@ -2308,10 +2380,12 @@ enum LintRule { INTERFACE_SUFFIX MERGED_NON_REPEATABLE_DIRECTIVE_ARGUMENTS NO_EXECUTABLE_DIRECTIVE_INTERSECTION + NULLABLE_PATH_VARIABLE OBJECT_PREFIX OBJECT_SUFFIX OVERRIDDEN_FIELD_CAN_BE_REMOVED OVERRIDE_DIRECTIVE_CAN_BE_REMOVED + OVERRIDE_MIGRATION_IN_PROGRESS QUERY_DOCUMENT_DECLARATION RESTY_FIELD_NAMES TAG_DIRECTIVE_USES_UNKNOWN_NAME @@ -2960,6 +3034,19 @@ type PersistedQueriesPublishOperationCounts { updated: Int! } +"""Full identifier for an operation in a Persisted Query List.""" +input PersistedQueryIdInput { + """ + An optional client name to associate with the operation. Two operations with the same ID but different client names are treated as distinct operations. An operation with the same ID and a null client name is treated as a distinct operation as well. + """ + clientName: String + + """ + An opaque identifier for this operation. For a given client name, this should map uniquely to an operation body; editing the body should generally result in a new ID. Apollo's tools generally use the lowercase hex SHA256 of the operation body. + """ + id: ID! +} + """Operations to be published to the Persisted Query List.""" input PersistedQueryInput { """ @@ -2967,6 +3054,11 @@ input PersistedQueryInput { """ body: GraphQLDocument! + """ + An optional client name to associate with the operation. Two operations with the same ID but different client names are treated as distinct operations. + """ + clientName: String + """ An opaque identifier for this operation. This should map uniquely to an operation body; editing the body should generally result in a new ID. Apollo's tools generally use the lowercase hex SHA256 of the operation body. """ @@ -2981,8 +3073,11 @@ input PersistedQueryInput { type: OperationType! } -"""TODO""" +"""A Persisted Query List for a graph.""" type PersistedQueryList { + """The current build of this PQL.""" + currentBuild: PersistedQueryListBuild! + """The immutable ID for this Persisted Query List.""" id: ID! @@ -3000,6 +3095,11 @@ type PersistedQueryListBuild { """The persisted query list that this build built.""" list: PersistedQueryList! + """ + The chunks that made up this build. We do not commit to keeping the full contents of older revisions indefinitely, so this may be null for suitably old revisions. + """ + manifestChunks: [PersistedQueryListManifestChunk!] + """Information about the publish operation that created this build.""" publish: PersistedQueriesPublish! @@ -3014,11 +3114,30 @@ type PersistedQueryListBuild { totalOperationsInList: Int! } +type PersistedQueryListManifestChunk { + """ + An immutable identifier for this particular chunk of a PQL. The contents referenced by this ID will never change. + """ + id: ID! + + """ + The chunk can be downloaded from any of these URLs, which might be transient. + """ + urls: [String!]! +} + type PersistedQueryListMutation { """ Updates this Persisted Query List by publishing a set of operations and removing other operations. Operations not mentioned remain in the list unchanged. """ - publishOperations(allowOverwrittenOperations: Boolean, operations: [PersistedQueryInput!], removeOperations: [ID!]): PublishOperationsResultOrError! + publishOperations( + allowOverwrittenOperations: Boolean + operations: [PersistedQueryInput!] + remove: [PersistedQueryIdInput!] + + """Deprecated. Use `remove` instead, which allows specifying clientName.""" + removeOperations: [ID!] + ): PublishOperationsResultOrError! } """An error related to an organization's Apollo Studio plan.""" @@ -3057,6 +3176,8 @@ type Proposal { True if only some of the changes in this proposal are currently published to the implementation variant """ isPartiallyImplemented: Boolean! + latestRevision: ProposalRevision! + reviews: [ProposalReview!]! """The variant this Proposal was cloned/sourced from.""" sourceVariant: GraphVariant! @@ -3309,8 +3430,14 @@ type ProposalRevision { createdAt: Timestamp! createdBy: Identity id: ID! + isMerge: Boolean! launch: Launch + """ + Latest composition ID of the proposal's source variant at the time this revision was created. + """ + mergeBaseCompositionId: ID + """null if this is the first revision""" previousRevision: ProposalRevision summary: String! @@ -4032,7 +4159,7 @@ type Graph implements Identity { """ A list of the proposals for this graph sorted by created at date. - limit defaults to Int.MAX_VALUE, offset defaults to 0 + limit defaults to 25, offset defaults to 0 """ proposals(filterBy: ProposalsFilterInput, limit: Int, offset: Int): ProposalsResult! diff --git a/crates/rover-client/src/operations/persisted_queries/publish/types.rs b/crates/rover-client/src/operations/persisted_queries/publish/types.rs index fecaf8ab7..919e89706 100644 --- a/crates/rover-client/src/operations/persisted_queries/publish/types.rs +++ b/crates/rover-client/src/operations/persisted_queries/publish/types.rs @@ -32,11 +32,13 @@ pub struct ApolloPersistedQueryManifest { } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Ord, PartialOrd)] +#[serde(rename_all = "camelCase")] pub struct PersistedQueryOperation { pub name: String, pub r#type: PersistedQueryOperationType, pub body: String, pub id: String, + pub client_name: Option, } #[derive(Clone, Debug, Eq, PartialEq, Serialize, Ord, PartialOrd)] @@ -96,6 +98,7 @@ impl From for QueryVariables { name: operation.name, body: operation.body, id: operation.id, + client_name: operation.client_name, type_: match operation.r#type { PersistedQueryOperationType::Mutation => { RoverClientOperationType::MUTATION @@ -183,6 +186,10 @@ impl TryFrom for ApolloPersistedQueryManifest { r#type: operation_type, body: body.to_string(), id: id.to_string(), + // Relay format has no way to include client names in + // the manifest file; you can still use the + // `--for-client-name` flag. + client_name: None, }); } else { // `apollo-parser` may sometimes be able to detect an operation name @@ -324,7 +331,8 @@ mod tests { name: "NewsfeedQuery".to_string(), r#type: PersistedQueryOperationType::Query, id: id.to_string(), - body: body.to_string() + body: body.to_string(), + client_name: None, } ); } @@ -344,7 +352,8 @@ mod tests { name: "NewsfeedQuery".to_string(), r#type: PersistedQueryOperationType::Query, id: id.to_string(), - body: body.to_string() + body: body.to_string(), + client_name: None, } ); } @@ -370,6 +379,7 @@ mod tests { name: "NamedMutation".to_string(), r#type: PersistedQueryOperationType::Mutation, id: id_two.to_string(), + client_name: None, body: body_two.to_string() } ); @@ -380,6 +390,7 @@ mod tests { name: "NewsfeedQuery".to_string(), r#type: PersistedQueryOperationType::Query, id: id_one.to_string(), + client_name: None, body: body_one.to_string() } ); diff --git a/src/command/persisted_queries/publish.rs b/src/command/persisted_queries/publish.rs index 39c41f883..520509f94 100644 --- a/src/command/persisted_queries/publish.rs +++ b/src/command/persisted_queries/publish.rs @@ -41,6 +41,11 @@ pub struct Publish { #[arg(long, value_enum, default_value_t = PersistedQueriesManifestFormat::Apollo)] manifest_format: PersistedQueriesManifestFormat, + /// If provided, overrides the `clientName` field in all operations in + /// the manifest file. + #[arg(long)] + for_client_name: Option, + #[clap(flatten)] profile: ProfileOpt, } @@ -57,7 +62,7 @@ impl Publish { format!("JSON in {manifest} did not match '--manifest-format {format}'") }; - let operation_manifest = match self.manifest_format { + let mut operation_manifest = match self.manifest_format { PersistedQueriesManifestFormat::Apollo => { serde_json::from_str::(&raw_manifest) .with_context(|| invalid_json_err(&self.manifest, "apollo"))? @@ -69,6 +74,14 @@ impl Publish { } }; + // Override any client names provided in the manifest (which is the only way to + // provide client names for the Relay format). + if let Some(for_client_name) = &self.for_client_name { + for op in &mut operation_manifest.operations { + op.client_name = Some(for_client_name.to_string()); + } + } + let (graph_id, list_id, list_name) = match (&self.graph.graph_ref, &self.graph_id, &self.list_id) { (Some(graph_ref), None, None) => { let persisted_query_list = resolve::run(ResolvePersistedQueryListInput { graph_ref: graph_ref.clone() }, &client).await?;