@magek/mcp-server 0.0.8

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.
Files changed (52) hide show
  1. package/README.md +42 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +25 -0
  4. package/dist/prompts/cqrs-flow.d.ts +15 -0
  5. package/dist/prompts/cqrs-flow.js +252 -0
  6. package/dist/prompts/troubleshooting.d.ts +15 -0
  7. package/dist/prompts/troubleshooting.js +239 -0
  8. package/dist/resources/cli-reference.d.ts +13 -0
  9. package/dist/resources/cli-reference.js +193 -0
  10. package/dist/resources/documentation.d.ts +18 -0
  11. package/dist/resources/documentation.js +62 -0
  12. package/dist/server.d.ts +5 -0
  13. package/dist/server.js +127 -0
  14. package/dist/utils/docs-loader.d.ts +19 -0
  15. package/dist/utils/docs-loader.js +111 -0
  16. package/docs/advanced/custom-templates.md +96 -0
  17. package/docs/advanced/data-migrations.md +181 -0
  18. package/docs/advanced/environment-configuration.md +74 -0
  19. package/docs/advanced/framework-packages.md +17 -0
  20. package/docs/advanced/health/sensor-health.md +389 -0
  21. package/docs/advanced/instrumentation.md +135 -0
  22. package/docs/advanced/register.md +119 -0
  23. package/docs/advanced/sensor.md +10 -0
  24. package/docs/advanced/testing.md +96 -0
  25. package/docs/advanced/touch-entities.md +45 -0
  26. package/docs/architecture/command.md +367 -0
  27. package/docs/architecture/entity.md +214 -0
  28. package/docs/architecture/event-driven.md +30 -0
  29. package/docs/architecture/event-handler.md +108 -0
  30. package/docs/architecture/event.md +145 -0
  31. package/docs/architecture/notifications.md +54 -0
  32. package/docs/architecture/queries.md +207 -0
  33. package/docs/architecture/read-model.md +507 -0
  34. package/docs/contributing.md +349 -0
  35. package/docs/docs-index.json +200 -0
  36. package/docs/features/error-handling.md +204 -0
  37. package/docs/features/event-stream.md +35 -0
  38. package/docs/features/logging.md +81 -0
  39. package/docs/features/schedule-actions.md +44 -0
  40. package/docs/getting-started/ai-coding-assistants.md +181 -0
  41. package/docs/getting-started/coding.md +543 -0
  42. package/docs/getting-started/installation.md +143 -0
  43. package/docs/graphql.md +1213 -0
  44. package/docs/index.md +62 -0
  45. package/docs/introduction.md +58 -0
  46. package/docs/magek-arch.png +0 -0
  47. package/docs/magek-cli.md +67 -0
  48. package/docs/magek-logo.svg +1 -0
  49. package/docs/security/authentication.md +189 -0
  50. package/docs/security/authorization.md +242 -0
  51. package/docs/security/security.md +16 -0
  52. package/package.json +46 -0
@@ -0,0 +1,1213 @@
1
+ ---
2
+ title: "GraphQL API"
3
+ group: "Guides"
4
+ ---
5
+
6
+ # GraphQL API
7
+
8
+ This is the main API of your application, as it allows you to:
9
+
10
+ - _Modify_ data by **sending commands**.
11
+ - _Read_ data by **querying read models**.
12
+ - _Receive data in real time_ by **subscribing to read models**.
13
+
14
+ All this is done through [GraphQL](https://graphql.org/), a query language for APIs that has useful advantages over simple REST APIs.
15
+
16
+ If you are not familiar with GraphQL, then, first of all, don't worry!
17
+ _Using_ a GraphQL API is simple and straightforward.
18
+ _Implementing it_ on the server side is usually the hard part, as you need to define your schema, operations, resolvers, etc.
19
+ Luckily, you can forget about that because Magek does all the work for you!
20
+
21
+ The GraphQL API is fully **auto-generated** based on your _commands_ and _read models_.
22
+
23
+ > **Note:**
24
+ > To get the full potential of the GraphQL API, it is **not** recommended to use `interface` types in any command or read model attributes. Use `class` types instead. This will allow you to perform complex graphQL filters, including over nested attributes. There's an example below:
25
+
26
+ ```typescript
27
+ // My type
28
+ export class ItemWithQuantity {
29
+ // Use "class", not "interface"
30
+ @field()
31
+ sku!: string
32
+
33
+ @field()
34
+ quantity!: number
35
+ }
36
+ ```
37
+
38
+ ```typescript
39
+ // The read-model file
40
+ @ReadModel({
41
+ authorize: 'all'
42
+ })
43
+ export class CartReadModel {
44
+ @field(type => UUID)
45
+ readonly id!: UUID
46
+
47
+ @field()
48
+ item!: ItemWithQuantity // As ItemWithQuantity is a class, you will be able to query over nested attributes like item `quantity`
49
+ ```
50
+
51
+ ## Relationship between GraphQL operations and commands and read models
52
+
53
+ GraphQL defines three kinds of operations that you can use: _mutations_, _queries_, and _subscriptions_.
54
+
55
+ The names are pretty meaningful, but we can say that you use a `mutation` when you want to change data, a `query` when you want to get
56
+ data on-demand, and a `subscription` when you want to receive data at the moment it is updated.
57
+
58
+ Knowing this, you can infer the relationship between those operations and your Magek components:
59
+
60
+ - You _send_ a **command** using a **mutation**.
61
+ - You _read_ a **read model** using a **query**.
62
+ - You _subscribe_ to a **read model** using a **subscription**.
63
+
64
+ ## How to send GraphQL request
65
+
66
+ GraphQL uses two existing protocols:
67
+
68
+ - _HTTP_ for `mutation` and `query` operations.
69
+ - _WebSocket_ for `subscription` operations.
70
+
71
+ The reason for the WebSocket protocol is that, in order for subscriptions to work, there must be a way for the server to send data to clients when it is changed. HTTP doesn't allow that, as it is the client the one which always initiates the request.
72
+
73
+ So you should use the **graphqlURL** to send GraphQL queries and mutations, and the **websocketURL** to send subscriptions. You can see both URLs when starting your application.
74
+
75
+ Therefore:
76
+
77
+ - To send a GraphQL mutation/query, you send an HTTP request to _"<graphqlURL>"_, with _method POST_, and a _JSON-encoded body_ with the mutation/query details.
78
+ - To send a GraphQL subscription, you first connect to the _"<websocketURL>"_, and then send a _JSON-encoded message_ with the subscription details, _following [the "GraphQL over WebSocket" protocol](#the-graphql-over-websocket-protocol)_.
79
+
80
+ > **Note:**
81
+ > You can also **send queries and mutations through the WebSocket** if that's convenient to you. See ["The GraphQL over WebSocket protocol"](#the-graphql-over-websocket-protocol) to know more.
82
+
83
+ While it is OK to know how to manually send GraphQL request, you normally don't need to deal with this low-level details, especially with the WebSocket stuff.
84
+
85
+ To have a great developer experience, we **strongly recommend** to use a GraphQL client for your platform of choice. Here are some great ones:
86
+
87
+ - **[Altair](https://altair.sirmuel.design/)**: Ideal for testing sending manual requests, getting the schema, etc.
88
+ - **Apollo clients**: These are the "go-to" SDKs to interact with a GraphQL API from your clients. It is very likely that there is a version for your client programming language. Check the ["Using Apollo Client"](#using-apollo-client) section to know more about this.
89
+
90
+ ## Get GraphQL schema from your application
91
+
92
+ After starting your application, you can get your GraphQL schema by using a tool like **[Altair](https://altair.sirmuel.design/)**. The graphqlURL endpoint has the following pattern:
93
+
94
+ `https://<base_url>/<environment>/graphql`
95
+
96
+ By entering this URL in Altair, the schema can be displayed as shown in the screenshot (You need to click on the Docs button in the URL bar). You can
97
+ check the available Queries and Mutations by clicking on their name:
98
+
99
+ ![Altair queries](/img/altair-queries.png)
100
+ ![Altair mutations](/img/altair-mutations.png)
101
+
102
+ ## Sending commands
103
+
104
+ As mentioned in the previous section, we need to use a "mutation" to send a command. The structure of a mutation (the body of the request) is the following:
105
+
106
+ ```graphql
107
+ mutation {
108
+ command_name(input: {
109
+ input_field_list
110
+ })
111
+ }
112
+ ```
113
+
114
+ Where:
115
+
116
+ - _**command_name**_ is the name of the class corresponding to the command you want to send
117
+ - _**input_field_list**_ is a list of pairs in the form of `fieldName: fieldValue` containing the data of your command. The field names correspond to the names of the properties you defined in the command class.
118
+
119
+ In the following example we send a command named "ChangeCart" that will add/remove an item to/from a shopping cart. The command requires the ID of the cart (`cartId`), the item identifier (`sku`) and the quantity of units we are adding/removing (`quantity`).
120
+
121
+ ```text
122
+ URL: "<graphqlURL>"
123
+ ```
124
+
125
+ ```graphql
126
+ mutation {
127
+ ChangeCart(input: { cartId: "demo", sku: "ABC_01", quantity: 2 })
128
+ }
129
+ ```
130
+
131
+ In case we are not using any GraphQL client, this would be the equivalent bare HTTP request:
132
+
133
+ ```text
134
+ URL: "<graphqlURL>"
135
+ METHOD: "POST"
136
+ ```
137
+
138
+ ```json
139
+ {
140
+ "query": "mutation { ChangeCart(input: { cartId: \"demo\" sku: \"ABC_01\" quantity: 2 }) }"
141
+ }
142
+ ```
143
+
144
+ And this would be the response:
145
+
146
+ ```json
147
+ {
148
+ "data": {
149
+ "ChangeCart": true
150
+ }
151
+ }
152
+ ```
153
+
154
+ > **Note:**
155
+ > Remember to set the proper **access token** for secured commands, check ["Authorizing operations"](#authorizing-operations).
156
+
157
+ ## Reading read models
158
+
159
+ To read a specific read model, we need to use a "query" operation. The structure of the "query" (the body
160
+ of the request) is the following:
161
+
162
+ ```graphql
163
+ query {
164
+ read_model_name(id: "<id of the read model>") {
165
+ selection_field_list
166
+ }
167
+ }
168
+ ```
169
+
170
+ Where:
171
+
172
+ - _read_model_name_ is the name of the class corresponding to the read model you want to retrieve.
173
+ - _&lt;id of the read model&gt;_ is the ID of the specific read model instance you are interested in.
174
+ - _selection_field_list_ is a list with the names of the specific read model fields you want to get as response.
175
+
176
+ In the following example we send a query to read a read model named `CartReadModel` whose ID is `demo`. We get back its `id` and the list of cart `items` as response.
177
+
178
+ ```text
179
+ URL: "<graphqlURL>"
180
+ ```
181
+
182
+ ```graphql
183
+ query {
184
+ CartReadModel(id: "demo") {
185
+ id
186
+ items
187
+ }
188
+ }
189
+ ```
190
+
191
+ In case we are not using any GraphQL client, this would be the equivalent bare HTTP request:
192
+
193
+ ```text
194
+ URL: "<graphqlURL>"
195
+ METHOD: "POST"
196
+ ```
197
+
198
+ ```json
199
+ {
200
+ "query": "query { CartReadModel(id: \"demo\") { id items } }"
201
+ }
202
+ ```
203
+
204
+ And we would get the following as response:
205
+
206
+ ```json
207
+ {
208
+ "data": {
209
+ "CartReadModel": {
210
+ "id": "demo",
211
+ "items": [
212
+ {
213
+ "sku": "ABC_01",
214
+ "quantity": 2
215
+ }
216
+ ]
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ > **Note:**
223
+ > Remember to set the proper **access token** for secured read models, check ["Authorizing operations"](#authorizing-operations).
224
+
225
+ ## Subscribing to read models
226
+
227
+ To subscribe to a specific read model, we need to use a subscription operation, and it must be _sent through the **websocketURL**_ using the [_GraphQL over WebSocket_ protocol](#the-graphql-over-websocket-protocol).
228
+
229
+ Doing this process manually is a bit cumbersome. _You will probably never need to do this_, as GraphQL clients like [Apollo](#using-apollo-client) abstract this process away. However, we will explain how to do it for learning purposes.
230
+
231
+ Before sending any subscription, you need to _connect_ to the WebSocket to open the two-way communication channel. This connection is done differently depending on the client/library you use to manage web sockets. In this section, we will show examples using the [`wscat`](https://github.com/websockets/wscat) command line program. You can also use the online tool [Altair](https://altair.sirmuel.design/)
232
+
233
+ Once you have connected successfully, you can use this channel to:
234
+
235
+ - Send the subscription messages.
236
+ - Listen for messages sent by the server with data corresponding to your active subscriptions.
237
+
238
+ The structure of the "subscription" (the body of the message) is exactly the same as the "query" operation:
239
+
240
+ ```graphql
241
+ subscription {
242
+ read_model_name(id: "<id of the read model>") {
243
+ selection_field_list
244
+ }
245
+ }
246
+ ```
247
+
248
+ Where:
249
+
250
+ - _read_model_name_ is the name of the class corresponding to the read model you want to subscribe to.
251
+ - _&lt;id of the read model&gt;_ is the ID of the specific read model instance you are interested in.
252
+ - _selection_field_list_ is a list with the names of the specific read model fields you want to get when data is sent back to you.
253
+
254
+ In the following examples we use [`wscat`](https://github.com/websockets/wscat) to connect to the web socket. After that, we send the required messages to conform the [_GraphQL over WebSocket_ protocol](#the-graphql-over-websocket-protocol), including the subscription operation to the read model `CartReadModel` with ID `demo`.
255
+
256
+ 1. Connect to the web socket:
257
+
258
+ ```sh
259
+ wscat -c <websocketURL> -s graphql-ws
260
+ ```
261
+
262
+ > **Note:**
263
+ > You should specify the `graphql-ws` subprotocol when connecting with your client via the `Sec-WebSocket-Protocol` header (in this case, `wscat` does that when you use the `-s` option).
264
+
265
+ Now we can start sending messages just by writing them and hitting the <kbd>Enter</kbd> key.
266
+
267
+ 2. Initiate the protocol connection :
268
+
269
+ ```json
270
+ { "type": "connection_init" }
271
+ ```
272
+
273
+ In case you want to authorize the connection, you need to send the authorization token in the `payload.Authorization` field:
274
+
275
+ ```json
276
+ { "type": "connection_init", "payload": { "Authorization": "<your token>" } }
277
+ ```
278
+
279
+ 3. Send a message with the subscription. We need to provide an ID for the operation. When the server sends us data back, it will include this same ID so that we know which subscription the received data belongs to (again, this is just for learning, [GraphQL clients](#using-apollo-client) manages this for you)
280
+
281
+ ```json
282
+ { "id": "1", "type": "start", "payload": { "query": "subscription { CartReadModel(id:\"demo\") { id items } }" } }
283
+ ```
284
+
285
+ After a successful subscription, you won't receive anything in return. Now, every time the read model you subscribed to is modified, a new incoming message will appear in the socket with the updated version of the read model. This message will have exactly the same format as if you were done a query with the same parameters.
286
+
287
+ Following with the previous example, we now send a command (using a mutation operation) that adds a new item with sku "ABC_02" to the `CartReadModel`. After it has been added, we receive the updated version of the read model through the socket.
288
+
289
+ 1. Send the following command (this time using an HTTP request):
290
+
291
+ ```
292
+ URL: "<graphqlURL>"
293
+ ```
294
+
295
+ ```graphql
296
+ mutation {
297
+ ChangeCart(input: { cartId: "demo", sku: "ABC_02", quantity: 3 })
298
+ }
299
+ ```
300
+
301
+ 2. The following message (after formatting it) appears through the socket connection we had opened:
302
+
303
+ ```json
304
+ {
305
+ "id": "1",
306
+ "type": "data",
307
+ "payload": {
308
+ "data": {
309
+ "CartReadModel": {
310
+ "id": "demo",
311
+ "items": [
312
+ {
313
+ "sku": "ABC_01",
314
+ "quantity": 2
315
+ },
316
+ {
317
+ "sku": "ABC_02",
318
+ "quantity": 3
319
+ }
320
+ ]
321
+ }
322
+ }
323
+ }
324
+ }
325
+ ```
326
+
327
+ > **Note:**
328
+ > Remember that, in case you want to subscribe to a read model that is restricted to a specific set of roles, you must send the **access token** retrieved upon sign-in. Check ["Authorizing operations"](#authorizing-operations) to know how to do this.
329
+
330
+ > **Note:**
331
+ > You can disable the creation of all the infrastructure and functionality needed to manage subscriptions by setting `config.enableSubscriptions=false` in your `Magek.config` block
332
+
333
+ ## Non exposing properties and parameters
334
+
335
+ By default, all properties and parameters of the command constructor and/or read model are accessible through GraphQL. It is possible to not expose any of them adding the `@nonExposed` annotation to the constructor property or parameter.
336
+
337
+ Example
338
+ ```typescript
339
+ @ReadModel({
340
+ authorize: 'all',
341
+ })
342
+ export class CartReadModel {
343
+ @field(type => UUID)
344
+ readonly id!: UUID
345
+
346
+ @field()
347
+ readonly cartItems!: Array<CartItem>
348
+
349
+ @field()
350
+ readonly checks!: number
351
+
352
+ @field()
353
+ public shippingAddress?: Address
354
+
355
+ @field()
356
+ public payment?: Payment
357
+
358
+ @field()
359
+ public cartItemsIds?: Array<string>
360
+
361
+ @nonExposed
362
+ private internalProperty?: number
363
+
364
+ @field()
365
+ @nonExposed
366
+ readonly internalParameter?: number
367
+ }
368
+ ```
369
+
370
+ ## Adding before hooks to your read models
371
+
372
+ When you send queries or subscriptions to your read models, you can tell Magek to execute some code before executing the operation. These are called `before` hooks, and they receive a `ReadModelRequestEnvelope` object representing the current request.
373
+
374
+ ```typescript
375
+ interface ReadModelRequestEnvelope<TReadModel> {
376
+ currentUser?: UserEnvelope // The current authenticated user
377
+ requestID: UUID // An ID assigned to this request
378
+ key?: { // If present, contains the id and sequenceKey that identify a specific read model
379
+ id: UUID
380
+ sequenceKey?: SequenceKey
381
+ }
382
+ className: string // The read model class name
383
+ filters: ReadModelRequestProperties<TReadModel> // Filters set in the GraphQL query
384
+ limit?: number // Query limit if set
385
+ afterCursor?: unknown // For paginated requests, id to start reading from
386
+ }
387
+ ```
388
+
389
+ In before hooks, you can either abort the request or alter and return the request object to change the behavior of your request. Before hooks are useful for many use cases, but they're especially useful to add fine-grained access control. For example, to enforce a filter that restrict a logged in user to access only read models objects they own.
390
+
391
+ When a `before` hook throws an exception, the request is aborted and the error is sent back to the user. In order to continue with the request, it's required that the request object is returned.
392
+
393
+ In order to define a before hook you pass a list of functions with the right signature to the read model decorator `before` parameter:
394
+
395
+ ```typescript
396
+ @ReadModel({
397
+ authorize: [User],
398
+ before: [validateUser],
399
+ })
400
+ export class CartReadModel {
401
+ @field(type => UUID)
402
+ readonly id!: UUID
403
+
404
+ @field()
405
+ readonly userId!: UUID
406
+
407
+ // Your projections go here
408
+ }
409
+
410
+ function validateUser(request: ReadModelRequestEnvelope<CartReadModel>): ReadModelRequestEnvelope<CartReadModel> {
411
+ if (request.filters?.userId?.eq !== request.currentUser?.id) throw NotAuthorizedError("...")
412
+ return request
413
+ }
414
+ ```
415
+
416
+ You can also define more than one `before` hook for a read model, and they will be chained, sending the resulting request object from a hook to the next one.
417
+
418
+ > **Note:**
419
+ > The order in which filters are specified matters.
420
+
421
+ ```typescript
422
+ @ReadModel({
423
+ authorize: [User],
424
+ before: [validateUser, validateEmail, changeFilters],
425
+ })
426
+ export class CartReadModel {
427
+ @field(type => UUID)
428
+ readonly id!: UUID
429
+
430
+ @field()
431
+ readonly userId!: UUID
432
+
433
+ // Your projections go here
434
+ }
435
+
436
+ function validateUser(request: ReadModelRequestEnvelope<CartReadModel>): ReadModelRequestEnvelope<CartReadModel> {
437
+ if (request.filters?.userId?.eq !== request.currentUser?.id) throw NotAuthorizedError("...")
438
+ return request
439
+ }
440
+
441
+ function validateEmail(request: ReadModelRequestEnvelope<CartReadModel>): ReadModelRequestEnvelope<CartReadModel> {
442
+ if (!request.filters.email.includes('myCompanyDomain.com')) throw NotAuthorizedError("...")
443
+ return request
444
+ }
445
+ ```
446
+
447
+ ## Adding before hooks to your commands
448
+
449
+ You can use `before` hooks also in your command handlers, and [they work as the Read Models ones](#Adding-before-hooks-to-your-read-models), with a slight difference: **we don't modify `filters` but `inputs` (the parameters sent with a command)**. Apart from that, it's pretty much the same, here's an example:
450
+
451
+ ```typescript
452
+ @Command({
453
+ authorize: [User],
454
+ before: [beforeFn],
455
+ })
456
+ export class ChangeCartItem {
457
+ @field()
458
+ readonly cartId!: UUID
459
+
460
+ @field()
461
+ readonly productId!: UUID
462
+
463
+ @field()
464
+ readonly quantity!: number
465
+ }
466
+
467
+ function beforeFn(input: CommandInput, currentUser?: UserEnvelope): CommandInput {
468
+ if (input.cartUserId !== currentUser.id) {
469
+ throw NonAuthorizedUserException() // We don't let this user to trigger the command
470
+ }
471
+ return input
472
+ }
473
+ ```
474
+
475
+ As you can see, we just check if the `cartUserId` is equal to the `currentUser.id`, which is the user id extracted from the auth token. This way, we can throw an exception and avoid this user to call this command.
476
+
477
+ ## Adding before hooks to your queries
478
+
479
+ You can use `before` hooks also in your queries, and [they work as the Read Models ones](#Adding-before-hooks-to-your-read-models), with a slight difference: **we don't modify `filters` but `inputs` (the parameters sent with a query)**. Apart from that, it's pretty much the same, here's an example:
480
+
481
+ ```typescript
482
+ @Query({
483
+ authorize: 'all',
484
+ before: [CartTotalQuantity.beforeFn],
485
+ })
486
+ export class CartTotalQuantity {
487
+ @field()
488
+ readonly cartId!: UUID
489
+
490
+ @field()
491
+ @nonExposed
492
+ readonly multiply!: number
493
+
494
+ public static async beforeFn(input: QueryInput, currentUser?: UserEnvelope): Promise<QueryInput> {
495
+ input.multiply = 100
496
+ return input
497
+ }
498
+ }
499
+ ```
500
+
501
+ ## Reading events
502
+
503
+ You can also fetch events directly if you need. To do so, there are two kind of queries that have the following structure:
504
+
505
+ ```graphql
506
+ query {
507
+ eventsByEntity(entity: <name of entity>, entityID: "<id of the entity>") {
508
+ selection_field_list
509
+ }
510
+ }
511
+
512
+ query {
513
+ eventsByType(type: <name of event>) {
514
+ selection_field_list
515
+ }
516
+ }
517
+ ```
518
+
519
+ Where:
520
+
521
+ - _&lt;name of your entity&gt;_ is the name of the class corresponding to the entity whose events you want to retrieve.
522
+ - _&lt;id of the entity&gt;_ is the ID of the specific entity instance whose events you are interested in. **This is optional**
523
+ - _&lt;name of event&gt;_ is the name of the class corresponding to the event type whose instances you want to retrieve.
524
+ - _selection_field_list_ is a list with the names of the specific fields you want to get as response. See the response example below to know more.
525
+
526
+ ### Examples
527
+
528
+ ```text
529
+ URL: "<graphqlURL>"
530
+ ```
531
+
532
+ **A) Read all events associated with a specific instance (a specific ID) of the entity Cart**
533
+
534
+ ```graphql
535
+ query {
536
+ eventsByEntity(entity: Cart, entityID: "ABC123") {
537
+ type
538
+ entity
539
+ entityID
540
+ requestID
541
+ createdAt
542
+ value
543
+ }
544
+ }
545
+ ```
546
+
547
+ **B) Read all events associated with any instance of the entity Cart**
548
+
549
+ ```graphql
550
+ query {
551
+ eventsByEntity(entity: Cart) {
552
+ type
553
+ entity
554
+ entityID
555
+ requestID
556
+ createdAt
557
+ value
558
+ }
559
+ }
560
+ ```
561
+
562
+ For these cases, you would get an array of event _envelopes_ as a response. This means that you get some metadata related to the event along with the event content, which can be found inside the `"value"` field.
563
+
564
+ The response look like this:
565
+
566
+ ```json
567
+ {
568
+ "data": {
569
+ "eventsByEntity": [
570
+ {
571
+ "type": "CartItemChanged",
572
+ "entity": "Cart",
573
+ "entityID": "ABC123",
574
+ "requestID": "7a9cc6a7-7c7f-4ef0-aef1-b226ae4d94fa",
575
+ "createdAt": "2021-05-12T08:41:13.792Z",
576
+ "value": {
577
+ "productId": "73f7818c-f83e-4482-be49-339c004b6fdf",
578
+ "cartId": "ABC123",
579
+ "quantity": 2
580
+ }
581
+ }
582
+ ]
583
+ }
584
+ }
585
+ ```
586
+
587
+ **C) Read events of a specific type**
588
+
589
+ ```graphql
590
+ query {
591
+ eventsByType(type: CartItemChanged) {
592
+ type
593
+ entity
594
+ entityID
595
+ requestID
596
+ createdAt
597
+ value
598
+ }
599
+ }
600
+ ```
601
+
602
+ The response would have the same structure as seen in the previous examples. The only difference is that this time you will get only the events with the type you have specified ("CartItemChanged")
603
+
604
+ ### Time filters
605
+
606
+ Optionally, for any of the previous queries, you can include a `from` and/or `to` time filters to get only those events that happened inside that time range. You must use a string with a time in ISO format with any precision you like, for example:
607
+
608
+ - `from:"2021"` : Events created on 2021 year or up.
609
+ - `from:"2021-02-12" to:"2021-02-13"` : Events created during February 12th.
610
+ - `from:"2021-03-16T16:16:25.178"` : Events created at that date and time, using millisecond precision, or later.
611
+
612
+ ### Time filters examples
613
+
614
+ **A) Cart events from February 23rd to July 20th, 2021**
615
+
616
+ ```graphql
617
+ query {
618
+ eventsByEntity(entity: Cart, from: "2021-02-23", to: "2021-07-20") {
619
+ type
620
+ entity
621
+ entityID
622
+ requestID
623
+ createdAt
624
+ value
625
+ }
626
+ }
627
+ ```
628
+
629
+ **B) CartItemChanged events from February 25th to February 28th, 2021**
630
+
631
+ ```graphql
632
+ query {
633
+ eventsByType(type: CartItemChanged, from: "2021-02-25", to: "2021-02-28") {
634
+ type
635
+ entity
636
+ entityID
637
+ requestID
638
+ createdAt
639
+ value
640
+ }
641
+ }
642
+ ```
643
+
644
+ ### Known limitations
645
+
646
+ - Subscriptions don't work for the events API yet
647
+ - You can only query events, but not write them through this API. Use a command for that.
648
+
649
+ ## Filter, Pagination and Projections
650
+
651
+ ### Filtering a read model
652
+
653
+ The Magek GraphQL API provides support for filtering Read Models on `queries` and `subscriptions`.
654
+
655
+ Using the GraphQL API endpoint you can retrieve the schema of your application so you can see what are the filters for every Read Model and its properties. You can filter like this:
656
+
657
+ Searching for a specific Read Model by `id`
658
+
659
+ ```graphql
660
+ query {
661
+ ProductReadModels(filter: { id: { eq: "test-id" } }) {
662
+ id
663
+ sku
664
+ availability
665
+ price
666
+ }
667
+ }
668
+ ```
669
+
670
+ ### Supported filters
671
+
672
+ The currently supported filters are the following ones:
673
+
674
+ #### Boolean filters
675
+
676
+ | Filter | Value | Description |
677
+ | :----- | :--------: | -----------: |
678
+ | eq | true/false | Equal to |
679
+ | ne | true/false | Not equal to |
680
+
681
+ Example:
682
+
683
+ ```graphql
684
+ query {
685
+ ProductReadModels(filter: { availability: { eq: true } }) {
686
+ id
687
+ sku
688
+ availability
689
+ price
690
+ }
691
+ }
692
+ ```
693
+
694
+ #### Number filters
695
+
696
+ | Filter | Value | Description |
697
+ | :----- | :-----: | --------------------: |
698
+ | eq | Float | Equal to |
699
+ | ne | Float | Not equal to |
700
+ | gt | Float | Greater than |
701
+ | gte | Float | Greater or equal than |
702
+ | lt | Float | Lower than |
703
+ | lte | Float | Lower or equal than |
704
+ | in | [Float] | Exists in given array |
705
+
706
+ Example:
707
+
708
+ ```graphql
709
+ query {
710
+ ProductReadModels(filter: { price: { gt: 200 } }) {
711
+ id
712
+ sku
713
+ availability
714
+ price
715
+ }
716
+ }
717
+ ```
718
+
719
+ #### String filters
720
+
721
+ | Filter | Value | Description |
722
+ |:-----------| :------: |------------------------------------:|
723
+ | eq | String | Equal to |
724
+ | ne | String | Not equal to |
725
+ | gt | String | Greater than |
726
+ | gte | String | Greater or equal than |
727
+ | lt | String | Lower than |
728
+ | lte | String | Lower or equal than |
729
+ | in | [String] | Exists in given array |
730
+ | beginsWith | String | Starts with a given substr |
731
+ | contains | String | Contains a given substr |
732
+ | regex* | String | Regular expression |
733
+ | iRegex* | String | Case insensitive Regular expression |
734
+
735
+ Example:
736
+
737
+ ```graphql
738
+ query {
739
+ ProductReadModels(filter: { sku: { begingsWith: "jewelry" } }) {
740
+ id
741
+ sku
742
+ availability
743
+ price
744
+ }
745
+ }
746
+ ```
747
+
748
+ > **Note:**
749
+ > `eq` and `ne` are valid filters for checking if a field value is null or not null.
750
+
751
+ #### Array filters
752
+
753
+ | Filter | Value | Description |
754
+ | :------- | :----: | ----------------------: |
755
+ | includes | Object | Includes a given object |
756
+
757
+ Example:
758
+
759
+ ```graphql
760
+ query {
761
+ CartReadModels(filter: { itemsIds: { includes: "test-item" } }) {
762
+ id
763
+ price
764
+ itemsIds
765
+ }
766
+ }
767
+ ```
768
+
769
+ > **Note:**
770
+ > Right now, with complex properties in Arrays, you just can filter them if you know the exact value of an element but is not possible to filter from a property of the element. As a workaround, you can use an array of ids of the complex property and filter for that property as in the example above.
771
+
772
+ #### Filter combinators
773
+
774
+ All the filters can be combined to create a more complex search on the same properties of the ReadModel.
775
+
776
+ | Filter | Value | Description |
777
+ | :----- | :-----------: | -----------------------------------------------: |
778
+ | and | [Filters] | AND - all filters on the list have a match |
779
+ | or | [Filters] | OR - At least one filter of the list has a match |
780
+ | not | Filter/and/or | The element does not match the filter |
781
+
782
+ Example:
783
+
784
+ ```graphql
785
+ query {
786
+ CartReadModels(filter: { or: [{ id: { contains: "a" } }, { id: { contains: "b" } }] }) {
787
+ id
788
+ price
789
+ itemsIds
790
+ }
791
+ }
792
+ ```
793
+
794
+ #### IsDefined operator
795
+
796
+ | Filter | Value | Description |
797
+ |:----------|:-----------:|--------------------:|
798
+ | isDefined | true/false | field exists or not |
799
+
800
+ Example:
801
+
802
+ ```graphql
803
+ query {
804
+ CartReadModels(filter: { price: { isDefined: true } }) {
805
+ id
806
+ price
807
+ itemsIds
808
+ }
809
+ }
810
+ ```
811
+
812
+ ### Getting, filtering and projecting read models data at code level
813
+
814
+ Magek allows you to get your read models data in your commands handlers and event handlers using the `Magek.readModel` method.
815
+
816
+ For example, you can filter and get the total number of the products that meet your criteria in your commands like this:
817
+
818
+ ```typescript
819
+ @Command({
820
+ authorize: 'all',
821
+ })
822
+ export class GetProductsCount {
823
+ @field()
824
+ readonly filters!: Record<string, any>
825
+
826
+ public static async handle(): Promise<void> {
827
+ const searcher = Magek.readModel(ProductReadModel)
828
+
829
+ searcher.filter({
830
+ sku: { contains: 'toy' },
831
+ or: [
832
+ {
833
+ description: { contains: 'fancy' },
834
+ },
835
+ {
836
+ description: { contains: 'great' },
837
+ },
838
+ ],
839
+ })
840
+
841
+ const result = await searcher.search()
842
+ return { count: result.length }
843
+ }
844
+ }
845
+ ```
846
+
847
+ You can select which fields you want to get in your read model using the `select` method:
848
+
849
+ ```typescript
850
+ @Command({
851
+ authorize: 'all',
852
+ })
853
+ export class GetProductsCount {
854
+ @field()
855
+ readonly filters!: Record<string, any>
856
+
857
+ public static async handle(): Promise<unknown> {
858
+ const searcher = Magek.readModel(ProductReadModel)
859
+ .filter({
860
+ sku: { contains: 'toy' },
861
+ or: [
862
+ {
863
+ description: { contains: 'fancy' },
864
+ },
865
+ {
866
+ description: { contains: 'great' },
867
+ },
868
+ ],
869
+ })
870
+ .select(['sku', 'description'])
871
+ const result = await searcher.search()
872
+ return { count: result.length }
873
+ }
874
+ }
875
+ ```
876
+
877
+ The searcher result using `select` will generate an array of objects with the `sku` and `description` fields of the `ProductReadModel` read model. If you don't use `select` the result will be an array of `ProductReadModel` instances.
878
+ You can also select properties in objects which are part of an array property in a model. For that, the parent array properties need to be notated with the `[]` suffix. For example:
879
+
880
+ ```typescript
881
+ @Command({
882
+ authorize: 'all',
883
+ })
884
+ export class GetCartItems {
885
+ @field()
886
+ readonly filters!: Record<string, any>
887
+
888
+ public static async handle(): Promise<unknown> {
889
+ const searcher = Magek.readModel(CartReadModel)
890
+ .select(['id', 'cartItems[].productId'])
891
+ const result = await searcher.search()
892
+ return { count: result.length }
893
+ }
894
+ }
895
+ ```
896
+
897
+ The above search will return an array of carts with their `id` property, as well as an array of the `cartItems` of each cart with only the `productId` for each item.
898
+
899
+ > **Warning:**
900
+ > Using `select` will skip any Read Models migrations that need to be applied to the result. If you need to apply migrations to the result, don't use `select`.
901
+
902
+ > **Warning:**
903
+ > Support for selecting fields from objects inside arrays is limited to arrays that are at most nested inside another property, e.g., `['category.relatedCategories[].name']`. Selecting fields from arrays that are nested deeper than that (e.g., `['foo.bar.items[].id']`) will return the entire object.
904
+
905
+ > **Warning:**
906
+ > Notice that `ReadModel`s are eventually consistent objects that are calculated as all events in all entities that affect the read model are settled. You should not assume that a read model is a proper source of truth, so you shouldn't use this feature for data validations. If you need to query the most up-to-date current state, consider fetching your Entities, instead of ReadModels, with `Magek.entity`
907
+
908
+ ### Using sorting
909
+
910
+ Magek allows you to sort your read models data in your commands handlers and event handlers using the `Magek.readModel` method.
911
+
912
+ For example, you can sort and get the products in your commands like this:
913
+
914
+ ```graphql
915
+ {
916
+ ListCartReadModels(filter: {}, limit: 5, sortBy: {
917
+ shippingAddress: {
918
+ firstName: ASC
919
+ }
920
+ }) {
921
+ items {
922
+ id
923
+ cartItems
924
+ checks
925
+ shippingAddress {
926
+ firstName
927
+ }
928
+ payment {
929
+ cartId
930
+ }
931
+ cartItemsIds
932
+ }
933
+ cursor
934
+ }
935
+ }
936
+ ```
937
+
938
+ This is a preview feature with some limitations:
939
+
940
+ - Sort by one field supported.
941
+ - Nested fields supported.
942
+ - Sort by more than one field: **unsupported**.
943
+
944
+ > **Warning:**
945
+ > It is not possible to sort by fields defined as Interface, only classes or primitives types.
946
+
947
+ ### Using pagination
948
+
949
+ The Magek GraphQL API includes a type for your read models that stands for `List{"your-read-model-name"}`, which is the official way to work with pagination. Alternative, there is another type without the `List` prefix, which will be deprecated in future versions.
950
+
951
+ The Read Model List type includes some new parameters that can be used on queries:
952
+
953
+ - `limit`; an integer that specifies the maximum number of items to be returned.
954
+ - `afterCursor`; a parameter to set the `cursor` property returned by the previous query, if not null.
955
+
956
+ Example:
957
+
958
+ ```graphql
959
+ query {
960
+ ListProductReadModels
961
+ (
962
+ limit: 1,
963
+ afterCursor: { id: "last-page-item"}
964
+ ) {
965
+ id
966
+ sku
967
+ availability
968
+ price
969
+ }
970
+ }
971
+ ```
972
+
973
+ Besides the parameters, this type also returns a type `{your-read-model-name}Connection`, it includes the following properties:
974
+
975
+ - `cursor`; if there are more results to paginate, it will return the object to pass to the `afterCursor` parameter on the next query. If there aren't more items to be shown, it will be undefined.
976
+ - `items`; the list of items returned by the query, if there aren't any, it will be an empty list.
977
+
978
+ ## Using Apollo Client
979
+
980
+ One of the best clients to connect to a GraphQL API is the [Apollo](https://www.apollographql.com/) client. There will probably be a version for your client technology of choice. These are the main ones:
981
+
982
+ - [For Javascript/Typescript](https://www.apollographql.com/docs/react/) ([Github](https://github.com/apollographql/apollo-client))
983
+ - [For iOS](https://www.apollographql.com/docs/ios/) ([Github)](https://github.com/apollographql/apollo-ios))
984
+ - [For Java/Kotlin/Android](https://www.apollographql.com/docs/android/) ([Github](https://github.com/apollographql/apollo-android))
985
+
986
+ We recommend referring to the documentation of those clients to know how to use them. Here is an example of how to fully instantiate the Javascript client so that it works for queries, mutations and subscriptions:
987
+
988
+ ```typescript
989
+
990
+ // Helper function that checks if a GraphQL operation is a subscription or not
991
+ function isSubscriptionOperation({ query }) {
992
+ const definition = getMainDefinition(query)
993
+ return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
994
+ }
995
+
996
+ // Create an HTTP link for sending queries and mutations
997
+ const httpLink = new HttpLink({
998
+ uri: '<graphqlURL>',
999
+ })
1000
+
1001
+ // Create a SusbscriptionClient and a WebSocket link for sending subscriptions
1002
+ const subscriptionClient = new SubscriptionClient('<websocketURL>', {
1003
+ reconnect: true,
1004
+ })
1005
+ const wsLink = new WebSocketLink(subscriptionClient)
1006
+
1007
+ // Combine both links so that depending on the operation, it uses one or another
1008
+ const splitLink = split(isSubscriptionOperation, wsLink, httpLink)
1009
+
1010
+ // Finally, create the client using the link created above
1011
+ const client = new ApolloClient({
1012
+ link: splitLink,
1013
+ cache: new InMemoryCache(),
1014
+ })
1015
+ ```
1016
+
1017
+ Now, we can send queries, mutations and subscriptions using the `client` instance:
1018
+
1019
+ ```typescript
1020
+
1021
+ // Query the CartReadModel
1022
+ const readModelData = await client.query({
1023
+ variables: {
1024
+ cartID: 'demo',
1025
+ },
1026
+ query: gql`
1027
+ query QueryCart($cartID: ID!) {
1028
+ CartReadModel(id: $cartID) {
1029
+ id
1030
+ items
1031
+ }
1032
+ }
1033
+ `,
1034
+ })
1035
+
1036
+ // Send a command (mutation)
1037
+ const commandResult = await client.mutate({
1038
+ variables: {
1039
+ cartID: 'demo',
1040
+ sku: 'ABC_02',
1041
+ },
1042
+ mutation: gql`
1043
+ mutation AddOneItemToCart($cartID: ID!, $sku: string!) {
1044
+ ChangeCart(input: { cartId: $cartID, sku: $sku, quantity: 1 })
1045
+ }
1046
+ `,
1047
+ })
1048
+
1049
+ // Subscribe to changes in the CartReadModel
1050
+ const subscriptionOperation = client.subscribe({
1051
+ variables: {
1052
+ cartID: 'demo',
1053
+ },
1054
+ query: gql`
1055
+ subscription SubscribeToCart($cartID: ID!) {
1056
+ CartReadModel(id: $cartID) {
1057
+ id
1058
+ cartItems
1059
+ }
1060
+ }
1061
+ `,
1062
+ })
1063
+
1064
+ subscriptionOperation.subscribe({
1065
+ next: (cartReadModel) => {
1066
+ // This function is called everytime the CartReadModel with ID="demo" is changed
1067
+ // Parameter "cartReadModel" contains the latest version of the cart
1068
+ },
1069
+ })
1070
+ ```
1071
+
1072
+ ## Authorizing operations
1073
+
1074
+ When you have a command or read model whose access is authorized to users with a specific set of roles (see [Authentication and Authorization](#authentication-and-authorization)), you need to use an authorization token to send queries, mutations or subscriptions to that command or read model.
1075
+
1076
+ You can use any JWT-based authentication provider to issue tokens. Once you have a token, the way to send it varies depending on the protocol you are using to send GraphQL operations:
1077
+
1078
+ - For **HTTP**, you need to send the HTTP header `Authorization` with the token, making sure you prefix it with `Bearer` (the kind of token Magek uses). For example:
1079
+
1080
+ ```http
1081
+ Authorization: Bearer <your token>
1082
+ ```
1083
+
1084
+ - For **WebSocket**, you need to adhere to the [GraphQL over WebSocket protocol](#the-graphql-over-websocket-protocol) to send authorization data. The way to do that is by sending the token in the payload of the first message you send when initializing the connection (see [Subscribing to read models](#subscribing-to-read-models)). For example:
1085
+
1086
+ ```json
1087
+ { "type": "connection_init", "payload": { "Authorization": "<your token>" } }
1088
+ ```
1089
+
1090
+ You normally won't be sending tokens in such a low-level way. GraphQL clients have easier ways to send these tokens. See [Sending tokens with Apollo client](#sending-tokens-with-apollo-clients)
1091
+
1092
+ ### Sending tokens with Apollo clients
1093
+
1094
+ We recommend going to the specific documentation of the specific Apollo client you are using to know how to send tokens. However, the basics of this guide remains the same. Here is an example of how you would configure the Javascript/Typescript Apollo client to send the authorization token. The example is exactly the same as the one shown in the [Using Apollo clients](#using-apollo-client) section, but with the changes needed to send the token.
1095
+
1096
+ ```typescript
1097
+
1098
+ function isSubscriptionOperation({ query }) {
1099
+ const definition = getMainDefinition(query)
1100
+ return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
1101
+ }
1102
+
1103
+ const httpLink = new HttpLink({
1104
+ uri: '<graphqlURL>',
1105
+ })
1106
+
1107
+ // Create an "authLink" that modifies the operation by adding the token to the headers
1108
+ const authLink = new ApolloLink((operation, forward) => {
1109
+ operation.setContext({
1110
+ headers: {
1111
+ Authorization: 'Bearer <idToken>',
1112
+ },
1113
+ })
1114
+ return forward(operation)
1115
+ })
1116
+
1117
+ // Concatenate the links so that the "httpLink" receives the operation with the headers set by the "authLink"
1118
+ const httpLinkWithAuth = authLink.concat(httpLink)
1119
+
1120
+ const subscriptionClient = new SubscriptionClient('<websocketURL>', {
1121
+ reconnect: true,
1122
+ // Add a "connectionParam" property with a function that returns the `Authorization` header containing our token
1123
+ connectionParams: () => {
1124
+ return {
1125
+ Authorization: 'Bearer <idToken>',
1126
+ }
1127
+ },
1128
+ })
1129
+ const wsLink = new WebSocketLink(subscriptionClient)
1130
+
1131
+ const splitLink = split(isSubscriptionOperation, wsLink, httpLinkWithAuth)
1132
+
1133
+ const client = new ApolloClient({
1134
+ link: splitLink,
1135
+ cache: new InMemoryCache(),
1136
+ })
1137
+ ```
1138
+
1139
+ ### Refreshing tokens with Apollo clients
1140
+
1141
+ Authorization tokens expire after a certain amount of time. When a token is expired, you will get an error and you will need to refresh the token using your authentication provider. After you have done so, you need to use the new token in your GraphQL operations.
1142
+
1143
+ There are several ways to do this. Here we show the simplest one for learning purposes.
1144
+
1145
+ First, we modify the example shown in the section [Sending tokens with apollo clients](#sending-tokens-with-apollo-clients) so that the token is stored in a global variable and the Apollo links get the token from it. That variable will be updated when the user signs-in and the token is refreshed:
1146
+
1147
+ ```typescript
1148
+
1149
+ let authToken = undefined // <-- CHANGED: This variable will hold the token and will be updated everytime the token is refreshed
1150
+
1151
+ function isSubscriptionOperation({ query }) {
1152
+ const definition = getMainDefinition(query)
1153
+ return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
1154
+ }
1155
+
1156
+ const httpLink = new HttpLink({
1157
+ uri: '<AuthApiEndpoint>',
1158
+ })
1159
+
1160
+ const authLink = new ApolloLink((operation, forward) => {
1161
+ if (authToken) {
1162
+ operation.setContext({
1163
+ headers: {
1164
+ Authorization: `Bearer ${authToken}`, // <-- CHANGED: We use the "authToken" global variable
1165
+ },
1166
+ })
1167
+ }
1168
+ return forward(operation)
1169
+ })
1170
+
1171
+ const httpLinkWithAuth = authLink.concat(httpLink)
1172
+
1173
+ const subscriptionClient = new SubscriptionClient('<websocketURL>', {
1174
+ reconnect: true,
1175
+ // CHANGED: added a "connectionParam" property with a function that returns the `Authorizaiton` header containing our token
1176
+ connectionParams: () => {
1177
+ if (authToken) {
1178
+ return {
1179
+ Authorization: `Bearer ${authToken}`, // <-- CHANGED: We use the "authToken" global variable
1180
+ }
1181
+ }
1182
+ return {}
1183
+ },
1184
+ })
1185
+ const wsLink = new WebSocketLink(subscriptionClient)
1186
+
1187
+ const splitLink = split(isSubscriptionOperation, wsLink, httpLinkWithAuth)
1188
+
1189
+ const client = new ApolloClient({
1190
+ link: splitLink,
1191
+ cache: new InMemoryCache(),
1192
+ })
1193
+ ```
1194
+
1195
+ Now, _when the user signs-in_ or _when the token is refreshed_, we need to do two things:
1196
+
1197
+ 1. Update the global variable `authToken` with the new token.
1198
+ 2. Reconnect the socket used by the subscription client by doing `subscriptionClient.close(false)`.
1199
+
1200
+ You might be wondering why we need to do the second step. The reason is that, with operations sent through HTTP, the token goes along with every operation, in the headers. However, with operations sent through WebSockets, like subscriptions, the token is only sent when the socket connection is established. For this reason, **everytime we update the token we need to reconnect the `SubscriptionClient`** so that it sends again the token (the updated one in this case).
1201
+
1202
+ ## The GraphQL over WebSocket protocol
1203
+
1204
+ Sockets are channels for two-way communication that doesn't follow the request-response cycle, a characteristic feature of the HTTP protocol. One part can send many messages and the other part can receive all of them but only answer to some specific ones. What is more, messages could come in any order. For example, one part can send two messages and receive the response of the second message before the response of the first message.
1205
+
1206
+ For these reasons, in order to have an effective non-trivial communication through sockets, a sub-protocol is needed. It would be in charge of making both parts understand each other, share authentication tokens, matching response to the corresponding requests, etc.
1207
+
1208
+ The Magek WebSocket communication uses the "GraphQL over WebSocket" protocol as subprotocol. It is in charge of all the low level stuff needed to properly send subscription operations to read models and receive the corresponding data.
1209
+
1210
+ You don't need to know anything about this to develop using Magek, neither in the backend side nor in the frontend side (as all the Apollo GraphQL clients uses this protocol), but it is good to know it is there to guarantee a proper communication. In case you are really curious, you can read about the protocol [here](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md).
1211
+
1212
+ > **Note:**
1213
+ > The WebSocket communication in Magek only supports this subprotocol, whose identifier is `graphql-ws`. For this reason, when you connect to the WebSocket provisioned by Magek, you must specify the `graphql-ws` subprotocol. If not, the connection won't succeed.