decidim-api 0.31.4 → 0.32.0.rc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -13
- data/app/controllers/decidim/api/sessions_controller.rb +1 -1
- data/app/models/decidim/api/api_user.rb +5 -1
- data/config/locales/ca-IT.yml +7 -0
- data/config/locales/ca.yml +7 -0
- data/config/locales/cs.yml +7 -0
- data/config/locales/el.yml +11 -0
- data/config/locales/en.yml +7 -0
- data/config/locales/es-MX.yml +7 -0
- data/config/locales/es-PY.yml +7 -0
- data/config/locales/es.yml +7 -0
- data/config/locales/eu.yml +7 -0
- data/config/locales/fi-plain.yml +7 -0
- data/config/locales/fi.yml +7 -0
- data/config/locales/fr-CA.yml +4 -0
- data/config/locales/fr.yml +4 -0
- data/config/locales/ja.yml +7 -0
- data/config/locales/no.yml +1 -0
- data/config/locales/pt-BR.yml +7 -0
- data/config/locales/ro-RO.yml +7 -0
- data/config/locales/sv.yml +7 -0
- data/decidim-api.gemspec +12 -15
- data/docs/usage.md +3 -634
- data/lib/decidim/api/component_mutation_type.rb +1 -2
- data/lib/decidim/api/errors/attribute_validation_error.rb +73 -0
- data/lib/decidim/api/errors/invalid_locale_error.rb +14 -0
- data/lib/decidim/api/errors/locale_error.rb +14 -0
- data/lib/decidim/api/errors/mutation_not_authorized_error.rb +14 -0
- data/lib/decidim/api/errors/not_found_error.rb +14 -0
- data/lib/decidim/api/errors/permission_not_set_error.rb +14 -0
- data/lib/decidim/api/errors/unauthorized_field_error.rb +14 -0
- data/lib/decidim/api/errors/unauthorized_object_error.rb +14 -0
- data/lib/decidim/api/errors/validation_error.rb +13 -0
- data/lib/decidim/api/graphiql/config.rb +1 -1
- data/lib/decidim/api/graphql_permissions.rb +17 -12
- data/lib/decidim/api/query_type.rb +91 -0
- data/lib/decidim/api/schema.rb +27 -1
- data/lib/decidim/api/test/component_context.rb +59 -51
- data/lib/decidim/api/test/shared_examples/commentable_interface_examples.rb +46 -0
- data/lib/decidim/api/test/shared_examples/followable_interface_examples.rb +12 -1
- data/lib/decidim/api/test/shared_examples/statistics_examples.rb +0 -2
- data/lib/decidim/api/test/type_context.rb +10 -2
- data/lib/decidim/api/test.rb +1 -0
- data/lib/decidim/api/types/access_mode_enum.rb +15 -0
- data/lib/decidim/api/types/base_mutation.rb +28 -0
- data/lib/decidim/api/types/base_object.rb +12 -0
- data/lib/decidim/api/types.rb +12 -1
- data/lib/decidim/api/version.rb +1 -1
- data/lib/decidim/api.rb +22 -32
- metadata +59 -34
- /data/lib/decidim/api/test/{mutation_context.rb → shared_examples/mutation_context.rb} +0 -0
data/docs/usage.md
CHANGED
|
@@ -16,6 +16,8 @@ Typically (although some particular installations may change that) you will find
|
|
|
16
16
|
|
|
17
17
|
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/).
|
|
18
18
|
|
|
19
|
+
For additional examples of queries and mutations, check the additional [GraphQL API documentation](https://docs.decidim.org/en/develop/develop/api/index.html) of Decidim.
|
|
20
|
+
|
|
19
21
|
Exercise caution when utilizing the output of this API, as it may include HTML that has not been escaped. Take particular care in handling this data, specially if you intend to render it on a webpage.
|
|
20
22
|
|
|
21
23
|
For instance, you can check the version of a Decidim installation by using `curl` in the terminal:
|
|
@@ -50,637 +52,4 @@ Response (formatted) should look something like this:
|
|
|
50
52
|
}
|
|
51
53
|
```
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
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.
|
|
56
|
-
|
|
57
|
-
### Signing in to the API
|
|
58
|
-
|
|
59
|
-
In case you want to use the API as a sign in user to perform mutations representing a user in Decidim, you have two available options for such integrations through the system administration panel:
|
|
60
|
-
|
|
61
|
-
1. Creating an OAuth application and implementing the OAuth authentication flow for the users of your application. Use this option for participant-facing applications where the participants represent themselves in Decidim through the API.
|
|
62
|
-
2. Creating API credentials and signing in to the API with these credentials to perform the operations as a signed in machine user. Use this option for machine-to-machine automations where there is no real end user interacting with Decidim.
|
|
63
|
-
|
|
64
|
-
If you only want to test the GraphQL queries as a signed in user, you can use the normal Decidim authentication functionality to sign in and then use the GraphiQL IDE to perform these queries as a signed in user.
|
|
65
|
-
|
|
66
|
-
#### OAuth flow for participant-facing applications
|
|
67
|
-
|
|
68
|
-
Participant-facing applications where the participants need to interact with Decidim through GraphQL mutations can be integrated using OAuth applications. In order to configure such integration capability from the system administration panel, create a new OAuth application and provide the necessary details for your integration. Note that the "application type" for such applications would typically be "Public". For more information regarding the application types, refer to [RFC 6749 Section 2.1. (OAuth client types)](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1).
|
|
69
|
-
|
|
70
|
-
In order to use the OAuth access tokens to represent the user through the API, please select the following scopes as "Available" scopes for the application:
|
|
71
|
-
|
|
72
|
-
* `user` - Authenticated users have the ability to represent a logged in user in Decidim
|
|
73
|
-
* `api:read` - Authenticated users have the ability to read data from the API
|
|
74
|
-
* `api:write` - Authenticated users have the ability to write data through the API (in case your external application needs to perform mutations over the API on behalf of the user)
|
|
75
|
-
|
|
76
|
-
Once configured, you can now use any OAuth authentication library to perform the OAuth authentication flow with your application users and receive an access token to utilize the Decidim API representing the signed in user. Please note that with public OAuth clients especially (and recommended also for confidential clients), you have to use [PKCE](https://datatracker.ietf.org/doc/html/rfc7636) with the authorization flow.
|
|
77
|
-
|
|
78
|
-
Once the OAuth application is created, you can authenticate against it with the following steps:
|
|
79
|
-
|
|
80
|
-
1. Send the user to perform an OAuth authorization request at Decidim with the required API scopes (`user`, `api:read` and `api:write` if you want to perform mutations over the API). Along with the authorization request, also send the additional parameters required by PKCE (`code_challenge` and `code_challenge_method`).
|
|
81
|
-
2. Receive an OAuth authorization code back to your application's configured redirect URI.
|
|
82
|
-
3. Utilizing the received authorization code, request an OAuth access token from the OAuth token endpoint. Along with the token request, also send the additional parameter required by PKCE `code_verifier`.
|
|
83
|
-
4. The issued token is a JSON Web Token (JWT) when the authorization request contains the defined scopes. This token can be now used to represent the user in further calls to the API by passing the token with its type (`Bearer`) within the HTTP Authorization header with the request to the API.
|
|
84
|
-
|
|
85
|
-
When doing the requests to the API, you also need to pass the OAuth client ID within the `X-Jwt-Aud` header of the requests in order for the token to be recognized as a valid token for the issued client. Passing the bearer token to the `Authorization` header and the OAuth client ID to the `X-Jwt-Aud` header, you can send the following HTTP request to the API to validate that the token works and the user is recognized as signed in:
|
|
86
|
-
|
|
87
|
-
```http
|
|
88
|
-
POST /api HTTP/1.1
|
|
89
|
-
Accept: application/json
|
|
90
|
-
Authorization: Bearer token
|
|
91
|
-
Content-Length: 53
|
|
92
|
-
Content-Type: application/json
|
|
93
|
-
Host: DOMAIN
|
|
94
|
-
X-Jwt-Aud: OAUTH_CLIENT_ID
|
|
95
|
-
|
|
96
|
-
{"query":"{ session { user { id name nickname } } }"}
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
You should see the user details in the response in case the token is valid and you have configured the API correctly. If the response does not contain the user details, please refer to the Decidim configuration documentation.
|
|
100
|
-
|
|
101
|
-
Once the interaction with the API is completed, it is recommended to revoke the tokens, which is similar to the user signing out of the application. This can be done utilizing the OAuth revocation endpoint provided by Decidim. After the token is revoked, it is no longer valid and the user has to perform a re-authorization the next time they want to utilize the API.
|
|
102
|
-
|
|
103
|
-
In case you need tokens with a longer life span, you can either look into the Decidim documentation to extend the validity period of the access tokens or enable refresh tokens for the OAuth application when configuring it. However, note that tokens with longer lifespan can weaken the security of your system and make your application users vulnerable to security threats. Such use cases should be carefully planned and the security concerns should be addressed seriously.
|
|
104
|
-
|
|
105
|
-
#### API credentials flow for machine-to-machine automations
|
|
106
|
-
|
|
107
|
-
The API credentials represent an administrative user in Decidim that performs administrative tasks on behalf of the end users. This type of integration flows should never live on devices that the participants have access to. These types of integrations are meant for different types of automations, such as transferring proposal answers or meeting reports back to Decidim from an external system automatically, e.g. once a day.
|
|
108
|
-
|
|
109
|
-
Note that these credentials are highly sensitive and have elevated permissions, so take good care of the system security where you are planning to store these credentials. If these credentials end up in participants' hands, the whole system is compromised and no longer secure. You should always primarily create OAuth integrations where the end users will manually perform the authorization for the application to perform actions on behalf of them.
|
|
110
|
-
|
|
111
|
-
Once you have validated that this is the correct way for your integration to operate, you can create the API credentials from the system administration panel. You will receive an API key and API secret after creating the credentials. These credentials should be also manually rotated on a regular basis to prevent unauthorized access to the system with these credentials in case they are leaked. The credentials have to be manually rotated in order to prevent external applications breaking because they cannot rotate the credentials themselves and they are typically statically configured for these applications.
|
|
112
|
-
|
|
113
|
-
Given you have issued the API key and API secret, you can now send a sign in request to the API using these credentials as follows:
|
|
114
|
-
|
|
115
|
-
```bash
|
|
116
|
-
curl -s -i -H "Content-type: application/x-www-form-urlencoded" \
|
|
117
|
-
-d "api_user[key]=PASTE_API_KEY_HERE" \
|
|
118
|
-
-d "api_user[secret]=PASTE_API_SECRET_HERE" \
|
|
119
|
-
-X POST https://DOMAIN/api/sign_in | grep 'Authorization' | cut -d ' ' -f2-
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
After running this command, you should see the following string in the console, where `token` is replaced with the access token:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
Bearer token
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
This string is passed to the following requests within the HTTP `Authorization` header to represent the user during API calls. You can use the following example query to test it out and confirm that signing in works as expected:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
curl -w "\n" -H "Content-Type: application/json" \
|
|
132
|
-
-H "Authorization: Bearer token" \
|
|
133
|
-
-d '{"query":"{ session { user { id name nickname } } }"}' \
|
|
134
|
-
-X POST https://DOMAIN/api
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
You should see the user details in the response in case the token is valid and you have configured the API correctly. If the response does not contain the user details, please refer to the Decidim configuration documentation.
|
|
138
|
-
|
|
139
|
-
Once the API interaction is done, you should always make an HTTP DELETE request to `/api/sign_out` with the same token in order to revoke the token from further access as follows:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
curl -s -o /dev/null -w "HTTP %{http_code}\n" \
|
|
143
|
-
-H "Authorization: Bearer token" \
|
|
144
|
-
-X DELETE http://DOMAIN/api/sign_out
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Usage limits
|
|
148
|
-
|
|
149
|
-
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).
|
|
150
|
-
|
|
151
|
-
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
|
|
152
|
-
|
|
153
|
-
### Decidim structure, Types, collections and Polymorphism
|
|
154
|
-
|
|
155
|
-
There are no endpoints in the GraphQL specification, instead objects are organized according to their "Type".
|
|
156
|
-
|
|
157
|
-
These objects can be grouped in a single, complex query. Also, objects may accept parameters, which are "Types" as well.
|
|
158
|
-
|
|
159
|
-
Each "Type" is just a pre-defined structure with fields, or just an Scalar (Strings, Integers, Booleans, ...).
|
|
160
|
-
|
|
161
|
-
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:
|
|
162
|
-
|
|
163
|
-
```graphql
|
|
164
|
-
{
|
|
165
|
-
participatoryProcesses(filter: {publishedSince: "2018-01-01"}, order: {publishedAt: "asc"}) {
|
|
166
|
-
slug
|
|
167
|
-
title {
|
|
168
|
-
translation(locale: "en")
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
Response should look like:
|
|
175
|
-
|
|
176
|
-
```json
|
|
177
|
-
{
|
|
178
|
-
"data": {
|
|
179
|
-
"participatoryProcesses": [
|
|
180
|
-
{
|
|
181
|
-
"slug": "consectetur-at",
|
|
182
|
-
"title": {
|
|
183
|
-
"translation": "Soluta consectetur quos fugit aut."
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
"slug": "nostrum-earum",
|
|
188
|
-
"title": {
|
|
189
|
-
"translation": "Porro hic ipsam cupiditate reiciendis."
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
]
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
#### What happened?
|
|
198
|
-
|
|
199
|
-
In the former query, each keyword represents a type, the words `publishedSince`, `publishedAt`, `slug`, `locale` are scalars, all of them Strings.
|
|
200
|
-
|
|
201
|
-
The other keywords however, are objects representing certain entities:
|
|
202
|
-
|
|
203
|
-
* `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.
|
|
204
|
-
* `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.
|
|
205
|
-
* `order` is a [ParticipatoryProcessSort](#ParticipatoryProcessSort) type, works the same way as the filter but with the goal of ordering the results.
|
|
206
|
-
* `title` is a [TranslatedField](#TranslatedField) type, which allows us to deal with multi-language fields.
|
|
207
|
-
|
|
208
|
-
Finally, note that the returned object is an array, each item of which is a representation of the object we requested.
|
|
209
|
-
|
|
210
|
-
> \***About how filters and sorting are organized**
|
|
211
|
-
>
|
|
212
|
-
> 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".
|
|
213
|
-
>
|
|
214
|
-
> The types used to filter participatory spaces are: [ParticipatoryProcessFilter](#ParticipatoryProcessFilter), [AssemblyFilter](#AssemblyFilter), and so on.
|
|
215
|
-
>
|
|
216
|
-
> Other collections (or connections) may have their own filters (i.e. [ComponentFilter](#ComponentFilter)).
|
|
217
|
-
>
|
|
218
|
-
> 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:
|
|
219
|
-
>
|
|
220
|
-
> We can 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?
|
|
221
|
-
>
|
|
222
|
-
> In our case, we have decided to search only one particular language of a multi-language field but we let you choose which language to search.
|
|
223
|
-
> 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, ...).
|
|
224
|
-
>
|
|
225
|
-
> Example (this is not a real Decidim query):
|
|
226
|
-
>
|
|
227
|
-
> ```graphql
|
|
228
|
-
> some_collection(filter: { locale: "en", title: "ideas"}) {
|
|
229
|
-
> id
|
|
230
|
-
> }
|
|
231
|
-
> ```
|
|
232
|
-
>
|
|
233
|
-
> The same applies to sorting ([ParticipatoryProcessSort](#ParticipatoryProcessSort), [AssemblySort](#AssemblySort), etc.)
|
|
234
|
-
>
|
|
235
|
-
> In this case, the content of the field (*title*) only allows 2 values: *ASC* and *DESC*.
|
|
236
|
-
>
|
|
237
|
-
> Example of ordering alphabetically by the title content in French language:
|
|
238
|
-
>
|
|
239
|
-
> ```graphql
|
|
240
|
-
> some_collection(order: { locale: "en", title: "asc"}) {
|
|
241
|
-
> id
|
|
242
|
-
> }
|
|
243
|
-
> ```
|
|
244
|
-
>
|
|
245
|
-
> Of course, you can combine both filter and order. Also remember to check availability of this type of behaviour for any particular filter/sort.
|
|
246
|
-
|
|
247
|
-
#### Decidim main types
|
|
248
|
-
|
|
249
|
-
Decidim has 2 main types of objects through which content is provided. These are Participatory Spaces and Components.
|
|
250
|
-
|
|
251
|
-
A participatory space is the first level, currently there are 5 officially supported: *Participatory Processes*, *Assemblies*, *Conferences* and *Initiatives*. For each participatory process there will correspond a collection type and a "single item" type.
|
|
252
|
-
|
|
253
|
-
The previous example uses the collection type for participatory processes. You can try `assemblies`, `conferences`, or `initiatives` for the others. Note that each collection can implement their own filter and order types with different properties.
|
|
254
|
-
|
|
255
|
-
As an example for a single item query, you can run:
|
|
256
|
-
|
|
257
|
-
```graphql
|
|
258
|
-
{
|
|
259
|
-
participatoryProcess(slug: "consectetur-at") {
|
|
260
|
-
slug
|
|
261
|
-
title {
|
|
262
|
-
translation(locale: "en")
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
And the response will be:
|
|
269
|
-
|
|
270
|
-
```json
|
|
271
|
-
{
|
|
272
|
-
"data": {
|
|
273
|
-
"participatoryProcess": {
|
|
274
|
-
"slug": "consectetur-at",
|
|
275
|
-
"title": {
|
|
276
|
-
"translation": "Soluta consectetur quos fugit aut."
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
#### What is different?
|
|
284
|
-
|
|
285
|
-
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.
|
|
286
|
-
|
|
287
|
-
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.
|
|
288
|
-
|
|
289
|
-
> \* The `slug` is a convenient way to find a participatory space as is (usually) in the URL.
|
|
290
|
-
>
|
|
291
|
-
> For instance, consider this real case from Barcelona:
|
|
292
|
-
>
|
|
293
|
-
> https://www.decidim.barcelona/processes/patrimonigracia
|
|
294
|
-
>
|
|
295
|
-
> The word `patrimonigracia` indicates the "slug".
|
|
296
|
-
|
|
297
|
-
#### Components
|
|
298
|
-
|
|
299
|
-
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.
|
|
300
|
-
|
|
301
|
-
If you know the `id`\* of a specific component you can obtain it by querying it directly:
|
|
302
|
-
|
|
303
|
-
```graphql
|
|
304
|
-
{
|
|
305
|
-
component(id:2) {
|
|
306
|
-
id
|
|
307
|
-
name {
|
|
308
|
-
translation(locale:"en")
|
|
309
|
-
}
|
|
310
|
-
__typename
|
|
311
|
-
participatorySpace {
|
|
312
|
-
id
|
|
313
|
-
type
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
Response:
|
|
320
|
-
|
|
321
|
-
```json
|
|
322
|
-
{
|
|
323
|
-
"data": {
|
|
324
|
-
"component": {
|
|
325
|
-
"id": "2",
|
|
326
|
-
"name": {
|
|
327
|
-
"translation": "Meetings"
|
|
328
|
-
},
|
|
329
|
-
"__typename": "Meetings",
|
|
330
|
-
"participatorySpace": {
|
|
331
|
-
"id": "1",
|
|
332
|
-
"type": "Decidim::ParticipatoryProcess"
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
The process is analogue as what has been explained in the case of searching for one specific participatory process.
|
|
340
|
-
|
|
341
|
-
> \*Note that the `id` of a component is present also in the URL after the letter "f":
|
|
342
|
-
>
|
|
343
|
-
> https://www.decidim.barcelona/processes/patrimonigracia/f/3257/
|
|
344
|
-
>
|
|
345
|
-
> In this case, 3257.
|
|
346
|
-
|
|
347
|
-
##### What about component's collections?
|
|
348
|
-
|
|
349
|
-
Glad you asked, component's collections cannot be retrieved directly, the are available *in the context* of a participatory space.
|
|
350
|
-
|
|
351
|
-
For instance, we can query all the components in an particular Assembly as follows:
|
|
352
|
-
|
|
353
|
-
```graphql
|
|
354
|
-
{
|
|
355
|
-
assembly(id: 3) {
|
|
356
|
-
components {
|
|
357
|
-
id
|
|
358
|
-
name {
|
|
359
|
-
translation(locale: "en")
|
|
360
|
-
}
|
|
361
|
-
__typename
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
The response will be similar to:
|
|
368
|
-
|
|
369
|
-
```json
|
|
370
|
-
{
|
|
371
|
-
"data": {
|
|
372
|
-
"assembly": {
|
|
373
|
-
"components": [
|
|
374
|
-
{
|
|
375
|
-
"id": "42",
|
|
376
|
-
"name": {
|
|
377
|
-
"translation": "Accountability"
|
|
378
|
-
},
|
|
379
|
-
"__typename": "Component"
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
"id": "38",
|
|
383
|
-
"name": {
|
|
384
|
-
"translation": "Meetings"
|
|
385
|
-
},
|
|
386
|
-
"__typename": "Meetings"
|
|
387
|
-
},
|
|
388
|
-
{
|
|
389
|
-
"id": "37",
|
|
390
|
-
"name": {
|
|
391
|
-
"translation": "Page"
|
|
392
|
-
},
|
|
393
|
-
"__typename": "Pages"
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
"id": "39",
|
|
397
|
-
"name": {
|
|
398
|
-
"translation": "Proposals"
|
|
399
|
-
},
|
|
400
|
-
"__typename": "Proposals"
|
|
401
|
-
}
|
|
402
|
-
]
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
```
|
|
407
|
-
|
|
408
|
-
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*:
|
|
409
|
-
|
|
410
|
-
```graphql
|
|
411
|
-
{
|
|
412
|
-
assembly(id: 2) {
|
|
413
|
-
components(filter: {withGeolocationEnabled: true}) {
|
|
414
|
-
id
|
|
415
|
-
name {
|
|
416
|
-
translation(locale: "en")
|
|
417
|
-
}
|
|
418
|
-
__typename
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
The response:
|
|
425
|
-
|
|
426
|
-
```json
|
|
427
|
-
{
|
|
428
|
-
"data": {
|
|
429
|
-
"assembly": {
|
|
430
|
-
"components": [
|
|
431
|
-
{
|
|
432
|
-
"id": "39",
|
|
433
|
-
"name": {
|
|
434
|
-
"translation": "Meetings"
|
|
435
|
-
},
|
|
436
|
-
"__typename": "Meetings"
|
|
437
|
-
}
|
|
438
|
-
]
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
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.
|
|
445
|
-
|
|
446
|
-
### Polymorphism and connections
|
|
447
|
-
|
|
448
|
-
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.
|
|
449
|
-
|
|
450
|
-
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*.
|
|
451
|
-
|
|
452
|
-
Another example are the case of linked resources, these are properties that may link objects of different nature between components or participatory spaces.
|
|
453
|
-
|
|
454
|
-
In a very simplified way (to know more please refer to the official guide), GraphQL polymorphism is handled through the operator `... on`. You will know when a field is polymorphic because the property `__typename`, which tells you the type of that particular object, will change accordingly.
|
|
455
|
-
|
|
456
|
-
In the previous examples we have queried for this property:
|
|
457
|
-
|
|
458
|
-
Response fragment:
|
|
459
|
-
|
|
460
|
-
```json
|
|
461
|
-
"components": [
|
|
462
|
-
{
|
|
463
|
-
"id": "38",
|
|
464
|
-
"name": {
|
|
465
|
-
"translation": "Meetings"
|
|
466
|
-
},
|
|
467
|
-
"__typename": "Meetings"
|
|
468
|
-
}
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
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:
|
|
472
|
-
|
|
473
|
-
```graphql
|
|
474
|
-
{
|
|
475
|
-
assembly(id: 2) {
|
|
476
|
-
components {
|
|
477
|
-
id
|
|
478
|
-
... on Proposals {
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
Consider this query:
|
|
487
|
-
|
|
488
|
-
```graphql
|
|
489
|
-
{
|
|
490
|
-
assembly(id: 3) {
|
|
491
|
-
components(filter: {type: "Proposals"}) {
|
|
492
|
-
id
|
|
493
|
-
name {
|
|
494
|
-
translation(locale: "en")
|
|
495
|
-
}
|
|
496
|
-
... on Proposals {
|
|
497
|
-
proposals(order: {likeCount: "desc"}, first: 2) {
|
|
498
|
-
edges {
|
|
499
|
-
node {
|
|
500
|
-
id
|
|
501
|
-
likes {
|
|
502
|
-
name
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
The response:
|
|
514
|
-
|
|
515
|
-
```json
|
|
516
|
-
{
|
|
517
|
-
"data": {
|
|
518
|
-
"assembly": {
|
|
519
|
-
"components": [
|
|
520
|
-
{
|
|
521
|
-
"id": "39",
|
|
522
|
-
"name": {
|
|
523
|
-
"translation": "Proposals"
|
|
524
|
-
},
|
|
525
|
-
"proposals": {
|
|
526
|
-
"edges": [
|
|
527
|
-
{
|
|
528
|
-
"node": {
|
|
529
|
-
"id": "35",
|
|
530
|
-
"likes": [
|
|
531
|
-
{
|
|
532
|
-
"name": "Ms. Johnathon Schaefer"
|
|
533
|
-
},
|
|
534
|
-
{
|
|
535
|
-
"name": "Linwood Lakin PhD 3 4 endr1"
|
|
536
|
-
},
|
|
537
|
-
{
|
|
538
|
-
"name": "Gracie Emmerich"
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
"name": "Randall Rath 3 4 endr3"
|
|
542
|
-
},
|
|
543
|
-
{
|
|
544
|
-
"name": "Jolene Schmitt MD"
|
|
545
|
-
},
|
|
546
|
-
{
|
|
547
|
-
"name": "Clarence Hammes IV 3 4 endr5"
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
"name": "Omar Mayer"
|
|
551
|
-
},
|
|
552
|
-
{
|
|
553
|
-
"name": "Raymundo Jaskolski 3 4 endr7"
|
|
554
|
-
}
|
|
555
|
-
]
|
|
556
|
-
}
|
|
557
|
-
},
|
|
558
|
-
{
|
|
559
|
-
"node": {
|
|
560
|
-
"id": "33",
|
|
561
|
-
"likes": [
|
|
562
|
-
{
|
|
563
|
-
"name": "Spring Brakus"
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
"name": "Reiko Simonis IV 3 2 endr1"
|
|
567
|
-
},
|
|
568
|
-
{
|
|
569
|
-
"name": "Dr. Jim Denesik"
|
|
570
|
-
},
|
|
571
|
-
{
|
|
572
|
-
"name": "Dr. Mack Schoen 3 2 endr3"
|
|
573
|
-
}
|
|
574
|
-
]
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
]
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
]
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
#### What is going on?
|
|
587
|
-
|
|
588
|
-
Until the `... on Proposals` line, there is 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.
|
|
589
|
-
|
|
590
|
-
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.
|
|
591
|
-
|
|
592
|
-
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.
|
|
593
|
-
|
|
594
|
-
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:
|
|
595
|
-
|
|
596
|
-
* `first`: Returns the first *n* elements from the list
|
|
597
|
-
* `after`: Returns the elements in the list that come after the specified *cursor*
|
|
598
|
-
* `last`: Returns the last *n* elements from the list
|
|
599
|
-
* `before`: Returns the elements in the list that come before the specified *cursor*
|
|
600
|
-
|
|
601
|
-
Example:
|
|
602
|
-
|
|
603
|
-
```graphql
|
|
604
|
-
{
|
|
605
|
-
assembly(id: 3) {
|
|
606
|
-
components(filter: {type: "Proposals"}) {
|
|
607
|
-
id
|
|
608
|
-
name {
|
|
609
|
-
translation(locale: "en")
|
|
610
|
-
}
|
|
611
|
-
... on Proposals {
|
|
612
|
-
proposals(first:2,after:"Mg") {
|
|
613
|
-
pageInfo {
|
|
614
|
-
endCursor
|
|
615
|
-
startCursor
|
|
616
|
-
hasPreviousPage
|
|
617
|
-
hasNextPage
|
|
618
|
-
}
|
|
619
|
-
edges {
|
|
620
|
-
node {
|
|
621
|
-
id
|
|
622
|
-
likes {
|
|
623
|
-
name
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
Being the response:
|
|
635
|
-
|
|
636
|
-
```json
|
|
637
|
-
{
|
|
638
|
-
"data": {
|
|
639
|
-
"assembly": {
|
|
640
|
-
"components": [
|
|
641
|
-
{
|
|
642
|
-
"id": "39",
|
|
643
|
-
"name": {
|
|
644
|
-
"translation": "Proposals"
|
|
645
|
-
},
|
|
646
|
-
"proposals": {
|
|
647
|
-
"pageInfo": {
|
|
648
|
-
"endCursor": "NA",
|
|
649
|
-
"startCursor": "Mw",
|
|
650
|
-
"hasPreviousPage": false,
|
|
651
|
-
"hasNextPage": true
|
|
652
|
-
},
|
|
653
|
-
"edges": [
|
|
654
|
-
{
|
|
655
|
-
"node": {
|
|
656
|
-
"id": "32",
|
|
657
|
-
"likes": []
|
|
658
|
-
}
|
|
659
|
-
},
|
|
660
|
-
{
|
|
661
|
-
"node": {
|
|
662
|
-
"id": "31",
|
|
663
|
-
"likes": [
|
|
664
|
-
{
|
|
665
|
-
"name": "Mr. Nicolas Raynor"
|
|
666
|
-
},
|
|
667
|
-
{
|
|
668
|
-
"name": "Gerry Fritsch PhD 3 1 endr1"
|
|
669
|
-
}
|
|
670
|
-
]
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
]
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
]
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
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.
|
|
683
|
-
|
|
684
|
-
For more info on how connections work, you can check the official guide:
|
|
685
|
-
|
|
686
|
-
https://graphql.org/learn/pagination/
|
|
55
|
+
For additional examples of queries and mutations, check the additional [GraphQL API documentation](https://docs.decidim.org/en/develop/develop/api/index.html) of Decidim.
|
|
@@ -11,8 +11,7 @@ module Decidim
|
|
|
11
11
|
mod = obj.manifest_name.camelize
|
|
12
12
|
"Decidim::#{mod}::#{mod}MutationType".constantize
|
|
13
13
|
rescue NameError
|
|
14
|
-
|
|
15
|
-
nil
|
|
14
|
+
raise GraphQL::ExecutionError, "Mutation type not found for #{mod}"
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
end
|