decidim-api 0.20.1 → 0.23.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ecb5f667879728e5fe57552b07ec210e01ef955c4ee52ff56c68c68abdf69f2
4
- data.tar.gz: a4cb07a0767a7db13d7e8710980cdc08e24ba71cba2f259f8dea10fd0f67e01f
3
+ metadata.gz: a34e25455a765f56502dc517b5989963ad33ce652b384d74d6ddca8139434fbf
4
+ data.tar.gz: cf2820cb0959d2e08b2be1cc0069c9fd03a8e4ed17a625f665b5352f6cc05d5c
5
5
  SHA512:
6
- metadata.gz: 8da082cfaee963daab37c8790f2d68f59c24cb1d107645836031f66c12cbfde239dee3c6da312d84797d8c31cc220e5e38c07c71abcbd058cb8c2f8dda466505
7
- data.tar.gz: 2ccf2d56e2af0c8bd94d1c3d7e733877f4132f48beb93da69aeeb4ce7c8e03740f58533080b5d60604990e3321345b06cfa37dcae26a5a66eb8cf4f59429accb
6
+ metadata.gz: c6a5ee7424d299a2261af6d93dfca377348a406c7aeedc810619500aba37ffddc9c491ec91926fd2527a8013765faeb8a8c64b2a16965c5aa7d519daffca58c4
7
+ data.tar.gz: 8727cc55d2a3cbbc748d75db7d2aa688b3cfaa582e0b6969cb97fb9a4b3d1b55c67785c284b332e021e42876ad4f4e21e36b412b2dbaed0e7c069aee802aff7c
@@ -0,0 +1,3 @@
1
+ /*
2
+ *= link decidim/api/docs.css
3
+ */
@@ -0,0 +1,61 @@
1
+ body{
2
+ max-width: 1200px;
3
+ }
4
+
5
+ .intro,
6
+ .info{
7
+ margin-left: 270px;
8
+ color: #333;
9
+ }
10
+
11
+ .info{
12
+ li{
13
+ margin-bottom: 1rem;
14
+ }
15
+
16
+ h3{
17
+ margin-top: 2em;
18
+ }
19
+ }
20
+
21
+ .version{
22
+ display: inline-block;
23
+ font-size: .8rem;
24
+ margin-right: 1rem;
25
+ padding: 3px 7px;
26
+ background: #f33;
27
+ border-radius: 10px;
28
+ margin-top: 5px;
29
+ color: white;
30
+ }
31
+
32
+ @media only screen and (max-width: 767px){
33
+ .intro,
34
+ .info{
35
+ margin: 10px;
36
+ }
37
+ }
38
+
39
+ code{
40
+ font-size: .8em;
41
+ background: #ddd;
42
+ padding: .1em;
43
+ }
44
+
45
+ pre{
46
+ padding: .5rem;
47
+ background: #f0f0f0;
48
+ border: 1px solid #e8e8e8;
49
+
50
+ code{
51
+ background: none;
52
+ }
53
+ }
54
+
55
+ blockquote{
56
+ border-left: 3px solid burlywood;
57
+ padding: .1em 0 .1em 1rem;
58
+ margin: 1rem 0;
59
+ background: #f9f7f5;
60
+ width: 100%;
61
+ }
@@ -6,8 +6,10 @@ module Decidim
6
6
  class ApplicationController < ::DecidimController
7
7
  skip_before_action :verify_authenticity_token
8
8
  include NeedsOrganization
9
+ include UseOrganizationTimeZone
9
10
  include NeedsPermission
10
11
  include ImpersonateUsers
12
+ include ForceAuthentication
11
13
 
12
14
  register_permissions(::Decidim::Api::ApplicationController,
13
15
  ::Decidim::Permissions)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Api
5
+ # Controller to serve the GraphiQL client. Used so that we can hook the
6
+ # `ForceAuthentication` module.
7
+ class GraphiQLController < ::GraphiQL::Rails::EditorsController
8
+ include NeedsOrganization
9
+ include ForceAuthentication
10
+
11
+ def self.controller_path
12
+ "graphiql/rails/editors"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "redcarpet"
4
+
5
+ module Decidim
6
+ module Api
7
+ # Custom helpers, scoped to the api engine.
8
+ #
9
+ module ApplicationHelper
10
+ def render_doc(file)
11
+ md_render File.read(File.join(File.dirname(__FILE__), "../../../../docs", "#{file}.md"))
12
+ end
13
+
14
+ def md_render(text)
15
+ text = Redcarpet::Markdown.new(markdown, autolink: true, tables: true, fenced_code_blocks: true).render(text)
16
+ text.html_safe
17
+ end
18
+
19
+ def markdown
20
+ @markdown ||= Redcarpet::Render::HTML.new
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,6 +1,15 @@
1
- <div style="text-align: center;">
1
+
2
+ <div class="intro">
3
+ <div class="version">Decidim <%= Decidim.version %></div>
4
+
2
5
  <h1><%= current_organization.name %> API documentation</h1>
3
- <%= link_to "Explore the API interactively with GraphiQL", graphiql_path %>
6
+ <% if defined?(graphiql_path) %>
7
+ <%= link_to "Explore the API interactively with GraphiQL", graphiql_path %>
8
+ <% end %>
9
+ </div>
10
+
11
+ <div class="info">
12
+ <%= render_doc("usage") %>
4
13
  </div>
5
14
 
6
15
  <div id="documentation"></div>
@@ -2,6 +2,9 @@
2
2
  <head>
3
3
  <title><%= current_organization.name %> - API Documentation</title>
4
4
  <%= javascript_include_tag "decidim/api/docs" %>
5
+ <%= stylesheet_link_tag "decidim/api/docs" %>
6
+ <style>
7
+ </style>
5
8
  </head>
6
9
  <body>
7
10
  <%= yield %>
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Decidim::Api::Engine.routes.draw do
4
- mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/api", as: :graphiql
4
+ get "/graphiql", to: "graphiql#show", graphql_path: "/api", as: :graphiql
5
5
  get "/docs", to: "documentation#show", as: :documentation
6
6
  get "/", to: redirect("/api/docs")
7
7
  post "/" => "queries#create", as: :root
@@ -0,0 +1,596 @@
1
+ ## About the GraphQL API
2
+
3
+ [Decidim](https://github.com/decidim/decidim) comes with an API that follows the [GraphQL](https://graphql.org/) specification. It has a comprehensive coverage of all the public content that can be found on the website.
4
+
5
+ Currently, it is read-only (except for posting comments) but intends to cover anything that is published on the regular website.
6
+
7
+ Typically (although some particular installations may change that) you will find 3 relevant folders:
8
+
9
+ * `URL/api` The route where to make requests. Request are usually in the POST format.
10
+ * `URL/api/docs` This documentation, every Decidim site should provide one.
11
+ * `URL/api/graphiql` [GraphiQL](https://github.com/graphql/graphiql) is a in-browser IDE for exploring GraphQL APIs. Some Decidim installations may choose to remove access to this tool. In that case you can use a [standalone version](https://electronjs.org/apps/graphiql) and use any `URL/api` as the endpoint
12
+
13
+ ### Using the GraphQL APi
14
+
15
+ The GraphQL format is a JSON formatted text that is specified in a query. Response is a JSON object as well. For details about specification check the official [GraphQL site](https://graphql.org/learn/).
16
+
17
+ For instance, you can check the version of a Decidim installation by using `curl` in the terminal:
18
+
19
+ ```
20
+ curl -sSH "Content-Type: application/json" \
21
+ -d '{"query": "{ decidim { version } }"}' \
22
+ https://www.decidim.barcelona/api/
23
+ ```
24
+
25
+ Note that `Content-Type` needs to be specified.
26
+
27
+ The query can also be used in GraphiQL, in that case you can skip the `"query"` text:
28
+
29
+ ```
30
+ {
31
+ decidim {
32
+ version
33
+ }
34
+ }
35
+ ```
36
+
37
+ Response (formatted) should look something like this:
38
+
39
+ ```json
40
+ {
41
+ "data": {
42
+ "decidim": {
43
+ "version": "0.18.1"
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ The most practical way to experiment with GraphQL, however, is just to use the in-browser IDE GraphiQL. It provides access to the documentation and auto-complete (use CTRL-Space) for writing queries.
50
+
51
+ From now on, we will skip the "query" keyword for the purpose of readability. You can skip it too if you are using GraphiQL, if you are querying directly (by using CURL for instance) you will need to include it.
52
+
53
+ ### Usage limits
54
+
55
+ Decidim is just a Rails application, meaning that any particular installation may implement custom limits in order to access the API (and the application in general).
56
+
57
+ By default (particular installations may change that), API uses the same limitations as the whole Decidim website, provided by the Gem [Rack::Attack](https://github.com/kickstarter/rack-attack). These are 100 maximum requests per minute per IP to prevent DoS attacks
58
+
59
+ ### Decidim structure, Types, collections and Polymorphism
60
+
61
+ There are no endpoints in the GraphQL specification, instead objects are organized according to their "Type".
62
+
63
+ These objects can be grouped in a single, complex query. Also, objects may accept parameters, which are "Types" as well.
64
+
65
+ Each "Type" is just a pre-defined structure with fields, or just an Scalar (Strings, Integers, Booleans, ...).
66
+
67
+ For instance, to obtain *all the participatory processes in a Decidim installation published since January 2018* and order them by published date, we could execute the next query:
68
+
69
+ ```
70
+ {
71
+ participatoryProcesses(filter: {publishedSince: "2018-01-01"}, order: {publishedAt: "asc"}) {
72
+ slug
73
+ title {
74
+ translation(locale: "en")
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Response should look like:
81
+
82
+ ```
83
+ {
84
+ "data": {
85
+ "participatoryProcesses": [
86
+ {
87
+ "slug": "consectetur-at",
88
+ "title": {
89
+ "translation": "Soluta consectetur quos fugit aut."
90
+ }
91
+ },
92
+ {
93
+ "slug": "nostrum-earum",
94
+ "title": {
95
+ "translation": "Porro hic ipsam cupiditate reiciendis."
96
+ }
97
+ }
98
+ ]
99
+ }
100
+ }
101
+ ```
102
+
103
+ #### What happened?
104
+
105
+ In the former query, each keyword represents a type, the words `publishedSince`, `publishedAt`, `slug`, `locale` are scalars, all of them Strings.
106
+
107
+ The other keywords however, are objects representing certain entities:
108
+
109
+ - `participatoryProcesses` is a type that represents a collection of participatory spaces. It accepts arguments (`filter` and `order`), which are other object types as well. `slug` and `title` are the fields of the participatory process we are interested in, there are "Types" too.
110
+ - `filter` is a [ParticipatoryProcessFilter](#ParticipatoryProcessFilter)\* input type, it has several properties that allows us to refine our search. One of them is the `publishedSince` property with the initial date from which to list entries.
111
+ - `order ` is a [ParticipatoryProcessSort](#ParticipatoryProcessSort) type, works the same way as the filter but with the goal of ordering the results.
112
+ - `title` is a [TranslatedField](#TranslatedField) type, which allows us to deal with multi-language fields.
113
+
114
+ Finally, note that the returned object is an array, each item of which is a representation of the object we requested.
115
+
116
+ > \***About how filters and sorting are organized**
117
+ >
118
+ > There are two types of objects to filter and ordering collections in Decidim, they all work in a similar fashion. The type involved in filtering always have the suffix "Filter", for ordering it has the suffix "Sort".
119
+ >
120
+ > The types used to filter participatory spaces are: [ParticipatoryProcessFilter](#ParticipatoryProcessFilter), [AssemblyFilter](#AssemblyFilter), [ConsultationFilter](#ConsultationFilter) and so on.
121
+ >
122
+ > Other collections (or connections) may have their own filters (i.e. [ComponentFilter](#ComponentFilter)).
123
+ >
124
+ > Each filter has its own properties, you should check any object in particular for details. The way they work with multi-languages fields, however, is the same:
125
+ >
126
+ > Let's say we have some searchable object with a multi-language field called *title*, and we have a filter that allows us to search through this field. How should it work? Should we look up content for every language in the field? or should we stick to a specific language?
127
+ >
128
+ > In our case, we've decided to search only one particular language of a multi-language field but we let you choose which language to search.
129
+ > If no language is specified, the configured as default in the organization will be used. The keyword to specify the language is `locale`, and it should be provided in the 2 letters ISO 639-1 format (en = English, es = Spanish, ...).
130
+ >
131
+ > Example (this is not a real Decidim query):
132
+ >
133
+ > ```
134
+ > some_collection(filter: { locale: "en", title: "ideas"}) {
135
+ > id
136
+ > }
137
+ > ```
138
+ >
139
+ > The same applies to sorting ([ParticipatoryProcessSort](#ParticipatoryProcessSort), [AssemblySort](#AssemblySort), etc.)
140
+ >
141
+ > In this case, the content of the field (*title*) only allows 2 values: *ASC* and *DESC*.
142
+ >
143
+ > Example of ordering alphabetically by the title content in French language:
144
+ >
145
+ > ```
146
+ > some_collection(order: { locale: "en", title: "asc"}) {
147
+ > id
148
+ > }
149
+ > ```
150
+ >
151
+ > Of course, you can combine both filter and order. Also remember to check availability of this type of behaviour for any particular filter/sort.
152
+
153
+ #### Decidim main types
154
+
155
+ Decidim has 2 main types of objects through which content is provided. These are Participatory Spaces and Components.
156
+
157
+ A participatory space is the first level, currently there are 5 officially supported: *Participatory Processes*, *Assemblies*, *Consultations*, *Conferences* and *Initiatives*. For each participatory process there will correspond a collection type and a "single item" type.
158
+
159
+ The previous example uses the collection type for participatory processes. You can try `assemblies`, `conferences`, `consultations` or `initiatives` for the others. Note that each collection can implement their own filter and order types with different properties.
160
+
161
+ As an example for a single item query, you can run:
162
+
163
+ ```
164
+ {
165
+ participatoryProcess(slug: "consectetur-at") {
166
+ slug
167
+ title {
168
+ translation(locale: "en")
169
+ }
170
+ }
171
+ }
172
+ ```
173
+
174
+ And the response will be:
175
+
176
+ ```
177
+ {
178
+ "data": {
179
+ "participatoryProcess": {
180
+ "slug": "consectetur-at",
181
+ "title": {
182
+ "translation": "Soluta consectetur quos fugit aut."
183
+ }
184
+ }
185
+ }
186
+ }
187
+ ```
188
+
189
+ #### What's different?
190
+
191
+ First, note that we are querying, in singular, the type `participatoryProcess`, with a different parameter, `slug`\*, (a String). We can use the `id` instead if we know it.
192
+
193
+ Second, the response is not an Array, it is just the object we requested. We can expect to return `null` if the object is not found.
194
+
195
+ > \* The `slug` is a convenient way to find a participatory space as is (usually) in the URL.
196
+ >
197
+ > For instance, consider this real case from Barcelona:
198
+ >
199
+ > https://www.decidim.barcelona/processes/patrimonigracia
200
+ >
201
+ > The word `patrimonigracia` indicates the "slug".
202
+
203
+ #### Components
204
+
205
+ Every participatory space may (and should) have some components. There are 9 official components, these are `Proposals`, `Page`, `Meetings`, `Budgets`, `Surveys`, `Accountability`, `Debates`, `Sortitions` and `Blog`. Plugins may add their own components.
206
+
207
+ If you know the `id`\* of a specific component you can obtain it by querying it directly:
208
+
209
+ ```
210
+ {
211
+ component(id:2) {
212
+ id
213
+ name {
214
+ translation(locale:"en")
215
+ }
216
+ __typename
217
+ participatorySpace {
218
+ id
219
+ type
220
+ }
221
+ }
222
+ }
223
+ ```
224
+
225
+ Response:
226
+
227
+ ```
228
+ {
229
+ "data": {
230
+ "component": {
231
+ "id": "2",
232
+ "name": {
233
+ "translation": "Meetings"
234
+ },
235
+ "__typename": "Meetings",
236
+ "participatorySpace": {
237
+ "id": "1",
238
+ "type": "Decidim::ParticipatoryProcess"
239
+ }
240
+ }
241
+ }
242
+ }
243
+ ```
244
+
245
+ The process is analogue as what has been explained in the case of searching for one specific participatory process.
246
+
247
+ > \*Note that the `id` of a component is present also in the URL after the letter "f":
248
+ >
249
+ > https://www.decidim.barcelona/processes/patrimonigracia/f/3257/
250
+ >
251
+ > In this case, 3257.
252
+
253
+ #### What about component's collections?
254
+
255
+ Glad you asked, component's collections cannot be retrieved directly, the are available *in the context* of a participatory space.
256
+
257
+ For instance, we can query all the components in an particular Assembly as follows:
258
+
259
+ ```
260
+ {
261
+ assembly(id: 3) {
262
+ components {
263
+ id
264
+ name {
265
+ translation(locale: "en")
266
+ }
267
+ __typename
268
+ }
269
+ }
270
+ }
271
+ ```
272
+
273
+ The response will be similar to:
274
+
275
+ ```
276
+ {
277
+ "data": {
278
+ "assembly": {
279
+ "components": [
280
+ {
281
+ "id": "42",
282
+ "name": {
283
+ "translation": "Accountability"
284
+ },
285
+ "__typename": "Component"
286
+ },
287
+ {
288
+ "id": "38",
289
+ "name": {
290
+ "translation": "Meetings"
291
+ },
292
+ "__typename": "Meetings"
293
+ },
294
+ {
295
+ "id": "37",
296
+ "name": {
297
+ "translation": "Page"
298
+ },
299
+ "__typename": "Pages"
300
+ },
301
+ {
302
+ "id": "39",
303
+ "name": {
304
+ "translation": "Proposals"
305
+ },
306
+ "__typename": "Proposals"
307
+ }
308
+ ]
309
+ }
310
+ }
311
+ }
312
+ ```
313
+
314
+ We can also apply some filters by using the [ComponentFilter](#ComponentFilter) type. In the next query we would like to *find all the components with geolocation enabled in the assembly with id=2*:
315
+
316
+ ```
317
+ {
318
+ assembly(id: 2) {
319
+ components(filter: {withGeolocationEnabled: true}) {
320
+ id
321
+ name {
322
+ translation(locale: "en")
323
+ }
324
+ __typename
325
+ }
326
+ }
327
+ }
328
+ ```
329
+
330
+ The response:
331
+
332
+ ```
333
+ {
334
+ "data": {
335
+ "assembly": {
336
+ "components": [
337
+ {
338
+ "id": "39",
339
+ "name": {
340
+ "translation": "Meetings"
341
+ },
342
+ "__typename": "Meetings"
343
+ }
344
+ ]
345
+ }
346
+ }
347
+ }
348
+ ```
349
+
350
+ Note that, in this case, there is only one component returned, "Meetings". In some cases Proposals can be geolocated too therefore would be returned in this query.
351
+
352
+ ### Polymorphism and connections
353
+
354
+ Many relationships between tables in Decidim are polymorphic, this means that the related object can belong to different classes and share just a few properties in common.
355
+
356
+ For instance, components in a participatory space are polymorphic, while the concept of component is generic and all of them share properties like *published date*, *name* or *weight*, they differ in the rest. *Proposals* have the *status* field while *Meetings* have an *agenda*.
357
+
358
+ Another example are the case of linked resources, these are properties that may link objects of different nature between components or participatory spaces.
359
+
360
+ In a very simplified way (to know more please refer to the official guide), GraphQL polymorphism is handled through the operator `... on`. You'll know when a field is polymorphic because the property `__typename`, which tells you the type of that particular object, will change accordingly.
361
+
362
+ In the previous examples we've queried for this property:
363
+
364
+ Response fragment:
365
+
366
+ ```
367
+ ...
368
+ "components": [
369
+ {
370
+ "id": "38",
371
+ "name": {
372
+ "translation": "Meetings"
373
+ },
374
+ "__typename": "Meetings"
375
+ }
376
+ ...
377
+ ```
378
+
379
+ So, if we want to access the rest of the properties in a polymorphic object, we should do it through the `... on` operator as follows:
380
+
381
+ ```
382
+ {
383
+ assembly(id: 2) {
384
+ components {
385
+ id
386
+ ... on Proposals {
387
+
388
+ }
389
+ }
390
+ }
391
+ }
392
+ ```
393
+
394
+ Consider this query:
395
+
396
+ ```
397
+ {
398
+ assembly(id: 3) {
399
+ components(filter: {type: "Proposals"}) {
400
+ id
401
+ name {
402
+ translation(locale: "en")
403
+ }
404
+ ... on Proposals {
405
+ proposals(order: {endorsementCount: "desc"}, first: 2) {
406
+ edges {
407
+ node {
408
+ id
409
+ endorsements {
410
+ name
411
+ }
412
+ }
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+ ```
420
+
421
+ The response:
422
+
423
+ ```
424
+ {
425
+ "data": {
426
+ "assembly": {
427
+ "components": [
428
+ {
429
+ "id": "39",
430
+ "name": {
431
+ "translation": "Proposals"
432
+ },
433
+ "proposals": {
434
+ "edges": [
435
+ {
436
+ "node": {
437
+ "id": "35",
438
+ "endorsements": [
439
+ {
440
+ "name": "Ms. Johnathon Schaefer"
441
+ },
442
+ {
443
+ "name": "Linwood Lakin PhD 3 4 endr1"
444
+ },
445
+ {
446
+ "name": "Gracie Emmerich"
447
+ },
448
+ {
449
+ "name": "Randall Rath 3 4 endr3"
450
+ },
451
+ {
452
+ "name": "Jolene Schmitt MD"
453
+ },
454
+ {
455
+ "name": "Clarence Hammes IV 3 4 endr5"
456
+ },
457
+ {
458
+ "name": "Omar Mayer"
459
+ },
460
+ {
461
+ "name": "Raymundo Jaskolski 3 4 endr7"
462
+ }
463
+ ]
464
+ }
465
+ },
466
+ {
467
+ "node": {
468
+ "id": "33",
469
+ "endorsements": [
470
+ {
471
+ "name": "Spring Brakus"
472
+ },
473
+ {
474
+ "name": "Reiko Simonis IV 3 2 endr1"
475
+ },
476
+ {
477
+ "name": "Dr. Jim Denesik"
478
+ },
479
+ {
480
+ "name": "Dr. Mack Schoen 3 2 endr3"
481
+ }
482
+ ]
483
+ }
484
+ }
485
+ ]
486
+ }
487
+ }
488
+ ]
489
+ }
490
+ }
491
+ }
492
+ ```
493
+
494
+ #### What's going on?
495
+
496
+ Until the `... on Proposals` line, there's nothing new. We are requesting the *Assembly* participatory space identified by the `id=3`, then listing all its components with the type "Proposals". All the components share the *id* and *name* properties, so we can just add them at the query.
497
+
498
+ After that, we want content specific from the *Proposals* type. In order to do that we must tell the server that the content we will request shall only be executed if the types matches *Proposals*. We do that by wrapping the rest of the query in the `... on Proposals` clause.
499
+
500
+ The next line is just a property of the type *Proposals* which is a type of collection called a "connection". A connection works similar as normal collection (such as *components*) but it can handle more complex cases.
501
+
502
+ Typically, a connection is used to paginate long results, for this purpose the results are not directly available but encapsulated inside the list *edges* in several *node* results. Also there are more arguments available in order to navigate between pages. This are the arguments:
503
+
504
+ - `first`: Returns the first *n* elements from the list
505
+ - `after`: Returns the elements in the list that come after the specified *cursor*
506
+ - `last`: Returns the last *n* elements from the list
507
+ - `before`: Returns the elements in the list that come before the specified *cursor*
508
+
509
+ Example:
510
+
511
+ ```
512
+ {
513
+ assembly(id: 3) {
514
+ components(filter: {type: "Proposals"}) {
515
+ id
516
+ name {
517
+ translation(locale: "en")
518
+ }
519
+ ... on Proposals {
520
+ proposals(first:2,after:"Mg") {
521
+ pageInfo {
522
+ endCursor
523
+ startCursor
524
+ hasPreviousPage
525
+ hasNextPage
526
+ }
527
+ edges {
528
+ node {
529
+ id
530
+ endorsements {
531
+ name
532
+ }
533
+ }
534
+ }
535
+ }
536
+ }
537
+ }
538
+ }
539
+ }
540
+ ```
541
+
542
+ Being the response:
543
+
544
+ ```
545
+ {
546
+ "data": {
547
+ "assembly": {
548
+ "components": [
549
+ {
550
+ "id": "39",
551
+ "name": {
552
+ "translation": "Proposals"
553
+ },
554
+ "proposals": {
555
+ "pageInfo": {
556
+ "endCursor": "NA",
557
+ "startCursor": "Mw",
558
+ "hasPreviousPage": false,
559
+ "hasNextPage": true
560
+ },
561
+ "edges": [
562
+ {
563
+ "node": {
564
+ "id": "32",
565
+ "endorsements": []
566
+ }
567
+ },
568
+ {
569
+ "node": {
570
+ "id": "31",
571
+ "endorsements": [
572
+ {
573
+ "name": "Mr. Nicolas Raynor"
574
+ },
575
+ {
576
+ "name": "Gerry Fritsch PhD 3 1 endr1"
577
+ }
578
+ ]
579
+ }
580
+ }
581
+ ]
582
+ }
583
+ }
584
+ ]
585
+ }
586
+ }
587
+ }
588
+ ```
589
+
590
+ As you can see, a part from the *edges* list, you can access to the object *pageInfo* which gives you the information needed to navigate through the different pages.
591
+
592
+ For more info on how connections work, you can check the official guide:
593
+
594
+ https://graphql.org/learn/pagination/
595
+
596
+