From fb03a11f8df9971fde1a6288fc4817b8f1f78e89 Mon Sep 17 00:00:00 2001 From: Toby Martin Date: Wed, 26 Aug 2026 14:24:31 +1000 Subject: [PATCH] feat(ai): bound relationship argument projection --- Cargo.lock | 4 +- Cargo.toml | 2 +- .../graphql-orm-ai-tool-profiles/CHANGELOG.md | 16 ++ .../graphql-orm-ai-tool-profiles/Cargo.toml | 2 +- .../graphql-orm-ai-tool-profiles/MIGRATION.md | 16 ++ crates/graphql-orm-ai-tool-profiles/README.md | 11 +- .../src/query_plans.rs | 236 ++++++++++++++++-- crates/graphql-orm-ai/CHANGELOG.md | 20 ++ crates/graphql-orm-ai/Cargo.toml | 2 +- crates/graphql-orm-ai/MIGRATION.md | 13 + crates/graphql-orm-ai/README.md | 2 +- .../docs/implementation-status.md | 2 +- docs/reference/workspace-packages.md | 4 +- 13 files changed, 304 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d25e5a..ebdd3c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai" -version = "0.95.2" +version = "0.95.3" dependencies = [ "agql-auth", "async-graphql", @@ -3136,7 +3136,7 @@ dependencies = [ [[package]] name = "graphql-orm-ai-tool-profiles" -version = "0.10.1" +version = "0.10.2" dependencies = [ "async-graphql", "async-graphql-parser", diff --git a/Cargo.toml b/Cargo.toml index 0e4837f..2bf1c9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ futures = "0.3" getrandom = "0.3" graphql-composition = "=0.12.2" graphql-orm = { path = "crates/graphql-orm", version = "0.27.0", default-features = false } -graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.10.1" } +graphql-orm-ai-tool-profiles = { path = "crates/graphql-orm-ai-tool-profiles", version = "0.10.2" } graphql-orm-backup = { path = "crates/graphql-orm-backup", version = "0.7.1", default-features = false } graphql-orm-operation-catalog = { path = "crates/graphql-orm-operation-catalog", version = "0.3.0" } graphql-orm-router-protocol = { path = "crates/graphql-orm-router-protocol", version = "0.2.1" } diff --git a/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md b/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md index 70dcac8..1c69345 100644 --- a/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md +++ b/crates/graphql-orm-ai-tool-profiles/CHANGELOG.md @@ -10,6 +10,22 @@ supersedes: [] # Changelog +## [0.10.2] - 2026-08-26 + +### Added + +- `AiGraphqlQueryCapabilityCatalog::compile_with_options` accepts a generic, + bounded relationship-argument projection depth. A host can keep every deep + scalar path and collection bound available while omitting typed relationship + argument objects beyond the selected depth. + +### Security + +- The option cannot add a path, field, argument, or bound. Omitted + relationship-argument paths are closed by `additionalProperties: false`, + while the canonical compiler, disclosure policy, result budgets, target + binding, and resolver authorization remain authoritative. + ## [0.10.1] - 2026-08-26 ### Fixed diff --git a/crates/graphql-orm-ai-tool-profiles/Cargo.toml b/crates/graphql-orm-ai-tool-profiles/Cargo.toml index dd29b53..7f05003 100644 --- a/crates/graphql-orm-ai-tool-profiles/Cargo.toml +++ b/crates/graphql-orm-ai-tool-profiles/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-orm-ai-tool-profiles" -version = "0.10.1" +version = "0.10.2" edition = "2024" authors = ["Toby Martin "] description = "Backend-neutral GraphQL AI tool profile compiler and manifest contracts" diff --git a/crates/graphql-orm-ai-tool-profiles/MIGRATION.md b/crates/graphql-orm-ai-tool-profiles/MIGRATION.md index f8791fa..10c7ef6 100644 --- a/crates/graphql-orm-ai-tool-profiles/MIGRATION.md +++ b/crates/graphql-orm-ai-tool-profiles/MIGRATION.md @@ -10,6 +10,22 @@ supersedes: [] # Migration Guide +## 0.10.1 to 0.10.2: bounded relationship-argument projection + +Adopt `graphql-orm-ai-tool-profiles` 0.10.2 from one reviewed full monorepo +revision. Existing callers of `AiGraphqlQueryCapabilityCatalog::compile` keep +the complete relationship-argument surface. Hosts with a relationship-rich +schema may opt into `compile_with_options` and select the maximum relationship +depth whose typed arguments should enter provider schemas. A depth of zero +keeps deep scalar selection and collection bounds but requires +`relationshipArguments` to remain empty. + +The option changes capability and provider-definition fingerprints. Rebuild +retained bindings after changing it. It grants no authority and does not +change GraphQL SDL, the canonical plan compiler, disclosure checks, result +budgets, target policy, or resolver authorization. There is no database, data, +protected-content, backup, restore, or persistent AI schema-module migration. + ## 0.10.0 to 0.10.1: provider-sized selection allow-lists Adopt `graphql-orm-ai-tool-profiles` 0.10.1 from one reviewed full monorepo diff --git a/crates/graphql-orm-ai-tool-profiles/README.md b/crates/graphql-orm-ai-tool-profiles/README.md index 0bf6ddc..0ea6f7b 100644 --- a/crates/graphql-orm-ai-tool-profiles/README.md +++ b/crates/graphql-orm-ai-tool-profiles/README.md @@ -24,7 +24,7 @@ are separate runtime decisions and must remain default-deny. ```toml [dependencies] -graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.10.1" } +graphql-orm-ai-tool-profiles = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.10.2" } serde_json = "1" ``` @@ -116,6 +116,15 @@ Handwritten recursive inputs fail readiness rather than being approximated. Relationship arguments, including the single nullable to-many `OrderByInput`, continue to match the finished SDL exactly. +For a relationship-rich schema whose complete typed argument map is larger +than the provider contract, use `compile_with_options` with +`AiGraphqlQueryCapabilityOptions::new(depth)`. The depth limits only +provider-authored relationship arguments: all exact scalar paths and +collection bounds remain available. A zero depth makes +`relationshipArguments` a closed empty object, so the provider can still make +one bounded deep read or use typed root filters without inventing nested +filters. The ordinary `compile` entrypoint keeps the complete argument surface. + ## Canonical capability index After compiling the generated catalogues, combine them with reviewed static diff --git a/crates/graphql-orm-ai-tool-profiles/src/query_plans.rs b/crates/graphql-orm-ai-tool-profiles/src/query_plans.rs index 873aa42..fcf5ae9 100644 --- a/crates/graphql-orm-ai-tool-profiles/src/query_plans.rs +++ b/crates/graphql-orm-ai-tool-profiles/src/query_plans.rs @@ -65,6 +65,71 @@ pub struct AiGraphqlQueryCapabilityLimits { pub maximum_schema_bytes: u32, } +/// Provider-schema projection options for automatic query capabilities. +/// +/// The canonical compiler and resolver validation remain unchanged. This +/// policy only controls which typed relationship-argument contracts are +/// offered to a provider. A depth of zero keeps every relationship selectable +/// and bounded while requiring its argument object to stay empty. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde( + rename_all = "camelCase", + deny_unknown_fields, + try_from = "AiGraphqlQueryCapabilityOptionsWire" +)] +pub struct AiGraphqlQueryCapabilityOptions { + maximum_relationship_argument_depth: u8, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +struct AiGraphqlQueryCapabilityOptionsWire { + maximum_relationship_argument_depth: u8, +} + +impl TryFrom for AiGraphqlQueryCapabilityOptions { + type Error = AiError; + + fn try_from(value: AiGraphqlQueryCapabilityOptionsWire) -> Result { + Self::new(value.maximum_relationship_argument_depth) + } +} + +impl AiGraphqlQueryCapabilityOptions { + /// Creates a provider projection with a maximum relationship-argument + /// depth. Zero disables provider-authored relationship arguments without + /// disabling relationship selection or collection bounds. + /// + /// # Errors + /// + /// Returns an error when the requested depth exceeds the compiler's + /// supported relationship depth. + pub fn new(maximum_relationship_argument_depth: u8) -> Result { + if maximum_relationship_argument_depth > 8 { + return Err(configuration_error( + "relationship argument depth is invalid", + )); + } + Ok(Self { + maximum_relationship_argument_depth, + }) + } + + /// Returns the maximum relationship depth whose typed arguments are + /// offered to the provider. + pub const fn maximum_relationship_argument_depth(self) -> u8 { + self.maximum_relationship_argument_depth + } +} + +impl Default for AiGraphqlQueryCapabilityOptions { + fn default() -> Self { + Self { + maximum_relationship_argument_depth: 8, + } + } +} + impl AiGraphqlQueryCapabilityLimits { /// Creates validated deployment ceilings. /// @@ -1009,6 +1074,36 @@ impl AiGraphqlQueryCapabilityCatalog { finished_sdl: &str, semantic_catalog: &GraphqlSemanticCatalog, limits: AiGraphqlQueryCapabilityLimits, + ) -> Result { + Self::compile_with_options( + subgraph_id, + target_id, + finished_sdl, + semantic_catalog, + limits, + AiGraphqlQueryCapabilityOptions::default(), + ) + } + + /// Compiles every exposed public query root with an explicit provider + /// projection policy. + /// + /// Relationship selection, scalar allow-lists, collection bounds, the + /// canonical typed compiler, and resolver-side validation are unaffected + /// by the relationship-argument depth. Paths beyond the selected depth + /// simply cannot carry provider-authored arguments. + /// + /// # Errors + /// + /// Returns the same fail-closed catalogue errors as [`Self::compile`], or + /// an error when the provider projection policy is invalid. + pub fn compile_with_options( + subgraph_id: &str, + target_id: GraphqlExecutionTargetId, + finished_sdl: &str, + semantic_catalog: &GraphqlSemanticCatalog, + limits: AiGraphqlQueryCapabilityLimits, + options: AiGraphqlQueryCapabilityOptions, ) -> Result { validate_public_token(subgraph_id, "subgraph ID")?; semantic_catalog @@ -1056,6 +1151,7 @@ impl AiGraphqlQueryCapabilityCatalog { semantic_catalog, operation, limits, + options, GraphqlOperationKind::Query, )?; if capabilities @@ -1349,6 +1445,7 @@ impl AiGraphqlMutationCapabilityCatalog { semantic_catalog, operation, limits, + AiGraphqlQueryCapabilityOptions::default(), GraphqlOperationKind::Mutation, )?; let capability = AiGraphqlMutationCapability { @@ -1770,6 +1867,7 @@ impl AiGraphqlSubscriptionCapabilityCatalog { semantic_catalog, operation, limits.query, + AiGraphqlQueryCapabilityOptions::default(), GraphqlOperationKind::Subscription, )?; base.id = AiToolId::parse(stable_subscription_capability_id( @@ -2365,14 +2463,21 @@ fn compile_capability( semantic_catalog: &GraphqlSemanticCatalog, operation: &GraphqlSemanticOperationDescriptor, limits: AiGraphqlQueryCapabilityLimits, + options: AiGraphqlQueryCapabilityOptions, expected_kind: GraphqlOperationKind, ) -> Result { let root_field = schema.operation_root_field(expected_kind, &operation.field_name)?; validate_operation_against_schema(operation, root_field, expected_kind)?; let output = resolve_query_output(schema, semantic_catalog, operation, &root_field.ty, limits)?; let argument_schema = build_plan_schema(schema, semantic_catalog, operation, &output, limits)?; - let compact_argument_schema = - build_compact_plan_schema(schema, semantic_catalog, operation, &output, limits)?; + let compact_argument_schema = build_compact_plan_schema( + schema, + semantic_catalog, + operation, + &output, + limits, + options, + )?; if serde_json::to_vec(&compact_argument_schema) .map(|encoded| encoded.len() > MAXIMUM_PROVIDER_SCHEMA_BYTES) .unwrap_or(true) @@ -2762,7 +2867,7 @@ fn build_plan_schema( #[derive(Clone)] struct CompactRelationshipSchema { path: String, - arguments: Value, + arguments: Option, maximum_items: Option, } @@ -2772,6 +2877,7 @@ fn build_compact_plan_schema( operation: &GraphqlSemanticOperationDescriptor, output: &QueryOutput, limits: AiGraphqlQueryCapabilityLimits, + options: AiGraphqlQueryCapabilityOptions, ) -> Result { let root = schema.operation_root_field(operation.kind, &operation.field_name)?; let inject_root_bound = root_bound_argument(schema, root, output)?; @@ -2806,6 +2912,7 @@ fn build_compact_plan_schema( 0, &mut vec![entity.clone()], "", + options.maximum_relationship_argument_depth(), &mut selection_paths, &mut relationship_schemas, )?; @@ -2818,7 +2925,9 @@ fn build_compact_plan_schema( let mut relationship_arguments = Map::new(); let mut relationship_limits = Map::new(); for relationship in relationship_schemas { - relationship_arguments.insert(relationship.path.clone(), relationship.arguments); + if let Some(arguments) = relationship.arguments { + relationship_arguments.insert(relationship.path.clone(), arguments); + } if let Some(maximum) = relationship.maximum_items { relationship_limits.insert( relationship.path, @@ -2918,6 +3027,7 @@ fn collect_compact_selection_paths( depth: u8, ancestry: &mut Vec, prefix: &str, + maximum_relationship_argument_depth: u8, selections: &mut Vec, relationships: &mut Vec, ) -> Result<(), AiError> { @@ -2961,17 +3071,22 @@ fn collect_compact_selection_paths( ) }) .collect::>(); - let mut argument_schema = input_object_schema( - schema, - catalog, - &actual.arguments, - &descriptions, - limits, - 0, - &mut Vec::new(), - collection.is_some_and(GraphqlSemanticCollectionBound::model_may_select_maximum), - )?; - insert_description(&mut argument_schema, Some(&field.description))?; + let argument_schema = if depth < maximum_relationship_argument_depth { + let mut argument_schema = input_object_schema( + schema, + catalog, + &actual.arguments, + &descriptions, + limits, + 0, + &mut Vec::new(), + collection.is_some_and(GraphqlSemanticCollectionBound::model_may_select_maximum), + )?; + insert_description(&mut argument_schema, Some(&field.description))?; + Some(argument_schema) + } else { + None + }; let path = join_selection_path(prefix, &field.field_name); let maximum_items = match collection { Some(GraphqlSemanticCollectionBound::Pageable { .. }) => { @@ -2993,6 +3108,7 @@ fn collect_compact_selection_paths( depth + 1, ancestry, &path, + maximum_relationship_argument_depth, selections, relationships, )?; @@ -3107,7 +3223,14 @@ fn compact_subscription_plan_schema( maximum_duration_seconds: u32, maximum_events: u32, ) -> Result { - let mut plan = build_compact_plan_schema(schema, catalog, operation, output, limits)?; + let mut plan = build_compact_plan_schema( + schema, + catalog, + operation, + output, + limits, + AiGraphqlQueryCapabilityOptions::default(), + )?; let object = plan .as_object_mut() .ok_or_else(|| configuration_error("compact subscription schema is not an object"))?; @@ -5006,6 +5129,87 @@ mod tests { assert_eq!(compiled.variables()["v1"]["limit"], json!(2)); } + #[test] + fn relationship_argument_projection_can_be_disabled_without_losing_deep_reads() { + let catalog = AiGraphqlQueryCapabilityCatalog::compile_with_options( + "inventory", + GraphqlExecutionTargetId::parse("inventory.graphql").expect("target"), + &query_sdl(), + &semantic_catalog(), + AiGraphqlQueryCapabilityLimits::default(), + AiGraphqlQueryCapabilityOptions::new(0).expect("argument policy"), + ) + .expect("bounded capability"); + let capability = catalog.capabilities().next().expect("query capability"); + assert_eq!( + capability + .compact_argument_schema() + .pointer("/properties/relationshipArguments/properties"), + Some(&json!({})) + ); + assert!( + capability + .compact_argument_schema() + .pointer("/properties/relationshipMaximumItems/properties/children") + .is_some() + ); + assert!( + capability + .compact_argument_schema() + .pointer("/properties/selections/items/enum") + .and_then(Value::as_array) + .is_some_and(|paths| paths.contains(&json!("children.label"))) + ); + + let plan = json!({ + "arguments": {"id": "parent-1"}, + "selections": ["id", "children.id", "children.label"], + "relationshipArguments": {}, + "relationshipMaximumItems": {"children": 2} + }); + assert!( + jsonschema::validator_for(capability.compact_argument_schema()) + .expect("provider schema") + .is_valid(&plan) + ); + let compiled = capability + .compile_compact(plan) + .expect("deep query without relationship arguments"); + assert!( + compiled + .descriptor() + .document + .contains("children(page: $v1) { edges { node { id label } } }") + ); + assert_eq!(compiled.variables()["v1"]["limit"], json!(2)); + } + + #[test] + fn relationship_argument_projection_rejects_excessive_depth() { + let decoded = serde_json::from_value::(json!({ + "maximumRelationshipArgumentDepth": 0 + })) + .expect("valid serialized projection policy"); + assert_eq!(decoded.maximum_relationship_argument_depth(), 0); + assert!(matches!( + AiGraphqlQueryCapabilityOptions::new(9), + Err(AiError::InvalidConfiguration(_)) + )); + assert!( + serde_json::from_value::(json!({ + "maximumRelationshipArgumentDepth": 9 + })) + .is_err() + ); + assert!( + serde_json::from_value::(json!({ + "maximumRelationshipArgumentDepth": 0, + "unexpected": true + })) + .is_err() + ); + } + #[test] fn compact_selection_allowlist_stays_within_provider_limits_for_wide_graphs() { const LEVELS: usize = 4; diff --git a/crates/graphql-orm-ai/CHANGELOG.md b/crates/graphql-orm-ai/CHANGELOG.md index 83eb894..8b0fdb8 100644 --- a/crates/graphql-orm-ai/CHANGELOG.md +++ b/crates/graphql-orm-ai/CHANGELOG.md @@ -18,6 +18,26 @@ checkpoint facts. For the current workspace baseline and active gates, use the [implementation status](docs/implementation-status.md) and the central [AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md). +## [0.95.3] - 2026-08-26 + +Persistent schema module: **0.63.0** (unchanged from 0.95.2). + +### Added + +- Hosts may use the dependency-owned bounded relationship-argument projection + policy when compiling generated query catalogues. Deep selections and + collection bounds remain available when a deployment omits provider-authored + relationship filters from wide schemas. + +### Security + +- Default compilation is unchanged. A restricted projection only removes + provider input; the canonical compiler, current target and principal policy, + disclosure enforcement, and resolver authorization remain mandatory. + +There is no database, data, table, column, index, constraint, backfill, +protected-payload, GraphQL SDL, backup, or restore migration. + ## [0.95.2] - 2026-08-26 Persistent schema module: **0.63.0** (unchanged from 0.95.1). diff --git a/crates/graphql-orm-ai/Cargo.toml b/crates/graphql-orm-ai/Cargo.toml index 67e4a88..e3cf94e 100644 --- a/crates/graphql-orm-ai/Cargo.toml +++ b/crates/graphql-orm-ai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "graphql-orm-ai" -version = "0.95.2" +version = "0.95.3" edition = "2024" authors = ["Toby Martin "] description = "Project-agnostic AI agent runtime for graphql-orm applications" diff --git a/crates/graphql-orm-ai/MIGRATION.md b/crates/graphql-orm-ai/MIGRATION.md index 712167d..6ce4a1b 100644 --- a/crates/graphql-orm-ai/MIGRATION.md +++ b/crates/graphql-orm-ai/MIGRATION.md @@ -19,6 +19,19 @@ they describe. For the current workspace baseline and active delivery gates, use [implementation status](docs/implementation-status.md) and the central [AI production-readiness plan](../../docs/plans/active/ai-production-readiness/README.md). +## 0.95.2 to 0.95.3: bounded relationship-argument projection + +Adopt `graphql-orm-ai` 0.95.3 and `graphql-orm-ai-tool-profiles` 0.10.2 from +one reviewed full monorepo revision. Existing hosts need no code change. A host +that opts into a restricted relationship-argument depth must rebuild exact +capability and provider-session bindings because the provider schema and its +fingerprint change. + +The compact execution payload remains closed: omitted relationship paths +cannot carry arguments, while deep scalar paths and collection bounds remain +available. The AI schema module remains **0.63.0**; there is no database, data, +GraphQL SDL, protected-payload, backup, or restore migration. + ## 0.95.1 to 0.95.2: compact closed selection allowlists Adopt `graphql-orm-ai` 0.95.2 and `graphql-orm-ai-tool-profiles` 0.10.1 from diff --git a/crates/graphql-orm-ai/README.md b/crates/graphql-orm-ai/README.md index 3a5aca9..1834492 100644 --- a/crates/graphql-orm-ai/README.md +++ b/crates/graphql-orm-ai/README.md @@ -28,7 +28,7 @@ for AI, ORM, storage, backup, and tool-profile packages: ```toml [dependencies] -graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.95.2", default-features = false, features = ["sqlite"] } +graphql-orm-ai = { git = "https://github.com/Dastari/graphql-orm.git", rev = "", version = "0.95.3", default-features = false, features = ["sqlite"] } ``` Exactly one persistence backend is required: `sqlite` (default), `postgres`, diff --git a/crates/graphql-orm-ai/docs/implementation-status.md b/crates/graphql-orm-ai/docs/implementation-status.md index 09bd3eb..bdc7b39 100644 --- a/crates/graphql-orm-ai/docs/implementation-status.md +++ b/crates/graphql-orm-ai/docs/implementation-status.md @@ -10,7 +10,7 @@ supersedes: [] # Implementation Status -`graphql-orm-ai` is at crate version `0.95.2` with AI schema module +`graphql-orm-ai` is at crate version `0.95.3` with AI schema module `0.63.0`. It uses workspace `graphql-orm` `0.26.0`, backend-neutral `graphql-orm-ai-tool-profiles` `0.10.0`, and external `agql-auth` `0.18.0` at `527d15d28e3c295a6f6b5e6d74559a7aecdc1322`. diff --git a/docs/reference/workspace-packages.md b/docs/reference/workspace-packages.md index f7bbfdc..4ffaa76 100644 --- a/docs/reference/workspace-packages.md +++ b/docs/reference/workspace-packages.md @@ -19,8 +19,8 @@ changes. | Package | Version | Path | Default features | Direct internal dependencies | | --- | --- | --- | --- | --- | | `graphql-orm` | `0.27.0` | `crates/graphql-orm` | `sqlite` | `graphql-orm-macros`, `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | -| `graphql-orm-ai` | `0.95.2` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | -| `graphql-orm-ai-tool-profiles` | `0.10.1` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | +| `graphql-orm-ai` | `0.95.3` | `crates/graphql-orm-ai` | `sqlite` | `graphql-orm`, `graphql-orm-ai-tool-profiles`, `graphql-orm-storage` | +| `graphql-orm-ai-tool-profiles` | `0.10.2` | `crates/graphql-orm-ai-tool-profiles` | none | `graphql-orm-operation-catalog`, `graphql-orm-router-protocol` (dev-only) | | `graphql-orm-backup` | `0.7.1` | `crates/graphql-orm-backup` | `local` | `graphql-orm` (optional), `graphql-orm-storage` | | `graphql-orm-macros` | `0.27.0` | `crates/graphql-orm-macros` | `sqlite` | none | | `graphql-orm-operation-catalog` | `0.3.0` | `crates/graphql-orm-operation-catalog` | none | `graphql-orm-router-protocol` (optional) |