Schema Auto-Generation¶
general_manager.api.graphql.GraphQL inspects manager interfaces and creates matching Graphene types and mutations. Classes without an Interface are ignored by interface and mutation registration.
Type mapping¶
For each manager class, GeneralManager:
- Registers a Graphene
ObjectTypewith fields derived from interface attribute types. - Creates resolvers that read values from the underlying manager or bucket.
- Adds fields for every
@graph_ql_propertymethod. Union types and optional hints are converted into GraphQL-friendly types. - Registers measurement scalars (
MeasurementScalar) and object wrappers so units stay intact. - Adds a
capabilitiesfield only when the manager exposes GraphQL permission capability declarations.
Single-valued manager relations are exposed as object fields. Relation fields whose Python-side name ends with _list are exposed as paginated list fields with filtering, grouping, sorting, and pagination arguments.
Relation annotation compatibility¶
Before mapping a relation, schema generation resolves its annotation to one registered GeneralManager class. The resolver accepts:
- a concrete manager class such as
User; - a generated or existing Django model class that carries the manager back-reference;
Bucket[User],list[User],tuple[User, ...], orset[User]; and- optional and union forms such as
User | None,Optional[User], and their postponed string equivalents, including"Bucket[User]"and"typing.List[User]".
This lets annotations remain useful with from __future__ import annotations and with manager classes declared in a different order. A postponed annotation must name a manager that GeneralManager registers during startup. An ambiguous union containing more than one manager target is not treated as a manager relation, so use one manager target per generated relation field.
The same resolution controls generated object fields, relation-list fields, relation filters, sort options, mutation inputs, and subscription identifiers. For a task-oriented declaration pattern, see Expose managers via GraphQL; the GraphQL query cookbook shows the resulting schema, and the GraphQL API reference records the compatibility note for 0.64.1 and 0.64.2.
Mutations¶
Create, update, and delete mutations are added automatically when the interface overrides the base method or advertises the matching capability from Interface.get_capabilities(). If a mutation factory returns no class for a supported operation, that operation is skipped and later supported operations are still considered. Each mutation returns:
success: boolean indicating whether the operation completed.errors: list of validation or permission errors when present.- A field with the manager name containing the affected object.
Generated create and update mutations expose writable, non-derived interface attributes and skip raw direct-relation ID aliases when the canonical relation field is already present. Raw many-relation ID-list aliases such as member_id_list are skipped when the canonical member_list relation exists. Create mutations omit manager constructor input fields such as id; update and delete mutations always require id so the resolver can locate the existing manager instance. Update mutations make every generated write field optional, filter out omitted Graphene NOT_PROVIDED values, and forward an explicit history comment as the manager history_comment. With Graphene's default camel-casing, clients send historyComment; Python-side tests and helpers use history_comment. Delete mutations also accept optional history-comment metadata and forward it to delete(). Explicit GraphQL null is forwarded as history_comment=None; when the history comment argument is omitted, the resolver does not send a history_comment keyword at all.
For relation inputs, the GraphQL schema exposes the canonical manager-facing field names and normalizes them before calling the ORM mutation layer. A direct manager relation such as owner: User is exposed as owner in the mutation and is forwarded as owner_id. If metadata also contains owner_id, that raw alias is not exposed separately. A many-valued relation such as member_list: list[User] is represented by the Python argument key member_list, exposed to GraphQL clients as memberList with Graphene's default camel-casing, and forwarded as member_id_list. If metadata also contains member_id_list, that raw alias is not exposed separately.
Custom mutations use the @graph_ql_mutation decorator from general_manager.api.mutation. The decorator analyses the function signature to generate GraphQL input arguments and return types.
Relation input contract¶
Automatic GraphQL mutations accept relation inputs in the GraphQL-facing forms below and normalize them to the ORM mutation contract before persistence:
- Single-valued relations:
<field>or<field>Id - Many-valued relations:
<field>Listor<field>IdList
Internally, GeneralManager treats the canonical mutation payload as:
- Single-valued relations:
<field>_id - Many-valued relations:
<field>_id_list
These public names assume Graphene's default auto_camelcase=True. If your schema disables auto-camelcase, the Python-side argument names remain available as <field>, <field>_id, <field>_list, and <field>_id_list.
This keeps GraphQL mutations compatible with Graphene field naming while preserving a predictable backend contract for ORM-backed interfaces.
Schema-generation expectations¶
Schema generation should remain resilient when interface metadata includes edge-case field types:
- Measurement fields continue to map to
MeasurementScalar/MeasurementType - Large integer ORM fields may opt into
BigIntScalarthroughgraphql_scalar="bigint" - Non-relational field types that do not map cleanly to a specific GraphQL scalar fall back to string-like handling instead of aborting schema construction
The intended behavior is that startup and schema registration remain reviewable and predictable even when a manager exposes less common field metadata.
Relation Filters¶
Generated root query fields use the same public camelCase convention as nested fields. For example, PartSoldType is exposed as partSoldType and partSoldTypeList, not partsoldtype or partsoldtypeList.
Generated list queries expose scalar filters and relation filters. Direct relations such as foreign keys and one-to-one fields use nested filter input:
query {
changeRequestFeasibilityList(filter: {
changeRequest: { title: "Primary" }
}) {
items { id score }
}
}
Collection relations such as reverse foreign keys and many-to-many fields expose any and none:
query {
changeRequestList(filter: {
changeRequestFeasibilityList: {
any: { score_Gte: 7 }
}
}) {
items { id title }
}
}
any keeps rows with at least one related object matching the nested filter. none removes rows that have a related object matching the nested filter.
The maximum relation nesting depth defaults to 1. Configure it with:
GENERAL_MANAGER = {
"GRAPHQL_FILTER_RELATION_DEPTH": 2,
}
Buckets and pagination¶
For bucket-returning fields, the schema registers list fields and page types. PageInfo exposes total_count, current_page, total_pages, and optional page_size so clients can implement cursor-less pagination quickly.
Extending the schema¶
- Override
_map_field_to_graphene_readto customise how specific Python types map to GraphQL fields (for example, using Relay nodes). - Register additional scalars or enums by updating
GraphQL.graphql_type_registrybefore building the schema. - Combine auto-generated queries with handcrafted ones by subclassing the generated query root and adding custom fields.
- Register additional schema directives with
GENERAL_MANAGER["GRAPHQL_DIRECTIVES"]:
from graphql import DirectiveLocation, GraphQLDirective
GENERAL_MANAGER = {
"GRAPHQL_DIRECTIVES": [
GraphQLDirective(
name="scenario",
locations=[DirectiveLocation.FIELD],
)
]
}
This setting only adds directives to the generated schema. If a directive needs runtime behavior, implement that separately with Graphene middleware or a custom execution context.