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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ 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.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.

- Runtime GraphQL composition explicitly rejects unsupported custom-directive factories before publication; built-in directives, bounded host extensions and checked subscription roots remain supported.

- Add opt-in runtime GraphQL reads, checked dynamic composition, whole-operation
host authority preflight and bounded runtime relation batches.
- Add lossless JSON-text/full-i64 scalars and host-managed authenticated encrypted
cursor framing with trusted schema/order/authorization/parent bindings.
- Align macros at 0.42.0; static derives/SDL and default feature behavior are unchanged.
- C remains an unpublished dependent review checkpoint; custom-directive forwarding
requires the documented contract decision. D is not included.

## 0.41.0 - unreleased

Companion macros crate: `graphql-orm-macros` **0.41.0** (alignment only).
Expand Down
6 changes: 4 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.41.0", default-features = false }
graphql-orm = { path = "crates/graphql-orm", version = "0.42.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
9 changes: 9 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ 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.41.0 to 0.42.0: opt-in runtime GraphQL reads

Enable `runtime-graphql` alongside an explicit SQLite or PostgreSQL feature. No
stored-data migration or static SDL change is required. Hosts supply request
read authority and AEAD cursor protection before installing a protected module.
The dynamic scalar wire forms are new and do not replace static scalar mappings.
See [runtime GraphQL](docs/reference/graphql-orm/runtime-graphql.md) for composition,
limits, cursor rotation, SDK mappings and pending release/contract gates.

## 0.40.0 to 0.41.0: transactional runtime mutations

Adopt aligned ORM/macros 0.41.0. This is an additive, unpublished successor to A;
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.41.0"
version = "0.42.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.41.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.42.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.41.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.42.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.41.0 aligns with the schema-bound transactional runtime mutation addition. Released host-managed
Version 0.42.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.

7 changes: 5 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.41.0"
version = "0.42.0"
edition = "2024"
description = "Runtime support crate for graphql-orm-macros"
license = "MIT"
Expand Down Expand Up @@ -39,6 +39,7 @@ mssql = [
"tokio/net",
"tokio/rt",
]
runtime-graphql = ["async-graphql/dynamic-schema", "dep:base64", "dep:async-trait"]
change-journal = []
auth-agql = ["dep:agql-auth"]
router-protocol = ["graphql-orm-operation-catalog/router-protocol"]
Expand All @@ -59,6 +60,8 @@ field-case-lower = ["graphql-orm-macros/field-case-lower"]
field-case-upper = ["graphql-orm-macros/field-case-upper"]

[dependencies]
base64 = { version = "0.22", optional = true }
async-trait = { version = "0.1", optional = true }
chacha20poly1305 = "0.10"
quick-xml = { version = "0.41", optional = true }
agql-auth = { workspace = true, optional = true }
Expand All @@ -68,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.41.0", default-features = false }
graphql-orm-macros = { path = "../graphql-orm-macros", version = "0.42.0", default-features = false }
graphql-orm-operation-catalog = { workspace = true }
rust_decimal = { workspace = true }
serde = { version = "1", features = ["derive"] }
Expand Down
4 changes: 3 additions & 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.41.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.42.0", default-features = false, features = ["sqlite"] }
```

This unpublished package has no docs.rs release. Use this Git README and the
Expand Down Expand Up @@ -208,3 +208,5 @@ Static migration APIs remain unchanged.
Schema-bound runtime create/patch/delete with pinned authority, structural CAS,
atomic host journals and cancellation poisoning are documented in
[runtime mutations](../../docs/reference/graphql-orm/runtime-mutations.md).

The opt-in `runtime-graphql` feature provides [authorized dynamic reads and checked composition](../../docs/reference/graphql-orm/runtime-graphql.md), with host-owned authority and confidential cursors. It does not change static SDL or add mutations.
196 changes: 196 additions & 0 deletions crates/graphql-orm/examples/runtime_graphql_reads.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
//! Disposable host composition example. Keys exist only for this process.
//! Run with exactly one backend and `runtime-graphql`; PostgreSQL uses owned Docker.
#[cfg(all(
feature = "runtime-graphql",
any(feature = "sqlite", feature = "postgres")
))]
mod example {
use chacha20poly1305::{
ChaCha20Poly1305, KeyInit,
aead::{Aead, AeadCore, OsRng, Payload},
};
use graphql_orm::{
async_graphql::{
Request,
dynamic::{FieldFuture, FieldValue, TypeRef},
},
futures::future::BoxFuture,
};
use graphql_orm::{
graphql::{orm::*, runtime::*},
prelude::*,
};
use std::sync::Arc;
#[cfg(feature = "postgres")]
type Backend = PostgresBackend;
#[cfg(not(feature = "postgres"))]
type Backend = SqliteBackend;
#[derive(RepositoryEntity, Clone, serde::Serialize, serde::Deserialize)]
#[repository_entity(table = "public_notes", plural = "PublicNotes")]
struct PublicNote {
#[primary_key]
#[sortable]
#[graphql_orm(auto_generated = false)]
id: String,
exact: i64,
label: String,
}
struct PublicRead;
impl RuntimeReadAuthority for PublicRead {
fn authorize<'a>(
&'a self,
_: RuntimeReadCheck<'a>,
) -> BoxFuture<'a, Result<RuntimeReadGrant, RuntimeGraphqlError>> {
// This disposable data set is intentionally public. A production host
// checks projection/filter/order/relation/count capabilities here.
Box::pin(async { Ok(RuntimeReadGrant::new(None)) })
}
}
struct HostCipher(ChaCha20Poly1305);
impl RuntimeCursorProtector for HostCipher {
fn seal<'a>(
&'a self,
context: &'a RuntimeCursorContext,
plaintext: &'a str,
limits: RuntimeCursorProtectionLimits,
) -> BoxFuture<'a, Result<RuntimeSealedCursor, RuntimeCursorProtectionError>> {
Box::pin(async move {
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
let mut bytes = nonce.to_vec();
bytes.extend(
self.0
.encrypt(
&nonce,
Payload {
msg: plaintext.as_bytes(),
aad: context.associated_data(),
},
)
.map_err(|_| RuntimeCursorProtectionError::CursorUnavailable)?,
);
RuntimeSealedCursor::new("example-ephemeral", bytes, limits)
})
}
fn open<'a>(
&'a self,
context: &'a RuntimeCursorContext,
key: &'a str,
bytes: &'a [u8],
limits: RuntimeCursorProtectionLimits,
) -> BoxFuture<'a, Result<String, RuntimeCursorProtectionError>> {
Box::pin(async move {
if key != "example-ephemeral" {
return Err(RuntimeCursorProtectionError::CursorUnavailable);
}
if bytes.len() < 28
|| bytes.len()
> limits
.max_plaintext_bytes
.saturating_add(limits.max_overhead_bytes)
{
return Err(RuntimeCursorProtectionError::InvalidCursor);
}
let value = self
.0
.decrypt(
(&bytes[..12]).into(),
Payload {
msg: &bytes[12..],
aad: context.associated_data(),
},
)
.map_err(|_| RuntimeCursorProtectionError::InvalidCursor)?;
String::from_utf8(value).map_err(|_| RuntimeCursorProtectionError::InvalidCursor)
})
}
}
pub async fn run(database: Database<Backend>) -> Result<(), Box<dyn std::error::Error>> {
// 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 plan = database
.schema()
.plan_owned_migration(
"example",
"disposable notes",
&target,
&ownership,
PlanOptions::strict(),
)
.await?;
database
.schema()
.apply_owned_migration(&plan, ApplyOptions::default())
.await?;
PublicNote::insert(
&database,
CreatePublicNoteInput {
id: "note-one".into(),
exact: 9_007_199_254_740_993,
label: "Exact integer".into(),
},
)
.await?;
let module = RuntimeGraphqlModule::compile(schema.clone(), Default::default())?;
let cipher = HostCipher(ChaCha20Poly1305::new(&ChaCha20Poly1305::generate_key(
&mut OsRng,
)));
let api = RuntimeGraphqlComposer::new(database, "Query", Default::default())?
.cursor_protection(
Arc::new(cipher),
RuntimeCursorAudience::new("upstream-example")?,
Default::default(),
)?
.query_field(RuntimeHostField::new(
"health",
TypeRef::named_nn("Boolean"),
|_| FieldFuture::new(async { Ok(Some(FieldValue::value(true))) }),
))?
.install(module)?
.finish()?;
let response = api.execute(Request::new("{ health publicNotes(first:1) { edges { cursor node { id exact label } } totalCount } }")
.data(RuntimeGraphqlRequest::<Backend>::new(schema.fingerprint(), Arc::new(PublicRead), None, Default::default()).with_cursor_scope("public-example"))).await;
if !response.errors.is_empty() {
return Err("runtime GraphQL example failed".into());
}
let data = response.data.into_json()?;
assert_eq!(
data["publicNotes"]["edges"][0]["node"]["exact"],
"9007199254740993"
);
assert_eq!(data["publicNotes"]["totalCount"], "1");
println!("authorized runtime GraphQL read/count and protected cursor passed");
Ok(())
}
}
#[cfg(all(feature = "runtime-graphql", feature = "postgres"))]
#[path = "../tests/support/owned_postgres.rs"]
mod owned_postgres;
#[cfg(all(
feature = "runtime-graphql",
any(feature = "sqlite", feature = "postgres")
))]
#[tokio::main]
pub async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "postgres")]
{
let mut owned = owned_postgres::OwnedPostgres::start("runtime-gql-example")?;
let result =
example::run(graphql_orm::db::Database::connect_postgres(&owned.url).await?).await;
owned.cleanup()?;
result
}
#[cfg(not(feature = "postgres"))]
{
example::run(graphql_orm::db::Database::connect_sqlite("sqlite::memory:").await?).await
}
}
#[cfg(not(all(
feature = "runtime-graphql",
any(feature = "sqlite", feature = "postgres")
)))]
pub fn main() {
eprintln!("enable runtime-graphql and exactly one SQLite/PostgreSQL backend");
}
3 changes: 3 additions & 0 deletions crates/graphql-orm/src/graphql/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ pub mod loaders;
pub mod orm;
pub mod pagination;
pub mod structural_auth;

#[cfg(feature = "runtime-graphql")]
pub mod runtime;
10 changes: 10 additions & 0 deletions crates/graphql-orm/src/graphql/orm/runtime_query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,16 @@ pub struct RuntimeReadRequest {
limits: RuntimeQueryLimits,
}

#[cfg(feature = "runtime-graphql")]
impl RuntimeReadRequest {
pub(crate) fn validate_graphql_backend(
&self,
backend: DatabaseBackend,
) -> Result<(), RuntimeQueryError> {
render_request(self, backend).map(|_| ())
}
}

impl fmt::Debug for RuntimeReadRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RuntimeReadRequest")
Expand Down
Loading