@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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Commands"
|
|
3
|
+
group: "Architecture"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Command
|
|
7
|
+
|
|
8
|
+
Commands are any action a user performs on your application. For example, `RemoveItemFromCart`, `RatePhoto` or `AddCommentToPost`. They express the intention of an user, and they are the main interaction mechanism of your application. They are a similar to the concept of a **request on a REST API**. Command issuers can also send data on a command as parameters.
|
|
9
|
+
|
|
10
|
+
## Creating a command
|
|
11
|
+
|
|
12
|
+
The Magek CLI will help you to create new commands. You just need to run the following command and the CLI will generate all the boilerplate for you:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx magek new:command CreateProduct --fields sku:SKU displayName:string description:string price:Money
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This will generate a new file called `create-product` in the `src/commands` directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI.
|
|
19
|
+
|
|
20
|
+
## Declaring a command
|
|
21
|
+
|
|
22
|
+
In Magek you define them as TypeScript classes decorated with the `@Command` decorator. The `Command` parameters will be declared as properties of the class.
|
|
23
|
+
|
|
24
|
+
```typescript title="src/commands/command-name.ts"
|
|
25
|
+
@Command({
|
|
26
|
+
authorize: 'all',
|
|
27
|
+
})
|
|
28
|
+
export class CommandName {
|
|
29
|
+
@field()
|
|
30
|
+
readonly fieldA!: SomeType
|
|
31
|
+
|
|
32
|
+
@field()
|
|
33
|
+
readonly fieldB!: SomeOtherType
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
These commands are handled by `Command Handlers`, the same way a **REST Controller** do with a request. To create a `Command handler` of a specific Command, you must declare a `handle` class function inside the corresponding command you want to handle. For example:
|
|
38
|
+
|
|
39
|
+
```typescript title="src/commands/command-name.ts"
|
|
40
|
+
@Command({
|
|
41
|
+
authorize: 'all',
|
|
42
|
+
})
|
|
43
|
+
export class CommandName {
|
|
44
|
+
@field()
|
|
45
|
+
readonly fieldA!: SomeType
|
|
46
|
+
|
|
47
|
+
@field()
|
|
48
|
+
readonly fieldB!: SomeOtherType
|
|
49
|
+
|
|
50
|
+
// highlight-start
|
|
51
|
+
public static async handle(command: CommandName, register: Register): Promise<void> {
|
|
52
|
+
// Validate inputs
|
|
53
|
+
// Run domain logic
|
|
54
|
+
// register.events([event1,...])
|
|
55
|
+
}
|
|
56
|
+
// highlight-end
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Magek will then generate the GraphQL mutation for the corresponding command, and the infrastructure to handle them. You only have to define the class and the handler function. Commands are part of the public API, so you can define authorization policies for them, you can read more about this on [the authorization section](/security/authorization).
|
|
61
|
+
|
|
62
|
+
> **Tip:** We recommend using command handlers to validate input data before registering events into the event store because they are immutable once there.
|
|
63
|
+
|
|
64
|
+
## The command handler function
|
|
65
|
+
|
|
66
|
+
Each command class must have a method called `handle`. This function is the command handler, and it will be called by the framework every time one instance of this command is submitted. Inside the handler you can run validations, return errors, query entities to make decisions, and register relevant domain events.
|
|
67
|
+
|
|
68
|
+
### Registering events
|
|
69
|
+
|
|
70
|
+
Within the command handler execution, it is possible to register domain events. The command handler function receives the `register` argument, so within the handler, it is possible to call `register.events(...)` with a list of events.
|
|
71
|
+
|
|
72
|
+
```typescript title="src/commands/create-product.ts"
|
|
73
|
+
@Command({
|
|
74
|
+
authorize: 'all',
|
|
75
|
+
})
|
|
76
|
+
export class CreateProduct {
|
|
77
|
+
@field()
|
|
78
|
+
readonly sku!: string
|
|
79
|
+
|
|
80
|
+
@field()
|
|
81
|
+
readonly price!: number
|
|
82
|
+
|
|
83
|
+
public static async handle(command: CreateProduct, register: Register): Promise<void> {
|
|
84
|
+
// highlight-next-line
|
|
85
|
+
register.event(new ProductCreated(/*...*/))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For more details about events and the register parameter, see the [`Events`](/architecture/event) section.
|
|
91
|
+
|
|
92
|
+
### Returning a value
|
|
93
|
+
|
|
94
|
+
The command handler function can return a value. This value will be the response of the GraphQL mutation. By default, the command handler function expects you to return a `void` as a return type. Since GrahpQL does not have a `void` type, the command handler function returns `true` when called through the GraphQL. This is because the GraphQL specification requires a response, and `true` is the most appropriate value to represent a successful execution with no return value.
|
|
95
|
+
|
|
96
|
+
If you want to return a value, you need to:
|
|
97
|
+
1. Change the return type of the handler function
|
|
98
|
+
2. Use the `@returns` decorator to specify the GraphQL return type
|
|
99
|
+
|
|
100
|
+
The `@returns` decorator tells Magek what type to use in the generated GraphQL schema. Without it, Magek defaults to `Boolean` for the mutation return type.
|
|
101
|
+
|
|
102
|
+
For example, if you want to return a `string`:
|
|
103
|
+
|
|
104
|
+
```typescript title="src/commands/create-product.ts"
|
|
105
|
+
@Command({
|
|
106
|
+
authorize: 'all',
|
|
107
|
+
})
|
|
108
|
+
export class CreateProduct {
|
|
109
|
+
@field()
|
|
110
|
+
readonly sku!: string
|
|
111
|
+
|
|
112
|
+
@field()
|
|
113
|
+
readonly price!: number
|
|
114
|
+
|
|
115
|
+
// highlight-next-line
|
|
116
|
+
@returns(type => String)
|
|
117
|
+
public static async handle(command: CreateProduct, register: Register): Promise<string> {
|
|
118
|
+
register.event(new ProductCreated(/*...*/))
|
|
119
|
+
// highlight-next-line
|
|
120
|
+
return 'Product created!'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### The @returns decorator
|
|
126
|
+
|
|
127
|
+
The `@returns` decorator is required when your command handler returns a value other than `void`. It uses the same type function pattern as `@field()`:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// Return a primitive type
|
|
131
|
+
@returns(type => String)
|
|
132
|
+
public static async handle(...): Promise<string> { ... }
|
|
133
|
+
|
|
134
|
+
// Return a UUID (maps to GraphQL ID)
|
|
135
|
+
@returns(type => UUID)
|
|
136
|
+
public static async handle(...): Promise<UUID> { ... }
|
|
137
|
+
|
|
138
|
+
// Return a number
|
|
139
|
+
@returns(type => Number)
|
|
140
|
+
public static async handle(...): Promise<number> { ... }
|
|
141
|
+
|
|
142
|
+
// Return an array
|
|
143
|
+
@returns(type => [CartItem])
|
|
144
|
+
public static async handle(...): Promise<CartItem[]> { ... }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
> **Note:** The type specified in `@returns` is the GraphQL return type, not the TypeScript type. TypeScript erases type information at compile time, so Magek cannot infer the return type automatically.
|
|
148
|
+
|
|
149
|
+
### Validating data
|
|
150
|
+
|
|
151
|
+
> **Tip:** Magek uses the typed nature of GraphQL to ensure that types are correct before reaching the handler, so **you don't have to validate types**.
|
|
152
|
+
|
|
153
|
+
#### Throw an error
|
|
154
|
+
|
|
155
|
+
A command will fail if there is an uncaught error during its handling. When a command fails, Magek will return a detailed error response with the message of the thrown error. This is useful for debugging, but it is also a security feature. Magek will never return an error stack trace to the client, so you don't have to worry about exposing internal implementation details.
|
|
156
|
+
|
|
157
|
+
One case where you might want to throw an error is when the command is invalid because it breaks a business rule. For example, if the command contains a negative price. In that case, you can throw an error in the handler. Magek will use the error's message as the response to make it descriptive. For example, given this command:
|
|
158
|
+
|
|
159
|
+
```typescript title="src/commands/create-product.ts"
|
|
160
|
+
@Command({
|
|
161
|
+
authorize: 'all',
|
|
162
|
+
})
|
|
163
|
+
export class CreateProduct {
|
|
164
|
+
@field()
|
|
165
|
+
readonly sku!: string
|
|
166
|
+
|
|
167
|
+
@field()
|
|
168
|
+
readonly price!: number
|
|
169
|
+
|
|
170
|
+
public static async handle(command: CreateProduct, register: Register): Promise<void> {
|
|
171
|
+
const priceLimit = 10
|
|
172
|
+
if (command.price >= priceLimit) {
|
|
173
|
+
// highlight-next-line
|
|
174
|
+
throw new Error(`price must be below ${priceLimit}, and it was ${command.price}`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
You'll get something like this response:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"errors": [
|
|
185
|
+
{
|
|
186
|
+
"message": "price must be below 10, and it was 19.99",
|
|
187
|
+
"path": ["CreateProduct"]
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### Register error events
|
|
194
|
+
|
|
195
|
+
There could be situations in which you want to register an event representing an error. For example, when moving items with insufficient stock from one location to another:
|
|
196
|
+
|
|
197
|
+
```typescript title="src/commands/move-stock.ts"
|
|
198
|
+
@Command({
|
|
199
|
+
authorize: 'all',
|
|
200
|
+
})
|
|
201
|
+
export class MoveStock {
|
|
202
|
+
@field()
|
|
203
|
+
readonly productID!: string
|
|
204
|
+
|
|
205
|
+
@field()
|
|
206
|
+
readonly origin!: string
|
|
207
|
+
|
|
208
|
+
@field()
|
|
209
|
+
readonly destination!: string
|
|
210
|
+
|
|
211
|
+
@field()
|
|
212
|
+
readonly quantity!: number
|
|
213
|
+
|
|
214
|
+
public static async handle(command: MoveStock, register: Register): Promise<void> {
|
|
215
|
+
if (!command.enoughStock(command.productID, command.origin, command.quantity)) {
|
|
216
|
+
// highlight-next-line
|
|
217
|
+
register.events(new ErrorEvent(`There is not enough stock for ${command.productID} at ${command.origin}`))
|
|
218
|
+
} else {
|
|
219
|
+
register.events(new StockMoved(/*...*/))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private enoughStock(productID: string, origin: string, quantity: number): boolean {
|
|
224
|
+
/* ... */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
In this case, the command operation can still be completed. An event handler will take care of that `ErrorEvent and proceed accordingly.
|
|
230
|
+
|
|
231
|
+
### Reading entities
|
|
232
|
+
|
|
233
|
+
Event handlers are a good place to make decisions and, to make better decisions, you need information. The `Magek.entity` function allows you to inspect the application state. This function receives two arguments, the `Entity`'s name to fetch and the `entityID`. Here is an example of fetching an entity called `Stock`:
|
|
234
|
+
|
|
235
|
+
```typescript title="src/commands/move-stock.ts"
|
|
236
|
+
@Command({
|
|
237
|
+
authorize: 'all',
|
|
238
|
+
})
|
|
239
|
+
export class MoveStock {
|
|
240
|
+
@field()
|
|
241
|
+
readonly productID!: string
|
|
242
|
+
|
|
243
|
+
@field()
|
|
244
|
+
readonly origin!: string
|
|
245
|
+
|
|
246
|
+
@field()
|
|
247
|
+
readonly destination!: string
|
|
248
|
+
|
|
249
|
+
@field()
|
|
250
|
+
readonly quantity!: number
|
|
251
|
+
|
|
252
|
+
public static async handle(command: MoveStock, register: Register): Promise<void> {
|
|
253
|
+
// highlight-next-line
|
|
254
|
+
const stock = await Magek.entity(Stock, command.productID)
|
|
255
|
+
if (!command.enoughStock(command.origin, command.quantity, stock)) {
|
|
256
|
+
register.events(new ErrorEvent(`There is not enough stock for ${command.productID} at ${command.origin}`))
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private enoughStock(origin: string, quantity: number, stock?: Stock): boolean {
|
|
261
|
+
const count = stock?.countByLocation[origin]
|
|
262
|
+
return !!count && count >= quantity
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Authorizing a command
|
|
268
|
+
|
|
269
|
+
Commands are part of the public API of a Magek application, so you can define who is authorized to submit them. All commands are protected by default, which means that no one can submit them. In order to allow users to submit a command, you must explicitly authorize them. You can use the `authorize` field of the `@Command` decorator to specify the authorization rule.
|
|
270
|
+
|
|
271
|
+
```typescript title="src/commands/create-product.ts"
|
|
272
|
+
@Command({
|
|
273
|
+
// highlight-next-line
|
|
274
|
+
authorize: 'all',
|
|
275
|
+
})
|
|
276
|
+
export class CreateProduct {
|
|
277
|
+
@field()
|
|
278
|
+
readonly sku!: Sku
|
|
279
|
+
|
|
280
|
+
@field()
|
|
281
|
+
readonly displayName!: string
|
|
282
|
+
|
|
283
|
+
@field()
|
|
284
|
+
readonly description!: string
|
|
285
|
+
|
|
286
|
+
@field()
|
|
287
|
+
readonly price!: number
|
|
288
|
+
|
|
289
|
+
public static async handle(command: CreateProduct, register: Register): Promise<void> {
|
|
290
|
+
register.events(/* YOUR EVENT HERE */)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
You can read more about this on the [Authorization section](/security/authorization).
|
|
296
|
+
|
|
297
|
+
## Submitting a command
|
|
298
|
+
|
|
299
|
+
Magek commands are accessible to the outside world as GraphQL mutations. GrahpQL fits very well with Magek's CQRS approach because it has two kinds of operations: Mutations and Queries. Mutations are actions that modify the server-side data, just like commands.
|
|
300
|
+
|
|
301
|
+
Magek automatically creates one mutation per command. The framework infers the mutation input type from the command fields. Given this `CreateProduct` command:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
@Command({
|
|
305
|
+
authorize: 'all',
|
|
306
|
+
})
|
|
307
|
+
export class CreateProduct {
|
|
308
|
+
@field()
|
|
309
|
+
readonly sku!: Sku
|
|
310
|
+
|
|
311
|
+
@field()
|
|
312
|
+
readonly displayName!: string
|
|
313
|
+
|
|
314
|
+
@field()
|
|
315
|
+
readonly description!: string
|
|
316
|
+
|
|
317
|
+
@field()
|
|
318
|
+
readonly price!: number
|
|
319
|
+
|
|
320
|
+
public static async handle(command: CreateProduct, register: Register): Promise<void> {
|
|
321
|
+
register.events(/* YOUR EVENT HERE */)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Magek generates the following GraphQL mutation:
|
|
327
|
+
|
|
328
|
+
```graphql
|
|
329
|
+
mutation CreateProduct($input: CreateProductInput!): Boolean
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
where the schema for `CreateProductInput` is
|
|
333
|
+
|
|
334
|
+
```text
|
|
335
|
+
{
|
|
336
|
+
sku: String
|
|
337
|
+
displayName: String
|
|
338
|
+
description: String
|
|
339
|
+
price: Float
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Commands naming convention
|
|
344
|
+
|
|
345
|
+
Semantics are very important in Magek as it will play an essential role in designing a coherent system. Your application should reflect your domain concepts, and commands are not an exception. Although you can name commands in any way you want, we strongly recommend you to **name them starting with verbs in imperative plus the object being affected**. If we were designing an e-commerce application, some commands would be:
|
|
346
|
+
|
|
347
|
+
- CreateProduct
|
|
348
|
+
- DeleteProduct
|
|
349
|
+
- UpdateProduct
|
|
350
|
+
- ChangeCartItems
|
|
351
|
+
- ConfirmPayment
|
|
352
|
+
- MoveStock
|
|
353
|
+
- UpdateCartShippingAddress
|
|
354
|
+
|
|
355
|
+
Despite you can place commands, and other Magek files, in any directory, we strongly recommend you to put them in `<project-root>/src/commands`. Having all the commands in one place will help you to understand your application's capabilities at a glance.
|
|
356
|
+
|
|
357
|
+
```text
|
|
358
|
+
<project-root>
|
|
359
|
+
├── src
|
|
360
|
+
│ ├── commands <------ put them here
|
|
361
|
+
│ ├── common
|
|
362
|
+
│ ├── config
|
|
363
|
+
│ ├── entities
|
|
364
|
+
│ ├── events
|
|
365
|
+
│ ├── index.ts
|
|
366
|
+
│ └── read-models
|
|
367
|
+
```
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Entities"
|
|
3
|
+
group: "Architecture"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Entity
|
|
7
|
+
|
|
8
|
+
If events are the _source of truth_ of your application, entities are the _current state_ of your application. For example, if you have an application that allows users to create bank accounts, the events would be something like `AccountCreated`, `MoneyDeposited`, `MoneyWithdrawn`, etc. But the entities would be the `BankAccount` themselves, with the current balance, owner, etc.
|
|
9
|
+
|
|
10
|
+
Entities are created by _reducing_ the whole event stream. Magek generates entities on the fly, so you don't have to worry about their creation. However, you must define them in order to instruct Magek how to generate them.
|
|
11
|
+
|
|
12
|
+
> **Info:** Under the hood, Magek stores snapshots of the entities in order to reduce the load on the event store. That way, Magek doesn't have to reduce the whole event stream whenever the current state of an entity is needed.
|
|
13
|
+
|
|
14
|
+
## Creating entities
|
|
15
|
+
|
|
16
|
+
The Magek CLI will help you to create new entities. You just need to run the following command and the CLI will generate all the boilerplate for you:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx magek new:entity Product --fields displayName:string description:string price:Money
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This will generate a new file called `product.ts` in the `src/entities` directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI.
|
|
23
|
+
|
|
24
|
+
## Declaring an entity
|
|
25
|
+
|
|
26
|
+
To declare an entity in Magek, you must define a class decorated with the `@Entity` decorator. Inside of the class, you must define a constructor with all the fields you want to have in your entity.
|
|
27
|
+
|
|
28
|
+
```typescript title="src/entities/entity-name.ts"
|
|
29
|
+
@Entity
|
|
30
|
+
export class EntityName {
|
|
31
|
+
@field(type => UUID)
|
|
32
|
+
public id!: UUID
|
|
33
|
+
|
|
34
|
+
@field()
|
|
35
|
+
readonly fieldA!: SomeType
|
|
36
|
+
|
|
37
|
+
@field()
|
|
38
|
+
readonly fieldB!: SomeOtherType
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## The reduce function
|
|
43
|
+
|
|
44
|
+
In order to tell Magek how to reduce the events, you must define a static method decorated with the `@reduces` decorator. This method will be called by the framework every time an event of the specified type is emitted. The reducer method must return a new entity instance with the current state of the entity.
|
|
45
|
+
|
|
46
|
+
```typescript title="src/entities/entity-name.ts"
|
|
47
|
+
@Entity
|
|
48
|
+
export class EntityName {
|
|
49
|
+
@field(type => UUID)
|
|
50
|
+
public id!: UUID
|
|
51
|
+
|
|
52
|
+
@field()
|
|
53
|
+
readonly fieldA!: SomeType
|
|
54
|
+
|
|
55
|
+
@field()
|
|
56
|
+
readonly fieldB!: SomeOtherType
|
|
57
|
+
|
|
58
|
+
// highlight-start
|
|
59
|
+
@reduces(SomeEvent)
|
|
60
|
+
public static reduceSomeEvent(event: SomeEvent, currentEntityState?: EntityName): EntityName {
|
|
61
|
+
return evolve(currentEntityState, {
|
|
62
|
+
id: event.entityID(),
|
|
63
|
+
fieldA: event.fieldA,
|
|
64
|
+
fieldB: event.fieldB,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
// highlight-end
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The reducer method receives two parameters:
|
|
72
|
+
|
|
73
|
+
- `event` - The event object that triggered the reducer
|
|
74
|
+
- `currentEntity?` - The current state of the entity instance that the event belongs to if it exists. **This parameter is optional** and will be `undefined` if the entity doesn't exist yet (For example, when you process a `ProductCreated` event that will generate the first version of a `Product` entity).
|
|
75
|
+
|
|
76
|
+
> **Tip:** The `evolve()` helper function creates a new immutable copy of the entity with the specified field updates. It safely handles the case where `currentEntityState` is `undefined` (for newly created entities) and ensures immutability by returning a fresh object rather than mutating the existing one.
|
|
77
|
+
|
|
78
|
+
### Reducing multiple events
|
|
79
|
+
|
|
80
|
+
You can define as many reducer methods as you want, each one for a different event type. For example, if you have a `Cart` entity, you could define a reducer for `ProductAdded` events and another one for `ProductRemoved` events.
|
|
81
|
+
|
|
82
|
+
```typescript title="src/entities/cart.ts"
|
|
83
|
+
@Entity
|
|
84
|
+
export class Cart {
|
|
85
|
+
@field(type => UUID)
|
|
86
|
+
public id!: UUID
|
|
87
|
+
|
|
88
|
+
@field()
|
|
89
|
+
readonly items!: Array<CartItem>
|
|
90
|
+
|
|
91
|
+
@reduces(ProductAdded)
|
|
92
|
+
public static reduceProductAdded(event: ProductAdded, currentCart?: Cart): Cart {
|
|
93
|
+
const newItems = addToCart(event.item, currentCart)
|
|
94
|
+
return evolve(currentCart, { id: event.entityID(), items: newItems })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@reduces(ProductRemoved)
|
|
98
|
+
public static reduceProductRemoved(event: ProductRemoved, currentCart?: Cart): Cart {
|
|
99
|
+
const newItems = removeFromCart(event.item, currentCart)
|
|
100
|
+
return evolve(currentCart, { items: newItems })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
> **Tip:** It's highly recommended to **keep your reducer functions pure**, which means that you should be able to produce the new entity version by just looking at the event and the current entity state. You should avoid calling third party services, reading or writing to a database, or changing any external state.
|
|
106
|
+
|
|
107
|
+
### Skipping Events with ReducerAction.Skip
|
|
108
|
+
|
|
109
|
+
Sometimes a reducer may need to skip an event instead of updating the entity state. This can happen when:
|
|
110
|
+
- Receiving an update event for an entity that doesn't exist
|
|
111
|
+
- Processing an event that is no longer relevant
|
|
112
|
+
- Handling events that should be ignored under certain conditions
|
|
113
|
+
|
|
114
|
+
To skip an event, return `ReducerAction.Skip` from your reducer. This tells Magek to keep the current entity state unchanged:
|
|
115
|
+
|
|
116
|
+
```typescript title="src/entities/product.ts"
|
|
117
|
+
import { ReducerAction, ReducerResult } from '@magek/common'
|
|
118
|
+
|
|
119
|
+
@Entity
|
|
120
|
+
export class Product {
|
|
121
|
+
@field(type => UUID)
|
|
122
|
+
public id!: UUID
|
|
123
|
+
|
|
124
|
+
@field()
|
|
125
|
+
readonly name!: string
|
|
126
|
+
|
|
127
|
+
@field()
|
|
128
|
+
readonly price!: number
|
|
129
|
+
|
|
130
|
+
@reduces(ProductUpdated)
|
|
131
|
+
public static reduceProductUpdated(
|
|
132
|
+
event: ProductUpdated,
|
|
133
|
+
currentProduct?: Product
|
|
134
|
+
): ReducerResult<Product> {
|
|
135
|
+
if (!currentProduct) {
|
|
136
|
+
// Can't update a non-existent product - skip this event
|
|
137
|
+
return ReducerAction.Skip
|
|
138
|
+
}
|
|
139
|
+
return new Product(currentProduct.id, event.newName, event.newPrice)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
When a reducer returns `ReducerAction.Skip`, the framework will:
|
|
145
|
+
- Keep the previous entity snapshot unchanged
|
|
146
|
+
- Continue processing subsequent events in the event stream
|
|
147
|
+
- Not store a new snapshot for this event
|
|
148
|
+
|
|
149
|
+
> **Note:** `ReducerAction.Skip` should be used sparingly and only for events that genuinely should not affect entity state. In most cases, properly designed events and reducers won't need to skip events.
|
|
150
|
+
|
|
151
|
+
There could be a lot of events being reduced concurrently among many entities, but, **for a specific entity instance, the events order is preserved**. This means that while one event is being reduced, all other events of any kind _that belong to the same entity instance_ will be waiting in a queue until the previous reducer has finished. This is how Magek guarantees that the entity state is consistent.
|
|
152
|
+
|
|
153
|
+

|
|
154
|
+
|
|
155
|
+
### Eventual Consistency
|
|
156
|
+
|
|
157
|
+
Additionally, due to the event driven and async nature of Magek, your data might not be instantly updated. Magek will consume the commands, generate events, and _eventually_ generate the entities. Most of the time this is not perceivable, but under huge loads, it could be noticed.
|
|
158
|
+
|
|
159
|
+
This property is called [Eventual Consistency](https://en.wikipedia.org/wiki/Eventual_consistency), and it is a trade-off to have high availability for extreme situations, where other systems might simply fail.
|
|
160
|
+
|
|
161
|
+
## Entity ID
|
|
162
|
+
|
|
163
|
+
In order to identify each entity instance, you must define an `id` field on each entity. This field will be used by the framework to identify the entity instance. If the value of the `id` field matches the value returned by the [`entityID()` method](./event.md#events-and-entities) of an Event, the framework will consider that the event belongs to that entity instance.
|
|
164
|
+
|
|
165
|
+
```typescript title="src/entities/entity-name.ts"
|
|
166
|
+
@Entity
|
|
167
|
+
export class EntityName {
|
|
168
|
+
// highlight-next-line
|
|
169
|
+
@field(type => UUID)
|
|
170
|
+
public id!: UUID
|
|
171
|
+
|
|
172
|
+
@field()
|
|
173
|
+
readonly fieldA!: SomeType
|
|
174
|
+
|
|
175
|
+
@field()
|
|
176
|
+
readonly fieldB!: SomeOtherType
|
|
177
|
+
|
|
178
|
+
@reduces(SomeEvent)
|
|
179
|
+
public static reduceSomeEvent(event: SomeEvent, currentEntityState?: EntityName): EntityName {
|
|
180
|
+
return evolve(currentEntityState, {
|
|
181
|
+
id: event.entityID(),
|
|
182
|
+
fieldA: event.fieldA,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> **Tip:** We recommend you to use the `UUID` type for the `id` field. You can generate a new `UUID` value by calling the `UUID.generate()` method already provided by the framework.
|
|
189
|
+
|
|
190
|
+
## Entities naming convention
|
|
191
|
+
|
|
192
|
+
Entities are a representation of your application state in a specific moment, so name them as closely to your domain objects as possible. Typical entity names are nouns that might appear when you think about your app. In an e-commerce application, some entities would be:
|
|
193
|
+
|
|
194
|
+
- Cart
|
|
195
|
+
- Product
|
|
196
|
+
- UserProfile
|
|
197
|
+
- Order
|
|
198
|
+
- Address
|
|
199
|
+
- PaymentMethod
|
|
200
|
+
- Stock
|
|
201
|
+
|
|
202
|
+
Entities live within the entities directory of the project source: `<project-root>/src/entities`.
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
<project-root>
|
|
206
|
+
├── src
|
|
207
|
+
│ ├── commands
|
|
208
|
+
│ ├── common
|
|
209
|
+
│ ├── config
|
|
210
|
+
│ ├── entities <------ put them here
|
|
211
|
+
│ ├── events
|
|
212
|
+
│ ├── index.ts
|
|
213
|
+
│ └── read-models
|
|
214
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Event-Driven Architecture"
|
|
3
|
+
group: "Architecture"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Magek architecture
|
|
7
|
+
|
|
8
|
+
Magek is a highly opinionated framework that provides a complete toolset to build production-ready event-driven serverless applications.
|
|
9
|
+
|
|
10
|
+
Two patterns influence the Magek's event-driven architecture: Command-Query Responsibility Segregation ([CQRS](https://www.martinfowler.com/bliki/CQRS.html)) and [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html). They're complex techniques to implement from scratch with lower-level frameworks, but Magek makes them feel natural and very easy to use.
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
As you can see in the diagram, Magek applications consist of four main building blocks: `Commands`, `Events`, `Entities`, and `Read Models`. `Commands` and `Read Models` are the public interface of the application, while `Events` and `Entities` are private implementation details. With Magek, clients submit `Commands`, query the `Read Models`, or subscribe to them for receiving real-time updates thanks to the out of the box [GraphQL API](/graphql)
|
|
15
|
+
|
|
16
|
+
Magek applications are event-driven and event-sourced so, **the source of truth is the whole history of events**. When a client submits a command, Magek _wakes up_ and handles it throght `Command Handlers`. As part of the process, some `Events` may be _registered_ as needed.
|
|
17
|
+
|
|
18
|
+
On the other side, the framework caches the current state by automatically _reducing_ all the registered events into `Entities`. You can also _react_ to events via `Event Handlers`, triggering side effect actions to certain events. Finally, `Entities` are not directly exposed, they are transformed or _projected_ into `ReadModels`, which are exposed to the public.
|
|
19
|
+
|
|
20
|
+
In this chapter you'll walk through these concepts in detail.
|
|
21
|
+
|
|
22
|
+
## Architecture Components
|
|
23
|
+
|
|
24
|
+
- [Commands](./command.md) - The input interface for your application
|
|
25
|
+
- [Events](./event.md) - Records of facts that represent the source of truth
|
|
26
|
+
- [Event Handlers](./event-handler.md) - React to events and trigger side effects
|
|
27
|
+
- [Entities](./entity.md) - Current state derived from events
|
|
28
|
+
- [Read Models](./read-model.md) - Public projections of your data
|
|
29
|
+
- [Notifications](./notifications.md) - Real-time updates and messaging
|
|
30
|
+
- [Queries](./queries.md) - How to query your read models
|