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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ This file is the authoritative user-facing release chronology. The former
[release-notes ledger](docs/archive/2026/graphql-orm-release-notes.md) is retained
for historical context.

## 0.43.0 (unreleased)

- Add opt-in typed runtime GraphQL mutations delegating to guarded B transactions,
with atomic host journals and independently authorized selected outputs.
- Add an object-safe, default-deny post-commit record-release callback, explicit
deleted preimages and sanitized committed-outcome denial/failure/timeout errors.
- Execute repeated runtime GraphQL mutation response-key groups once, merging
payload projections, preserving distinct-alias order and sharing terminal
outcomes after failure or cancellation. Budgets charge each group once.
- Align ORM/macros at 0.43.0; static APIs/SDL and default features remain unchanged.

## 0.42.0 (unreleased)

- Fix runtime GraphQL alias swaps and scalar/relation output collisions; retain requested filter capabilities before empty-list constant lowering so authorization cannot be bypassed.
Expand Down
4 changes: 2 additions & 2 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 @@ -37,7 +37,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.42.0", default-features = false }
graphql-orm = { path = "crates/graphql-orm", version = "0.43.0", default-features = false }
graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.15.0" }
graphql-orm-backup = { path = "crates/graphql-orm-backup", version = "0.7.2", default-features = false }
graphql-orm-operation-catalog = { path = "crates/graphql-orm-operation-catalog", version = "0.4.0" }
Expand Down
17 changes: 17 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ supersedes: []
`graphql-orm` is distributed from GitHub only. Use a reviewed full 40-character commit in `rev`;
neither the runtime nor macros crate is published to crates.io.

## 0.42.0 to 0.43.0: opt-in runtime GraphQL mutations

No data migration or static API/SDL change. Mutation registration requires an
explicit options builder, checked mutation root, certified environment and host
write authority. Existing read authorities remain source-compatible; releasing
mutation records now requires an explicit post-commit callback override. Default
denial leaves committed writes/journals intact and reports a committed outcome.
Repeated mutation fields with the same validated response key now execute once
with a merged projection, including fragment repeats. Distinct aliases retain
separate transactions in document order. Repeated occurrences reuse terminal
payloads/errors and never restart canceled work. A host extension continuing
execution after resolver cancellation receives nonretryable committed or unknown
outcome errors according to commit confirmation; whole-request cancellation can
still prevent delivery.
See [runtime GraphQL](docs/reference/graphql-orm/runtime-graphql.md) for callback,
null/default/key behavior, cancellation uncertainty and compiled examples.

## 0.41.0 to 0.42.0: opt-in runtime GraphQL reads

Enable `runtime-graphql` alongside an explicit SQLite or PostgreSQL feature. No
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.42.0"
version = "0.43.0"
edition = "2024"
authors = ["Toby Martin"]
description = "Procedural macros for async-graphql and ORM-backed entities, relations, and CRUD operations."
Expand Down
6 changes: 3 additions & 3 deletions crates/graphql-orm-macros/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ macro/runtime versions aligned:

```toml
[dependencies]
graphql-orm = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.42.0", default-features = false, features = ["sqlite"] }
graphql-orm = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.43.0", default-features = false, features = ["sqlite"] }
```

Direct use is supported for tooling that needs the macro package:

```toml
graphql-orm-macros = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.42.0", default-features = false, features = ["sqlite"] }
graphql-orm-macros = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.43.0", default-features = false, features = ["sqlite"] }
```

The direct dependency still requires a compatible `graphql-orm` runtime in the
Expand Down Expand Up @@ -183,6 +183,6 @@ See [host timestamp inputs, migration compatibility and cancellation limits](../

Complete SQLite/PostgreSQL text-group pages and SQL-visible ordinary aggregates: see [typed aggregates](../../docs/reference/graphql-orm/typed-aggregates.md).

Version 0.42.0 aligns with the schema-bound transactional runtime mutation addition. Released host-managed
Version 0.43.0 aligns with the schema-bound transactional runtime mutation addition. Released host-managed
timestamps, framework-neutral repository aggregate helpers and static GraphQL SDL
remain unchanged.

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

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.42.0"
version = "0.43.0"
edition = "2024"
description = "Runtime support crate for graphql-orm-macros"
license = "MIT"
Expand Down Expand Up @@ -71,7 +71,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.42.0", default-features = false }
graphql-orm-macros = { path = "../graphql-orm-macros", version = "0.43.0", default-features = false }
graphql-orm-operation-catalog = { workspace = true }
rust_decimal = { workspace = true }
serde = { version = "1", features = ["derive"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/graphql-orm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ backend:

```toml
[dependencies]
graphql-orm = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.42.0", default-features = false, features = ["sqlite"] }
graphql-orm = { git = "https://github.com/Dastari/graphql-orm.git", rev = "<reviewed-full-40-character-commit-sha>", version = "0.43.0", default-features = false, features = ["sqlite"] }
```

This unpublished package has no docs.rs release. Use this Git README and the
Expand Down
138 changes: 133 additions & 5 deletions crates/graphql-orm/examples/runtime_graphql_reads.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,98 @@ mod example {
#[graphql_orm(auto_generated = false)]
id: String,
exact: i64,
#[filterable]
label: String,
}
#[derive(RepositoryEntity, Clone, serde::Serialize, serde::Deserialize)]
#[repository_entity(table = "example_journal", plural = "ExampleJournal")]
struct ExampleJournal {
#[primary_key]
#[graphql_orm(auto_generated = false)]
id: String,
action: String,
}
struct JournalHook;
impl RuntimeMutationHook<Backend> for JournalHook {
fn before_commit<'a>(
&'a self,
tx: &'a mut MutationContext<'_, Backend>,
effect: &'a RuntimeMutationEffect,
) -> BoxFuture<'a, Result<(), RuntimeMutationError>> {
Box::pin(async move {
tx.insert::<ExampleJournal>(CreateExampleJournalInput {
id: graphql_orm::uuid::Uuid::new_v4().to_string(),
action: format!("{:?}", effect.action()),
})
.await
.map_err(|_| RuntimeMutationError::new(RuntimeMutationErrorCode::HookFailed))?;
Ok(())
})
}
}
struct PublicWrite(Arc<ValidatedRuntimeSchema>);
impl RuntimeWriteAuthority<Backend> for PublicWrite {
fn authorize_intent<'a>(
&'a self,
check: RuntimeWriteIntent<'a>,
_: &'a mut MutationContext<'_, Backend>,
) -> BoxFuture<'a, Result<RuntimeWriteGrant, RuntimeMutationError>> {
Box::pin(async move {
// Explicitly public disposable fixture; production hosts pin policy/revision here.
let metadata = self
.0
.schema()
.collections
.iter()
.find(|c| &c.id == check.collection().id())
.ok_or_else(|| RuntimeMutationError::new(RuntimeMutationErrorCode::Denied))?;
let fields = metadata
.fields
.iter()
.map(|f| self.0.resolve_field(check.collection(), &f.id))
.collect::<Result<Vec<_>, _>>()?;
RuntimeWriteGrant::new(
self.0.resolve_projection(check.collection(), &fields)?,
None,
)
})
}
fn authorize_preimage<'a>(
&'a self,
_: RuntimePreimageCheck<'a>,
_: &'a mut MutationContext<'_, Backend>,
) -> BoxFuture<'a, Result<(), RuntimeMutationError>> {
Box::pin(async { Ok(()) })
}
fn authorize_result<'a>(
&'a self,
check: RuntimeResultCheck<'a>,
_: &'a mut MutationContext<'_, Backend>,
) -> BoxFuture<'a, Result<RuntimeReturnGrant, RuntimeMutationError>> {
Box::pin(async move { Ok(RuntimeReturnGrant::new(check.intent.returning().cloned())) })
}
}
struct PublicRead;
impl RuntimeReadAuthority for PublicRead {
fn authorize_mutation_result<'a>(
&'a self,
check: RuntimeMutationOutputCheck<'a>,
) -> BoxFuture<'a, Result<(), RuntimeGraphqlError>> {
Box::pin(async move {
// Explicit public-data release, including immutable deleted preimages.
// Policy-required values must be loaded; no hidden field fallback.
for field in check.projection.fields() {
if matches!(
check.record.state(field),
Ok(RuntimeFieldState::Unloaded) | Err(_)
) {
return Err(RuntimeGraphqlError::denied());
}
}
Ok(())
})
}

fn authorize<'a>(
&'a self,
_: RuntimeReadCheck<'a>,
Expand Down Expand Up @@ -108,8 +196,10 @@ mod example {
// Typed repository setup is independent of GraphQL registration.
let schema =
Arc::new(RuntimeSchema::from_static_entities(&[PublicNote::metadata()])?.validate()?);
let target = schema.physical_schema::<Backend>(Default::default())?;
let ownership = ManagedTableSet::new(["public_notes".into()])?;
let target = schema
.physical_schema::<Backend>(Default::default())?
.with_static_entities(&[ExampleJournal::metadata()])?;
let ownership = ManagedTableSet::new(["public_notes".into(), "example_journal".into()])?;
let plan = database
.schema()
.plan_owned_migration(
Expand All @@ -133,11 +223,21 @@ mod example {
},
)
.await?;
let module = RuntimeGraphqlModule::compile(schema.clone(), Default::default())?;
let environment = Arc::new(
database
.schema()
.runtime_mutation_environment(schema.clone(), &target, &ownership)
.await?,
);
let module = RuntimeGraphqlModule::compile(
schema.clone(),
RuntimeGraphqlOptions::default().with_mutations(Default::default()),
)?;
let cipher = HostCipher(ChaCha20Poly1305::new(&ChaCha20Poly1305::generate_key(
&mut OsRng,
)));
let api = RuntimeGraphqlComposer::new(database, "Query", Default::default())?
let api = RuntimeGraphqlComposer::new(database.clone(), "Query", Default::default())?
.mutation_root("Mutation")?
.cursor_protection(
Arc::new(cipher),
RuntimeCursorAudience::new("upstream-example")?,
Expand All @@ -161,7 +261,35 @@ mod example {
"9007199254740993"
);
assert_eq!(data["publicNotes"]["totalCount"], "1");
println!("authorized runtime GraphQL read/count and protected cursor passed");
for operation in [
r#"createPublicNote(input:{id:"note-two",label:"draft",exact:"9007199254740993"},expectedSchema:$schema)"#,
r#"updatePublicNote(key:{id:"note-two"},input:{label:"published"},expected:{label:{eq:"draft"}},expectedSchema:$schema)"#,
r#"deletePublicNote(key:{id:"note-two"},expectedSchema:$schema)"#,
] {
let request = Request::new(format!(
"mutation($schema:String!){{{operation}{{record{{label exact}}}}}}"
))
.variables(graphql_orm::async_graphql::Variables::from_json(
graphql_orm::serde_json::json!({"schema":schema.fingerprint().to_string()}),
))
.data(
RuntimeGraphqlRequest::<Backend>::new(
schema.fingerprint(),
Arc::new(PublicRead),
None,
Default::default(),
)
.with_mutation_environment(environment.clone())
.with_write_authority(Arc::new(PublicWrite(schema.clone())))
.with_mutation_hook(Arc::new(JournalHook)),
);
let response = api.execute(request).await;
assert!(response.errors.is_empty(), "{:?}", response.errors);
}
assert_eq!(ExampleJournal::count_all(&database).await?, 3);
println!(
"authorized runtime GraphQL reads, confidential cursors and committed typed mutations/journals passed"
);
Ok(())
}
}
Expand Down
19 changes: 19 additions & 0 deletions crates/graphql-orm/src/graphql/orm/runtime_mutation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,25 @@ pub struct RuntimeWriteGrant {
predicate: Option<RuntimePredicate>,
}
impl RuntimeWriteGrant {
#[cfg(feature = "runtime-graphql")]
pub(crate) fn graphql_restrict(
self,
schema: &ValidatedRuntimeSchema,
collection: &RuntimeCollectionHandle,
visibility: Option<RuntimePredicate>,
limits: RuntimeQueryLimits,
) -> Result<Self, RuntimeMutationError> {
let predicate = match (self.predicate, visibility) {
(Some(a), Some(b)) => Some(
schema
.runtime_and(collection, vec![a, b], limits)
.map_err(|e| err(RuntimeMutationErrorCode::InvalidInput).with_source(e))?,
),
(a, b) => a.or(b),
};
Self::new(self.projection, predicate)
}

/// Reject cross-schema/collection predicates; this does not narrow input approval.
pub fn new(
projection: RuntimeProjection,
Expand Down
Loading