.ai/principles/distilled/graphql.md
required: false) to required (required: true) without deprecation.null: true) to non-nullable (null: false) without deprecation.deprecated: property with reason: and milestone: keys; DO NOT remove them directly.description: on deprecated items; append the reason via the reason: key, not by editing the description.Use \otherFieldName`` as the deprecation reason when a field is replaced by another.Deprecations issue template with ~GraphQL and ~deprecation labelsTypes::DeprecatedMutations and test them in Types::MutationType unit tests.mount_aliased_mutation to alias a mutation when renaming, to preserve the old name during the deprecation period.Deprecation entry to Gitlab::GlobalId::Deprecations::DEPRECATIONS instead of making a breaking change.experiment: { milestone: '...' }.experiment: property when the feature flag is removed to make the item public.@gl_introduced directive on fields for Self-Managed/Dedicated to strip future nodes from queries hitting older backend versions.@gl_introduced on arguments, fragments, or single future fields that are the only selection in a query or object.@gl_introduced as still requiring null-checks on the frontend.description: value ending with a period (.).The or A.{x} of the {y} phrasing for field descriptions where possible.Types::TimeType for all Time/DateTime fields and include the word timestamp in the description.Indicates the issue is confidential.).Values for sorting {x}.description:.copy_field_description(Types::SomeType, :field_name) to keep descriptions in sync between a type field and a mutation argument.see: property for external documentation references instead of embedding URLs in descriptions.Premium and Ultimate only.) for fields restricted to higher tiers.null: true) over non-nullable ones; reserve non-nullable for fields that are required, unlikely to become optional, and cheap to compute (for example, id).Types::GlobalIDType[Model] (not plain ID or database primary key integers) for all Global ID inputs and outputs.id-named fields that expose Global IDs manually using Gitlab::GlobalId.build or #to_global_id.GraphQL::Types::ID only for full paths; DO NOT use it for database IDs or IIDs.Types::TimeType for all Ruby Time and DateTime fields and arguments.markdown_field helper for all fields that return rendered Markdown.calls_gitaly: true; annotate resolvers that call Gitaly with calls_gitaly!.complexity: 0 for trivially cheap fields (for example, id, title); set higher complexity for expensive fields.extension(::Gitlab::Graphql::Limit::FieldCallCount, limit: N) and update the field description: when limiting field call count to prevent N+1 problems of last resort.CountableConnectionType when exact counts matter; use LimitedCountableConnectionType when the collection can be very large and exact counts are not critical.GraphQL::Types::JSON unless the data is truly unstructured; use typed objects or unions instead.latest_pipeline); use pagination arguments instead (for example, pipelines(last: 1)).Enum, graphql_name does not contain Enum, and all values are uppercase.each_key to keep them in sync.graphql_name as the first line of a mutation class.{Resource}{Action} or {Resource}{Action}{Attribute}; use Create, Update/Set/Add/Toggle, and Delete/Remove verbs.expose_permissions with a type inheriting BasePermissionType.present_using.loads: option in argument definitions; accept the Global ID and load the object manually with authorized_find!.required: :nullable when an argument must be provided but its value can be null.validates: { allow_null: false } for optional arguments where null is not a valid value.validates mutually_exclusive: or validates exactly_one_of: for mutually exclusive or exactly-one-of argument groups.{PROPERTY}_{DIRECTION} format.project_path, group_path, or namespace_path; use iid with a parent path for IID-identified objects; use Types::GlobalIDType[Model] for all other object identifiers.validates: { length: { maximum: Types::BaseArgument::MAX_ARRAY_SIZE } } to all array arguments; DO NOT rely solely on the automatic 100-item limit from BaseArgument.Types::BaseArgument::MAX_ARRAY_SIZE or a named module constant, and document the limit in the argument description:.authorized_find! in resolvers to load and authorize objects; DO NOT raise errors for unauthorized resources — return null instead.#ready? for set-up or early-return logic; use validators for argument validation instead of #ready?.BaseResolver.single derived resolvers have more restrictive arguments than the collection resolver via a when_single block.LooksAhead concern and implement preloads / unconditional_includes to avoid N+1 queries via lookahead.before_connection_authorization to preload data for type authorization checks and avoid N+1s from permission checks.BatchModelLoader for ID-based record lookups; DO NOT implement custom ID batch loaders.batch.sync or Lazy.force in resolver code; use Lazy.with_value instead.GraphqlTriggers to trigger subscriptions; DO NOT call GitlabSchema.subscriptions.trigger directly in application code.#authorized? in subscription classes and call unauthorized! when authorization fails; use #authorize_object_or_gid! for the typical permission check on the subscribed object.errors as an empty array on success and populate it with user-relevant error messages on failure; raise raise_resource_not_available_error! for authorization/not-found errors.null: true.#reset if needed).Gitlab::Graphql::Errors types; DO NOT let StandardError propagate uncaught (it becomes Internal server error).for(data) call.authorize :ability on types, resolvers, or fields using DeclarativePolicy abilities.authorizes_object! when a resolver should authorize against the parent object.authorize: for scalar fields with different access levels or to avoid expensive per-object checks.skip_type_authorization on a field only when the resolver already authorizes the resolved objects and the permission checks are equivalent, to avoid redundant N+1 authorization calls.ActiveRecord::Relation) over offset pagination; fall back to offset_pagination(result) only when the sort order is too complex for keyset pagination.items.order('created_at DESC')); use the hash syntax (items.order(created_at: :desc)) so sort information is correctly embedded in cursors.Gitlab::Graphql::ExternallyPaginatedArray.new(previous_cursor, next_cursor, *items) when proxying externally paginated data (for example, from a third-party API).development.log, the performance bar, or request specs with QueryRecorder.BatchLoader::GraphQL for batching queries in resolvers; pass all needed data through for(data) and DO NOT close over instance state in batch blocks.#sync.QueryRecorder tests to avoid false positives from authentication queries.includes(); build at the class level to avoid Arel::Nodes::LeadingJoin errors.spec/requests/api/graphql as the primary test vehicle; DO NOT rely on resolver unit specs for behavior testing.authorize declarations.post_graphql / post_graphql_mutation helpers and GraphqlHelpers methods in integration specs.graphql_mutation, post_graphql_mutation, and graphql_mutation_response helpers for mutation specs.a_graphql_entity_for, graphql_data_at, and graphql_dig_at helpers to access and match result fields.empty_schema instead of manually constructing a schema in unit tests.get_graphql_query_as_string to test frontend .graphql query files.app/graphql/types in spec/requests/api/graphql.batch_sync or Gitlab::Graphql::Lazy.force only in tests when lazy values must be forced; prefer Schema.execute in request specs to avoid manual lifecycle management.sorted paginated query shared example for any GraphQL field that supports pagination and sorting to verify sort key compatibility and cursor correctness.describe 'sorting and pagination' block in request specs for paginated and sortable fields.For the full picture, see: