Skip to content

GraphQL Query Patterns

Reuse identifier variables

Use ID for actual ORM primary keys and scalar foreign-key references, and [ID] for membership filters. The same ID variable works for a detail query and an equality filter:

query ProjectIdentity($id: ID!, $ids: [ID]) {
  project(id: $id) { id name }
  exact: projectList(filter: {id_Exact: $id}) {
    items { id name }
  }
  selected: projectList(filter: {id_In: $ids}) {
    items { id name }
  }
}
{"id": "42", "ids": ["42", "43"]}

Returned IDs are strings, including integer-backed keys. Keep those strings in comparisons, application state, and cache keys; avoid numeric coercion. When migrating from Int/String declarations, update identity variables to ID, their lists to [ID], and regenerate client types. Range variables and ordinary business inputs keep their native types, including a composite calculation/request input named id. Manager references retain object outputs and scalar or structured input shapes. See the identifier guide for explicit UUID/string input configuration and the full migration steps.

Project native identities

Using the Record declaration from the native identity property guide, create a row with PK 2147483648 and query:

query RecordIdentity($id: ID!) {
  record(id: $id) { id recordKey recordKeys externalId }
}
{"id": "2147483648"}

The result is:

{
  "record": {
    "id": "2147483648",
    "recordKey": "2147483648",
    "recordKeys": ["2147483648"],
    "externalId": 7
  }
}

Only the properties declared with is_identifier=True adopt identity output. The native record.record_key and values_list("id", flat=True) values remain integers. Keep schema-generated ID outputs as strings; update client types and equality/cache-key handling when adding the declaration to an existing schema.

Filter global search by native identity

With leader_id declared as a real foreign-key filter on the global index:

query SearchByLeader($filters: JSONString) {
  search(query: "", filters: $filters) {
    total
    results { __typename }
  }
}
{"filters": "[{\"field\": \"leader_id\", \"values\": [\"9007199254740993\"]}]"}

The resolver sends the native integer 9007199254740993 to the selected manager's backend. Business integer filters still require JSON numbers, and string keys such as "007" remain strings. Native property filters require the explicit identity declaration shown above; backend collection membership semantics remain unchanged. See the search guide.

Paginated listings

query ProjectList($page: Int!, $pageSize: Int!) {
  projectList(page: $page, pageSize: $pageSize, orderBy: [{field: startDate, direction: DESC}]) {
    items {
      id
      name
      startDate
    }
    pageInfo {
      totalCount
      totalPages
    }
  }
}

Group through a dedicated endpoint

orderBy orders eligible aggregate scalars after grouping and before pagination. This keeps descending group order stable across pages:

query ProjectsByStatus {
  projectGroups(
    groupBy: ["status"]
    orderBy: [{field: status, direction: DESC}]
    page: 1
    pageSize: 10
  ) {
    items { status name }
    pageInfo { totalCount currentPage totalPages pageSize }
  }
}

If the filter produces no groups, the same query shape returns items: [] and page metadata rather than an empty-group slicing error.

Group by the scalar relation ID and retrieve distinct original related managers:

query ProjectsByCommercial {
  projectGroups(groupBy: ["commercials_id"]) {
    items {
      commercialsId
      commercialsList { items { id name } pageInfo { totalCount } }
    }
    pageInfo { totalCount }
  }
}

Bucket relations also expose …Groups for recursive grouping, with independent filters, ordering and pagination. Plain List[GraphQLType] properties remain output lists without query controls. See the grouped-data concept, GraphQL how-to, and core API reference for the full grouping and error contract.

Aggregate unique text values in groups

Generated group fields can return a text field directly under items as a compact, stable summary:

query ProjectNamesByStatus {
  projectGroups(
    groupBy: ["status"]
    orderBy: [{field: name, direction: ASC}]
  ) {
    items {
      status
      name
    }
    pageInfo { totalCount }
  }
}

For member values ["Alpha", "Alpha", "Beta", null], the name aggregate is "Alpha, Beta". Values are deduplicated in encounter order, nulls are excluded, and an all-null group returns null. Numeric sums retain their existing addition behavior. orderBy sorts the aggregated name values even though name is not selected in groupBy; totalCount counts groups. See the grouping concept, GraphQL how-to, and GraphQL API reference for permissions, arguments, and compatibility details.

Nested buckets

query ProjectWithDerivatives($id: ID!) {
  project(id: $id) {
    name
    derivativeList(filter: { maturity_date__gte: "2024-01-01" }) {
      id
      maturityDate
      volume
    }
  }
}

Query a manager relation

For a manager declaration such as owner: User | None and reviewer_list: Bucket[User], generated GraphQL exposes an object field and a paginated relation-list field. Query both fields directly:

query ProjectRelations($projectId: ID!) {
  project(id: $projectId) {
    owner { id name }
    reviewerList(page: 1, pageSize: 20) {
      items { id name }
      pageInfo { totalCount currentPage totalPages pageSize }
    }
  }
}

The ordinary root and relation pages use the related manager's item-derived page type. A relation to ProjectType therefore uses ProjectTypeTypePage and items of type ProjectTypeType; update named fragments and regenerate schema-derived clients after the page-type rename. The query field, arguments, items/pageInfo shape, IDs, and permissions are unchanged.

Nested relation filters use the same resolved manager type. A direct relation uses a nested object, while a collection relation uses any or none:

query ProjectsWithRelatedUsers {
  projectList(filter: {
    owner: { name: "Alice" }
    reviewerList: { any: { name: "Alice" } }
  }) {
    items { id name owner { id name } }
  }
}

For the Python annotation forms and the generated mutation/subscription contracts, see the GraphQL concept guide, the task guide, and the API reference.

Update only the fields you supply

Generated ORM update and delete mutations require a stable id: ID! target. Primary keys are excluded from update payload fields; other editable fields are optional. Omit a field when the stored value should stay unchanged, even if the model declares a default for that field:

mutation RenameProject($id: ID!) {
  updateProject(id: $id, name: "Renamed") {
    success
    project { id name score }
  }
}

The omission rule also applies to nullable variables that are not present in the variables object:

mutation UpdateOptionalScore($id: ID!, $score: Int) {
  updateProject(id: $id, score: $score) {
    success
    project { id score }
  }
}

Use {"id": "42"} to preserve score, or {"id": "42", "score": null} to explicitly clear a nullable score. A concrete score value is written as supplied. Create mutations continue to apply model defaults when their fields are omitted. See the GraphQL how-to, the mutation concept, and the API reference.

Create with a manual primary key

For a manager with an editable integer PK field named id and no default, declare the create variable as ID!:

mutation CreateManualRecord($id: ID!, $name: String!) {
  createManualRecord(id: $id, name: $name) {
    success
  }
}
{"id": "42", "name": "Manual record"}

Auto-increment PKs are omitted from create arguments. A manual PK with a model default is optional. Literal defaults remain in the schema; NOT_PROVIDED and callable defaults are omitted, and the ORM applies callable defaults when the create write omits the field. Schema construction does not evaluate them, and input/model validation still applies. Required fields without real defaults need non-null variable declarations, such as $name: String! above. See the manual-PK guide.

Sort by a compound relation key

Generated list fields accept typed orderBy terms. This request sorts projects by the related commercial name first, then by project name and unique project ID to make ties deterministic:

query ProjectsByCommercialName($order: [ProjectOrderBy!]) {
  projectList(orderBy: $order, page: 1, pageSize: 20) {
    items {
      id
      name
      commercialsList { items { id name } pageInfo { totalCount } }
    }
    pageInfo {
      totalCount
      currentPage
      totalPages
      pageSize
    }
  }
}
{"order": [{"field": "commercials__name"}, {"field": "name"}, {"field": "id"}]}

The enum values are exposed by the generated ProjectOrderField type. Each input has a required field and an ASC default direction; an empty list is a no-op. See the sorting concept, the generated-list how-to, and the GraphQL API reference for the supported relation paths and error behavior.

Subscribe to a protected manager instance

Detail subscriptions use the same generated identification arguments as the manager's other operations:

subscription ProjectDetails($id: ID!) {
  onProjectChange(id: $id) {
    action
    item { id name }
  }
}

The initial target must be readable by the authenticated user. GeneralManager checks that permission before evaluating item properties, checks again before the snapshot is delivered, and rechecks before each queued update. Revoking access or deleting the target ends the detail stream after cleanup; it does not emit an event with item: null. An allowed target can still mask individual fields according to their normal read rules. See the subscription concept for dependency and lifecycle details.

Subscribe to committed class changes

subscription ProjectChanges {
  onProjectClassChange {
    action
    item { id name }
  }
}

Identified class-wide events are checked against the subscribing user's read permission in an async-safe worker after commit; unreadable objects are omitted. Aggregate refresh events have item: null and do not disclose a row ID.

Subscribe to fields with read permissions

Field-level read rules also apply to the fields selected inside a subscription payload. This manager keeps internalNote visible only to staff users while leaving the public name field readable for any authenticated user:

from general_manager import GeneralManager
from general_manager.permission import AdditiveManagerPermission, register_permission


@register_permission("isStaff")
def is_staff(_instance, user, _config):
    return bool(getattr(user, "is_staff", False))


class Project(GeneralManager):
    name: str
    internal_note: str

    class Permission(AdditiveManagerPermission):
        __read__ = ["isAuthenticated"]
        internal_note = {"read": ["isStaff"]}

Subscribe with the generated field names:

subscription ProjectChangesWithFieldRules {
  onProjectClassChange {
    action
    item {
      id
      name
      internalNote
    }
  }
}

For an authenticated non-staff subscriber, internalNote resolves to null while name and the event action remain available. The same rule applies to normal, measurement, and stored-file payload fields; a denied field is not accessed. For an allowed subscription field, permission evaluation, lazy value access, measurement conversion, and stored-file formatting run together in an async-safe worker, so uncached foreign-key values to other GeneralManagers are safe to resolve. Query and mutation field resolution remains synchronous. See the GraphQL how-to and API reference for execution and exception details.

Bound run-scoped cache memory

For a worker that serves long-lived GraphQL requests, configure the optional process-local run-cache budget:

GENERAL_MANAGER = {
    "RUN_CONTEXT_CACHE_MAX_BYTES": 256 * 1024 * 1024,
}

The value is an estimated-memory LRU budget shared by live run contexts in that worker. Omit it or use None for unlimited retention; pending dependency-cache publications remain pinned until their lifecycle completes.

Filter a calculation by manager input

For a calculation manager with project = Input(Project), use the same nested direct-relation shape as a persisted manager:

query ProjectCommercials($projectId: ID!) {
  projectCommercialList(filter: {project: {id: $projectId}}) {
    items {
      project { id name }
      targetDate
    }
  }
}
{"projectId": "42"}

The generated filter is directly usable with a normal GraphQL request. The server translates project: {id: ...} to the calculation lookup project__id=...; replace id with a supported nested field or lookup when needed. See the calculation how-to and API reference for the declaration and compatibility rules.

Custom mutation with Measurement input

Custom scalar arguments keep their declared Python annotation. For a resolver whose argument is annotated id: int, use Int; the argument name alone does not make it an ID. Annotate a manager reference to generate an ID or structured composite input instead.

mutation UpdateInventory($id: Int!, $price: MeasurementScalar!) {
  updateInventoryItem(id: $id, price: $price) {
    success
    errors
    inventoryItem {
      id
      price
    }
  }
}

Aggregation via GraphQL property

query ProjectSummary($id: ID!) {
  project(id: $id) {
    name
    totalCapex
    duration
    derivativeSummary
  }
}

Use these patterns as a starting point and adapt filters or selections to your domain.