@magek/mcp-server 0.0.9 → 0.0.11
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/dist/prompts/cqrs-flow.js +21 -13
- package/dist/prompts/troubleshooting.js +43 -4
- package/docs/advanced/custom-adapters.md +371 -0
- package/docs/advanced/framework-packages.md +100 -7
- package/docs/advanced/sensor.md +111 -2
- package/docs/architecture/best-practices.md +133 -0
- package/docs/architecture/command.md +12 -5
- package/docs/architecture/entity.md +23 -1
- package/docs/architecture/event-driven.md +1 -1
- package/docs/architecture/read-model.md +13 -6
- package/docs/docs-index.json +17 -5
- package/docs/getting-started/coding.md +1 -1
- package/docs/getting-started/installation.md +7 -0
- package/docs/index.md +21 -13
- package/package.json +3 -3
|
@@ -94,10 +94,13 @@ npx magek new:entity <EntityName> --fields <field1:type1> <field2:type2> --reduc
|
|
|
94
94
|
**Best Practices:**
|
|
95
95
|
- One entity per aggregate root
|
|
96
96
|
- Entities should only contain state derived from events
|
|
97
|
-
- Use the \`@
|
|
97
|
+
- Use the \`@reduces\` decorator to specify which events affect this entity
|
|
98
|
+
- **Always use \`evolve()\` for state updates** - it ensures immutability
|
|
98
99
|
|
|
99
100
|
**Example:**
|
|
100
101
|
\`\`\`typescript
|
|
102
|
+
import { evolve } from '@magek/common'
|
|
103
|
+
|
|
101
104
|
@Entity
|
|
102
105
|
export class <EntityName> {
|
|
103
106
|
public constructor(
|
|
@@ -106,20 +109,22 @@ export class <EntityName> {
|
|
|
106
109
|
readonly field2: number,
|
|
107
110
|
) {}
|
|
108
111
|
|
|
109
|
-
@
|
|
112
|
+
@reduces(<EventName>)
|
|
110
113
|
public static reduce<EventName>(
|
|
111
114
|
event: <EventName>,
|
|
112
115
|
currentEntity?: <EntityName>
|
|
113
116
|
): <EntityName> {
|
|
114
|
-
return
|
|
115
|
-
event.entityId,
|
|
116
|
-
event.field1,
|
|
117
|
-
event.field2,
|
|
118
|
-
)
|
|
117
|
+
return evolve(currentEntity, {
|
|
118
|
+
id: event.entityId,
|
|
119
|
+
field1: event.field1,
|
|
120
|
+
field2: event.field2,
|
|
121
|
+
})
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
\`\`\`
|
|
122
125
|
|
|
126
|
+
> **Important:** Always use \`evolve()\` from \`@magek/common\` for state updates. It ensures immutability and handles both new entities (when \`currentEntity\` is undefined) and updates.
|
|
127
|
+
|
|
123
128
|
## Step 4: Define the Read Model
|
|
124
129
|
|
|
125
130
|
Read models provide optimized query access to your data.
|
|
@@ -132,9 +137,12 @@ npx magek new:read-model <ReadModelName> --fields <field1:type1> <field2:type2>
|
|
|
132
137
|
- Design read models for specific query use cases
|
|
133
138
|
- Include only the fields needed for queries
|
|
134
139
|
- You can have multiple read models projecting the same entity
|
|
140
|
+
- **Use \`evolve()\` for projection updates** - same pattern as entities
|
|
135
141
|
|
|
136
142
|
**Example:**
|
|
137
143
|
\`\`\`typescript
|
|
144
|
+
import { evolve } from '@magek/common'
|
|
145
|
+
|
|
138
146
|
@ReadModel({
|
|
139
147
|
authorize: 'all'
|
|
140
148
|
})
|
|
@@ -145,16 +153,16 @@ export class <ReadModelName> {
|
|
|
145
153
|
readonly field2: number,
|
|
146
154
|
) {}
|
|
147
155
|
|
|
148
|
-
@
|
|
156
|
+
@projects(<EntityName>, 'id')
|
|
149
157
|
public static project<EntityName>(
|
|
150
158
|
entity: <EntityName>,
|
|
151
159
|
currentReadModel?: <ReadModelName>
|
|
152
160
|
): ProjectionResult<<ReadModelName>> {
|
|
153
|
-
return
|
|
154
|
-
entity.id,
|
|
155
|
-
entity.field1,
|
|
156
|
-
entity.field2,
|
|
157
|
-
)
|
|
161
|
+
return evolve(currentReadModel, {
|
|
162
|
+
id: entity.id,
|
|
163
|
+
field1: entity.field1,
|
|
164
|
+
field2: entity.field2,
|
|
165
|
+
})
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
\`\`\`
|
|
@@ -43,13 +43,18 @@ export * from './read-models/ProductReadModel'
|
|
|
43
43
|
3. Ensure the reduce method returns a new entity instance (not mutating)
|
|
44
44
|
|
|
45
45
|
\`\`\`typescript
|
|
46
|
+
import { evolve } from '@magek/common'
|
|
47
|
+
|
|
46
48
|
@Entity
|
|
47
49
|
export class Product {
|
|
48
50
|
// Make sure decorator references the actual event class
|
|
49
51
|
@Reduces(ProductCreated) // ✅ Correct
|
|
50
52
|
@Reduces('ProductCreated') // ❌ Won't work
|
|
51
|
-
public static reduceProductCreated(event: ProductCreated): Product {
|
|
52
|
-
return
|
|
53
|
+
public static reduceProductCreated(event: ProductCreated, current?: Product): Product {
|
|
54
|
+
return evolve(current, {
|
|
55
|
+
id: event.entityId,
|
|
56
|
+
name: event.name,
|
|
57
|
+
})
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
\`\`\`
|
|
@@ -78,8 +83,9 @@ export class Product {
|
|
|
78
83
|
1. Check the \`authorize\` option in decorators:
|
|
79
84
|
\`\`\`typescript
|
|
80
85
|
@Command({ authorize: 'all' }) // Public access
|
|
81
|
-
@Command({ authorize:
|
|
82
|
-
@Command({ authorize: [
|
|
86
|
+
@Command({ authorize: [User] }) // Requires User role
|
|
87
|
+
@Command({ authorize: [Admin] }) // Requires Admin role
|
|
88
|
+
@Command({ authorize: [User, Admin] }) // Requires User OR Admin role
|
|
83
89
|
\`\`\`
|
|
84
90
|
|
|
85
91
|
2. If using authentication, ensure JWT token is valid
|
|
@@ -138,6 +144,39 @@ export class Product {
|
|
|
138
144
|
}
|
|
139
145
|
\`\`\`
|
|
140
146
|
|
|
147
|
+
### Reducer returns incorrect state
|
|
148
|
+
|
|
149
|
+
**Symptoms:**
|
|
150
|
+
- Entity state not updating as expected
|
|
151
|
+
- State appears corrupted or missing fields
|
|
152
|
+
- Immutability violations
|
|
153
|
+
|
|
154
|
+
**Solutions:**
|
|
155
|
+
1. **Always use \`evolve()\` for state updates** - never mutate directly:
|
|
156
|
+
\`\`\`typescript
|
|
157
|
+
// ✅ Correct - use evolve()
|
|
158
|
+
return evolve(current, { name: event.newName })
|
|
159
|
+
|
|
160
|
+
// ❌ Incorrect - mutates state directly
|
|
161
|
+
current.name = event.newName
|
|
162
|
+
return current
|
|
163
|
+
|
|
164
|
+
// ❌ Incorrect - manual construction doesn't handle undefined
|
|
165
|
+
return new Product(event.id, event.name)
|
|
166
|
+
\`\`\`
|
|
167
|
+
|
|
168
|
+
2. Handle the case when \`current\` is undefined (new entity):
|
|
169
|
+
\`\`\`typescript
|
|
170
|
+
// evolve() handles this automatically
|
|
171
|
+
return evolve(current, { id: event.id, name: event.name })
|
|
172
|
+
\`\`\`
|
|
173
|
+
|
|
174
|
+
3. For update-only reducers, skip if entity doesn't exist:
|
|
175
|
+
\`\`\`typescript
|
|
176
|
+
if (!current) return ReducerAction.Skip
|
|
177
|
+
return evolve(current, { name: event.newName })
|
|
178
|
+
\`\`\`
|
|
179
|
+
|
|
141
180
|
---
|
|
142
181
|
|
|
143
182
|
## Development Server Issues
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# Custom Adapters
|
|
2
|
+
|
|
3
|
+
Magek uses an adapter pattern to abstract away database and storage implementation details. This allows you to use different storage backends without modifying your application code. This guide explains how to create custom adapters for the Magek framework.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Magek has three types of adapters:
|
|
8
|
+
|
|
9
|
+
1. **Event Store Adapter** - Stores events and entity snapshots for event sourcing
|
|
10
|
+
2. **Read Model Store Adapter** - Stores and queries read model projections
|
|
11
|
+
3. **Session Store Adapter** - Manages WebSocket connections and GraphQL subscriptions
|
|
12
|
+
|
|
13
|
+
Each adapter type has a well-defined interface that you must implement. The framework provides built-in adapters for NeDB (file-based) and in-memory storage.
|
|
14
|
+
|
|
15
|
+
## Adapter Interfaces
|
|
16
|
+
|
|
17
|
+
### Event Store Adapter
|
|
18
|
+
|
|
19
|
+
The `EventStoreAdapter` interface (defined in `@magek/common`) handles event sourcing operations:
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
interface EventStoreAdapter {
|
|
23
|
+
// Convert raw data to EventEnvelope objects
|
|
24
|
+
rawToEnvelopes(rawEvents: unknown): Array<EventEnvelope>
|
|
25
|
+
|
|
26
|
+
// Streaming methods (optional - can throw "not implemented")
|
|
27
|
+
rawStreamToEnvelopes(config: MagekConfig, context: unknown, dedupEventStream: EventStream): Array<EventEnvelope>
|
|
28
|
+
dedupEventStream(config: MagekConfig, rawEvents: unknown): Promise<EventStream>
|
|
29
|
+
produce(entityName: string, entityID: UUID, eventEnvelopes: Array<EventEnvelope>, config: MagekConfig): Promise<void>
|
|
30
|
+
|
|
31
|
+
// Core event operations
|
|
32
|
+
forEntitySince(config: MagekConfig, entityTypeName: string, entityID: UUID, since?: string): Promise<Array<EventEnvelope>>
|
|
33
|
+
latestEntitySnapshot(config: MagekConfig, entityTypeName: string, entityID: UUID): Promise<EntitySnapshotEnvelope | undefined>
|
|
34
|
+
store(eventEnvelopes: Array<NonPersistedEventEnvelope>, config: MagekConfig): Promise<Array<EventEnvelope>>
|
|
35
|
+
storeSnapshot(snapshotEnvelope: NonPersistedEntitySnapshotEnvelope, config: MagekConfig): Promise<EntitySnapshotEnvelope>
|
|
36
|
+
|
|
37
|
+
// Search and pagination
|
|
38
|
+
search(config: MagekConfig, parameters: EventSearchParameters): Promise<Array<EventSearchResponse>>
|
|
39
|
+
searchEntitiesIDs(config: MagekConfig, limit: number, afterCursor: Record<string, string> | undefined, entityTypeName: string): Promise<PaginatedEntitiesIdsResult>
|
|
40
|
+
|
|
41
|
+
// Dispatch tracking
|
|
42
|
+
storeDispatched(eventEnvelope: EventEnvelope, config: MagekConfig): Promise<boolean>
|
|
43
|
+
|
|
44
|
+
// Deletion operations
|
|
45
|
+
findDeletableEvent(config: MagekConfig, parameters: EventDeleteParameters): Promise<Array<EventEnvelopeFromDatabase>>
|
|
46
|
+
findDeletableSnapshot(config: MagekConfig, parameters: SnapshotDeleteParameters): Promise<Array<EntitySnapshotEnvelopeFromDatabase>>
|
|
47
|
+
deleteEvent(config: MagekConfig, events: Array<EventEnvelopeFromDatabase>): Promise<void>
|
|
48
|
+
deleteSnapshot(config: MagekConfig, snapshots: Array<EntitySnapshotEnvelopeFromDatabase>): Promise<void>
|
|
49
|
+
|
|
50
|
+
// Health check (optional)
|
|
51
|
+
healthCheck?: {
|
|
52
|
+
isUp(config: MagekConfig): Promise<boolean>
|
|
53
|
+
details(config: MagekConfig): Promise<unknown>
|
|
54
|
+
urls(config: MagekConfig): Promise<Array<string>>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Async event processing (optional)
|
|
58
|
+
fetchUnprocessedEvents?(config: MagekConfig): Promise<Array<EventEnvelope>>
|
|
59
|
+
markEventProcessed?(config: MagekConfig, eventId: UUID): Promise<void>
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Read Model Store Adapter
|
|
64
|
+
|
|
65
|
+
The `ReadModelStoreAdapter` interface handles read model projections:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
interface ReadModelStoreAdapter {
|
|
69
|
+
// Fetch a specific read model by ID
|
|
70
|
+
fetch<TReadModel extends ReadModelInterface>(
|
|
71
|
+
config: MagekConfig,
|
|
72
|
+
readModelName: string,
|
|
73
|
+
readModelID: UUID,
|
|
74
|
+
sequenceKey?: SequenceKey
|
|
75
|
+
): Promise<ReadOnlyNonEmptyArray<TReadModel> | undefined>
|
|
76
|
+
|
|
77
|
+
// Search read models with filters, sorting, and pagination
|
|
78
|
+
search<TReadModel extends ReadModelInterface>(
|
|
79
|
+
config: MagekConfig,
|
|
80
|
+
readModelName: string,
|
|
81
|
+
filters: FilterFor<unknown>,
|
|
82
|
+
sortBy?: SortFor<unknown>,
|
|
83
|
+
limit?: number,
|
|
84
|
+
afterCursor?: unknown,
|
|
85
|
+
paginatedVersion?: boolean,
|
|
86
|
+
select?: ProjectionFor<TReadModel>
|
|
87
|
+
): Promise<Array<TReadModel> | ReadModelListResult<TReadModel>>
|
|
88
|
+
|
|
89
|
+
// Store or update a read model
|
|
90
|
+
store<TReadModel extends ReadModelInterface>(
|
|
91
|
+
config: MagekConfig,
|
|
92
|
+
readModelName: string,
|
|
93
|
+
readModel: ReadModelStoreEnvelope<TReadModel>
|
|
94
|
+
): Promise<ReadModelStoreEnvelope<TReadModel>>
|
|
95
|
+
|
|
96
|
+
// Delete a read model
|
|
97
|
+
delete(config: MagekConfig, readModelName: string, readModelID: UUID): Promise<void>
|
|
98
|
+
|
|
99
|
+
// Convert raw data to envelopes
|
|
100
|
+
rawToEnvelopes<TReadModel extends ReadModelInterface>(
|
|
101
|
+
config: MagekConfig,
|
|
102
|
+
rawReadModels: unknown
|
|
103
|
+
): Promise<Array<ReadModelStoreEnvelope<TReadModel>>>
|
|
104
|
+
|
|
105
|
+
// Health check (optional)
|
|
106
|
+
healthCheck?: {
|
|
107
|
+
isUp(config: MagekConfig): Promise<boolean>
|
|
108
|
+
details(config: MagekConfig): Promise<unknown>
|
|
109
|
+
urls(config: MagekConfig): Promise<Array<string>>
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Session Store Adapter
|
|
115
|
+
|
|
116
|
+
The `SessionStoreAdapter` interface manages real-time connections:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
interface SessionStoreAdapter {
|
|
120
|
+
// Connection management
|
|
121
|
+
storeConnection(config: MagekConfig, connectionId: UUID, connectionData: Record<string, any>): Promise<void>
|
|
122
|
+
fetchConnection(config: MagekConfig, connectionId: UUID): Promise<Record<string, any> | undefined>
|
|
123
|
+
deleteConnection(config: MagekConfig, connectionId: UUID): Promise<void>
|
|
124
|
+
|
|
125
|
+
// Subscription management
|
|
126
|
+
storeSubscription(config: MagekConfig, connectionId: UUID, subscriptionId: UUID, subscriptionData: Record<string, any>): Promise<void>
|
|
127
|
+
fetchSubscription(config: MagekConfig, subscriptionId: UUID): Promise<Record<string, any> | undefined>
|
|
128
|
+
deleteSubscription(config: MagekConfig, connectionId: UUID, subscriptionId: UUID): Promise<void>
|
|
129
|
+
|
|
130
|
+
// Bulk operations
|
|
131
|
+
fetchSubscriptionsForConnection(config: MagekConfig, connectionId: UUID): Promise<Array<Record<string, any>>>
|
|
132
|
+
deleteSubscriptionsForConnection(config: MagekConfig, connectionId: UUID): Promise<void>
|
|
133
|
+
fetchSubscriptionsByClassName(config: MagekConfig, className: string): Promise<Array<SubscriptionEnvelope>>
|
|
134
|
+
|
|
135
|
+
// Health check (optional)
|
|
136
|
+
healthCheck?: {
|
|
137
|
+
isUp(config: MagekConfig): Promise<boolean>
|
|
138
|
+
details(config: MagekConfig): Promise<unknown>
|
|
139
|
+
urls(config: MagekConfig): Promise<Array<string>>
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Implementation Guide
|
|
145
|
+
|
|
146
|
+
### Package Structure
|
|
147
|
+
|
|
148
|
+
Create your adapter package with this structure:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
adapters/<database>/<adapter-type>/
|
|
152
|
+
├── src/
|
|
153
|
+
│ ├── index.ts # Export adapter singleton
|
|
154
|
+
│ └── <database>-<type>.ts # Implementation
|
|
155
|
+
├── test/
|
|
156
|
+
│ ├── expect.ts # Test utilities
|
|
157
|
+
│ └── <adapter>.test.ts # Tests
|
|
158
|
+
├── package.json
|
|
159
|
+
├── tsconfig.json
|
|
160
|
+
├── tsconfig.test.json
|
|
161
|
+
├── tsconfig.eslint.json
|
|
162
|
+
├── eslint.config.mjs
|
|
163
|
+
└── .mocharc.yml
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Package Naming Convention
|
|
167
|
+
|
|
168
|
+
Use this naming pattern: `@magek/adapter-{type}-{database}`
|
|
169
|
+
|
|
170
|
+
Examples:
|
|
171
|
+
- `@magek/adapter-event-store-nedb`
|
|
172
|
+
- `@magek/adapter-read-model-store-memory`
|
|
173
|
+
- `@magek/adapter-session-store-redis`
|
|
174
|
+
|
|
175
|
+
### Implementing the Event Store
|
|
176
|
+
|
|
177
|
+
Key implementation considerations:
|
|
178
|
+
|
|
179
|
+
1. **Event Storage**: Store events with `kind: 'event'` and snapshots with `kind: 'snapshot'`
|
|
180
|
+
2. **Timestamps**: Generate `createdAt` when storing events
|
|
181
|
+
3. **Soft Deletes**: Events should be soft-deleted by setting `deletedAt` and clearing `value`
|
|
182
|
+
4. **Snapshots**: Hard delete snapshots when requested
|
|
183
|
+
5. **Dispatch Tracking**: Track which events have been dispatched to prevent duplicate processing
|
|
184
|
+
6. **Async Processing** (optional): Implement `fetchUnprocessedEvents` and `markEventProcessed` for polling-based event dispatch
|
|
185
|
+
|
|
186
|
+
Example implementation pattern:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
import { EventStoreAdapter } from '@magek/common'
|
|
190
|
+
|
|
191
|
+
const eventRegistry = new YourEventRegistry()
|
|
192
|
+
|
|
193
|
+
export const eventStore: EventStoreAdapter = {
|
|
194
|
+
rawToEnvelopes: (rawEvents) => rawEvents as Array<EventEnvelope>,
|
|
195
|
+
|
|
196
|
+
forEntitySince: async (config, entityTypeName, entityID, since) => {
|
|
197
|
+
const query = {
|
|
198
|
+
entityTypeName,
|
|
199
|
+
entityID,
|
|
200
|
+
kind: 'event',
|
|
201
|
+
createdAt: { $gt: since || new Date(0).toISOString() },
|
|
202
|
+
deletedAt: { $exists: false },
|
|
203
|
+
}
|
|
204
|
+
return eventRegistry.query(query)
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
store: async (eventEnvelopes, config) => {
|
|
208
|
+
const persisted = []
|
|
209
|
+
for (const envelope of eventEnvelopes) {
|
|
210
|
+
const withTimestamp = { ...envelope, createdAt: new Date().toISOString() }
|
|
211
|
+
await eventRegistry.store(withTimestamp)
|
|
212
|
+
persisted.push(withTimestamp)
|
|
213
|
+
}
|
|
214
|
+
return persisted
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// ... implement other methods
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Implementing the Read Model Store
|
|
222
|
+
|
|
223
|
+
Key implementation considerations:
|
|
224
|
+
|
|
225
|
+
1. **Optimistic Concurrency**: Check version before updates, throw `OptimisticConcurrencyUnexpectedVersionError` on conflicts
|
|
226
|
+
2. **Filter Support**: Implement filtering operations (eq, ne, lt, gt, lte, gte, in, contains, etc.)
|
|
227
|
+
3. **Sorting**: Support multi-field sorting with ASC/DESC
|
|
228
|
+
4. **Pagination**: Implement cursor-based pagination
|
|
229
|
+
5. **Field Projection**: Support selecting specific fields
|
|
230
|
+
|
|
231
|
+
Filter operations to support:
|
|
232
|
+
|
|
233
|
+
| Operation | Description | Example |
|
|
234
|
+
|-----------|-------------|---------|
|
|
235
|
+
| `eq` | Equals | `{ name: { eq: 'John' } }` |
|
|
236
|
+
| `ne` | Not equals | `{ status: { ne: 'deleted' } }` |
|
|
237
|
+
| `lt` | Less than | `{ age: { lt: 18 } }` |
|
|
238
|
+
| `gt` | Greater than | `{ price: { gt: 100 } }` |
|
|
239
|
+
| `lte` | Less than or equal | `{ age: { lte: 65 } }` |
|
|
240
|
+
| `gte` | Greater than or equal | `{ rating: { gte: 4 } }` |
|
|
241
|
+
| `in` | In array | `{ status: { in: ['active', 'pending'] } }` |
|
|
242
|
+
| `isDefined` | Field exists | `{ email: { isDefined: true } }` |
|
|
243
|
+
| `contains` | String contains | `{ name: { contains: 'smith' } }` |
|
|
244
|
+
| `beginsWith` | String starts with | `{ name: { beginsWith: 'Dr.' } }` |
|
|
245
|
+
| `regex` | Regular expression | `{ email: { regex: '@example.com$' } }` |
|
|
246
|
+
| `iRegex` | Case-insensitive regex | `{ name: { iRegex: 'john' } }` |
|
|
247
|
+
| `includes` | Array includes | `{ tags: { includes: 'featured' } }` |
|
|
248
|
+
| `and` | Logical AND | `{ and: [filter1, filter2] }` |
|
|
249
|
+
| `or` | Logical OR | `{ or: [filter1, filter2] }` |
|
|
250
|
+
| `not` | Logical NOT | `{ not: { status: { eq: 'deleted' } } }` |
|
|
251
|
+
|
|
252
|
+
### Implementing the Session Store
|
|
253
|
+
|
|
254
|
+
Key implementation considerations:
|
|
255
|
+
|
|
256
|
+
1. **Connection Indexing**: Index connections by ID for fast lookup
|
|
257
|
+
2. **Subscription Indexing**: Index subscriptions by connection ID and class name
|
|
258
|
+
3. **Cleanup**: Ensure deleting a connection also cleans up its subscriptions
|
|
259
|
+
4. **Class Name Queries**: Support fetching subscriptions by read model class name
|
|
260
|
+
|
|
261
|
+
### Health Checks
|
|
262
|
+
|
|
263
|
+
Implement health checks to support monitoring:
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
healthCheck: {
|
|
267
|
+
isUp: async (config) => {
|
|
268
|
+
try {
|
|
269
|
+
// Test database connectivity
|
|
270
|
+
await database.ping()
|
|
271
|
+
return true
|
|
272
|
+
} catch {
|
|
273
|
+
return false
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
details: async (config) => ({
|
|
278
|
+
type: 'your-database',
|
|
279
|
+
status: 'healthy',
|
|
280
|
+
eventsCount: await database.count('events'),
|
|
281
|
+
}),
|
|
282
|
+
|
|
283
|
+
urls: async (config) => ['your-database://connection-string'],
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Async Event Processing (Optional)
|
|
288
|
+
|
|
289
|
+
Magek supports async event processing through polling. When implemented, the server polls for unprocessed events and dispatches them in batches, preventing duplicate processing.
|
|
290
|
+
|
|
291
|
+
#### Configuration Options
|
|
292
|
+
|
|
293
|
+
| Option | Default | Description |
|
|
294
|
+
|--------|---------|-------------|
|
|
295
|
+
| `eventPollingIntervalMs` | 1000 | Milliseconds between polling cycles |
|
|
296
|
+
| `eventProcessingBatchSize` | 100 | Maximum events per batch |
|
|
297
|
+
|
|
298
|
+
#### Implementation Pattern
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Track cursor position
|
|
302
|
+
let processingCursor: string = new Date(0).toISOString()
|
|
303
|
+
|
|
304
|
+
async function fetchUnprocessedEvents(config: MagekConfig): Promise<Array<EventEnvelope>> {
|
|
305
|
+
const query = {
|
|
306
|
+
kind: 'event',
|
|
307
|
+
deletedAt: { $exists: false },
|
|
308
|
+
processedAt: { $exists: false },
|
|
309
|
+
createdAt: { $gt: processingCursor },
|
|
310
|
+
}
|
|
311
|
+
return database.query(query, { sort: 'createdAt', limit: config.eventProcessingBatchSize })
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function markEventProcessed(config: MagekConfig, eventId: UUID): Promise<void> {
|
|
315
|
+
const event = await database.findById(eventId)
|
|
316
|
+
if (!event) return
|
|
317
|
+
await database.update(eventId, { processedAt: new Date().toISOString() })
|
|
318
|
+
processingCursor = event.createdAt
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Important Considerations
|
|
323
|
+
|
|
324
|
+
1. **Remove Synchronous Dispatch**: When implementing async processing, remove any `eventDispatcher()` call from your `store()` method
|
|
325
|
+
2. **Cursor Persistence**: Persistent adapters should persist the cursor; in-memory can use a variable
|
|
326
|
+
3. **Event Ordering**: Process events in `createdAt` order for consistency
|
|
327
|
+
4. **Optional Implementation**: If not implemented, the server uses synchronous dispatch
|
|
328
|
+
|
|
329
|
+
## Configuration
|
|
330
|
+
|
|
331
|
+
To use your custom adapter, configure it in your Magek application:
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { MagekConfig } from '@magek/core'
|
|
335
|
+
import { eventStore } from '@magek/adapter-event-store-your-db'
|
|
336
|
+
import { readModelStore } from '@magek/adapter-read-model-store-your-db'
|
|
337
|
+
import { sessionStore } from '@magek/adapter-session-store-your-db'
|
|
338
|
+
|
|
339
|
+
const config = new MagekConfig('production')
|
|
340
|
+
|
|
341
|
+
// Set custom adapters
|
|
342
|
+
;(config as any).eventStoreAdapter = eventStore
|
|
343
|
+
;(config as any).readModelStoreAdapter = readModelStore
|
|
344
|
+
;(config as any).sessionStoreAdapter = sessionStore
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
## Best Practices
|
|
348
|
+
|
|
349
|
+
1. **Export Singleton Instances**: Export pre-configured adapter instances for easy usage:
|
|
350
|
+
```typescript
|
|
351
|
+
export const eventStore: EventStoreAdapter = { /* ... */ }
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
2. **Use Logging**: Use `getLogger(config, 'AdapterName#methodName')` for consistent logging
|
|
355
|
+
|
|
356
|
+
3. **Handle Errors Gracefully**: Wrap database errors in appropriate Magek error types
|
|
357
|
+
|
|
358
|
+
4. **Support TypeScript**: Export types and use generics where appropriate
|
|
359
|
+
|
|
360
|
+
5. **Write Comprehensive Tests**: Test all interface methods including edge cases
|
|
361
|
+
|
|
362
|
+
6. **Document Connection Requirements**: Clearly document any environment variables or configuration needed
|
|
363
|
+
|
|
364
|
+
## Example Adapters
|
|
365
|
+
|
|
366
|
+
For reference implementations, see:
|
|
367
|
+
|
|
368
|
+
- **NeDB Adapters** (file-based): `adapters/nedb/`
|
|
369
|
+
- **Memory Adapters** (in-memory): `adapters/memory/`
|
|
370
|
+
|
|
371
|
+
These implementations demonstrate all required patterns and can serve as templates for your custom adapters.
|
|
@@ -3,15 +3,108 @@ title: "Framework Packages"
|
|
|
3
3
|
group: "Advanced"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Framework
|
|
7
|
-
The framework is already splitted into different packages:
|
|
6
|
+
# Framework Packages
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
Magek is organized as a monorepo with these packages:
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
## Core Packages
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
### @magek/core
|
|
14
13
|
|
|
15
|
-
|
|
14
|
+
The event sourcing engine with CQRS patterns, GraphQL generation, and decorators. This is the main package that powers Magek applications.
|
|
16
15
|
|
|
17
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npm install @magek/core
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### @magek/common
|
|
21
|
+
|
|
22
|
+
Shared types, utilities, and helper functions used across the framework. Includes essential helpers like:
|
|
23
|
+
|
|
24
|
+
- `evolve()` - Immutable state updates for entities and read models
|
|
25
|
+
- `UUID` - Unique identifier generation
|
|
26
|
+
- `createInstance()` - Instantiate classes from raw objects
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @magek/common
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Development Tools
|
|
33
|
+
|
|
34
|
+
### @magek/cli
|
|
35
|
+
|
|
36
|
+
Command-line tool for scaffolding projects, generating components, and running the development server.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Generate new components
|
|
40
|
+
npx magek new:command CreateProduct
|
|
41
|
+
npx magek new:event ProductCreated
|
|
42
|
+
npx magek new:entity Product
|
|
43
|
+
npx magek new:read-model ProductReadModel
|
|
44
|
+
|
|
45
|
+
# Start development server
|
|
46
|
+
npx magek start
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### @magek/server
|
|
50
|
+
|
|
51
|
+
Fastify-based runtime for local development. Provides:
|
|
52
|
+
|
|
53
|
+
- Hot reloading
|
|
54
|
+
- GraphQL playground
|
|
55
|
+
- Local event store and read model storage
|
|
56
|
+
|
|
57
|
+
### create-magek
|
|
58
|
+
|
|
59
|
+
Project scaffolding tool for creating new Magek applications with a single command.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx create-magek my-app
|
|
63
|
+
cd my-app
|
|
64
|
+
npm install
|
|
65
|
+
npx magek start
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Storage Adapters
|
|
69
|
+
|
|
70
|
+
Adapters provide pluggable storage backends for Magek applications. The NeDB adapters are used for local development.
|
|
71
|
+
|
|
72
|
+
### @magek/adapter-event-store-nedb
|
|
73
|
+
|
|
74
|
+
NeDB-based event store adapter. Stores events in a local file-based database, ideal for development and testing.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm install @magek/adapter-event-store-nedb
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### @magek/adapter-read-model-store-nedb
|
|
81
|
+
|
|
82
|
+
NeDB-based read model store adapter. Stores read model projections locally.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install @magek/adapter-read-model-store-nedb
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### @magek/adapter-session-store-nedb
|
|
89
|
+
|
|
90
|
+
NeDB-based session store adapter. Manages WebSocket connections and subscriptions for real-time features.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install @magek/adapter-session-store-nedb
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## AI Integration
|
|
97
|
+
|
|
98
|
+
### @magek/mcp-server
|
|
99
|
+
|
|
100
|
+
Model Context Protocol (MCP) server that provides documentation, CLI reference, and guided prompts for AI coding assistants. Use this to integrate Magek knowledge into your AI-powered development workflow.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx @magek/mcp-server
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Features:
|
|
107
|
+
- Documentation resources accessible via MCP
|
|
108
|
+
- CQRS implementation guide prompt
|
|
109
|
+
- Troubleshooting assistance prompt
|
|
110
|
+
- CLI command reference
|
package/docs/advanced/sensor.md
CHANGED
|
@@ -3,8 +3,117 @@ title: "Sensors"
|
|
|
3
3
|
group: "Advanced"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Sensors
|
|
7
|
+
|
|
8
|
+
Sensors are monitoring components in Magek that track and report the status of your application. They provide visibility into application health and performance through HTTP endpoints.
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Magek's sensor system allows you to:
|
|
13
|
+
|
|
14
|
+
- Monitor the health status of application components
|
|
15
|
+
- Expose status information via REST endpoints
|
|
16
|
+
- Create custom monitoring for your own services
|
|
17
|
+
- Integrate with external monitoring tools and dashboards
|
|
18
|
+
|
|
19
|
+
## Health Sensors
|
|
20
|
+
|
|
21
|
+
The primary sensor type in Magek is the **Health Sensor**, which monitors application health and exposes status via the `/sensor/health/` endpoint.
|
|
22
|
+
|
|
23
|
+
### Built-in Health Indicators
|
|
24
|
+
|
|
25
|
+
Magek provides these built-in health indicators:
|
|
26
|
+
|
|
27
|
+
| Indicator | Endpoint | Description |
|
|
28
|
+
|-----------|----------|-------------|
|
|
29
|
+
| `magek` | `/sensor/health/magek` | Overall application health |
|
|
30
|
+
| `magek/function` | `/sensor/health/magek/function` | GraphQL function status, CPU, and memory |
|
|
31
|
+
| `magek/database` | `/sensor/health/magek/database` | Database availability |
|
|
32
|
+
| `magek/database/events` | `/sensor/health/magek/database/events` | Event store health |
|
|
33
|
+
| `magek/database/readmodels` | `/sensor/health/magek/database/readmodels` | Read model store health |
|
|
34
|
+
|
|
35
|
+
### Quick Start
|
|
36
|
+
|
|
37
|
+
Enable health sensors in your configuration:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
Magek.configure('local', (config: MagekConfig): void => {
|
|
41
|
+
config.appName = 'my-app'
|
|
42
|
+
config.runtime = ServerRuntime
|
|
43
|
+
|
|
44
|
+
// Enable all health indicators
|
|
45
|
+
Object.values(config.sensorConfiguration.health.magek).forEach((indicator) => {
|
|
46
|
+
indicator.enabled = true
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then access health status at `http://localhost:3000/sensor/health/`.
|
|
52
|
+
|
|
53
|
+
### Creating Custom Health Sensors
|
|
54
|
+
|
|
55
|
+
Use the `@HealthSensor` decorator to create custom health indicators:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import {
|
|
59
|
+
MagekConfig,
|
|
60
|
+
HealthIndicatorResult,
|
|
61
|
+
HealthIndicatorMetadata,
|
|
62
|
+
HealthStatus,
|
|
63
|
+
} from '@magek/common'
|
|
64
|
+
import { HealthSensor } from '@magek/core'
|
|
65
|
+
|
|
66
|
+
@HealthSensor({
|
|
67
|
+
id: 'external-api',
|
|
68
|
+
name: 'External API Health',
|
|
69
|
+
enabled: true,
|
|
70
|
+
details: true,
|
|
71
|
+
})
|
|
72
|
+
export class ExternalApiHealthIndicator {
|
|
73
|
+
public async health(
|
|
74
|
+
config: MagekConfig,
|
|
75
|
+
metadata: HealthIndicatorMetadata
|
|
76
|
+
): Promise<HealthIndicatorResult> {
|
|
77
|
+
// Check your external service
|
|
78
|
+
const isHealthy = await checkExternalApi()
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
status: isHealthy ? HealthStatus.UP : HealthStatus.DOWN,
|
|
82
|
+
details: {
|
|
83
|
+
lastCheck: new Date().toISOString(),
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Health Statuses
|
|
91
|
+
|
|
92
|
+
| Status | Description |
|
|
93
|
+
|--------|-------------|
|
|
94
|
+
| `UP` | Component is working as expected |
|
|
95
|
+
| `PARTIALLY_UP` | Component has reduced functionality |
|
|
96
|
+
| `DOWN` | Component is not working |
|
|
97
|
+
| `OUT_OF_SERVICE` | Component is temporarily unavailable |
|
|
98
|
+
| `UNKNOWN` | Component state cannot be determined |
|
|
99
|
+
|
|
100
|
+
### HTTP Response Codes
|
|
101
|
+
|
|
102
|
+
- **200 OK**: All components are healthy (`UP`)
|
|
103
|
+
- **503 Service Unavailable**: One or more components are unhealthy
|
|
104
|
+
|
|
105
|
+
## Configuration Options
|
|
106
|
+
|
|
107
|
+
Each health indicator supports these options:
|
|
108
|
+
|
|
109
|
+
| Option | Type | Default | Description |
|
|
110
|
+
|--------|------|---------|-------------|
|
|
111
|
+
| `id` | `string` | required | Unique identifier (used in URL path) |
|
|
112
|
+
| `name` | `string` | required | Display name in responses |
|
|
113
|
+
| `enabled` | `boolean` | `false` | Enable/disable the indicator |
|
|
114
|
+
| `details` | `boolean` | `true` | Include detailed information |
|
|
115
|
+
| `showChildren` | `boolean` | `true` | Include child components |
|
|
7
116
|
|
|
8
117
|
## Related Topics
|
|
9
118
|
|
|
10
|
-
- [Sensor
|
|
119
|
+
- [Health Sensor Details](health/sensor-health.md) - Complete health monitoring guide with all configuration options, response formats, and examples
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Best Practices"
|
|
3
|
+
group: "Architecture"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Best Practices
|
|
7
|
+
|
|
8
|
+
Quick reference for writing idiomatic Magek code.
|
|
9
|
+
|
|
10
|
+
## State Updates with `evolve()`
|
|
11
|
+
|
|
12
|
+
**Always use `evolve()` for entity and read model state updates.**
|
|
13
|
+
|
|
14
|
+
The `evolve()` helper from `@magek/common`:
|
|
15
|
+
- Creates immutable copies (never mutates)
|
|
16
|
+
- Handles undefined state (new entities)
|
|
17
|
+
- Provides type safety
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { evolve } from '@magek/common'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Entity Reducers
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// DO: Use evolve()
|
|
27
|
+
@reduces(ProductCreated)
|
|
28
|
+
public static reduceProductCreated(event: ProductCreated, current?: Product): Product {
|
|
29
|
+
return evolve(current, {
|
|
30
|
+
id: event.entityID(),
|
|
31
|
+
name: event.name,
|
|
32
|
+
price: event.price,
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// DON'T: Manual construction
|
|
37
|
+
public static reduceProductCreated(event: ProductCreated): Product {
|
|
38
|
+
return new Product(event.entityID(), event.name, event.price) // Missing immutability!
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Read Model Projections
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
// DO: Use evolve()
|
|
46
|
+
@projects(Product, 'id')
|
|
47
|
+
public static projectProduct(entity: Product, current?: ProductReadModel): ProjectionResult<ProductReadModel> {
|
|
48
|
+
return evolve(current, {
|
|
49
|
+
id: entity.id,
|
|
50
|
+
displayName: entity.name,
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Common Patterns
|
|
56
|
+
|
|
57
|
+
### Creating vs Updating Entities
|
|
58
|
+
|
|
59
|
+
`evolve()` handles both cases automatically:
|
|
60
|
+
- When `current` is `undefined` → creates a new entity
|
|
61
|
+
- When `current` exists → updates with the provided changes
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
@reduces(ProductCreated)
|
|
65
|
+
public static reduceProductCreated(event: ProductCreated, current?: Product): Product {
|
|
66
|
+
// Works for both new entities and updates
|
|
67
|
+
return evolve(current, {
|
|
68
|
+
id: event.entityID(),
|
|
69
|
+
name: event.name,
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Providing Defaults for New Entities
|
|
75
|
+
|
|
76
|
+
When creating entities, you can provide default values:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
return evolve(undefined, { id: event.id, name: event.name }, { status: 'active', createdAt: new Date() })
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The third parameter provides defaults that are applied when `current` is `undefined`.
|
|
83
|
+
|
|
84
|
+
### Partial Updates
|
|
85
|
+
|
|
86
|
+
For update events, only include the fields that change:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
@reduces(ProductRenamed)
|
|
90
|
+
public static reduceProductRenamed(event: ProductRenamed, current?: Product): Product {
|
|
91
|
+
if (!current) return ReducerAction.Skip
|
|
92
|
+
return evolve(current, { name: event.newName })
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Anti-Patterns
|
|
97
|
+
|
|
98
|
+
### Mutating State Directly
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
// NEVER do this - breaks immutability
|
|
102
|
+
current.balance += amount
|
|
103
|
+
return current
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Using Spread Operator Instead of evolve()
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
// Avoid - evolve() is clearer and handles edge cases
|
|
110
|
+
return { ...current, balance: current.balance + amount }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Constructing Entities Manually
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
// Avoid - doesn't handle undefined current state properly
|
|
117
|
+
return new Product(event.id, event.name, event.price)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Utility Helpers Reference
|
|
121
|
+
|
|
122
|
+
| Helper | Import | Purpose |
|
|
123
|
+
|--------|--------|---------|
|
|
124
|
+
| `evolve(current, changes)` | `@magek/common` | Immutable state updates |
|
|
125
|
+
| `evolve(undefined, changes, defaults)` | `@magek/common` | Create with defaults |
|
|
126
|
+
| `UUID.generate()` | `@magek/common` | Generate unique IDs |
|
|
127
|
+
| `createInstance(Class, raw)` | `@magek/common` | Instantiate class from raw object |
|
|
128
|
+
|
|
129
|
+
## Further Reading
|
|
130
|
+
|
|
131
|
+
- [Entity](./entity.md) - Learn about entity reducers
|
|
132
|
+
- [Read Model](./read-model.md) - Learn about projections
|
|
133
|
+
- [Event](./event.md) - Learn about events and the `entityID()` method
|
|
@@ -5,7 +5,7 @@ group: "Architecture"
|
|
|
5
5
|
|
|
6
6
|
# Command
|
|
7
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
|
|
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 similar to the concept of a **request on a REST API**. Command issuers can also send data on a command as parameters.
|
|
9
9
|
|
|
10
10
|
## Creating a command
|
|
11
11
|
|
|
@@ -82,7 +82,7 @@ export class CreateProduct {
|
|
|
82
82
|
|
|
83
83
|
public static async handle(command: CreateProduct, register: Register): Promise<void> {
|
|
84
84
|
// highlight-next-line
|
|
85
|
-
register.
|
|
85
|
+
register.events(new ProductCreated(/*...*/))
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
```
|
|
@@ -91,7 +91,7 @@ For more details about events and the register parameter, see the [`Events`](/ar
|
|
|
91
91
|
|
|
92
92
|
### Returning a value
|
|
93
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
|
|
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 GraphQL 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
95
|
|
|
96
96
|
If you want to return a value, you need to:
|
|
97
97
|
1. Change the return type of the handler function
|
|
@@ -115,7 +115,7 @@ export class CreateProduct {
|
|
|
115
115
|
// highlight-next-line
|
|
116
116
|
@returns(type => String)
|
|
117
117
|
public static async handle(command: CreateProduct, register: Register): Promise<string> {
|
|
118
|
-
register.
|
|
118
|
+
register.events(new ProductCreated(/*...*/))
|
|
119
119
|
// highlight-next-line
|
|
120
120
|
return 'Product created!'
|
|
121
121
|
}
|
|
@@ -296,7 +296,7 @@ You can read more about this on the [Authorization section](/security/authorizat
|
|
|
296
296
|
|
|
297
297
|
## Submitting a command
|
|
298
298
|
|
|
299
|
-
Magek commands are accessible to the outside world as GraphQL mutations.
|
|
299
|
+
Magek commands are accessible to the outside world as GraphQL mutations. GraphQL 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
300
|
|
|
301
301
|
Magek automatically creates one mutation per command. The framework infers the mutation input type from the command fields. Given this `CreateProduct` command:
|
|
302
302
|
|
|
@@ -365,3 +365,10 @@ Despite you can place commands, and other Magek files, in any directory, we stro
|
|
|
365
365
|
│ ├── index.ts
|
|
366
366
|
│ └── read-models
|
|
367
367
|
```
|
|
368
|
+
|
|
369
|
+
## Related Topics
|
|
370
|
+
|
|
371
|
+
- [Events](./event.md) - Events registered by command handlers
|
|
372
|
+
- [Event Handlers](./event-handler.md) - Side effects triggered by events
|
|
373
|
+
- [Authorization](../security/authorization.md) - Securing commands with roles
|
|
374
|
+
- [GraphQL API](../graphql.md) - How commands become mutations
|
|
@@ -39,9 +39,25 @@ export class EntityName {
|
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
## Working with Entity State
|
|
43
|
+
|
|
44
|
+
Magek entities use immutable state updates. Always use the `evolve()` helper from `@magek/common`:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { evolve } from '@magek/common'
|
|
48
|
+
|
|
49
|
+
// In your reducer:
|
|
50
|
+
return evolve(currentEntityState, {
|
|
51
|
+
fieldA: newValue,
|
|
52
|
+
fieldB: anotherValue,
|
|
53
|
+
})
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
> **Why `evolve()`?** It ensures immutability, handles undefined state for new entities, and provides clear, consistent patterns across your codebase. See [Best Practices](./best-practices.md) for more details.
|
|
57
|
+
|
|
42
58
|
## The reduce function
|
|
43
59
|
|
|
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
|
|
60
|
+
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 using `evolve()`.
|
|
45
61
|
|
|
46
62
|
```typescript title="src/entities/entity-name.ts"
|
|
47
63
|
@Entity
|
|
@@ -212,3 +228,9 @@ Entities live within the entities directory of the project source: `<project-roo
|
|
|
212
228
|
│ ├── index.ts
|
|
213
229
|
│ └── read-models
|
|
214
230
|
```
|
|
231
|
+
|
|
232
|
+
## Related Topics
|
|
233
|
+
|
|
234
|
+
- [Best Practices](./best-practices.md) - Recommended patterns for `evolve()` and state management
|
|
235
|
+
- [Events](./event.md) - Events that trigger entity reducers
|
|
236
|
+
- [Read Models](./read-model.md) - Project entities into query-optimized views
|
|
@@ -13,7 +13,7 @@ Two patterns influence the Magek's event-driven architecture: Command-Query Resp
|
|
|
13
13
|
|
|
14
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
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
|
|
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 through `Command Handlers`. As part of the process, some `Events` may be _registered_ as needed.
|
|
17
17
|
|
|
18
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
19
|
|
|
@@ -5,7 +5,7 @@ group: "Architecture"
|
|
|
5
5
|
|
|
6
6
|
# Read model
|
|
7
7
|
|
|
8
|
-
A read model contains the data of your application that is exposed to the client through the GraphQL API. It's a _projection_ of one or more entities, so you
|
|
8
|
+
A read model contains the data of your application that is exposed to the client through the GraphQL API. It's a _projection_ of one or more entities, so you don't have to directly expose them to the client. Magek generates the GraphQL queries that allow you to fetch your read models.
|
|
9
9
|
|
|
10
10
|
In other words, Read Models are cached data optimized for read operations. They're updated reactively when [Entities](./entity.md) are updated after reducing [events](./event.md).
|
|
11
11
|
|
|
@@ -43,7 +43,7 @@ export class ReadModelName {
|
|
|
43
43
|
|
|
44
44
|
## The projection function
|
|
45
45
|
|
|
46
|
-
The projection function is a static method decorated with the `@projects` decorator. It is used to define how the read model is updated when an entity is modified.
|
|
46
|
+
The projection function is a static method decorated with the `@projects` decorator. It is used to define how the read model is updated when an entity is modified. The projection function must return a new instance of the read model, it receives two arguments:
|
|
47
47
|
|
|
48
48
|
- `entity`: The entity that has been modified
|
|
49
49
|
- `current?`: The current read model instance. If it's the first time the read model is created, this argument will be `undefined`
|
|
@@ -233,7 +233,7 @@ export class UserReadModel {
|
|
|
233
233
|
@projects(User, 'id')
|
|
234
234
|
public static projectUser(entity: User, current?: UserReadModel): ProjectionResult<UserReadModel> {
|
|
235
235
|
if (entity.deleted) {
|
|
236
|
-
return
|
|
236
|
+
return ProjectionAction.Delete
|
|
237
237
|
}
|
|
238
238
|
return evolve(current, { id: entity.id, username: entity.username })
|
|
239
239
|
}
|
|
@@ -259,7 +259,7 @@ export class UserReadModel {
|
|
|
259
259
|
@projects(User, 'id')
|
|
260
260
|
public static projectUser(entity: User, current?: UserReadModel): ProjectionResult<UserReadModel> {
|
|
261
261
|
if (!entity.modified) {
|
|
262
|
-
return
|
|
262
|
+
return ProjectionAction.Skip
|
|
263
263
|
}
|
|
264
264
|
return evolve(current, { id: entity.id, username: entity.username })
|
|
265
265
|
}
|
|
@@ -357,7 +357,7 @@ And here is an example of the corresponding JSON response when this query is exe
|
|
|
357
357
|
}
|
|
358
358
|
```
|
|
359
359
|
|
|
360
|
-
Notice that getters are not cached in the read models database, so the getters will be executed every time you include these fields in the queries. If access to nested queries is frequent or the size of the responses are big, you could
|
|
360
|
+
Notice that getters are not cached in the read models database, so the getters will be executed every time you include these fields in the queries. If access to nested queries is frequent or the size of the responses are big, you could improve your API response performance by querying the read models separately and joining the results in the client application.
|
|
361
361
|
|
|
362
362
|
## Authorizing a read model
|
|
363
363
|
|
|
@@ -396,7 +396,7 @@ You can read more about this on the [Authorization section](/security/authorizat
|
|
|
396
396
|
|
|
397
397
|
## Querying a read model
|
|
398
398
|
|
|
399
|
-
Magek read models are accessible to the outside world through GraphQL queries.
|
|
399
|
+
Magek read models are accessible to the outside world through GraphQL queries. GraphQL fits very well with Magek's CQRS approach because it has two kinds of reading operations: Queries and Subscriptions. They are read-only operations that do not modify the state of the application. Magek uses them to fetch data from the read models.
|
|
400
400
|
|
|
401
401
|
Magek automatically creates the queries and subscriptions for each read model. You can use them to fetch the data from the read models. For example, given the following read model:
|
|
402
402
|
|
|
@@ -505,3 +505,10 @@ Despite you can place your read models in any directory, we strongly recommend y
|
|
|
505
505
|
│ ├── index.ts
|
|
506
506
|
│ └── read-models
|
|
507
507
|
```
|
|
508
|
+
|
|
509
|
+
## Related Topics
|
|
510
|
+
|
|
511
|
+
- [Best Practices](./best-practices.md) - Recommended patterns for `evolve()` and projections
|
|
512
|
+
- [Entities](./entity.md) - Source data for read model projections
|
|
513
|
+
- [Queries](./queries.md) - Alternative approach for complex query logic
|
|
514
|
+
- [GraphQL API](../graphql.md) - How read models are exposed via GraphQL
|
package/docs/docs-index.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"uri": "magek://docs/advanced/custom-adapters",
|
|
4
|
+
"path": "advanced/custom-adapters.md",
|
|
5
|
+
"title": "Custom Adapters",
|
|
6
|
+
"description": "Magek uses an adapter pattern to abstract away database and storage implementation details. This allows you to use different storage backends without modifying your application code. This guide exp..."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"uri": "magek://docs/advanced/custom-templates",
|
|
4
10
|
"path": "advanced/custom-templates.md",
|
|
@@ -20,8 +26,8 @@
|
|
|
20
26
|
{
|
|
21
27
|
"uri": "magek://docs/advanced/framework-packages",
|
|
22
28
|
"path": "advanced/framework-packages.md",
|
|
23
|
-
"title": "Framework
|
|
24
|
-
"description": "
|
|
29
|
+
"title": "Framework Packages",
|
|
30
|
+
"description": "Magek is organized as a monorepo with these packages:"
|
|
25
31
|
},
|
|
26
32
|
{
|
|
27
33
|
"uri": "magek://docs/advanced/health/sensor-health",
|
|
@@ -44,8 +50,8 @@
|
|
|
44
50
|
{
|
|
45
51
|
"uri": "magek://docs/advanced/sensor",
|
|
46
52
|
"path": "advanced/sensor.md",
|
|
47
|
-
"title": "
|
|
48
|
-
"description": "
|
|
53
|
+
"title": "Sensors",
|
|
54
|
+
"description": "Sensors are monitoring components in Magek that track and report the status of your application. They provide visibility into application health and performance through HTTP endpoints."
|
|
49
55
|
},
|
|
50
56
|
{
|
|
51
57
|
"uri": "magek://docs/advanced/testing",
|
|
@@ -59,6 +65,12 @@
|
|
|
59
65
|
"title": "TouchEntities",
|
|
60
66
|
"description": "Magek provides a way to refresh the value of an entity and update the corresponding ReadModels that depend on it."
|
|
61
67
|
},
|
|
68
|
+
{
|
|
69
|
+
"uri": "magek://docs/architecture/best-practices",
|
|
70
|
+
"path": "architecture/best-practices.md",
|
|
71
|
+
"title": "Best Practices",
|
|
72
|
+
"description": "Quick reference for writing idiomatic Magek code."
|
|
73
|
+
},
|
|
62
74
|
{
|
|
63
75
|
"uri": "magek://docs/architecture/command",
|
|
64
76
|
"path": "architecture/command.md",
|
|
@@ -105,7 +117,7 @@
|
|
|
105
117
|
"uri": "magek://docs/architecture/read-model",
|
|
106
118
|
"path": "architecture/read-model.md",
|
|
107
119
|
"title": "Read model",
|
|
108
|
-
"description": "A read model contains the data of your application that is exposed to the client through the GraphQL API. It's a _projection_ of one or more entities, so you
|
|
120
|
+
"description": "A read model contains the data of your application that is exposed to the client through the GraphQL API. It's a _projection_ of one or more entities, so you don't have to directly expose them to t..."
|
|
109
121
|
},
|
|
110
122
|
{
|
|
111
123
|
"uri": "magek://docs/contributing",
|
|
@@ -394,7 +394,7 @@ Now, let's run our application to see it working. It is as simple as running:
|
|
|
394
394
|
npx magek start -e local
|
|
395
395
|
```
|
|
396
396
|
|
|
397
|
-
This will execute a local `
|
|
397
|
+
This will execute a local `Fastify` server and will try to expose it on port `3000`. You can change the port by using the `-p` option:
|
|
398
398
|
|
|
399
399
|
```bash
|
|
400
400
|
npx magek start -e local -p 8080
|
|
@@ -141,3 +141,10 @@ npx magek version
|
|
|
141
141
|
> @magek/cli/0.16.1 darwin-x64 node-v22.12.0
|
|
142
142
|
|
|
143
143
|
> **Tip:** The CLI is automatically included in every Magek project - no global installation needed! Use `npx magek` for all CLI commands within your project directory.
|
|
144
|
+
|
|
145
|
+
## Next Steps
|
|
146
|
+
|
|
147
|
+
Now that you have Magek installed, continue with:
|
|
148
|
+
|
|
149
|
+
- **[CLI Reference](../magek-cli.md)** - Learn all available CLI commands
|
|
150
|
+
- **[Coding Tutorial](./coding.md)** - Build your first Magek application step by step
|
package/docs/index.md
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Documentation"
|
|
3
3
|
children:
|
|
4
|
+
# Getting Started
|
|
4
5
|
- ./introduction.md
|
|
5
6
|
- ./getting-started/installation.md
|
|
7
|
+
- ./magek-cli.md
|
|
6
8
|
- ./getting-started/coding.md
|
|
9
|
+
- ./getting-started/ai-coding-assistants.md
|
|
10
|
+
# Architecture (follows CQRS flow: Command → Event → Entity → Read Model → Handler)
|
|
7
11
|
- ./architecture/event-driven.md
|
|
8
12
|
- ./architecture/command.md
|
|
9
13
|
- ./architecture/event.md
|
|
10
|
-
- ./architecture/event-handler.md
|
|
11
14
|
- ./architecture/entity.md
|
|
12
15
|
- ./architecture/read-model.md
|
|
16
|
+
- ./architecture/event-handler.md
|
|
13
17
|
- ./architecture/notifications.md
|
|
14
18
|
- ./architecture/queries.md
|
|
19
|
+
- ./architecture/best-practices.md
|
|
20
|
+
# Features
|
|
21
|
+
- ./graphql.md
|
|
15
22
|
- ./features/event-stream.md
|
|
16
23
|
- ./features/schedule-actions.md
|
|
17
24
|
- ./features/logging.md
|
|
18
25
|
- ./features/error-handling.md
|
|
26
|
+
# Security
|
|
19
27
|
- ./security/security.md
|
|
20
28
|
- ./security/authentication.md
|
|
21
29
|
- ./security/authorization.md
|
|
22
|
-
|
|
23
|
-
- ./graphql.md
|
|
24
|
-
- ./advanced/custom-templates.md
|
|
25
|
-
- ./advanced/data-migrations.md
|
|
30
|
+
# Advanced
|
|
26
31
|
- ./advanced/environment-configuration.md
|
|
27
|
-
- ./advanced/
|
|
32
|
+
- ./advanced/testing.md
|
|
33
|
+
- ./advanced/data-migrations.md
|
|
28
34
|
- ./advanced/instrumentation.md
|
|
29
|
-
- ./advanced/register.md
|
|
30
35
|
- ./advanced/sensor.md
|
|
31
|
-
- ./advanced/testing.md
|
|
32
|
-
- ./advanced/touch-entities.md
|
|
33
36
|
- ./advanced/health/sensor-health.md
|
|
37
|
+
- ./advanced/register.md
|
|
38
|
+
- ./advanced/touch-entities.md
|
|
39
|
+
- ./advanced/custom-templates.md
|
|
40
|
+
- ./advanced/framework-packages.md
|
|
34
41
|
- ./contributing.md
|
|
35
42
|
---
|
|
36
43
|
|
|
@@ -42,21 +49,22 @@ Welcome to the Magek documentation! Use the navigation on the left to browse gui
|
|
|
42
49
|
|
|
43
50
|
- **[Introduction](./introduction.md)** - Learn what Magek is
|
|
44
51
|
- **[Installation](./getting-started/installation.md)** - Get started quickly
|
|
52
|
+
- **[CLI Reference](./magek-cli.md)** - Command-line tool
|
|
45
53
|
- **[Coding Tutorial](./getting-started/coding.md)** - Build your first app
|
|
46
54
|
|
|
47
55
|
## Sections
|
|
48
56
|
|
|
49
57
|
### Getting Started
|
|
50
|
-
Set up your environment and build your first Magek application.
|
|
58
|
+
Set up your environment, learn the CLI, and build your first Magek application.
|
|
51
59
|
|
|
52
60
|
### Architecture
|
|
53
|
-
Understand Commands, Events, Entities, Read Models, and
|
|
61
|
+
Understand the CQRS pattern: Commands, Events, Entities, Read Models, and Event Handlers.
|
|
54
62
|
|
|
55
63
|
### Features
|
|
56
|
-
|
|
64
|
+
GraphQL API, Event Streams, Scheduled Actions, Logging, and Error Handling.
|
|
57
65
|
|
|
58
66
|
### Security
|
|
59
67
|
Implement Authentication and Authorization in your applications.
|
|
60
68
|
|
|
61
69
|
### Advanced Topics
|
|
62
|
-
|
|
70
|
+
Environment configuration, Testing, Migrations, Instrumentation, and more.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magek/mcp-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "MCP server for Magek documentation and CLI reference",
|
|
5
5
|
"author": "Boosterin Labs SLU",
|
|
6
6
|
"homepage": "https://magek.ai",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@magek/eslint-config": "^0.0.
|
|
35
|
-
"@types/node": "22.19.
|
|
34
|
+
"@magek/eslint-config": "^0.0.11",
|
|
35
|
+
"@types/node": "22.19.9",
|
|
36
36
|
"rimraf": "6.1.2",
|
|
37
37
|
"typescript": "5.9.3"
|
|
38
38
|
},
|