Skip to content

feat: GenericGraphQlPlugin builder, AuthPort, proactive cost throttling (v0.1.12)#15

Merged
greysquirr3l merged 7 commits intomainfrom
release/v0.1.12
Mar 4, 2026
Merged

feat: GenericGraphQlPlugin builder, AuthPort, proactive cost throttling (v0.1.12)#15
greysquirr3l merged 7 commits intomainfrom
release/v0.1.12

Conversation

@greysquirr3l
Copy link
Owner

v0.1.12

Added

  • AuthPort — trait for runtime credential management: load_token(), expiry detection, and refresh_token(); ErasedAuthPort object-safe wrapper with blanket impl; EnvAuthPort convenience implementation; resolve_token helper
  • CostThrottleConfig + LiveBudget / PluginBudget — proactive point-budget tracking for cost-bearing GraphQL APIs (Shopify Admin API pattern); pre_flight_delay sleeps before a request if the projected budget is too low; update_budget parses extensions.cost.throttleStatus; reactive_backoff_ms for exponential back-off on throttle responses
  • GenericGraphQlPlugin + GenericGraphQlPluginBuilder — fluent builder API to construct a GraphQlTargetPlugin without writing a dedicated struct; supports name, endpoint, bearer_auth, auth, header, headers, cost_throttle, page_size, description
  • GraphQlService::with_auth_port() — attach a runtime ErasedAuthPort; token resolved lazily per request with automatic refresh on expiry
  • GraphQlTargetPlugin::cost_throttle_config() — new default method; plugins opt in to proactive throttling by returning Some(CostThrottleConfig)
  • GitHub GraphQL example pipelines in examples/pipelines/github/
  • mdBook: new GraphQL Plugins page, adapters.md GraphQL section, SUMMARY.md link

Removed

  • JobberPlugin and jobber_integration tests — consumer-specific plugins belong in the consuming application; use GenericGraphQlPlugin or implement GraphQlTargetPlugin directly
  • Jobber example pipelines (examples/pipelines/jobber/)

232 tests passing · zero clippy warnings · cargo fmt clean

- Add AuthPort trait + ErasedAuthPort (object-safe blanket impl) + EnvAuthPort;
  resolve_token helper handles expiry detection and refresh automatically
- Add CostThrottleConfig (port layer), LiveBudget / PluginBudget with
  pre_flight_delay, update_budget, and reactive_backoff_ms for proactive
  point-budget management on cost-bearing GraphQL APIs
- Add GenericGraphQlPlugin + GenericGraphQlPluginBuilder — fluent builder API
  to construct a GraphQlTargetPlugin without writing a dedicated struct;
  supports name, endpoint, bearer_auth, auth, headers, cost_throttle,
  page_size, description
- Wire auth_port and per-plugin budgets into GraphQlService; token resolved
  lazily per request, budget updated from extensions.cost.throttleStatus
- Remove JobberPlugin and jobber_integration tests — consumer-specific plugins
  belong in the consuming application; use GenericGraphQlPlugin instead
- Add GitHub GraphQL example pipe- Add GitHub GraphQL example pipe- Add GitHub GraphQL example pipe- Add GitHub GraphQLd GraphQL section,
  SUMMARY.md link
- Bump version 0.1.11 → 0.1.12
Copilot AI review requested due to automatic review settings March 4, 2026 14:41
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR bumps the workspace to v0.1.12 and expands stygian-graph’s GraphQL support with (1) runtime credential injection via an auth port and (2) opt-in proactive cost throttling, while replacing the library’s consumer-specific Jobber plugin/examples with GitHub GraphQL example pipelines and updated mdBook docs.

Changes:

  • Add AuthPort / ErasedAuthPort (+ EnvAuthPort and resolve_token) and wire optional runtime auth into GraphQlService.
  • Add proactive cost throttling (CostThrottleConfig, LiveBudget / PluginBudget, pre_flight_delay, update_budget, reactive_backoff_ms) and expose opt-in via GraphQlTargetPlugin::cost_throttle_config().
  • Replace Jobber-specific plugin/tests/examples with GenericGraphQlPlugin builder + new GitHub example pipelines, plus mdBook docs updates.

Reviewed changes

Copilot reviewed 42 out of 43 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
examples/pipelines/jobber/visits.toml Remove Jobber visits pipeline example.
examples/pipelines/jobber/schemas/visit.schema.json Remove Jobber Visit JSON schema example.
examples/pipelines/jobber/schemas/quote.schema.json Remove Jobber Quote JSON schema example.
examples/pipelines/jobber/schemas/job.schema.json Remove Jobber Job JSON schema example.
examples/pipelines/jobber/schemas/invoice.schema.json Remove Jobber Invoice JSON schema example.
examples/pipelines/jobber/schemas/expense.schema.json Remove Jobber Expense JSON schema example.
examples/pipelines/jobber/schemas/client.schema.json Remove Jobber Client JSON schema example.
examples/pipelines/jobber/quotes.toml Remove Jobber quotes pipeline example.
examples/pipelines/jobber/jobs.toml Remove Jobber jobs pipeline example.
examples/pipelines/jobber/invoices.toml Remove Jobber invoices pipeline example.
examples/pipelines/jobber/introspect.toml Remove Jobber schema introspection example.
examples/pipelines/jobber/full_sync.toml Remove Jobber full DAG sync example.
examples/pipelines/jobber/expenses.toml Remove Jobber expenses pipeline example.
examples/pipelines/jobber/clients.toml Remove Jobber clients pipeline example.
examples/pipelines/jobber/README.md Remove Jobber pipeline documentation.
examples/pipelines/github/starred.toml Add GitHub “starred repositories” GraphQL pipeline example.
examples/pipelines/github/schemas/repository.schema.json Add GitHub repository JSON schema for normalisation.
examples/pipelines/github/schemas/pull_request.schema.json Add GitHub pull request JSON schema for normalisation.
examples/pipelines/github/schemas/profile_summary.schema.json Add JSON schema for AI-generated GitHub profile summary.
examples/pipelines/github/schemas/issue.schema.json Add GitHub issue JSON schema for normalisation.
examples/pipelines/github/repositories.toml Add GitHub owned repositories pipeline example.
examples/pipelines/github/pull_requests.toml Add GitHub pull requests pipeline example.
examples/pipelines/github/issues.toml Add GitHub issues pipeline example.
examples/pipelines/github/introspect.toml Add GitHub schema introspection pipeline example.
examples/pipelines/github/full_sync.toml Add GitHub full sync DAG example with Claude analysis node.
examples/pipelines/github/README.md Add GitHub pipeline documentation.
crates/stygian-graph/tests/jobber_integration.rs Remove live Jobber integration test.
crates/stygian-graph/src/ports/graphql_plugin.rs Add CostThrottleConfig and plugin opt-in hook cost_throttle_config().
crates/stygian-graph/src/ports/auth.rs Add auth port traits, helpers, env implementation, and tests.
crates/stygian-graph/src/ports.rs Export the new auth module.
crates/stygian-graph/src/adapters/graphql_throttle.rs Add proactive throttle tracking + backoff utilities and tests.
crates/stygian-graph/src/adapters/graphql_plugins/mod.rs Remove Jobber plugin module; document + expose generic plugin module.
crates/stygian-graph/src/adapters/graphql_plugins/jobber.rs Remove Jobber plugin implementation.
crates/stygian-graph/src/adapters/graphql_plugins/generic.rs Add GenericGraphQlPlugin and fluent builder + tests.
crates/stygian-graph/src/adapters/graphql.rs Wire in ErasedAuthPort fallback and per-plugin budgets + budget updates.
crates/stygian-graph/src/adapters.rs Export the new GraphQL throttling adapter module.
book/src/graph/graphql-plugins.md Add mdBook page documenting plugins, auth port, and cost throttling.
book/src/graph/adapters.md Add GraphQL adapter section linking to the plugin docs.
book/src/SUMMARY.md Add link to the new GraphQL Plugins page.
Cargo.toml Bump workspace version to 0.1.12.
Cargo.lock Bump crate versions to 0.1.12.
CHANGELOG.md Add v0.1.12 changelog entry describing new features and removals.
.gitleaks.toml Add gitleaks config + allowlist for generated and example paths.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

- Fix auth fallback: plugin.default_auth() returning None no longer
  blocks the auth_port fallback path (was unreachable)
- Fix auth error handling: auth port failure now returns
  AuthenticationFailed error instead of warn-and-proceed silently
- Fix budget race: double-check pattern under write lock prevents two
  concurrent slow-path inits from overwriting each other's budget state
- Add PluginBudget::config() accessor; wire reactive_backoff_ms into
  validate_body so throttle back-off uses exponential/config-aware delay
  when a budget is available, falling back to fixed-clamp otherwise
- Fix graphql-plugins.md: correct TokenSet fields (access_token,
  refresh_token, expires_at, scopes), load_token/refresh_token sigs,
  GraphQlService::new signature, BuildError return type, CostThrottleConfig
  field name (restore_per_sec) and defaults, EnvAuthPort None behaviour,
  and env-var template expansion attribution
@greysquirr3l greysquirr3l self-assigned this Mar 4, 2026
@greysquirr3l greysquirr3l added the enhancement New feature or request label Mar 4, 2026
@greysquirr3l greysquirr3l requested a review from Copilot March 4, 2026 15:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 42 out of 43 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +254 to +258
use stygian_graph::adapters::graphql::GraphQlService;
use stygian_graph::ports::auth::{EnvAuthPort, ErasedAuthPort};

let service = GraphQlService::new(registry)
.with_auth_port(Arc::new(EnvAuthPort::new("MY_API_TOKEN")) as Arc<dyn ErasedAuthPort>);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code sample constructs GraphQlService with GraphQlService::new(registry), but the actual constructor takes (GraphQlConfig, Option<Arc<GraphQlPluginRegistry>>) (see crates/stygian-graph/src/adapters/graphql.rs). Update the docs snippet to use the real signature (and show how to pass the registry via Some(Arc::new(registry))).

Suggested change
use stygian_graph::adapters::graphql::GraphQlService;
use stygian_graph::ports::auth::{EnvAuthPort, ErasedAuthPort};
let service = GraphQlService::new(registry)
.with_auth_port(Arc::new(EnvAuthPort::new("MY_API_TOKEN")) as Arc<dyn ErasedAuthPort>);
use stygian_graph::adapters::graphql::{GraphQlConfig, GraphQlService};
use stygian_graph::ports::auth::{EnvAuthPort, ErasedAuthPort};
let service = GraphQlService::new(
GraphQlConfig::default(),
Some(Arc::new(registry)),
)
.with_auth_port(Arc::new(EnvAuthPort::new("MY_API_TOKEN")) as Arc<dyn ErasedAuthPort>);

Copilot uses AI. Check for mistakes.
Register it the same way as any built-in plugin:

```rust
use stygian_graph::adapters::graphql::GraphQlPluginRegistry;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This snippet imports GraphQlPluginRegistry from stygian_graph::adapters::graphql, but the type lives under stygian_graph::application::graphql_plugin_registry::GraphQlPluginRegistry (and this example also needs use std::sync::Arc;). As written, the example won’t compile.

Suggested change
use stygian_graph::adapters::graphql::GraphQlPluginRegistry;
use std::sync::Arc;
use stygian_graph::application::graphql_plugin_registry::GraphQlPluginRegistry;

Copilot uses AI. Check for mistakes.
Comment on lines +338 to +344
fn validate_body(body: &Value, budget: Option<&PluginBudget>) -> Result<()> {
// Throttle check takes priority so callers can retry with backoff.
if let Some(retry_after_ms) = Self::detect_throttle(body) {
if Self::detect_throttle(body).is_some() {
let retry_after_ms = budget.map_or_else(
|| Self::throttle_backoff(body),
|b| reactive_backoff_ms(b.config(), body, 0),
);
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reactive_backoff_ms supports exponential backoff via its attempt parameter, but validate_body always passes attempt = 0, so retries will never increase the delay. Either (a) plumb an attempt counter from the retrying caller into this path, or (b) switch to returning a deterministic base delay here and let the retry loop apply exponential backoff externally.

Copilot uses AI. Check for mistakes.
Comment on lines +461 to +483
// ── 4b. Lazy-init and acquire per-plugin budget ───────────────────
let maybe_budget: Option<PluginBudget> = if let Some(ref p) = plugin {
if let Some(throttle_cfg) = p.cost_throttle_config() {
let name = p.name().to_string();
let budget = {
let read = self.budgets.read().await;
if let Some(b) = read.get(&name) {
b.clone()
} else {
drop(read);
// Slow path: initialise under write lock with double-check
// to prevent two concurrent requests both inserting a fresh
// budget and one overwriting any updates the other has applied.
let mut write = self.budgets.write().await;
write
.entry(name)
.or_insert_with(|| PluginBudget::new(throttle_cfg))
.clone()
}
};
pre_flight_delay(&budget).await;
Some(budget)
} else {
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proactive throttle gate (pre_flight_delay) does not reserve/debit any points before sending the request. With concurrent GraphQlService::execute calls for the same plugin, multiple requests can pass the pre-flight check simultaneously and still get throttled, undermining the goal of proactive throttling. Consider adding a reservation step (decrement budget by an estimated cost) or serializing requests per budget while the projected budget is below the threshold.

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +159
/// Attach a runtime auth port.
///
/// When set, the port's `erased_resolve_token()` will be called to obtain
/// a bearer token whenever `params.auth` is absent and the plugin supplies
/// no `default_auth`.
///
/// # Example
///
/// ```no_run
/// use std::sync::Arc;
/// use stygian_graph::adapters::graphql::{GraphQlService, GraphQlConfig};
/// use stygian_graph::ports::auth::{EnvAuthPort, ErasedAuthPort};
///
/// let auth: Arc<dyn ErasedAuthPort> = Arc::new(EnvAuthPort::new("API_TOKEN"));
/// let service = GraphQlService::new(GraphQlConfig::default(), None)
/// .with_auth_port(auth);
/// ```
#[must_use]
pub fn with_auth_port(mut self, port: Arc<dyn ErasedAuthPort>) -> Self {
self.auth_port = Some(port);
self
}
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with_auth_port introduces new auth-selection behavior (fallback to the runtime port when params.auth and plugin.default_auth() are absent), but there’s no unit test exercising this path in graphql.rs (this file already has extensive request-capture tests). Adding a test that asserts a request uses the port-provided bearer token would prevent regressions.

Copilot uses AI. Check for mistakes.
- Add `estimated_cost_per_request` field to `CostThrottleConfig` (default 100.0)
- Add `pending` field to `LiveBudget`; deduct from projected_available()
- Replace `pre_flight_delay` with `pre_flight_reserve` (returns reserved cost)
- Add `release_reservation` called at every exit path (success + error)
- Per-request reserve/release replaces the single pre-execute delay
- Add `concurrent_reservations_reduce_projected_available` test

NOTE: explicit call-site cleanup used in place of RAII guard because
AsyncDrop is not stabilised in Rust 1.93.1 (stable). TODOs added at each
site to revisit once AsyncDrop lands.
@greysquirr3l greysquirr3l merged commit 9c3cac1 into main Mar 4, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants