diff options
Diffstat (limited to 'lib/gitlab/graphql')
-rw-r--r-- | lib/gitlab/graphql/authorize/authorize_resource.rb | 3 | ||||
-rw-r--r-- | lib/gitlab/graphql/deprecation.rb | 9 | ||||
-rw-r--r-- | lib/gitlab/graphql/docs/helper.rb | 434 | ||||
-rw-r--r-- | lib/gitlab/graphql/docs/renderer.rb | 54 | ||||
-rw-r--r-- | lib/gitlab/graphql/docs/templates/default.md.haml | 224 | ||||
-rw-r--r-- | lib/gitlab/graphql/standard_graphql_error.rb | 10 |
6 files changed, 18 insertions, 716 deletions
diff --git a/lib/gitlab/graphql/authorize/authorize_resource.rb b/lib/gitlab/graphql/authorize/authorize_resource.rb index 4d575b964e5..dc49c806398 100644 --- a/lib/gitlab/graphql/authorize/authorize_resource.rb +++ b/lib/gitlab/graphql/authorize/authorize_resource.rb @@ -51,14 +51,11 @@ module Gitlab object end - # authorizes the object using the current class authorization. def authorize!(object) raise_resource_not_available_error! unless authorized_resource?(object) end def authorized_resource?(object) - # Sanity check. We don't want to accidentally allow a developer to authorize - # without first adding permissions to authorize against raise ConfigurationError, "#{self.class.name} has no authorizations" if self.class.authorization.none? self.class.authorization.ok?(object, current_user) diff --git a/lib/gitlab/graphql/deprecation.rb b/lib/gitlab/graphql/deprecation.rb index 8b73eeb4e52..20068758502 100644 --- a/lib/gitlab/graphql/deprecation.rb +++ b/lib/gitlab/graphql/deprecation.rb @@ -41,7 +41,7 @@ module Gitlab parts = [ "#{deprecated_in(format: :markdown)}.", reason_text, - replacement.then { |r| "Use: [`#{r}`](##{r.downcase.tr('.', '')})." if r } + replacement_markdown.then { |r| "Use: #{r}." if r } ].compact case context @@ -52,6 +52,13 @@ module Gitlab end end + def replacement_markdown + return unless replacement.present? + return "`#{replacement}`" unless replacement.include?('.') # only fully qualified references can be linked + + "[`#{replacement}`](##{replacement.downcase.tr('.', '')})" + end + def edit_description(original_description) @original_description = original_description return unless original_description diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb deleted file mode 100644 index b598b605141..00000000000 --- a/lib/gitlab/graphql/docs/helper.rb +++ /dev/null @@ -1,434 +0,0 @@ -# frozen_string_literal: true - -return if Rails.env.production? - -module Gitlab - module Graphql - module Docs - # We assume a few things about the schema. We use the graphql-ruby gem, which enforces: - # - All mutations have a single input field named 'input' - # - All mutations have a payload type, named after themselves - # - All mutations have an input type, named after themselves - # If these things change, then some of this code will break. Such places - # are guarded with an assertion that our assumptions are not violated. - ViolatedAssumption = Class.new(StandardError) - - SUGGESTED_ACTION = <<~MSG - We expect it to be impossible to violate our assumptions about - how mutation arguments work. - - If that is not the case, then something has probably changed in the - way we generate our schema, perhaps in the library we use: graphql-ruby - - Please ask for help in the #f_graphql or #backend channels. - MSG - - CONNECTION_ARGS = %w[after before first last].to_set - - FIELD_HEADER = <<~MD - #### Fields - - | Name | Type | Description | - | ---- | ---- | ----------- | - MD - - ARG_HEADER = <<~MD - # Arguments - - | Name | Type | Description | - | ---- | ---- | ----------- | - MD - - CONNECTION_NOTE = <<~MD - This field returns a [connection](#connections). It accepts the - four standard [pagination arguments](#connection-pagination-arguments): - `before: String`, `after: String`, `first: Int`, `last: Int`. - MD - - # Helper with functions to be used by HAML templates - # This includes graphql-docs gem helpers class. - # You can check the included module on: https://github.com/gjtorikian/graphql-docs/blob/v1.6.0/lib/graphql-docs/helpers.rb - module Helper - include GraphQLDocs::Helpers - include Gitlab::Utils::StrongMemoize - - def auto_generated_comment - <<-MD.strip_heredoc - --- - stage: Plan - group: Project Management - info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers - --- - - <!--- - This documentation is auto generated by a script. - - Please do not edit this file directly, check compile_docs task on lib/tasks/gitlab/graphql.rake. - ---> - MD - end - - # Template methods: - # Methods that return chunks of Markdown for insertion into the document - - def render_full_field(field, heading_level: 3, owner: nil) - conn = connection?(field) - args = field[:arguments].reject { |arg| conn && CONNECTION_ARGS.include?(arg[:name]) } - arg_owner = [owner, field[:name]] - - chunks = [ - render_name_and_description(field, level: heading_level, owner: owner), - render_return_type(field), - render_input_type(field), - render_connection_note(field), - render_argument_table(heading_level, args, arg_owner), - render_return_fields(field, owner: owner) - ] - - join(:block, chunks) - end - - def render_argument_table(level, args, owner) - arg_header = ('#' * level) + ARG_HEADER - render_field_table(arg_header, args, owner) - end - - def render_name_and_description(object, owner: nil, level: 3) - content = [] - - heading = '#' * level - name = [owner, object[:name]].compact.join('.') - - content << "#{heading} `#{name}`" - content << render_description(object, owner, :block) - - join(:block, content) - end - - def render_object_fields(fields, owner:, level_bump: 0) - return if fields.blank? - - (with_args, no_args) = fields.partition { |f| args?(f) } - type_name = owner[:name] if owner - header_prefix = '#' * level_bump - sections = [ - render_simple_fields(no_args, type_name, header_prefix), - render_fields_with_arguments(with_args, type_name, header_prefix) - ] - - join(:block, sections) - end - - def render_enum_value(enum, value) - render_row(render_name(value, enum[:name]), render_description(value, enum[:name], :inline)) - end - - def render_union_member(member) - "- [`#{member}`](##{member.downcase})" - end - - # QUERIES: - - # Methods that return parts of the schema, or related information: - - def connection_object_types - objects.select { |t| t[:is_edge] || t[:is_connection] } - end - - def object_types - objects.reject { |t| t[:is_edge] || t[:is_connection] || t[:is_payload] } - end - - def interfaces - graphql_interface_types.map { |t| t.merge(fields: t[:fields] + t[:connections]) } - end - - def fields_of(type_name) - graphql_operation_types - .find { |type| type[:name] == type_name } - .values_at(:fields, :connections) - .flatten - .then { |fields| sorted_by_name(fields) } - end - - # Place the arguments of the input types on the mutation itself. - # see: `#input_types` - this method must not call `#input_types` to avoid mutual recursion - def mutations - @mutations ||= sorted_by_name(graphql_mutation_types).map do |t| - inputs = t[:input_fields] - input = inputs.first - name = t[:name] - - assert!(inputs.one?, "Expected exactly 1 input field named #{name}. Found #{inputs.count} instead.") - assert!(input[:name] == 'input', "Expected the input of #{name} to be named 'input'") - - input_type_name = input[:type][:name] - input_type = graphql_input_object_types.find { |t| t[:name] == input_type_name } - assert!(input_type.present?, "Cannot find #{input_type_name} for #{name}.input") - - arguments = input_type[:input_fields] - seen_type!(input_type_name) - t.merge(arguments: arguments) - end - end - - # We assume that the mutations have been processed first, marking their - # inputs as `seen_type?` - def input_types - mutations # ensure that mutations have seen their inputs first - graphql_input_object_types.reject { |t| seen_type?(t[:name]) } - end - - # We ignore the built-in enum types, and sort values by name - def enums - graphql_enum_types - .reject { |type| type[:values].empty? } - .reject { |enum_type| enum_type[:name].start_with?('__') } - .map { |type| type.merge(values: sorted_by_name(type[:values])) } - end - - private # DO NOT CALL THESE METHODS IN TEMPLATES - - # Template methods - - def render_return_type(query) - return unless query[:type] # for example, mutations - - "Returns #{render_field_type(query[:type])}." - end - - def render_simple_fields(fields, type_name, header_prefix) - render_field_table(header_prefix + FIELD_HEADER, fields, type_name) - end - - def render_fields_with_arguments(fields, type_name, header_prefix) - return if fields.empty? - - level = 5 + header_prefix.length - sections = sorted_by_name(fields).map do |f| - render_full_field(f, heading_level: level, owner: type_name) - end - - <<~MD.chomp - #{header_prefix}#### Fields with arguments - - #{join(:block, sections)} - MD - end - - def render_field_table(header, fields, owner) - return if fields.empty? - - fields = sorted_by_name(fields) - header + join(:table, fields.map { |f| render_field(f, owner) }) - end - - def render_field(field, owner) - render_row( - render_name(field, owner), - render_field_type(field[:type]), - render_description(field, owner, :inline) - ) - end - - def render_return_fields(mutation, owner:) - fields = mutation[:return_fields] - return if fields.blank? - - name = owner.to_s + mutation[:name] - render_object_fields(fields, owner: { name: name }) - end - - def render_connection_note(field) - return unless connection?(field) - - CONNECTION_NOTE.chomp - end - - def render_row(*values) - "| #{values.map { |val| val.to_s.squish }.join(' | ')} |" - end - - def render_name(object, owner = nil) - rendered_name = "`#{object[:name]}`" - rendered_name += ' **{warning-solid}**' if deprecated?(object, owner) - - return rendered_name unless owner - - owner = Array.wrap(owner).join('') - id = (owner + object[:name]).downcase - - %(<a id="#{id}"></a>) + rendered_name - end - - # Returns the object description. If the object has been deprecated, - # the deprecation reason will be returned in place of the description. - def render_description(object, owner = nil, context = :block) - if deprecated?(object, owner) - render_deprecation(object, owner, context) - else - render_description_of(object, owner, context) - end - end - - def deprecated?(object, owner) - return true if object[:is_deprecated] # only populated for fields, not arguments! - - key = [*Array.wrap(owner), object[:name]].join('.') - deprecations.key?(key) - end - - def render_description_of(object, owner, context = nil) - desc = if object[:is_edge] - base = object[:name].chomp('Edge') - "The edge type for [`#{base}`](##{base.downcase})." - elsif object[:is_connection] - base = object[:name].chomp('Connection') - "The connection type for [`#{base}`](##{base.downcase})." - else - object[:description]&.strip - end - - return if desc.blank? - - desc += '.' unless desc.ends_with?('.') - see = doc_reference(object, owner) - desc += " #{see}" if see - desc += " (see [Connections](#connections))" if connection?(object) && context != :block - desc - end - - def doc_reference(object, owner) - field = schema_field(owner, object[:name]) if owner - return unless field - - ref = field.try(:doc_reference) - return if ref.blank? - - parts = ref.to_a.map do |(title, url)| - "[#{title.strip}](#{url.strip})" - end - - "See #{parts.join(', ')}." - end - - def render_deprecation(object, owner, context) - buff = [] - deprecation = schema_deprecation(owner, object[:name]) - - buff << (deprecation&.original_description || render_description_of(object, owner)) if context == :block - buff << if deprecation - deprecation.markdown(context: context) - else - "**Deprecated:** #{object[:deprecation_reason]}" - end - - join(context, buff) - end - - def render_field_type(type) - "[`#{type[:info]}`](##{type[:name].downcase})" - end - - def join(context, chunks) - chunks.compact! - return if chunks.blank? - - case context - when :block - chunks.join("\n\n") - when :inline - chunks.join(" ").squish.presence - when :table - chunks.join("\n") - end - end - - # Queries - - def sorted_by_name(objects) - return [] unless objects.present? - - objects.sort_by { |o| o[:name] } - end - - def connection?(field) - type_name = field.dig(:type, :name) - type_name.present? && type_name.ends_with?('Connection') - end - - # We are ignoring connections and built in types for now, - # they should be added when queries are generated. - def objects - strong_memoize(:objects) do - mutations = schema.mutation&.fields&.keys&.to_set || [] - - graphql_object_types - .reject { |object_type| object_type[:name]["__"] || object_type[:name] == 'Subscription' } # We ignore introspection and subscription types. - .map do |type| - name = type[:name] - type.merge( - is_edge: name.ends_with?('Edge'), - is_connection: name.ends_with?('Connection'), - is_payload: name.ends_with?('Payload') && mutations.include?(name.chomp('Payload').camelcase(:lower)), - fields: type[:fields] + type[:connections] - ) - end - end - end - - def args?(field) - args = field[:arguments] - return false if args.blank? - return true unless connection?(field) - - args.any? { |arg| CONNECTION_ARGS.exclude?(arg[:name]) } - end - - # returns the deprecation information for a field or argument - # See: Gitlab::Graphql::Deprecation - def schema_deprecation(type_name, field_name) - key = [*Array.wrap(type_name), field_name].join('.') - deprecations[key] - end - - def render_input_type(query) - input_field = query[:input_fields]&.first - return unless input_field - - "Input type: `#{input_field[:type][:name]}`" - end - - def schema_field(type_name, field_name) - type = schema.types[type_name] - return unless type && type.kind.fields? - - type.fields[field_name] - end - - def deprecations - strong_memoize(:deprecations) do - mapping = {} - - schema.types.each do |type_name, type| - next unless type.kind.fields? - - type.fields.each do |field_name, field| - mapping["#{type_name}.#{field_name}"] = field.try(:deprecation) - field.arguments.each do |arg_name, arg| - mapping["#{type_name}.#{field_name}.#{arg_name}"] = arg.try(:deprecation) - end - end - end - - mapping.compact - end - end - - def assert!(claim, message) - raise ViolatedAssumption, "#{message}\n#{SUGGESTED_ACTION}" unless claim - end - end - end - end -end diff --git a/lib/gitlab/graphql/docs/renderer.rb b/lib/gitlab/graphql/docs/renderer.rb deleted file mode 100644 index ae0898e6198..00000000000 --- a/lib/gitlab/graphql/docs/renderer.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -return if Rails.env.production? - -module Gitlab - module Graphql - module Docs - # Gitlab renderer for graphql-docs. - # Uses HAML templates to parse markdown and generate .md files. - # It uses graphql-docs helpers and schema parser, more information in https://github.com/gjtorikian/graphql-docs. - # - # Arguments: - # schema - the GraphQL schema definition. For GitLab should be: GitlabSchema - # output_dir: The folder where the markdown files will be saved - # template: The path of the haml template to be parsed - class Renderer - include Gitlab::Graphql::Docs::Helper - - attr_reader :schema - - def initialize(schema, output_dir:, template:) - @output_dir = output_dir - @template = template - @layout = Haml::Engine.new(File.read(template)) - @parsed_schema = GraphQLDocs::Parser.new(schema.graphql_definition, {}).parse - @schema = schema - @seen = Set.new - end - - def contents - # Render and remove an extra trailing new line - @contents ||= @layout.render(self).sub!(/\n(?=\Z)/, '') - end - - def write - filename = File.join(@output_dir, 'index.md') - - FileUtils.mkdir_p(@output_dir) - File.write(filename, contents) - end - - private - - def seen_type?(name) - @seen.include?(name) - end - - def seen_type!(name) - @seen << name - end - end - end - end -end diff --git a/lib/gitlab/graphql/docs/templates/default.md.haml b/lib/gitlab/graphql/docs/templates/default.md.haml deleted file mode 100644 index 7d42fb3a9f8..00000000000 --- a/lib/gitlab/graphql/docs/templates/default.md.haml +++ /dev/null @@ -1,224 +0,0 @@ --# haml-lint:disable UnnecessaryStringOutput - -= auto_generated_comment - -:plain - # GraphQL API Resources - - This documentation is self-generated based on GitLab current GraphQL schema. - - The API can be explored interactively using the [GraphiQL IDE](../index.md#graphiql). - - Each table below documents a GraphQL type. Types match loosely to models, but not all - fields and methods on a model are available via GraphQL. - - WARNING: - Fields that are deprecated are marked with **{warning-solid}**. - Items (fields, enums, etc) that have been removed according to our [deprecation process](../index.md#deprecation-and-removal-process) can be found - in [Removed Items](../removed_items.md). - - <!-- vale off --> - <!-- Docs linting disabled after this line. --> - <!-- See https://docs.gitlab.com/ee/development/documentation/testing.html#disable-vale-tests --> -\ - -:plain - ## `Query` type - - The `Query` type contains the API's top-level entry points for all executable queries. -\ - -- fields_of('Query').each do |field| - = render_full_field(field, heading_level: 3, owner: 'Query') - \ - -:plain - ## `Mutation` type - - The `Mutation` type contains all the mutations you can execute. - - All mutations receive their arguments in a single input object named `input`, and all mutations - support at least a return field `errors` containing a list of error messages. - - All input objects may have a `clientMutationId: String` field, identifying the mutation. - - For example: - - ```graphql - mutation($id: NoteableID!, $body: String!) { - createNote(input: { noteableId: $id, body: $body }) { - errors - } - } - ``` -\ - -- mutations.each do |field| - = render_full_field(field, heading_level: 3, owner: 'Mutation') - \ - -:plain - ## Connections - - Some types in our schema are `Connection` types - they represent a paginated - collection of edges between two nodes in the graph. These follow the - [Relay cursor connections specification](https://relay.dev/graphql/connections.htm). - - ### Pagination arguments {#connection-pagination-arguments} - - All connection fields support the following pagination arguments: - - | Name | Type | Description | - |------|------|-------------| - | `after` | [`String`](#string) | Returns the elements in the list that come after the specified cursor. | - | `before` | [`String`](#string) | Returns the elements in the list that come before the specified cursor. | - | `first` | [`Int`](#int) | Returns the first _n_ elements from the list. | - | `last` | [`Int`](#int) | Returns the last _n_ elements from the list. | - - Since these arguments are common to all connection fields, they are not repeated for each connection. - - ### Connection fields - - All connections have at least the following fields: - - | Name | Type | Description | - |------|------|-------------| - | `pageInfo` | [`PageInfo!`](#pageinfo) | Pagination information. | - | `edges` | `[edge!]` | The edges. | - | `nodes` | `[item!]` | The items in the current page. | - - The precise type of `Edge` and `Item` depends on the kind of connection. A - [`ProjectConnection`](#projectconnection) will have nodes that have the type - [`[Project!]`](#project), and edges that have the type [`ProjectEdge`](#projectedge). - - ### Connection types - - Some of the types in the schema exist solely to model connections. Each connection - has a distinct, named type, with a distinct named edge type. These are listed separately - below. -\ - -- connection_object_types.each do |type| - = render_name_and_description(type, level: 4) - \ - = render_object_fields(type[:fields], owner: type, level_bump: 1) - \ - -:plain - ## Object types - - Object types represent the resources that the GitLab GraphQL API can return. - They contain _fields_. Each field has its own type, which will either be one of the - basic GraphQL [scalar types](https://graphql.org/learn/schema/#scalar-types) - (e.g.: `String` or `Boolean`) or other object types. Fields may have arguments. - Fields with arguments are exactly like top-level queries, and are listed beneath - the table of fields for each object type. - - For more information, see - [Object Types and Fields](https://graphql.org/learn/schema/#object-types-and-fields) - on `graphql.org`. -\ - -- object_types.each do |type| - = render_name_and_description(type) - \ - = render_object_fields(type[:fields], owner: type) - \ - -:plain - ## Enumeration types - - Also called _Enums_, enumeration types are a special kind of scalar that - is restricted to a particular set of allowed values. - - For more information, see - [Enumeration Types](https://graphql.org/learn/schema/#enumeration-types) - on `graphql.org`. -\ - -- enums.each do |enum| - = render_name_and_description(enum) - \ - ~ "| Value | Description |" - ~ "| ----- | ----------- |" - - enum[:values].each do |value| - = render_enum_value(enum, value) - \ - -:plain - ## Scalar types - - Scalar values are atomic values, and do not have fields of their own. - Basic scalars include strings, boolean values, and numbers. This schema also - defines various custom scalar values, such as types for times and dates. - - This schema includes custom scalar types for identifiers, with a specific type for - each kind of object. - - For more information, read about [Scalar Types](https://graphql.org/learn/schema/#scalar-types) on `graphql.org`. -\ - -- graphql_scalar_types.each do |type| - = render_name_and_description(type) - \ - -:plain - ## Abstract types - - Abstract types (unions and interfaces) are ways the schema can represent - values that may be one of several concrete types. - - - A [`Union`](https://graphql.org/learn/schema/#union-types) is a set of possible types. - The types might not have any fields in common. - - An [`Interface`](https://graphql.org/learn/schema/#interfaces) is a defined set of fields. - Types may `implement` an interface, which - guarantees that they have all the fields in the set. A type may implement more than - one interface. - - See the [GraphQL documentation](https://graphql.org/learn/) for more information on using - abstract types. -\ - -:plain - ### Unions -\ - -- graphql_union_types.each do |type| - = render_name_and_description(type, level: 4) - \ - One of: - \ - - type[:possible_types].each do |member| - = render_union_member(member) - \ - -:plain - ### Interfaces -\ - -- interfaces.each do |type| - = render_name_and_description(type, level: 4) - \ - Implementations: - \ - - type[:implemented_by].each do |type_name| - ~ "- [`#{type_name}`](##{type_name.downcase})" - \ - = render_object_fields(type[:fields], owner: type, level_bump: 1) - \ - -:plain - ## Input types - - Types that may be used as arguments (all scalar types may also - be used as arguments). - - Only general use input types are listed here. For mutation input types, - see the associated mutation type above. -\ - -- input_types.each do |type| - = render_name_and_description(type) - \ - = render_argument_table(3, type[:input_fields], type[:name]) - \ diff --git a/lib/gitlab/graphql/standard_graphql_error.rb b/lib/gitlab/graphql/standard_graphql_error.rb new file mode 100644 index 00000000000..8364c232af2 --- /dev/null +++ b/lib/gitlab/graphql/standard_graphql_error.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# rubocop:disable Cop/CustomErrorClass + +module Gitlab + module Graphql + class StandardGraphqlError < StandardError + end + end +end |