Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'doc/development/api_graphql_styleguide.md')
-rw-r--r--doc/development/api_graphql_styleguide.md246
1 files changed, 231 insertions, 15 deletions
diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md
index f807ed0f85e..de6840b2c6c 100644
--- a/doc/development/api_graphql_styleguide.md
+++ b/doc/development/api_graphql_styleguide.md
@@ -94,11 +94,16 @@ discussed in [Nullable fields](#nullable-fields).
- Lowering the global limits for query complexity and depth.
- Anything else that can result in queries hitting a limit that previously was allowed.
-Fields that use the [`feature_flag` property](#feature_flag-property) and the flag is disabled by default are exempt
-from the deprecation process, and can be removed at any time without notice.
-
See the [deprecating schema items](#deprecating-schema-items) section for how to deprecate items.
+### Breaking change exemptions
+
+Two scenarios exist where schema items are exempt from the deprecation process,
+and can be removed or changed at any time without notice. These are schema items that either:
+
+- Use the [`feature_flag` property](#feature_flag-property) _and_ the flag is disabled by default.
+- Are [marked as alpha](#marking-schema-items-as-alpha).
+
## Global IDs
The GitLab GraphQL API uses Global IDs (i.e: `"gid://gitlab/MyObject/123"`)
@@ -718,6 +723,28 @@ aware of the support.
The documentation will mention that the old Global ID style is now deprecated.
+## Marking schema items as Alpha
+
+Fields, arguments, enum values, and mutations can be marked as being in
+[alpha](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga).
+
+An item marked as "alpha" is exempt from the deprecation process and can be removed
+at any time without notice.
+
+This leverages GraphQL deprecations to cause the schema item to appear as deprecated,
+and will be described as being in "alpha" in our generated docs and its GraphQL description.
+
+To mark a schema item as being in "alpha", use the `deprecated:` keyword with `reason: :alpha`.
+You must provide the `milestone:` that introduced the alpha item.
+
+For example:
+
+```ruby
+field :token, GraphQL::Types::String, null: true,
+ deprecated: { reason: :alpha, milestone: '10.0' },
+ description: 'Token for login.'
+```
+
## Enums
GitLab GraphQL enums are defined in `app/graphql/types`. When defining new enums, the
@@ -1848,35 +1875,59 @@ field :created_at, Types::TimeType, null: true, description: 'Timestamp of when
## Testing
-### Writing unit tests
+For testing mutations and resolvers, consider the unit of
+test a full GraphQL request, not a call to a resolver. The reasons for this are
+that we want to avoid lots of coupling to the framework, since this makes
+upgrades to dependencies much more difficult.
-Before creating unit tests, review the following examples:
+You should:
-- [`spec/graphql/resolvers/users_resolver_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/graphql/resolvers/users_resolver_spec.rb)
-- [`spec/graphql/mutations/issues/create_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/graphql/mutations/issues/create_spec.rb)
+- Prefer request specs (either using the full API endpoint or going through
+ `GitlabSchema.execute`) to unit specs for resolvers and mutations.
+- Prefer `GraphqlHelpers#execute_query` and `GraphqlHelpers#run_with_clean_state` to
+ `GraphqlHelpers#resolve` and `GraphqlHelpers#resolve_field`.
-It's faster to test as much of the logic from your GraphQL queries and mutations
-with unit tests, which are stored in `spec/graphql`.
+For example:
-Use unit tests to verify that:
+```ruby
+# Good:
+gql_query = %q(some query text...)
+post_graphql(gql_query, current_user: current_user)
+# or:
+GitlabSchema.execute(gql_query, context: { current_user: current_user })
-- Types have the expected fields.
-- Resolvers and mutations apply authorizations and return expected data.
-- Edge cases are handled correctly.
+# Deprecated: avoid
+resolve(described_class, obj: project, ctx: { current_user: current_user })
+```
+
+### Writing unit tests (deprecated)
+
+WARNING:
+Avoid writing unit tests if the same thing can be tested with
+a full GraphQL request.
+
+Before creating unit tests, review the following examples:
+
+- [`spec/graphql/resolvers/users_resolver_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/graphql/resolvers/users_resolver_spec.rb)
+- [`spec/graphql/mutations/issues/create_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/graphql/mutations/issues/create_spec.rb)
### Writing integration tests
Integration tests check the full stack for a GraphQL query or mutation and are stored in
`spec/requests/api/graphql`.
-For speed, you should test most logic in unit tests instead of integration tests.
-However, integration tests that check if data is returned verify the following
+For speed, consider calling `GitlabSchema.execute` directly, or making use
+of smaller test schemas that only contain the types under test.
+
+However, full request integration tests that check if data is returned verify the following
additional items:
- The mutation is actually queryable in the schema (was mounted in `MutationType`).
- The data returned by a resolver or mutation correctly matches the
[return types](https://graphql-ruby.org/fields/introduction.html#field-return-type) of
the fields and resolves without errors.
+- The arguments coerce correctly on input, and the fields serialize correctly
+ on output.
Integration tests can also verify the following items, because they invoke the
full stack:
@@ -1929,6 +1980,55 @@ end
### Testing tips and tricks
+- Become familiar with the methods in the `GraphqlHelpers` support module.
+ Many of these methods make writing GraphQL tests easier.
+
+- Use traversal helpers like `GraphqlHelpers#graphql_data_at` and
+ `GraphqlHelpers#graphql_dig_at` to access result fields. For example:
+
+ ```ruby
+ result = GitlabSchema.execute(query)
+
+ mr_iid = graphql_dig_at(result.to_h, :data, :project, :merge_request, :iid)
+ ```
+
+- Use `GraphqlHelpers#a_graphql_entity_for` to match against results.
+ For example:
+
+ ```ruby
+ post_graphql(some_query)
+
+ # checks that it is a hash containing { id => global_id_of(issue) }
+ expect(graphql_data_at(:project, :issues, :nodes))
+ .to contain_exactly(a_graphql_entity_for(issue))
+
+ # Additional fields can be passed, either as names of methods, or with values
+ expect(graphql_data_at(:project, :issues, :nodes))
+ .to contain_exactly(a_graphql_entity_for(issue, :iid, :title, created_at: some_time))
+ ```
+
+- Use `GraphqlHelpers#empty_schema` to create an empty schema, rather than creating
+ one by hand. For example:
+
+ ```ruby
+ # good
+ let(:schema) { empty_schema }
+
+ # bad
+ let(:query_type) { GraphQL::ObjectType.new }
+ let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
+ ```
+
+- Use `GraphqlHelpers#query_double(schema: nil)` of `double('query', schema: nil)`. For example:
+
+ ```ruby
+ # good
+ let(:query) { query_double(schema: GitlabSchema) }
+
+ # bad
+ let(:query) { double('Query', schema: GitlabSchema) }
+ ```
+
- Avoid false positives:
Authenticating a user with the `current_user:` argument for `post_graphql`
@@ -1983,6 +2083,122 @@ end
`spec/requests/api/graphql/ci/pipeline_spec.rb` regardless of the query being
used to fetch the pipeline data.
+- There can be possible cyclic dependencies within our GraphQL types.
+ See [Adding field with resolver on a Type causes "Can't determine the return type " error on a different Type](https://github.com/rmosolgo/graphql-ruby/issues/3974#issuecomment-1084444214)
+ and [Fix unresolved name due to cyclic definition](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84202/diffs#diff-content-32d14251082fd45412e1fdbf5820e62d157e70d2).
+
+ In particular, this can happen with `connection_type`. Normally we might use the following in a resolver:
+
+ ```ruby
+ type Types::IssueType.connection_type, null: true
+ ```
+
+ However this might cause a cyclic definition, which can result in errors like:
+
+ ```ruby
+ NameError: uninitialized constant Resolvers::GroupIssuesResolver
+ ```
+
+ To fix this, we must create a new file that encapsulates the connection type,
+ and then reference it using double quotes. This gives a delayed resolution,
+ and the proper connection type. For example:
+
+ ```ruby
+ module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class IssueConnectionType < CountableConnectionType
+ end
+ end
+
+ Types::IssueConnectionType.prepend_mod_with('Types::IssueConnectionType')
+ ```
+
+ in [types/issue_connection_type.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/types/issue_connection_type.rb)
+ defines a new `Types::IssueConnectionType`, and is then referenced in
+ [app/graphql/resolvers/base_issues_resolver.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/graphql/resolvers/base_issues_resolver.rb)
+
+ ```ruby
+ type "Types::IssueConnection", null: true
+ ```
+
+ Only use this style if you are having spec failures. This is not intended to be a new
+ pattern that we use. This issue may disappear after we've upgraded to `2.x`.
+
+- There can be instances where a spec fails because the class is not loaded correctly.
+ It relates to the
+ [circular dependencies problem](https://github.com/rmosolgo/graphql-ruby/issues/1929) and
+ [Adding field with resolver on a Type causes "Can't determine the return type " error on a different Type](https://github.com/rmosolgo/graphql-ruby/issues/3974).
+
+ Unfortunately, the errors generated don't really indicate what the problem is. For example,
+ remove the quotes from the `Rspec.descrbe` in
+ [ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb).
+ Then run `rspec ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb`.
+
+ This generates errors with the expectations. For example:
+
+ ```ruby
+ 1) Resolvers::ComplianceManagement::MergeRequests::ComplianceViolationResolver#resolve user is authorized filtering the results when given an array of project IDs finds the filtered compliance violations
+ Failure/Error: expect(subject).to contain_exactly(compliance_violation)
+
+ expected collection contained: [#<MergeRequests::ComplianceViolation id: 4, violating_user_id: 26, merge_request_id: 4, reason: "approved_by_committer", severity_level: "low">]
+ actual collection contained: [#<MergeRequests::ComplianceViolation id: 4, violating_user_id: 26, merge_request_id: 4, reason: "app...er_id: 27, merge_request_id: 5, reason: "approved_by_merge_request_author", severity_level: "high">]
+ the extra elements were: [#<MergeRequests::ComplianceViolation id: 5, violating_user_id: 27, merge_request_id: 5, reason: "approved_by_merge_request_author", severity_level: "high">]
+ # ./ee/spec/graphql/resolvers/compliance_management/merge_requests/compliance_violation_resolver_spec.rb:55:in `block (6 levels) in <top (required)>'
+ ```
+
+ However, this is not a case of the wrong result being generated, it's because of the loading order
+ of the `ComplianceViolationResolver` class.
+
+ The only way we've found to fix this is by quoting the class name in the spec. For example, changing
+
+ ```ruby
+ RSpec.describe Resolvers::ComplianceManagement::MergeRequests::ComplianceViolationResolver do
+ ```
+
+ into:
+
+ ```ruby
+ RSpec.describe 'Resolvers::ComplianceManagement::MergeRequests::ComplianceViolationResolver' do
+ ```
+
+ See [this merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87295#note_946174036) for some discussion.
+
+ Only use this style if you are having spec failures. This is not intended to be a new
+ pattern that we use. This issue may disappear after we've upgraded to `2.x`.
+
+- When testing resolvers using `GraphqlHelpers#resolve`, arguments for the resolver can be handled two ways.
+
+ 1. 95% of the resolver specs use arguments that are Ruby objects, as opposed to when using the GraphQL API
+ only strings and integers are used. This works fine in most cases.
+ 1. If your resolver takes arguments that use a `prepare` proc, such as a resolver that accepts timeframe
+ arguments (`TimeFrameArguments`), you must pass the `arg_style: :internal_prepared` parameter into
+ the `resolve` method. This tells the code to convert the arguments into strings and integers and pass
+ them through regular argument handling, ensuring that the `prepare` proc is called correctly.
+ For example in [`iterations_resolver_spec.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/spec/graphql/resolvers/iterations_resolver_spec.rb):
+
+ ```ruby
+ def resolve_group_iterations(args = {}, obj = group, context = { current_user: current_user })
+ resolve(described_class, obj: obj, args: args, ctx: context, arg_style: :internal_prepared)
+ end
+ ```
+
+ One additional caveat is that if you are passing enums as a resolver argument, you must use the
+ external representation of the enum, rather than the internal. For example:
+
+ ```ruby
+ # good
+ resolve_group_iterations({ search: search, in: ['CADENCE_TITLE'] })
+
+ # bad
+ resolve_group_iterations({ search: search, in: [:cadence_title] })
+ ```
+
+ The use of `:internal_prepared` was added as a bridge for the
+ [GraphQL gem](https://graphql-ruby.org) upgrade. Testing resolvers directly will be
+ [removed eventually](https://gitlab.com/gitlab-org/gitlab/-/issues/363121),
+ and writing unit tests for resolvers/mutations is
+ [already deprecated](#writing-unit-tests-deprecated)
+
## Notes about Query flow and GraphQL infrastructure
The GitLab GraphQL infrastructure can be found in `lib/gitlab/graphql`.