Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ for historical context.

## Unreleased

Companion runtime/macros version: **0.61.0**.

- Generated single-primary-key `…Changed` subscriptions accept an optional typed `key` argument. Omission retains all authorized changes; a key composes with the action filter. Registration and every delivered event retain current authorization and provenance checks. The generated operation catalogue includes the same argument.

## 0.60.0 (unreleased)

Companion runtime/macros version: **0.60.0**.

- Add explicit `query_shapes = "list,keyset,scan"` selection on generated entities. Omitted query shapes disappear from both the GraphQL root and operation catalogue, allowing a host to retain a differently shaped legacy singleton during migration. Default generation remains unchanged; repository APIs, relations, mutations and subscriptions are unaffected.
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ cynic-parser = { version = "=0.11.2", features = ["pretty"] }
futures = "0.3"
getrandom = "0.3"
graphql-composition = "=0.12.2"
graphql-orm = { path = "crates/graphql-orm", version = "0.60.0", default-features = false }
graphql-orm = { path = "crates/graphql-orm", version = "0.61.0", default-features = false }
graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.16.2" }
graphql-orm-backup = { path = "crates/graphql-orm-backup", version = "0.10.0", default-features = false }
graphql-orm-operation-catalog = { path = "crates/graphql-orm-operation-catalog", version = "0.4.2" }
Expand Down
6 changes: 5 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ workspace release tag for every chosen workspace package and record its full
commit from the release manifest. Neither the runtime nor macros crate is
published to crates.io.

## 0.60.0 to 0.61.0: subscription primary-key selection

Upgrade runtime/macros together. Generated single-key subscriptions gain an optional `key` argument, using the entity primary key scalar and configured argument casing. Existing operations remain valid. For example, `recordChanged(key: "…", filter: {actions: [UPDATED]})` watches one row. Key selection narrows notifications only: it never grants row, field, scope, assurance, deleted-preimage or propagated-source authority. Refresh schema/catalogue snapshots. Composite-key subscriptions remain unsupported by this change. No data or schema migration is required.

## 0.59.0 to 0.60.0: generated query-shape selection

Upgrade runtime/macros together. `#[graphql_entity(query_shapes = "list,keyset,scan")]` exposes only those generated query shapes. Supported selectors are `single`, `list`, `keyset`, `scan`, `search` and `aggregate`. The list must be nonempty and contain no duplicates; selected optional capabilities must already be configured. An omitted selector retains all existing generated queries. Disabled shapes are absent from both SDL and operation/semantic metadata; repository methods and entity relations remain available. Mutation and subscription exposure stay independent. This permits a recorded legacy root to coexist while its generated same-name query is deliberately omitted; it grants no authorization and does not make the legacy root an accepted permanent exception. No data or schema migration is required.
Expand Down Expand Up @@ -52,7 +56,7 @@ Regenerate derived capability manifests and semantic/router descriptors from
the actual finished SDL and semantic catalogue because fingerprints change.
Existing resolver, field, row and provider-egress authorization remains
independent. GraphQL SDL, stored data and schema ownership are unchanged; no
DDL or data migration is needed. Runtime/macros package versions stay 0.60.0.
DDL or data migration is needed. This companion patch does not itself change runtime/macros; use their paired versions from the selected workspace tag.

## 0.56.0 to 0.57.0: exact SQL Server MONEY reads

Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm-macros"
version = "0.60.0"
version = "0.61.0"
edition = "2024"
authors = ["Toby Martin"]
description = "Procedural macros for async-graphql and ORM-backed entities, relations, and CRUD operations."
Expand Down
4 changes: 4 additions & 0 deletions crates/graphql-orm-macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ supersedes: []

# `graphql-orm-macros`

Version 0.61.0 adds an optional typed `key` argument to generated single-primary-key
change subscriptions, preserving current authorization and action filtering. See the
[migration guide](../../MIGRATION.md#0600-to-0610-subscription-primary-key-selection).

Version 0.60.0 adds explicit generated query-shape selection for controlled
coexistence with recorded legacy roots. Defaults are unchanged; see the
[migration guide](../../MIGRATION.md#0590-to-0600-generated-query-shape-selection).
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 27 additions & 5 deletions crates/graphql-orm-macros/src/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1851,6 +1851,9 @@ fn generate_graphql_operations_unfiltered(
let id_arg_name = apply_graphql_case("id", argument_case);
let input_arg_name = apply_graphql_case("input", argument_case);
let filter_arg_name = apply_graphql_case("filter", argument_case);
let key_arg_name = apply_graphql_case("key", argument_case);
let key_arg_description =
"Receive changes only for this exact primary key. Omit to watch every authorized row.";
let where_arg_description = semantic_argument_description(&where_arg_name);
let search_arg_description = semantic_argument_description(&search_arg_name);
let order_by_arg_description = semantic_argument_description(&order_by_arg_name);
Expand Down Expand Up @@ -5061,6 +5064,7 @@ fn generate_graphql_operations_unfiltered(
&self,
ctx: &::graphql_orm::async_graphql::Context<'_>,
#[graphql(name = #filter_arg_name, desc = #filter_arg_description)] _filter: Option<::graphql_orm::graphql::orm::SubscriptionFilterInput>,
#[graphql(name = #key_arg_name, desc = #key_arg_description)] key: Option<#pk_type_ty>,
) -> ::graphql_orm::async_graphql::Result<impl ::graphql_orm::futures::Stream<Item = #changed_event>> {
use ::graphql_orm::futures::StreamExt;
let _auth_subject = ::graphql_orm::graphql::auth::enforce_resolver_auth(ctx, #resolver_auth_mode)?;
Expand Down Expand Up @@ -5091,9 +5095,11 @@ fn generate_graphql_operations_unfiltered(
Ok(::graphql_orm::tokio_stream::wrappers::BroadcastStream::new(rx).filter_map(move |result| {
let db = db.clone();
let actions = actions.clone();
let key = key.clone();
async move {
let witness = result.ok()?;
let event = witness.event.as_ref();
if key.as_ref().is_some_and(|key| key != &event.id) { return None; }
if ::graphql_orm::graphql::loaders::RelationKey::from_sql_values(&witness.keys)
!= ::graphql_orm::graphql::loaders::RelationKey::from_sql_values(&#struct_name::__gom_key_values(&event.id)) { return None; }
if actions.as_ref().is_some_and(|actions| !actions.contains(&event.action)) { return None; }
Expand All @@ -5110,6 +5116,9 @@ fn generate_graphql_operations_unfiltered(
let preimage = if witness.trusted_delete && event.action == ::graphql_orm::graphql::orm::ChangeAction::Deleted { event.entity.as_ref() } else { None };
let row = ::graphql_orm::graphql::subscriptions::authorize_subscription_row::<#struct_name, #backend_marker>(&db, ctx, &witness.keys, preimage).await?
.ok_or_else(|| ::graphql_orm::graphql::errors::OrmPublicError::forbidden().into_graphql_error())?;
if key.is_some() {
::graphql_orm::graphql::subscriptions::authorize_subscription_keys::<#struct_name, #backend_marker>(&db, ctx, &row).await?;
}
::graphql_orm::graphql::subscriptions::authorize_subscription_fields(&db, ctx, &row, #entity_result_field_name).await?;
// Query and field-policy awaits must not outlive current authority.
db.ensure_entity_access(Some(ctx), #entity_name_lit,
Expand Down Expand Up @@ -6265,7 +6274,11 @@ fn generate_graphql_operations_unfiltered(
};

let argument_descriptor = |graphql_name: &str, rust_type: proc_macro2::TokenStream| {
let description = semantic_argument_description(graphql_name);
let description = if graphql_name == key_arg_name {
key_arg_description.to_owned()
} else {
semantic_argument_description(graphql_name)
};
quote! {
::graphql_orm::graphql::orm::GraphqlOperationArgumentDescriptor::generated_with_description(
#graphql_name,
Expand Down Expand Up @@ -6548,10 +6561,13 @@ fn generate_graphql_operations_unfiltered(
quote! { ::graphql_orm::graphql::orm::GraphqlOperationKind::Subscription },
quote! { ::graphql_orm::graphql::orm::GeneratedGraphqlOperationCategory::Subscription },
&subscription_name,
vec![argument_descriptor(
&filter_arg_name,
quote! { Option<::graphql_orm::graphql::orm::SubscriptionFilterInput> },
)],
vec![
argument_descriptor(
&filter_arg_name,
quote! { Option<::graphql_orm::graphql::orm::SubscriptionFilterInput> },
),
argument_descriptor(&key_arg_name, quote! { Option<#pk_type_ty> }),
],
quote! { #changed_event },
&subscription_operation_authorization,
)
Expand Down Expand Up @@ -7826,6 +7842,7 @@ fn generate_graphql_operations_unfiltered(
&self,
ctx: &::graphql_orm::async_graphql::Context<'_>,
#[graphql(name = #filter_arg_name, desc = #filter_arg_description)] _filter: Option<::graphql_orm::graphql::orm::SubscriptionFilterInput>,
#[graphql(name = #key_arg_name, desc = #key_arg_description)] key: Option<#pk_type_ty>,
) -> ::graphql_orm::async_graphql::Result<impl ::graphql_orm::futures::Stream<Item = #changed_event>> {
use ::graphql_orm::futures::StreamExt;
let _auth_subject = ::graphql_orm::graphql::auth::enforce_resolver_auth(ctx, #resolver_auth_mode)?;
Expand Down Expand Up @@ -7857,9 +7874,11 @@ fn generate_graphql_operations_unfiltered(
Ok(::graphql_orm::tokio_stream::wrappers::BroadcastStream::new(rx).filter_map(move |result| {
let db = db.clone();
let actions = actions.clone();
let key = key.clone();
async move {
let witness = result.ok()?;
let event = witness.event.as_ref();
if key.as_ref().is_some_and(|key| key != &event.id) { return None; }
if ::graphql_orm::graphql::loaders::RelationKey::from_sql_values(&witness.keys)
!= ::graphql_orm::graphql::loaders::RelationKey::from_sql_values(&#struct_name::__gom_key_values(&event.id)) { return None; }
if actions.as_ref().is_some_and(|actions| !actions.contains(&event.action)) { return None; }
Expand All @@ -7876,6 +7895,9 @@ fn generate_graphql_operations_unfiltered(
let preimage = if witness.trusted_delete && event.action == ::graphql_orm::graphql::orm::ChangeAction::Deleted { event.entity.as_ref() } else { None };
let row = ::graphql_orm::graphql::subscriptions::authorize_subscription_row::<#struct_name, #backend_marker>(&db, ctx, &witness.keys, preimage).await?
.ok_or_else(|| ::graphql_orm::graphql::errors::OrmPublicError::forbidden().into_graphql_error())?;
if key.is_some() {
::graphql_orm::graphql::subscriptions::authorize_subscription_keys::<#struct_name, #backend_marker>(&db, ctx, &row).await?;
}
::graphql_orm::graphql::subscriptions::authorize_subscription_fields(&db, ctx, &row, #entity_result_field_name).await?;
// Query and field-policy awaits must not outlive current authority.
db.ensure_entity_access(Some(ctx), #entity_name_lit,
Expand Down
6 changes: 6 additions & 0 deletions crates/graphql-orm-router/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ supersedes: []

# Changelog

## 0.6.5 (unreleased)

- Forward spawned federation wire-test router stderr to hosted CI, so an early
child exit exposes its cause. This changes test diagnostics only; production
routing, authorization, SDL and persisted data are unchanged.

## 0.6.4 - 2026-10-22

- Refresh the workspace and affected fixture locks to published
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm-router/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm-router"
version = "0.6.4"
version = "0.6.5"
edition = "2024"
rust-version = "1.90"
description = "Federated GraphQL router for graphql-orm and project-neutral subgraphs"
Expand Down
6 changes: 6 additions & 0 deletions crates/graphql-orm-router/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ supersedes: []

# graphql-orm-router migration guide

## 0.6.4 to 0.6.5: test startup diagnostics

No application, protocol or data migration is required. The package identity
records a test-only diagnostic correction. No executable is distributed by this
source change; binary publication retains its separate approval boundary.

## 0.6.3 to 0.6.4: TLS certificate distrust patches

Refresh the consuming lockfile to `rustls-webpki` 0.103.16 or later in its
Expand Down
9 changes: 8 additions & 1 deletion crates/graphql-orm-router/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ correction. Direct Auth consumers must select the same tag and refresh their
locks to one source; see the
[workspace migration](../../MIGRATION.md#signed-role-catalogue-transport-bound).

Version `0.6.5` retains test-child startup errors in hosted CI diagnostics.
Production routing and authorization are unchanged; this source update does
not distribute or authorize a new router executable.

Version `0.6.4` selects the published TLS certificate-distrust patches in the
workspace lockfile. Rebuild with that lock and refresh downstream locks to retain
`rustls-webpki` 0.103.16 and `webpki-roots` / `webpki-root-certs` 1.0.10.
Expand Down Expand Up @@ -51,7 +55,7 @@ This unpublished package is Git-only:

```toml
[dependencies]
graphql-orm-router = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.6.4" }
graphql-orm-router = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.6.5" }
```

Version 0.6 makes `AuthenticationProvider::current_time` fallible. The agql
Expand Down Expand Up @@ -83,6 +87,9 @@ command alone does not enforce those restrictions.
Hive's separate `graphiql` feature selects static GraphiQL instead, skipping
Laboratory generation and ignoring Laboratory seed configuration.

The in-tree federation wire tests forward spawned router-child stderr to the
test runner, preserving startup diagnostics when a child exits early.

## Minimal programmatic start

For trusted local development, anonymous mode is explicit. Production must
Expand Down
8 changes: 4 additions & 4 deletions crates/graphql-orm-router/src/federation/wire_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2360,7 +2360,7 @@ impl TestRouter {
.env("RUST_TEST_THREADS", "1")
.env("RUST_LOG", "error")
.stdout(Stdio::null())
.stderr(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.expect("router child should start");
Self {
Expand Down Expand Up @@ -2424,7 +2424,7 @@ impl TestRouter {
.env("RUST_TEST_THREADS", "1")
.env("RUST_LOG", "error")
.stdout(Stdio::null())
.stderr(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.expect("static router child should start");
Self {
Expand Down Expand Up @@ -2523,7 +2523,7 @@ impl TestRouter {
.env("RUST_TEST_THREADS", "1")
.env("RUST_LOG", "error")
.stdout(Stdio::null())
.stderr(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.expect("administrative router child should start");
(
Expand Down Expand Up @@ -2561,7 +2561,7 @@ impl TestRouter {
.env("RUST_TEST_THREADS", "1")
.env("RUST_LOG", "error")
.stdout(Stdio::null())
.stderr(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.expect("authenticated router child should start");
Self {
Expand Down
4 changes: 2 additions & 2 deletions crates/graphql-orm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "graphql-orm"
version = "0.60.0"
version = "0.61.0"
edition = "2024"
description = "Runtime support crate for graphql-orm-macros"
license = "MIT"
Expand Down Expand Up @@ -73,7 +73,7 @@ futures = "0.3"
geo = { version = "0.33", optional = true, default-features = false }
geo-types = { version = "0.7", optional = true }
geojson = { version = "1", optional = true, default-features = true }
graphql-orm-macros = { path = "../graphql-orm-macros", version = "0.60.0", default-features = false }
graphql-orm-macros = { path = "../graphql-orm-macros", version = "0.61.0", default-features = false }
graphql-orm-operation-catalog = { workspace = true }
rust_decimal = { workspace = true }
serde = { version = "1", features = ["derive"] }
Expand Down
4 changes: 4 additions & 0 deletions crates/graphql-orm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ supersedes: []

# `graphql-orm`

Version 0.61.0 adds an optional typed `key` argument to generated single-primary-key
change subscriptions, preserving current authorization and action filtering. See the
[migration guide](../../MIGRATION.md#0600-to-0610-subscription-primary-key-selection).

Version 0.60.0 adds explicit generated query-shape selection for controlled
coexistence with recorded legacy roots. Defaults are unchanged; see the
[migration guide](../../MIGRATION.md#0590-to-0600-generated-query-shape-selection).
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading