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:
-rw-r--r--app/graphql/resolvers/base_resolver.rb4
-rw-r--r--app/graphql/types/query_type.rb2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql240
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json692
-rw-r--r--doc/api/graphql/reference/index.md31
-rw-r--r--lib/gitlab/database/migration_helpers.rb40
-rw-r--r--lib/gitlab/database/with_lock_retries.rb158
-rw-r--r--spec/graphql/types/query_type_spec.rb13
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb13
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb131
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb2
-rw-r--r--spec/requests/api/graphql/tasks/task_completion_status_spec.rb2
-rw-r--r--spec/support/helpers/graphql_helpers.rb41
13 files changed, 1351 insertions, 18 deletions
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index f2b015edfa1..66cb224f157 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -58,5 +58,9 @@ module Resolvers
def single?
false
end
+
+ def current_user
+ context[:current_user]
+ end
end
end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 199a6226c6d..e8f6eeff3e9 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -40,3 +40,5 @@ module Types
resolver: Resolvers::EchoResolver
end
end
+
+Types::QueryType.prepend_if_ee('EE::Types::QueryType')
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 7d18a0abfe5..4e083142514 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -655,6 +655,16 @@ type Design implements DesignFields & Noteable {
before: String
"""
+ The Global ID of the most recent acceptable version
+ """
+ earlierOrEqualToId: ID
+
+ """
+ The SHA256 of the most recent acceptable version
+ """
+ earlierOrEqualToSha: String
+
+ """
Returns the first _n_ elements from the list.
"""
first: Int
@@ -667,10 +677,130 @@ type Design implements DesignFields & Noteable {
}
"""
+A design pinned to a specific version. The image field reflects the design as of the associated version.
+"""
+type DesignAtVersion implements DesignFields {
+ """
+ The underlying design.
+ """
+ design: Design!
+
+ """
+ The diff refs for this design
+ """
+ diffRefs: DiffRefs!
+
+ """
+ How this design was changed in the current version
+ """
+ event: DesignVersionEvent!
+
+ """
+ The filename of the design
+ """
+ filename: String!
+
+ """
+ The full path to the design file
+ """
+ fullPath: String!
+
+ """
+ The ID of this design
+ """
+ id: ID!
+
+ """
+ The URL of the image
+ """
+ image: String!
+
+ """
+ The issue the design belongs to
+ """
+ issue: Issue!
+
+ """
+ The total count of user-created notes for this design
+ """
+ notesCount: Int!
+
+ """
+ The project the design belongs to
+ """
+ project: Project!
+
+ """
+ The version this design-at-versions is pinned to
+ """
+ version: DesignVersion!
+}
+
+"""
+The connection type for DesignAtVersion.
+"""
+type DesignAtVersionConnection {
+ """
+ A list of edges.
+ """
+ edges: [DesignAtVersionEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [DesignAtVersion]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type DesignAtVersionEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: DesignAtVersion
+}
+
+"""
A collection of designs.
"""
type DesignCollection {
"""
+ Find a specific design
+ """
+ design(
+ """
+ Find a design by its filename
+ """
+ filename: String
+
+ """
+ Find a design by its ID
+ """
+ id: ID
+ ): Design
+
+ """
+ Find a design as of a version
+ """
+ designAtVersion(
+ """
+ The Global ID of the design at this version
+ """
+ id: ID!
+ ): DesignAtVersion
+
+ """
All designs for the design collection
"""
designs(
@@ -722,6 +852,21 @@ type DesignCollection {
project: Project!
"""
+ A specific version
+ """
+ version(
+ """
+ The Global ID of the version
+ """
+ id: ID
+
+ """
+ The SHA256 of a specific version
+ """
+ sha: String
+ ): DesignVersion
+
+ """
All versions related to all designs, ordered newest first
"""
versions(
@@ -736,6 +881,16 @@ type DesignCollection {
before: String
"""
+ The Global ID of the most recent acceptable version
+ """
+ earlierOrEqualToId: ID
+
+ """
+ The SHA256 of the most recent acceptable version
+ """
+ earlierOrEqualToSha: String
+
+ """
Returns the first _n_ elements from the list.
"""
first: Int
@@ -829,6 +984,28 @@ interface DesignFields {
project: Project!
}
+type DesignManagement {
+ """
+ Find a design as of a version
+ """
+ designAtVersion(
+ """
+ The Global ID of the design at this version
+ """
+ id: ID!
+ ): DesignAtVersion
+
+ """
+ Find a version
+ """
+ version(
+ """
+ The Global ID of the version
+ """
+ id: ID!
+ ): DesignVersion
+}
+
"""
Autogenerated input type of DesignManagementDelete
"""
@@ -924,8 +1101,31 @@ type DesignManagementUploadPayload {
skippedDesigns: [Design!]!
}
+"""
+A specific version in which designs were added, modified or deleted
+"""
type DesignVersion {
"""
+ A particular design as of this version, provided it is visible at this version
+ """
+ designAtVersion(
+ """
+ The ID of a specific design
+ """
+ designId: ID
+
+ """
+ The filename of a specific design
+ """
+ filename: String
+
+ """
+ The ID of the DesignAtVersion
+ """
+ id: ID
+ ): DesignAtVersion!
+
+ """
All designs that were changed in the version
"""
designs(
@@ -951,6 +1151,41 @@ type DesignVersion {
): DesignConnection!
"""
+ All designs that are visible at this version, as of this version
+ """
+ designsAtVersion(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Filters designs by their filename
+ """
+ filenames: [String!]
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Filters designs by their ID
+ """
+ ids: [ID!]
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): DesignAtVersionConnection!
+
+ """
ID of the design version
"""
id: ID!
@@ -5605,6 +5840,11 @@ type Query {
currentUser: User
"""
+ Fields related to design management
+ """
+ designManagement: DesignManagement!
+
+ """
Text to echo back
"""
echo(
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 9f888eb89c4..5c1c05d6d4e 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -49,6 +49,24 @@
"deprecationReason": null
},
{
+ "name": "designManagement",
+ "description": "Fields related to design management",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignManagement",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "echo",
"description": "Text to echo back",
"args": [
@@ -9737,6 +9755,66 @@
"description": "A collection of designs.",
"fields": [
{
+ "name": "design",
+ "description": "Find a specific design",
+ "args": [
+ {
+ "name": "id",
+ "description": "Find a design by its ID",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "filename",
+ "description": "Find a design by its filename",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Design",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "designAtVersion",
+ "description": "Find a design as of a version",
+ "args": [
+ {
+ "name": "id",
+ "description": "The Global ID of the design at this version",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignAtVersion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "designs",
"description": "All designs for the design collection",
"args": [
@@ -9876,10 +9954,63 @@
"deprecationReason": null
},
{
+ "name": "version",
+ "description": "A specific version",
+ "args": [
+ {
+ "name": "sha",
+ "description": "The SHA256 of a specific version",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "id",
+ "description": "The Global ID of the version",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignVersion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "versions",
"description": "All versions related to all designs, ordered newest first",
"args": [
{
+ "name": "earlierOrEqualToSha",
+ "description": "The SHA256 of the most recent acceptable version",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "earlierOrEqualToId",
+ "description": "The Global ID of the most recent acceptable version",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
@@ -11133,6 +11264,26 @@
"description": "All versions related to this design ordered newest first",
"args": [
{
+ "name": "earlierOrEqualToSha",
+ "description": "The SHA256 of the most recent acceptable version",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "earlierOrEqualToId",
+ "description": "The Global ID of the most recent acceptable version",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
@@ -11378,6 +11529,11 @@
"kind": "OBJECT",
"name": "Design",
"ofType": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DesignAtVersion",
+ "ofType": null
}
]
},
@@ -11531,9 +11687,56 @@
{
"kind": "OBJECT",
"name": "DesignVersion",
- "description": null,
+ "description": "A specific version in which designs were added, modified or deleted",
"fields": [
{
+ "name": "designAtVersion",
+ "description": "A particular design as of this version, provided it is visible at this version",
+ "args": [
+ {
+ "name": "id",
+ "description": "The ID of the DesignAtVersion",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "designId",
+ "description": "The ID of a specific design",
+ "type": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "filename",
+ "description": "The filename of a specific design",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignAtVersion",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "designs",
"description": "All designs that were changed in the version",
"args": [
@@ -11591,6 +11794,99 @@
"deprecationReason": null
},
{
+ "name": "designsAtVersion",
+ "description": "All designs that are visible at this version, as of this version",
+ "args": [
+ {
+ "name": "ids",
+ "description": "Filters designs by their ID",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "filenames",
+ "description": "Filters designs by their filename",
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignAtVersionConnection",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "id",
"description": "ID of the design version",
"args": [
@@ -11636,6 +11932,333 @@
},
{
"kind": "OBJECT",
+ "name": "DesignAtVersionConnection",
+ "description": "The connection type for DesignAtVersion.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignAtVersionEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignAtVersion",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DesignAtVersionEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignAtVersion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "DesignAtVersion",
+ "description": "A design pinned to a specific version. The image field reflects the design as of the associated version.",
+ "fields": [
+ {
+ "name": "design",
+ "description": "The underlying design.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Design",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "diffRefs",
+ "description": "The diff refs for this design",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DiffRefs",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "event",
+ "description": "How this design was changed in the current version",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "DesignVersionEvent",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "filename",
+ "description": "The filename of the design",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "fullPath",
+ "description": "The full path to the design file",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "id",
+ "description": "The ID of this design",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "image",
+ "description": "The URL of the image",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "issue",
+ "description": "The issue the design belongs to",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Issue",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "notesCount",
+ "description": "The total count of user-created notes for this design",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "project",
+ "description": "The project the design belongs to",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "Project",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "version",
+ "description": "The version this design-at-versions is pinned to",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "DesignVersion",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+ {
+ "kind": "INTERFACE",
+ "name": "DesignFields",
+ "ofType": null
+ }
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "EpicDescendantCount",
"description": "Counts of descendent epics.",
"fields": [
@@ -16745,6 +17368,73 @@
},
{
"kind": "OBJECT",
+ "name": "DesignManagement",
+ "description": null,
+ "fields": [
+ {
+ "name": "designAtVersion",
+ "description": "Find a design as of a version",
+ "args": [
+ {
+ "name": "id",
+ "description": "The Global ID of the design at this version",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignAtVersion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "version",
+ "description": "Find a version",
+ "args": [
+ {
+ "name": "id",
+ "description": "The Global ID of the version",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "ID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "DesignVersion",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Mutation",
"description": null,
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1177ffff36c..87e83090394 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -130,6 +130,24 @@ A single design
| `event` | DesignVersionEvent! | How this design was changed in the current version |
| `notesCount` | Int! | The total count of user-created notes for this design |
+## DesignAtVersion
+
+A design pinned to a specific version. The image field reflects the design as of the associated version.
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `id` | ID! | The ID of this design |
+| `project` | Project! | The project the design belongs to |
+| `issue` | Issue! | The issue the design belongs to |
+| `filename` | String! | The filename of the design |
+| `fullPath` | String! | The full path to the design file |
+| `image` | String! | The URL of the image |
+| `diffRefs` | DiffRefs! | The diff refs for this design |
+| `event` | DesignVersionEvent! | How this design was changed in the current version |
+| `notesCount` | Int! | The total count of user-created notes for this design |
+| `version` | DesignVersion! | The version this design-at-versions is pinned to |
+| `design` | Design! | The underlying design. |
+
## DesignCollection
A collection of designs.
@@ -138,6 +156,16 @@ A collection of designs.
| --- | ---- | ---------- |
| `project` | Project! | Project associated with the design collection |
| `issue` | Issue! | Issue associated with the design collection |
+| `version` | DesignVersion | A specific version |
+| `designAtVersion` | DesignAtVersion | Find a design as of a version |
+| `design` | Design | Find a specific design |
+
+## DesignManagement
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `version` | DesignVersion | Find a version |
+| `designAtVersion` | DesignAtVersion | Find a design as of a version |
## DesignManagementDeletePayload
@@ -162,10 +190,13 @@ Autogenerated return type of DesignManagementUpload
## DesignVersion
+A specific version in which designs were added, modified or deleted
+
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | ID of the design version |
| `sha` | ID! | SHA of the design version |
+| `designAtVersion` | DesignAtVersion! | A particular design as of this version, provided it is visible at this version |
## DestroyNotePayload
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index b7d510c19f9..5077143e15e 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -280,6 +280,46 @@ module Gitlab
end
end
+ # Executes the block with a retry mechanism that alters the +lock_timeout+ and +sleep_time+ between attempts.
+ # The timings can be controlled via the +timing_configuration+ parameter.
+ # If the lock was not acquired within the retry period, a last attempt is made without using +lock_timeout+.
+ #
+ # ==== Examples
+ # # Invoking without parameters
+ # with_lock_retries do
+ # drop_table :my_table
+ # end
+ #
+ # # Invoking with custom +timing_configuration+
+ # t = [
+ # [1.second, 1.second],
+ # [2.seconds, 2.seconds]
+ # ]
+ #
+ # with_lock_retries(timing_configuration: t) do
+ # drop_table :my_table # this will be retried twice
+ # end
+ #
+ # # Disabling the retries using an environment variable
+ # > export DISABLE_LOCK_RETRIES=true
+ #
+ # with_lock_retries do
+ # drop_table :my_table # one invocation, it will not retry at all
+ # end
+ #
+ # ==== Parameters
+ # * +timing_configuration+ - [[ActiveSupport::Duration, ActiveSupport::Duration], ...] lock timeout for the block, sleep time before the next iteration, defaults to `Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION`
+ # * +logger+ - [Gitlab::JsonLogger]
+ # * +env+ - [Hash] custom environment hash, see the example with `DISABLE_LOCK_RETRIES`
+ def with_lock_retries(**args, &block)
+ merged_args = {
+ klass: self.class,
+ logger: Gitlab::BackgroundMigration::Logger
+ }.merge(args)
+
+ Gitlab::Database::WithLockRetries.new(merged_args).run(&block)
+ end
+
def true_value
Database.true_value
end
diff --git a/lib/gitlab/database/with_lock_retries.rb b/lib/gitlab/database/with_lock_retries.rb
new file mode 100644
index 00000000000..37f7e8fbdac
--- /dev/null
+++ b/lib/gitlab/database/with_lock_retries.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Database
+ class WithLockRetries
+ NULL_LOGGER = Gitlab::JsonLogger.new('/dev/null')
+
+ # Each element of the array represents a retry iteration.
+ # - DEFAULT_TIMING_CONFIGURATION.size provides the iteration count.
+ # - First element: DB lock_timeout
+ # - Second element: Sleep time after unsuccessful lock attempt (LockWaitTimeout error raised)
+ # - Worst case, this configuration would retry for about 40 minutes.
+ DEFAULT_TIMING_CONFIGURATION = [
+ [0.1.seconds, 0.05.seconds], # short timings, lock_timeout: 100ms, sleep after LockWaitTimeout: 50ms
+ [0.1.seconds, 0.05.seconds],
+ [0.2.seconds, 0.05.seconds],
+ [0.3.seconds, 0.10.seconds],
+ [0.4.seconds, 0.15.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [1.second, 5.seconds], # probably high traffic, increase timings
+ [1.second, 1.minute],
+ [0.1.seconds, 0.05.seconds],
+ [0.1.seconds, 0.05.seconds],
+ [0.2.seconds, 0.05.seconds],
+ [0.3.seconds, 0.10.seconds],
+ [0.4.seconds, 0.15.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [3.seconds, 3.minutes], # probably high traffic or long locks, increase timings
+ [0.1.seconds, 0.05.seconds],
+ [0.1.seconds, 0.05.seconds],
+ [0.5.seconds, 2.seconds],
+ [0.5.seconds, 2.seconds],
+ [5.seconds, 2.minutes],
+ [0.5.seconds, 0.5.seconds],
+ [0.5.seconds, 0.5.seconds],
+ [7.seconds, 5.minutes],
+ [0.5.seconds, 0.5.seconds],
+ [0.5.seconds, 0.5.seconds],
+ [7.seconds, 5.minutes],
+ [0.5.seconds, 0.5.seconds],
+ [0.5.seconds, 0.5.seconds],
+ [7.seconds, 5.minutes],
+ [0.1.seconds, 0.05.seconds],
+ [0.1.seconds, 0.05.seconds],
+ [0.5.seconds, 2.seconds],
+ [10.seconds, 10.minutes],
+ [0.1.seconds, 0.05.seconds],
+ [0.5.seconds, 2.seconds],
+ [10.seconds, 10.minutes]
+ ].freeze
+
+ def initialize(logger: NULL_LOGGER, timing_configuration: DEFAULT_TIMING_CONFIGURATION, klass: nil, env: ENV)
+ @logger = logger
+ @klass = klass
+ @timing_configuration = timing_configuration
+ @env = env
+ @current_iteration = 1
+ @log_params = { method: 'with_lock_retries', class: klass.to_s }
+ end
+
+ def run(&block)
+ raise 'no block given' unless block_given?
+
+ @block = block
+
+ if lock_retries_disabled?
+ log(message: 'DISABLE_LOCK_RETRIES environment variable is true, executing the block without retry')
+
+ return run_block
+ end
+
+ begin
+ run_block_with_transaction
+ rescue ActiveRecord::LockWaitTimeout
+ if retry_with_lock_timeout?
+ wait_until_next_retry
+
+ retry
+ else
+ run_block_without_lock_timeout
+ end
+ end
+ end
+
+ private
+
+ attr_reader :logger, :env, :block, :current_iteration, :log_params, :timing_configuration
+
+ def run_block
+ block.call
+ end
+
+ def run_block_with_transaction
+ ActiveRecord::Base.transaction(requires_new: true) do
+ execute("SET LOCAL lock_timeout TO '#{current_lock_timeout_in_ms}ms'")
+
+ log(message: 'Lock timeout is set', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
+
+ run_block
+
+ log(message: 'Migration finished', current_iteration: current_iteration, lock_timeout_in_ms: current_lock_timeout_in_ms)
+ end
+ end
+
+ def retry_with_lock_timeout?
+ current_iteration != retry_count
+ end
+
+ def wait_until_next_retry
+ log(message: 'ActiveRecord::LockWaitTimeout error, retrying after sleep', current_iteration: current_iteration, sleep_time_in_seconds: current_sleep_time_in_seconds)
+
+ sleep(current_sleep_time_in_seconds)
+
+ @current_iteration += 1
+ end
+
+ def run_block_without_lock_timeout
+ log(message: "Couldn't acquire lock to perform the migration", current_iteration: current_iteration)
+ log(message: "Executing the migration without lock timeout", current_iteration: current_iteration)
+
+ execute("SET LOCAL lock_timeout TO '0'")
+
+ run_block
+
+ log(message: 'Migration finished', current_iteration: current_iteration)
+ end
+
+ def lock_retries_disabled?
+ Gitlab::Utils.to_boolean(env['DISABLE_LOCK_RETRIES'])
+ end
+
+ def log(params)
+ logger.info(log_params.merge(params))
+ end
+
+ def execute(statement)
+ ActiveRecord::Base.connection.execute(statement)
+ end
+
+ def retry_count
+ timing_configuration.size
+ end
+
+ def current_lock_timeout_in_ms
+ timing_configuration[current_iteration - 1][0].in_milliseconds
+ end
+
+ def current_sleep_time_in_seconds
+ timing_configuration[current_iteration - 1][1].to_i
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 39a363cb913..ab210f2e918 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -7,15 +7,10 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
- it do
- is_expected.to have_graphql_fields(:project,
- :namespace,
- :group,
- :echo,
- :metadata,
- :current_user,
- :snippets
- ).at_least
+ it 'has the expected fields' do
+ expected_fields = %i[project namespace group echo metadata current_user snippets]
+
+ expect(described_class).to have_graphql_fields(*expected_fields).at_least
end
describe 'namespace field' do
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index e0b4c8ae1f7..f71d3a67eb9 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -1518,4 +1518,17 @@ describe Gitlab::Database::MigrationHelpers do
model.create_or_update_plan_limit('project_hooks', 'free', 10)
end
end
+
+ describe '#with_lock_retries' do
+ let(:buffer) { StringIO.new }
+ let(:in_memory_logger) { Gitlab::JsonLogger.new(buffer) }
+ let(:env) { { 'DISABLE_LOCK_RETRIES' => 'true' } }
+
+ it 'sets the migration class name in the logs' do
+ model.with_lock_retries(env: env, logger: in_memory_logger) { }
+
+ buffer.rewind
+ expect(buffer.read).to include("\"class\":\"#{model.class}\"")
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
new file mode 100644
index 00000000000..c3be6510584
--- /dev/null
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Database::WithLockRetries do
+ let(:env) { {} }
+ let(:logger) { Gitlab::Database::WithLockRetries::NULL_LOGGER }
+ let(:subject) { described_class.new(env: env, logger: logger, timing_configuration: timing_configuration) }
+
+ let(:timing_configuration) do
+ [
+ [1.second, 1.second],
+ [1.second, 1.second],
+ [1.second, 1.second],
+ [1.second, 1.second],
+ [1.second, 1.second]
+ ]
+ end
+
+ describe '#run' do
+ it 'requires block' do
+ expect { subject.run }.to raise_error(StandardError, 'no block given')
+ end
+
+ context 'when DISABLE_LOCK_RETRIES is set' do
+ let(:env) { { 'DISABLE_LOCK_RETRIES' => 'true' } }
+
+ it 'executes the passed block without retrying' do
+ object = double
+
+ expect(object).to receive(:method).once
+
+ subject.run { object.method }
+ end
+ end
+
+ context 'when lock retry is enabled' do
+ class ActiveRecordSecond < ActiveRecord::Base
+ end
+
+ let(:lock_fiber) do
+ Fiber.new do
+ # Initiating a second DB connection for the lock
+ conn = ActiveRecordSecond.establish_connection(Rails.configuration.database_configuration[Rails.env]).connection
+ conn.transaction do
+ conn.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+
+ Fiber.yield
+ end
+ ActiveRecordSecond.remove_connection # force disconnect
+ end
+ end
+
+ before do
+ lock_fiber.resume # start the transaction and lock the table
+ end
+
+ context 'lock_fiber' do
+ it 'acquires lock successfully' do
+ check_exclusive_lock_query = """
+ SELECT 1
+ FROM pg_locks l
+ JOIN pg_class t ON l.relation = t.oid
+ WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}'
+ """
+
+ expect(ActiveRecord::Base.connection.execute(check_exclusive_lock_query).to_a).to be_present
+ end
+ end
+
+ shared_examples 'retriable exclusive lock on `projects`' do
+ it 'succeeds executing the given block' do
+ lock_attempts = 0
+ lock_acquired = false
+
+ expect_any_instance_of(Gitlab::Database::WithLockRetries).to receive(:sleep).exactly(retry_count - 1).times # we don't sleep in the last iteration
+
+ allow_any_instance_of(Gitlab::Database::WithLockRetries).to receive(:run_block_with_transaction).and_wrap_original do |method|
+ lock_fiber.resume if lock_attempts == retry_count
+
+ method.call
+ end
+
+ subject.run do
+ lock_attempts += 1
+
+ if lock_attempts == retry_count # we reached the last retry iteration, if we kill the thread, the last try (no lock_timeout) will succeed)
+ lock_fiber.resume
+ end
+
+ ActiveRecord::Base.transaction do
+ ActiveRecord::Base.connection.execute("LOCK TABLE #{Project.table_name} in exclusive mode")
+ lock_acquired = true
+ end
+ end
+
+ expect(lock_attempts).to eq(retry_count)
+ expect(lock_acquired).to eq(true)
+ end
+ end
+
+ context 'after 3 iterations' do
+ let(:retry_count) { 4 }
+
+ it_behaves_like 'retriable exclusive lock on `projects`'
+ end
+
+ context 'after the retries, without setting lock_timeout' do
+ let(:retry_count) { timing_configuration.size }
+
+ it_behaves_like 'retriable exclusive lock on `projects`'
+ end
+
+ context 'when statement timeout is reached' do
+ it 'raises QueryCanceled error' do
+ lock_acquired = false
+ ActiveRecord::Base.connection.execute("SET LOCAL statement_timeout='100ms'")
+
+ expect do
+ subject.run do
+ ActiveRecord::Base.connection.execute("SELECT 1 FROM pg_sleep(0.11)") # 110ms
+ lock_acquired = true
+ end
+ end.to raise_error(ActiveRecord::QueryCanceled)
+
+ expect(lock_acquired).to eq(false)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index 70c21666799..e1fe6470881 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -14,7 +14,7 @@ describe 'getting merge request information nested in a project' do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
- query_graphql_field('mergeRequest', iid: merge_request.iid)
+ query_graphql_field('mergeRequest', iid: merge_request.iid.to_s)
)
end
diff --git a/spec/requests/api/graphql/tasks/task_completion_status_spec.rb b/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
index c457a6d7c25..566d0fe636a 100644
--- a/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
+++ b/spec/requests/api/graphql/tasks/task_completion_status_spec.rb
@@ -25,7 +25,7 @@ describe 'getting task completion status information' do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
- query_graphql_field(type, { iid: iid }, fields)
+ query_graphql_field(type, { iid: iid.to_s }, fields)
)
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index bb8b0dfde21..353c632fced 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -16,6 +16,20 @@ module GraphqlHelpers
resolver_class.new(object: obj, context: ctx).resolve(args)
end
+ # Eagerly run a loader's named resolver
+ # (syncs any lazy values returned by resolve)
+ def eager_resolve(resolver_class, **opts)
+ sync(resolve(resolver_class, **opts))
+ end
+
+ def sync(value)
+ if GitlabSchema.lazy?(value)
+ GitlabSchema.sync_lazy(value)
+ else
+ value
+ end
+ end
+
# Runs a block inside a BatchLoader::Executor wrapper
def batch(max_queries: nil, &blk)
wrapper = proc do
@@ -39,7 +53,7 @@ module GraphqlHelpers
def batch_sync(max_queries: nil, &blk)
wrapper = proc do
lazy_vals = yield
- lazy_vals.is_a?(Array) ? lazy_vals.map(&:sync) : lazy_vals&.sync
+ lazy_vals.is_a?(Array) ? lazy_vals.map { |val| sync(val) } : sync(lazy_vals)
end
batch(max_queries: max_queries, &wrapper)
@@ -164,16 +178,26 @@ module GraphqlHelpers
def attributes_to_graphql(attributes)
attributes.map do |name, value|
- value_str = if value.is_a?(Array)
- '["' + value.join('","') + '"]'
- else
- "\"#{value}\""
- end
+ value_str = as_graphql_literal(value)
"#{GraphqlHelpers.fieldnamerize(name.to_s)}: #{value_str}"
end.join(", ")
end
+ # Fairly dumb Ruby => GraphQL rendering function. Only suitable for testing.
+ # Missing support for Enums (feel free to add if you need it).
+ def as_graphql_literal(value)
+ case value
+ when Array then "[#{value.map { |v| as_graphql_literal(v) }.join(',')}]"
+ when Integer, Float then value.to_s
+ when String then "\"#{value.gsub(/"/, '\\"')}\""
+ when nil then 'null'
+ when true then 'true'
+ when false then 'false'
+ else raise ArgumentError, "Cannot represent #{value} as GraphQL literal"
+ end
+ end
+
def post_multiplex(queries, current_user: nil, headers: {})
post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
end
@@ -216,6 +240,11 @@ module GraphqlHelpers
json_response['data'] || (raise NoData, graphql_errors)
end
+ def graphql_data_at(*path)
+ keys = path.map { |segment| GraphqlHelpers.fieldnamerize(segment) }
+ graphql_data.dig(*keys)
+ end
+
def graphql_errors
case json_response
when Hash # regular query