control-plane-api: GraphQL createDiscover mutation and discover(id) query - #3464
Draft
bbartman wants to merge 6 commits into
Draft
control-plane-api: GraphQL createDiscover mutation and discover(id) query#3464bbartman wants to merge 6 commits into
bbartman wants to merge 6 commits into
Conversation
The GraphQL discover API needs to map the stored `discovers.job_status` into a schema enum, so the type must live where both the agent's executor and control-plane-api can name it. Serde attributes are unchanged: the camelCase `type` tags are a PostgREST wire contract, and the new snapshot test pins every variant. `is_success` becomes a plain public function. Also refreshes two stale `.sqlx` entries whose `connector_status?` column became nullable under current migrations; `ci:sqlx-check` failed at HEAD. Part of #3453.
A unit-variant projection of `discovers::JobStatus`, which `async_graphql::Enum` requires and which the data-carrying `Success` variant cannot satisfy. Serde names stay camelCase so a stored `job_status->>'type'` parses into it directly; only the GraphQL names are SCREAMING_SNAKE_CASE, matching the newer hand-written status enums. Variants follow the schema's order. The wire-tag test now also snapshots the tag-to-variant mapping, pinning agreement with `From<&JobStatus>`. The SDL is unchanged: nothing in the schema reaches the type yet. Part of #3453.
Reads one user-initiated discover by id. The `discovers` table has no `user_id` of its own and this API queries Postgres on a privileged connection where row-level security does not apply, so the join to `drafts` on the caller's id is the access check. A missing id, another owner's discover, and a draft-cascaded deletion are all the same null, so ids cannot probe for other users' discovers or their `logsToken`. The SDL gains `scalar Capture`, `Discover`, `DiscoverStatusType`, and `QueryRoot.discover`. The read path lands first so the mutation's tests can use it. Regenerating `.sqlx/` from a freshly reset database reverts the two `connector_status?` nullability flips shipped in 3ca088f: those came from a local database whose planner state differed after test runs, not from stale migrations. `ci:sqlx-check` passes against master's values. Part of #3453.
Queues a user-initiated discover by writing the same `discovers` row the PostgREST insert writes today, so the existing trigger enqueues the executor's task and both paths coexist. The mutation gates synchronously on what the executor would otherwise reject after queuing: the capture name is validated, the caller must hold SpecEdit on it and read on the data plane, and the connector tag must exist, be successfully processed, and be a capture connector. A given draft must belong to the caller; otherwise a caller-owned draft is created. Two deviations from the plan in #3453. The data-plane check evaluates the authorization outcome directly rather than through `verify_authorization`, so a terminal denial is the same "not found" as an absent plane while a stale-Snapshot denial still returns 307; a literal `verify_authorization` would have made the two distinguishable. And `endpointConfig` is taken as `Json<async_graphql::Value>` rather than `Json<Box<RawValue>>`, which cannot be parsed from GraphQL input; the ordered map preserves field order, which the stored-row snapshot pins. The plan's A3 case does not hold under current code: an `editor` bundle alone passes the SpecEdit gate but cannot reach the public data plane, because legacy `read` on `ops/dp/public/` requires Viewer bits including ViewDataPlanePrivateNetworking and a Delegate edge passes through only the bits its parent holds. The executor makes the identical check. The test and the SDL description record the actual behaviour. The SDL gains `MutationRoot.createDiscover`. Part of #3453.
One snapshot of every synchronous rejection: invalid capture name, unknown, unprocessed, and non-capture connector tags, unknown data plane, not-owned and missing drafts (asserted byte-identical, so a draft id cannot probe for other users' drafts), and an unauthenticated caller. Neither a discover nor a draft row is written by any of them. The unprocessed-tag case exposed a defect in the mutation: `protocol` is null until a tag's spec job succeeds, so `protocol = 'capture'` was null and the non-null decode failed with a database error instead of the intended message. The comparison is now coalesced to false. Part of #3453.
`createDiscover` runs against the real schema in-process, the real `DiscoverExecutor` polls the task the row's trigger enqueued, and `discover(id)` reports the outcome. Covers a discover into a new draft, a re-discover that keeps a draft's existing specs and merges bindings, a connector failure surfacing as DISCOVER_FAILED with `draft_errors`, and an unauthorized capture rejected synchronously with no task enqueued. The connector tag id is obtained the way a client would, through `connector(imageName).spec(imageTag).id`, so removing that field will correctly fail these tests until the mutation's input changes with it. Part of #3453.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the unified plan in #3453 (comment of 2026-09-04): a GraphQL
createDiscovermutation anddiscover(id)polling query incontrol-plane-api, replacing the PostgREST insert-and-poll surface for user-initiated discovers. The agent'sDiscoverExecutorand the existing PostgREST path are unchanged and coexist.Landing as the plan's six-commit sequence, each compiling and tested on its own:
JobStatusfrom the agent intomodels::discoversDiscoverStatusTypewithSCREAMING_SNAKE_CASEGraphQL names andFrom<&JobStatus>discover(id)query (Q1鈥換6, S1)createDiscovermutation with synchronousSpecEditand data-plane gates (C1鈥揅9, A1鈥揂5)Deviations from the plan
editorbundle alone passes theSpecEditgate but cannot reach the public data plane: legacyreadonops/dp/public/requires the Viewer bits includingViewDataPlanePrivateNetworking, and aDelegateedge passes through only the bits its parent holds, which Editor lacks. The executor makes the identical check, so this predates the mutation and also affects publishing. The test and the SDL description record the actual behaviour; section 6 of the plan needs amending, or the plane's required capability needs a separate product decision.endpointConfigisJson<async_graphql::Value>, not the plan'sJson<Box<RawValue>>, which cannot be parsed from GraphQL input. The ordered map preserves field order, pinned by a stored-row snapshot.verify_authorization, so a terminal denial is the same "not found" as an absent plane while a stale-Snapshot denial still returns 307. A literalverify_authorizationwould have made the two cases distinguishable, against the plan's own intent.protocol, which the non-null decode rejected with a database error instead of the intended message.bob_cofixture needssso_tenantfor Bob's user row, so the discover tests insert Bob, Carol, and a service account inline.connector_status?nullability entries based on a non-fresh local database; commit 3 reverts them. The cache must be regenerated from a freshly reset database.Closes #3453.