@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.
- package/README.md +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/prompts/cqrs-flow.d.ts +15 -0
- package/dist/prompts/cqrs-flow.js +252 -0
- package/dist/prompts/troubleshooting.d.ts +15 -0
- package/dist/prompts/troubleshooting.js +239 -0
- package/dist/resources/cli-reference.d.ts +13 -0
- package/dist/resources/cli-reference.js +193 -0
- package/dist/resources/documentation.d.ts +18 -0
- package/dist/resources/documentation.js +62 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +127 -0
- package/dist/utils/docs-loader.d.ts +19 -0
- package/dist/utils/docs-loader.js +111 -0
- package/docs/advanced/custom-templates.md +96 -0
- package/docs/advanced/data-migrations.md +181 -0
- package/docs/advanced/environment-configuration.md +74 -0
- package/docs/advanced/framework-packages.md +17 -0
- package/docs/advanced/health/sensor-health.md +389 -0
- package/docs/advanced/instrumentation.md +135 -0
- package/docs/advanced/register.md +119 -0
- package/docs/advanced/sensor.md +10 -0
- package/docs/advanced/testing.md +96 -0
- package/docs/advanced/touch-entities.md +45 -0
- package/docs/architecture/command.md +367 -0
- package/docs/architecture/entity.md +214 -0
- package/docs/architecture/event-driven.md +30 -0
- package/docs/architecture/event-handler.md +108 -0
- package/docs/architecture/event.md +145 -0
- package/docs/architecture/notifications.md +54 -0
- package/docs/architecture/queries.md +207 -0
- package/docs/architecture/read-model.md +507 -0
- package/docs/contributing.md +349 -0
- package/docs/docs-index.json +200 -0
- package/docs/features/error-handling.md +204 -0
- package/docs/features/event-stream.md +35 -0
- package/docs/features/logging.md +81 -0
- package/docs/features/schedule-actions.md +44 -0
- package/docs/getting-started/ai-coding-assistants.md +181 -0
- package/docs/getting-started/coding.md +543 -0
- package/docs/getting-started/installation.md +143 -0
- package/docs/graphql.md +1213 -0
- package/docs/index.md +62 -0
- package/docs/introduction.md +58 -0
- package/docs/magek-arch.png +0 -0
- package/docs/magek-cli.md +67 -0
- package/docs/magek-logo.svg +1 -0
- package/docs/security/authentication.md +189 -0
- package/docs/security/authorization.md +242 -0
- package/docs/security/security.md +16 -0
- package/package.json +46 -0
package/docs/graphql.md
ADDED
|
@@ -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
|
+

|
|
100
|
+

|
|
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
|
+
- _<id of the read model>_ 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
|
+
- _<id of the read model>_ 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
|
+
- _<name of your entity>_ is the name of the class corresponding to the entity whose events you want to retrieve.
|
|
522
|
+
- _<id of the entity>_ is the ID of the specific entity instance whose events you are interested in. **This is optional**
|
|
523
|
+
- _<name of event>_ 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.
|