@powerhousedao/academy 5.1.0-staging.0 → 5.1.0
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/CHANGELOG.md +46 -1148
- package/blog/BeyondCommunication-ABlueprintForDevelopment.md +1 -2
- package/blog/TheChallengeOfChange.md +0 -1
- package/docs/academy/00-EthereumArgentinaHackathon.md +207 -0
- package/docs/academy/01-GetStarted/00-ExploreDemoPackage.mdx +27 -24
- package/docs/academy/01-GetStarted/01-CreateNewPowerhouseProject.md +10 -155
- package/docs/academy/01-GetStarted/02-DefineToDoListDocumentModel.md +35 -122
- package/docs/academy/01-GetStarted/03-ImplementOperationReducers.md +155 -178
- package/docs/academy/01-GetStarted/04-BuildToDoListEditor.md +218 -0
- package/docs/academy/{02-MasteryTrack/01-BuilderEnvironment → 01-GetStarted}/05-VetraStudio.md +22 -62
- package/docs/academy/01-GetStarted/06-ReactorMCP.md +58 -0
- package/docs/academy/01-GetStarted/_04-BuildToDoListEditor +1 -1
- package/docs/academy/02-MasteryTrack/01-BuilderEnvironment/03-BuilderTools.md +2 -2
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/02-SpecifyTheStateSchema.md +44 -75
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/03-SpecifyDocumentOperations.md +22 -28
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/04-UseTheDocumentModelGenerator.md +31 -28
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/05-ImplementDocumentReducers.md +206 -211
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/06-ImplementDocumentModelTests.md +62 -176
- package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/07-ExampleToDoListRepository.md +0 -21
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/01-BuildingDocumentEditors.md +319 -309
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/00-DocumentToolbar.mdx +0 -4
- package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/01-OperationHistory.md +0 -4
- package/docs/academy/02-MasteryTrack/05-Launch/04-ConfigureEnvironment.md +1 -1
- package/docs/academy/03-ExampleUsecases/Chatroom/02-CreateNewPowerhouseProject.md +35 -111
- package/docs/academy/03-ExampleUsecases/Chatroom/03-DefineChatroomDocumentModel.md +79 -195
- package/docs/academy/03-ExampleUsecases/Chatroom/04-ImplementOperationReducers.md +241 -435
- package/docs/academy/03-ExampleUsecases/Chatroom/05-ImplementChatroomEditor.md +27 -388
- package/docs/academy/03-ExampleUsecases/Chatroom/06-LaunchALocalReactor.md +7 -95
- package/docs/academy/03-ExampleUsecases/Chatroom/_category_.json +1 -1
- package/docs/academy/04-APIReferences/00-PowerhouseCLI.md +2 -6
- package/docs/academy/04-APIReferences/01-ReactHooks.md +501 -291
- package/docs/academy/05-Architecture/00-PowerhouseArchitecture.md +39 -7
- package/docs/academy/05-Architecture/02-ReferencingMonorepoPackages +65 -0
- package/docs/academy/05-Architecture/04-MovingBeyondCRUD +61 -0
- package/docs/academy/06-ComponentLibrary/00-DocumentEngineering.md +24 -72
- package/docs/academy/08-Glossary.md +0 -7
- package/docusaurus.config.ts +3 -28
- package/package.json +1 -1
- package/sidebars.ts +13 -49
- package/src/css/custom.css +18 -26
- package/docs/academy/01-GetStarted/04-WriteDocumentModelTests.md +0 -425
- package/docs/academy/01-GetStarted/05-BuildToDoListEditor.md +0 -557
- package/docs/academy/02-MasteryTrack/01-BuilderEnvironment/images/Modules.png +0 -0
- package/docs/academy/02-MasteryTrack/01-BuilderEnvironment/images/VetraStudioDrive.png +0 -0
- package/docs/academy/02-MasteryTrack/05-Launch/05-DockerDeployment.md +0 -384
- package/docs/academy/03-ExampleUsecases/TodoList/00-GetTheStarterCode.md +0 -24
- package/docs/academy/03-ExampleUsecases/TodoList/01-GenerateTodoListDocumentModel.md +0 -211
- package/docs/academy/03-ExampleUsecases/TodoList/02-ImplementTodoListDocumentModelReducerOperationHandlers.md +0 -171
- package/docs/academy/03-ExampleUsecases/TodoList/03-AddTestsForTodoListActions.md +0 -462
- package/docs/academy/03-ExampleUsecases/TodoList/04-GenerateTodoListDocumentEditor.md +0 -45
- package/docs/academy/03-ExampleUsecases/TodoList/05-ImplementTodoListDocumentEditorUIComponents.md +0 -422
- package/docs/academy/03-ExampleUsecases/TodoList/06-GenerateTodoDriveExplorer.md +0 -61
- package/docs/academy/03-ExampleUsecases/TodoList/07-AddSharedComponentForShowingTodoListStats.md +0 -384
- package/docs/academy/03-ExampleUsecases/TodoList/_category_.json +0 -8
- package/docs/academy/03-ExampleUsecases/VetraPackageLibrary/VetraPackageLibrary.md +0 -7
- package/docs/academy/03-ExampleUsecases/VetraPackageLibrary/_category_.json +0 -9
- package/docs/academy/04-APIReferences/06-VetraRemoteDrive.md +0 -160
- package/docs/academy/04-APIReferences/renown-sdk/00-Overview.md +0 -316
- package/docs/academy/04-APIReferences/renown-sdk/01-Authentication.md +0 -672
- package/docs/academy/04-APIReferences/renown-sdk/02-APIReference.md +0 -957
- package/docs/academy/04-APIReferences/renown-sdk/_category_.json +0 -8
- package/docs/academy/10-TodoListTutorial/02-ImplementTodoListDocumentModelReducerOperationHandlers.md +0 -171
- package/docs/academy/10-TodoListTutorial/03-AddTestsForTodoListActions.md +0 -462
- package/docs/academy/10-TodoListTutorial/05-ImplementTodoListDocumentEditorUIComponents.md +0 -422
- package/docs/academy/10-TodoListTutorial/07-AddSharedComponentForShowingTodoListStats.md +0 -370
- package/static/img/Vetra-logo-dark.svg +0 -11
- package/static/img/vetra-logo-light.svg +0 -11
- /package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/{02-RevisionHistoryTimeline/360/237/232/247" → 02-RevisionHistoryTimeline} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{360/237/232/247 /01-WhatIsADocumentModel" → 01-WhatIsADocumentModel} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{360/237/232/247 /02-DAOandDocumentsModelsQ+A" → 02-DAOandDocumentsModelsQ+A} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{360/237/232/247 /02-domain-modeling" → 02-domain-modeling} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{360/237/232/247 /03-BenefitsOfDocumentModels" → 03-BenefitsOfDocumentModels} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{360/237/232/247 /04-UtilitiesAndTips" → 04-UtilitiesAndTips} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{360/237/232/247 /05-best-practices" → 05-best-practices} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{360/237/232/247 /_category_.json" → _category_.json} +0 -0
- /package/docs/academy/05-Architecture/05-DocumentModelTheory/{360/237/232/247 /three-data-layers.png" → three-data-layers.png} +0 -0
package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/05-ImplementDocumentReducers.md
CHANGED
|
@@ -10,9 +10,9 @@ Reducers are the core logic units of your document model. They are the functions
|
|
|
10
10
|
|
|
11
11
|
Before diving into the specifics of writing reducers, let's recall the preceding steps:
|
|
12
12
|
|
|
13
|
-
1. **State Schema Definition**: You designed the GraphQL `type` definitions for your document's data structure (e.g., `
|
|
13
|
+
1. **State Schema Definition**: You designed the GraphQL `type` definitions for your document's data structure (e.g., `ToDoListState`, `ToDoItem`).
|
|
14
14
|
2. **Document Operation Specification**: You defined the GraphQL `input` types that specify the parameters for each allowed modification to your document (e.g., `AddTodoItemInput`, `UpdateTodoItemInput`). These were then associated with named operations (e.g., `ADD_TODO_ITEM`) in the Connect application.
|
|
15
|
-
3. **Code Generation**: You used `ph generate <YourModelName.
|
|
15
|
+
3. **Code Generation**: You used `ph generate <YourModelName.phdm.zip>` to create the necessary TypeScript types, action creators, and, crucially, the skeleton file for your reducers (typically `document-models/<YourModelName>/src/reducers/<your-model-name>.ts`).
|
|
16
16
|
|
|
17
17
|
This generated reducer file is our starting point. It will contain function stubs or an object structure expecting your reducer implementations, all typed according to your schema.
|
|
18
18
|
|
|
@@ -27,7 +27,7 @@ Let's break down its components and principles:
|
|
|
27
27
|
- **`currentState`**: This is the complete, current state of your document model instance before the operation is applied. It's crucial to treat this as **immutable**.
|
|
28
28
|
- **`action`**: This is an object describing the operation to be performed. It typically has:
|
|
29
29
|
- A `type` property: A string identifying the operation (e.g., `'ADD_TODO_ITEM'`).
|
|
30
|
-
- An `input` property (or similar, like `payload`): An object containing the data necessary for the operation, matching the GraphQL `input` type you defined (e.g., `{ text: 'Buy groceries' }` for `AddTodoItemInput`).
|
|
30
|
+
- An `input` property (or similar, like `payload`): An object containing the data necessary for the operation, matching the GraphQL `input` type you defined (e.g., `{ id: '1', text: 'Buy groceries' }` for `AddTodoItemInput`).
|
|
31
31
|
- **`newState`**: The reducer must return a _new_ state object representing the state after the operation has been applied. If the operation does not result in a state change, the reducer should return the `currentState` object itself.
|
|
32
32
|
|
|
33
33
|
### Key principles guiding reducer implementation:
|
|
@@ -39,211 +39,204 @@ Let's break down its components and principles:
|
|
|
39
39
|
2. **Immutability**:
|
|
40
40
|
- **Never Mutate `currentState`**: You must never directly modify the `currentState` object or any of its nested properties.
|
|
41
41
|
- **Always Return a New Object for Changes**: If the state changes, you must create and return a brand new object. If the state does not change, you return the original `currentState` object.
|
|
42
|
-
- This is fundamental to Powerhouse's event sourcing architecture, enabling time travel, efficient change detection, and a clear audit trail.
|
|
43
|
-
|
|
44
|
-
:::tip Powerhouse uses Immer.js
|
|
45
|
-
Powerhouse uses **Immer.js** under the hood, which means you can write code that _looks like_ it's mutating the state directly (e.g., `state.items.push(...)`), but Immer ensures it results in an immutable update. This gives you the best of both worlds: readable code and immutable state.
|
|
46
|
-
:::
|
|
42
|
+
- This is fundamental to Powerhouse's event sourcing architecture, enabling time travel, efficient change detection, and a clear audit trail. We'll explore techniques for immutability shortly.
|
|
47
43
|
|
|
48
44
|
3. **Single Source of Truth**: The document state managed by reducers is the single source of truth for that document instance. All UI rendering and data queries are derived from this state.
|
|
49
45
|
|
|
50
46
|
4. **Delegation to specific operation handlers**:
|
|
51
|
-
While you can write one large reducer that uses a `switch` statement or `if/else if` blocks based on `action.type`, Powerhouse's generated code typically encourages a more modular approach. You'll often implement a separate function for each operation, which are then combined into a main reducer object or map. The `ph generate` command usually sets up this structure for you. For example, in your `document-models/
|
|
47
|
+
While you can write one large reducer that uses a `switch` statement or `if/else if` blocks based on `action.type`, Powerhouse's generated code typically encourages a more modular approach. You'll often implement a separate function for each operation, which are then combined into a main reducer object or map. The `ph generate` command usually sets up this structure for you. For example, in your `document-models/to-do-list/src/reducers/to-do-list.ts`, you'll find an object structure like this:
|
|
52
48
|
|
|
53
49
|
```typescript
|
|
54
|
-
import
|
|
50
|
+
import { ToDoListToDoListOperations } from "../../gen/to-do-list/operations.js"; // Generated type for operations
|
|
51
|
+
import { ToDoListState } from "../../gen/types.js"; // Generated type for state
|
|
55
52
|
|
|
56
|
-
export const
|
|
57
|
-
addTodoItemOperation(state, action) {
|
|
53
|
+
export const reducer: ToDoListToDoListOperations = {
|
|
54
|
+
addTodoItemOperation(state: ToDoListState, action, dispatch) {
|
|
58
55
|
// Your logic for ADD_TODO_ITEM
|
|
56
|
+
// ...
|
|
57
|
+
return newState;
|
|
59
58
|
},
|
|
60
|
-
updateTodoItemOperation(state, action) {
|
|
59
|
+
updateTodoItemOperation(state: ToDoListState, action, dispatch) {
|
|
61
60
|
// Your logic for UPDATE_TODO_ITEM
|
|
61
|
+
// ...
|
|
62
|
+
return newState;
|
|
62
63
|
},
|
|
63
|
-
deleteTodoItemOperation(state, action) {
|
|
64
|
+
deleteTodoItemOperation(state: ToDoListState, action, dispatch) {
|
|
64
65
|
// Your logic for DELETE_TODO_ITEM
|
|
66
|
+
// ...
|
|
67
|
+
return newState;
|
|
65
68
|
},
|
|
69
|
+
// ... other operations
|
|
66
70
|
};
|
|
67
71
|
```
|
|
68
72
|
|
|
69
|
-
The `
|
|
73
|
+
The `ToDoListToDoListOperations` type (or similar, depending on your model name) is generated by Powerhouse and ensures your reducer object correctly implements all defined operations. The `state` and `action` parameters within these methods will also be strongly typed based on your schema.
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
The `dispatch` parameter is an advanced feature allowing a reducer to trigger subsequent operations. While powerful for complex workflows, it's often not needed for basic operations and can be ignored if unused.
|
|
72
76
|
|
|
73
|
-
|
|
77
|
+
## Implementing reducer logic: A practical guide
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
Let's use our familiar `ToDoList` example to illustrate common patterns. For this example, we'll assume our state schema has been updated to include a `stats` object to track the number of total, checked, and unchecked items.
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
Our `ToDoListState` now looks like this:
|
|
78
82
|
|
|
79
83
|
```typescript
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Generate a unique ID for the new todo item
|
|
86
|
-
const id = generateId();
|
|
87
|
-
|
|
88
|
-
// Add the new item to the state (Immer handles immutability)
|
|
89
|
-
state.items.push({ ...action.input, id, checked: false });
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
updateTodoItemOperation(state, action) {
|
|
93
|
-
// Find the item to update by its ID
|
|
94
|
-
const item = state.items.find((item) => item.id === action.input.id);
|
|
95
|
-
|
|
96
|
-
// Return early if item not found
|
|
97
|
-
if (!item) return;
|
|
98
|
-
|
|
99
|
-
// Update only the fields that are provided (partial update)
|
|
100
|
-
item.text = action.input.text ?? item.text;
|
|
101
|
-
item.checked = action.input.checked ?? item.checked;
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
deleteTodoItemOperation(state, action) {
|
|
105
|
-
// Filter out the item with the matching ID
|
|
106
|
-
state.items = state.items.filter((item) => item.id !== action.input.id);
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
:::info Key Pattern: ID Generation
|
|
112
|
-
Notice that `addTodoItemOperation` uses `generateId()` from `document-model/core` to create a unique ID. This is the recommended pattern — the ID is generated in the reducer, not passed from the UI. This ensures consistent, unique IDs across all operations.
|
|
113
|
-
:::
|
|
114
|
-
|
|
115
|
-
### Advanced implementation (with statistics tracking)
|
|
116
|
-
|
|
117
|
-
:::info Advanced Feature
|
|
118
|
-
This section extends the basic reducers with statistics tracking, matching the advanced schema from the previous section. This demonstrates how to update computed/derived state alongside your primary data.
|
|
119
|
-
:::
|
|
120
|
-
|
|
121
|
-
For the advanced version with `stats`, we need to update the statistics whenever items are added, updated, or deleted:
|
|
84
|
+
interface ToDoItem {
|
|
85
|
+
id: string;
|
|
86
|
+
text: string;
|
|
87
|
+
checked: boolean;
|
|
88
|
+
}
|
|
122
89
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
addTodoItemOperation(state, action) {
|
|
129
|
-
// Generate a unique ID for the new todo item
|
|
130
|
-
const id = generateId();
|
|
131
|
-
|
|
132
|
-
// Update statistics
|
|
133
|
-
state.stats.total += 1;
|
|
134
|
-
state.stats.unchecked += 1;
|
|
90
|
+
interface ToDoListStats {
|
|
91
|
+
total: number;
|
|
92
|
+
checked: number;
|
|
93
|
+
unchecked: number;
|
|
94
|
+
}
|
|
135
95
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
142
|
-
},
|
|
96
|
+
interface ToDoListState {
|
|
97
|
+
items: ToDoItem[];
|
|
98
|
+
stats: ToDoListStats;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
143
101
|
|
|
144
|
-
|
|
145
|
-
// Find the specific item we want to update
|
|
146
|
-
const item = state.items.find((item) => item.id === action.input.id);
|
|
102
|
+
And our action creators (from `../../gen/creators` or `../../gen/operations.js`) provide actions like:
|
|
147
103
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
104
|
+
- `actions.addTodoItem({ id: 'some-id', text: 'New Task' })`
|
|
105
|
+
- `actions.updateTodoItem({ id: 'item-id', text: 'Updated Task Text', checked: true })`
|
|
106
|
+
- `actions.deleteTodoItem({ id: 'item-id' })`
|
|
151
107
|
|
|
152
|
-
|
|
153
|
-
if (action.input.text !== undefined) {
|
|
154
|
-
item.text = action.input.text;
|
|
155
|
-
}
|
|
108
|
+
### 1. Adding an item (e.g., `addTodoItemOperation`)
|
|
156
109
|
|
|
157
|
-
|
|
158
|
-
if (action.input.checked !== undefined && action.input.checked !== item.checked) {
|
|
159
|
-
if (action.input.checked) {
|
|
160
|
-
state.stats.unchecked -= 1;
|
|
161
|
-
state.stats.checked += 1;
|
|
162
|
-
} else {
|
|
163
|
-
state.stats.unchecked += 1;
|
|
164
|
-
state.stats.checked -= 1;
|
|
165
|
-
}
|
|
166
|
-
item.checked = action.input.checked;
|
|
167
|
-
}
|
|
168
|
-
},
|
|
110
|
+
To add a new item to the `items` array immutably:
|
|
169
111
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
112
|
+
```typescript
|
|
113
|
+
addTodoItemOperation(state: ToDoListState, action: /* AddTodoItemActionType */ any, dispatch) {
|
|
114
|
+
const newItem: ToDoItem = {
|
|
115
|
+
id: action.input.id,
|
|
116
|
+
text: action.input.text,
|
|
117
|
+
checked: false, // New items default to unchecked
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// Return a new state object
|
|
121
|
+
return {
|
|
122
|
+
...state, // Copy all existing properties from the current state
|
|
123
|
+
items: [...state.items, newItem], // Create a new items array: spread existing items, add the new one
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
```
|
|
173
127
|
|
|
174
|
-
|
|
175
|
-
// Update statistics
|
|
176
|
-
state.stats.total -= 1;
|
|
177
|
-
if (item.checked) {
|
|
178
|
-
state.stats.checked -= 1;
|
|
179
|
-
} else {
|
|
180
|
-
state.stats.unchecked -= 1;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
128
|
+
**Explanation**:
|
|
183
129
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
```
|
|
130
|
+
- We use the spread operator (`...state`) to copy top-level properties from the old state into the new state object.
|
|
131
|
+
- For the `items` array, we create a _new_ array by spreading the existing `state.items` and then appending the `newItem`.
|
|
189
132
|
|
|
190
|
-
###
|
|
133
|
+
### 2. Updating an item (e.g., `updateTodoItemOperation`)
|
|
191
134
|
|
|
192
|
-
|
|
135
|
+
To update an existing item in the `items` array immutably:
|
|
193
136
|
|
|
194
137
|
```typescript
|
|
195
|
-
|
|
196
|
-
const id
|
|
197
|
-
|
|
138
|
+
updateTodoItemOperation(state: ToDoListState, action: /* UpdateTodoItemActionType */ any, dispatch) {
|
|
139
|
+
const { id, text, checked } = action.input;
|
|
140
|
+
|
|
141
|
+
// Return a new state object
|
|
142
|
+
return {
|
|
143
|
+
...state,
|
|
144
|
+
items: state.items.map(item => {
|
|
145
|
+
if (item.id === id) {
|
|
146
|
+
// This is the item to update. Return a *new* item object.
|
|
147
|
+
return {
|
|
148
|
+
...item, // Copy existing properties of the item
|
|
149
|
+
// Update only fields that are provided in the action input
|
|
150
|
+
...(text !== undefined && { text: text }),
|
|
151
|
+
...(checked !== undefined && { checked: checked }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// This is not the item we're looking for, return it unchanged.
|
|
155
|
+
return item;
|
|
156
|
+
}),
|
|
157
|
+
};
|
|
198
158
|
}
|
|
199
159
|
```
|
|
200
160
|
|
|
201
|
-
|
|
202
|
-
- We spread `action.input` to get the text, add the generated ID and default `checked: false`
|
|
203
|
-
- With Immer, this "mutation" is actually immutable
|
|
161
|
+
**Explanation**:
|
|
204
162
|
|
|
205
|
-
|
|
163
|
+
- We use the `map` array method, which always returns a _new_ array.
|
|
164
|
+
- For the item that matches `action.input.id`, we create a new item object using the spread operator (`...item`) and then overwrite the properties (`text`, `checked`) that are present in `action.input`.
|
|
165
|
+
- The conditional spread (`...(condition && { property: value })`) is a concise way to only include a property in the new object if its corresponding input value is provided. This elegantly handles partial updates.
|
|
166
|
+
- If an item doesn't match the ID, it's returned as is.
|
|
167
|
+
|
|
168
|
+
**Error Handling Note**: In a real application, you might want to add a check to see if an item with `action.input.id` actually exists. If not, you could throw an error or handle it according to your application's requirements:
|
|
206
169
|
|
|
207
170
|
```typescript
|
|
208
|
-
updateTodoItemOperation
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
171
|
+
// Inside updateTodoItemOperation, before returning:
|
|
172
|
+
const itemToUpdate = state.items.find((item) => item.id === action.input.id);
|
|
173
|
+
if (!itemToUpdate) {
|
|
174
|
+
// Option 1: Throw an error (Powerhouse runtime might catch this)
|
|
175
|
+
throw new Error(`Item with id ${action.input.id} not found.`);
|
|
176
|
+
// Option 2: Return current state (no change)
|
|
177
|
+
// return state;
|
|
214
178
|
}
|
|
179
|
+
// ... proceed with map
|
|
215
180
|
```
|
|
216
181
|
|
|
217
|
-
|
|
218
|
-
- We use nullish coalescing (`??`) to only update fields that were provided
|
|
219
|
-
- This allows partial updates (e.g., just changing `checked` without touching `text`)
|
|
182
|
+
### 3. Deleting an item (e.g., `deleteTodoItemOperation`)
|
|
220
183
|
|
|
221
|
-
|
|
184
|
+
To remove an item from the `items` array immutably:
|
|
222
185
|
|
|
223
186
|
```typescript
|
|
224
|
-
deleteTodoItemOperation(state, action) {
|
|
225
|
-
|
|
187
|
+
deleteTodoItemOperation(state: ToDoListState, action: /* DeleteTodoItemActionType */ any, dispatch) {
|
|
188
|
+
const { id } = action.input;
|
|
189
|
+
|
|
190
|
+
// Return a new state object
|
|
191
|
+
return {
|
|
192
|
+
...state,
|
|
193
|
+
items: state.items.filter(item => item.id !== id), // Create a new array excluding the item to delete
|
|
194
|
+
};
|
|
226
195
|
}
|
|
227
196
|
```
|
|
228
197
|
|
|
229
|
-
|
|
230
|
-
|
|
198
|
+
**Explanation**:
|
|
199
|
+
|
|
200
|
+
- We use the `filter` array method, which returns a _new_ array containing only the elements for which the callback function returns `true`.
|
|
231
201
|
|
|
232
202
|
## Leveraging generated types
|
|
233
203
|
|
|
234
|
-
As highlighted in [Using the Document Model Generator](04-UseTheDocumentModelGenerator.md), `ph generate` produces TypeScript types for your state (e.g., `
|
|
204
|
+
As highlighted in [Using the Document Model Generator](04-UseTheDocumentModelGenerator.md), `ph generate` produces TypeScript types for your state (e.g., `ToDoListState`, `ToDoItem`) and the inputs for your operations (e.g., `AddTodoItemInput`, `UpdateTodoItemInput`).
|
|
235
205
|
|
|
236
206
|
**Always use these generated types in your reducer implementations!**
|
|
237
207
|
|
|
238
208
|
```typescript
|
|
239
|
-
import {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
209
|
+
import {
|
|
210
|
+
ToDoListState,
|
|
211
|
+
AddTodoItemInput, // Generated input type
|
|
212
|
+
// ... other types
|
|
213
|
+
} from "../../gen/types.js";
|
|
214
|
+
import { ToDoListToDoListOperations } from "../../gen/to-do-list/operations.js"; // Generated operations type
|
|
215
|
+
|
|
216
|
+
// Define the type for the action more explicitly if needed, or rely on inferred types
|
|
217
|
+
// from ToDoListToDoListOperations. For complex actions, defining specific action types can be beneficial.
|
|
218
|
+
// For example:
|
|
219
|
+
// interface AddTodoItemAction {
|
|
220
|
+
// type: 'ADD_TODO_ITEM'; // Or the specific string constant used by the action creator
|
|
221
|
+
// input: AddTodoItemInput;
|
|
222
|
+
// }
|
|
223
|
+
|
|
224
|
+
export const reducer: ToDoListToDoListOperations = {
|
|
225
|
+
addTodoItemOperation(
|
|
226
|
+
state: ToDoListState,
|
|
227
|
+
action: { input: AddTodoItemInput /* plus type property */ },
|
|
228
|
+
dispatch,
|
|
229
|
+
) {
|
|
230
|
+
// Now 'action.input.text' and 'action.input.id' are type-checked
|
|
231
|
+
const newItem = {
|
|
232
|
+
id: action.input.id,
|
|
233
|
+
text: action.input.text,
|
|
234
|
+
checked: false,
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
...state,
|
|
238
|
+
items: [...state.items, newItem],
|
|
239
|
+
};
|
|
247
240
|
},
|
|
248
241
|
// ... other reducers
|
|
249
242
|
};
|
|
@@ -255,99 +248,101 @@ Using these types provides:
|
|
|
255
248
|
- **Autocompletion and IntelliSense**: Improved developer experience in your IDE.
|
|
256
249
|
- **Clearer code**: Types serve as documentation for the expected data structures.
|
|
257
250
|
|
|
258
|
-
## Practical implementation: Writing the `
|
|
251
|
+
## Practical implementation: Writing the `ToDoList` reducers
|
|
259
252
|
|
|
260
|
-
Now that you understand the principles, let's put them into practice by implementing the reducers for our `
|
|
253
|
+
Now that you understand the principles, let's put them into practice by implementing the reducers for our `ToDoList` document model.
|
|
261
254
|
|
|
262
255
|
<details>
|
|
263
|
-
<summary>Tutorial: Implementing the
|
|
256
|
+
<summary>Tutorial: Implementing the ToDoList reducers</summary>
|
|
264
257
|
|
|
265
|
-
This tutorial assumes you have followed the steps in the previous chapters, especially using `ph generate
|
|
258
|
+
This tutorial assumes you have followed the steps in the previous chapters, especially using `ph generate ToDoList.phdm.zip` to scaffold your document model's code.
|
|
266
259
|
|
|
267
260
|
### Implement the operation reducers
|
|
268
261
|
|
|
269
|
-
Navigate to `document-models/
|
|
270
|
-
|
|
271
|
-
**Basic version (without stats):**
|
|
272
|
-
|
|
273
|
-
```typescript
|
|
274
|
-
import { generateId } from "document-model/core";
|
|
275
|
-
import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
|
|
276
|
-
|
|
277
|
-
export const todoListTodosOperations: TodoListTodosOperations = {
|
|
278
|
-
addTodoItemOperation(state, action) {
|
|
279
|
-
const id = generateId();
|
|
280
|
-
state.items.push({ ...action.input, id, checked: false });
|
|
281
|
-
},
|
|
282
|
-
|
|
283
|
-
updateTodoItemOperation(state, action) {
|
|
284
|
-
const item = state.items.find((item) => item.id === action.input.id);
|
|
285
|
-
if (!item) return;
|
|
286
|
-
|
|
287
|
-
item.text = action.input.text ?? item.text;
|
|
288
|
-
item.checked = action.input.checked ?? item.checked;
|
|
289
|
-
},
|
|
290
|
-
|
|
291
|
-
deleteTodoItemOperation(state, action) {
|
|
292
|
-
state.items = state.items.filter((item) => item.id !== action.input.id);
|
|
293
|
-
},
|
|
294
|
-
};
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
**Advanced version (with stats):**
|
|
262
|
+
Navigate to `document-models/to-do-list/src/reducers/to-do-list.ts`. The generator will have created a skeleton file. Replace its contents with the following logic.
|
|
298
263
|
|
|
299
264
|
```typescript
|
|
300
|
-
import {
|
|
301
|
-
import
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
265
|
+
import { ToDoListToDoListOperations } from "../../gen/to-do-list/operations.js";
|
|
266
|
+
import { ToDoListState } from "../../gen/types.js"; // Assuming this now includes the 'stats' object
|
|
267
|
+
|
|
268
|
+
// REMARKS: This is our main reducer object. It implements all operations defined in the schema.
|
|
269
|
+
// The ToDoListToDoListOperations type is auto-generated from our GraphQL specification and ensures type safety.
|
|
270
|
+
export const reducer: ToDoListToDoListOperations = {
|
|
271
|
+
// REMARKS: The addTodoItemOperation adds a new item and updates our tracking statistics.
|
|
272
|
+
// - state: The current document state. Powerhouse uses a library like Immer.js,
|
|
273
|
+
// so you can write code that looks like it's mutating the state directly.
|
|
274
|
+
// Behind the scenes, Powerhouse ensures this results in an immutable update.
|
|
275
|
+
// - action: Contains the operation's 'type' and 'input' data from the client.
|
|
276
|
+
// - dispatch: A function to trigger subsequent operations (advanced, not used here).
|
|
277
|
+
addTodoItemOperation(state, action, dispatch) {
|
|
278
|
+
// REMARKS: We update our statistics for total and unchecked items.
|
|
307
279
|
state.stats.total += 1;
|
|
308
280
|
state.stats.unchecked += 1;
|
|
309
281
|
|
|
282
|
+
// REMARKS: We push the new to-do item into the items array.
|
|
283
|
+
// The data for the new item comes from the operation's input.
|
|
310
284
|
state.items.push({
|
|
311
|
-
id,
|
|
285
|
+
id: action.input.id,
|
|
312
286
|
text: action.input.text,
|
|
313
|
-
checked: false,
|
|
287
|
+
checked: false, // New items always start as unchecked.
|
|
314
288
|
});
|
|
315
289
|
},
|
|
316
290
|
|
|
317
|
-
updateTodoItemOperation
|
|
291
|
+
// REMARKS: The updateTodoItemOperation modifies an existing to-do item.
|
|
292
|
+
// It handles partial updates for text and checked status.
|
|
293
|
+
updateTodoItemOperation(state, action, dispatch) {
|
|
294
|
+
// REMARKS: First, we find the specific item we want to update using its ID.
|
|
318
295
|
const item = state.items.find((item) => item.id === action.input.id);
|
|
296
|
+
|
|
297
|
+
// REMARKS: It's good practice to handle cases where the item might not be found.
|
|
319
298
|
if (!item) {
|
|
320
299
|
throw new Error(`Item with id ${action.input.id} not found`);
|
|
321
300
|
}
|
|
322
301
|
|
|
323
|
-
|
|
302
|
+
// REMARKS: We only update the text if it was provided in the input.
|
|
303
|
+
// This allows for partial updates (e.g., just checking an item without changing its text).
|
|
304
|
+
if (action.input.text) {
|
|
324
305
|
item.text = action.input.text;
|
|
325
306
|
}
|
|
326
307
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
308
|
+
// REMARKS: When the checked status changes, we also update our statistics.
|
|
309
|
+
// We check for `true` and `false` explicitly.
|
|
310
|
+
if (action.input.checked) {
|
|
311
|
+
// This is true only if action.input.checked is true
|
|
312
|
+
// Note: This assumes the item was previously unchecked. For a more robust implementation,
|
|
313
|
+
// you could check `if (item.checked === false)` before updating stats to prevent inconsistencies.
|
|
314
|
+
state.stats.unchecked -= 1;
|
|
315
|
+
state.stats.checked += 1;
|
|
316
|
+
item.checked = action.input.checked;
|
|
317
|
+
}
|
|
318
|
+
if (action.input.checked === false) {
|
|
319
|
+
// Note: This assumes the item was previously checked.
|
|
320
|
+
state.stats.unchecked += 1;
|
|
321
|
+
state.stats.checked -= 1;
|
|
335
322
|
item.checked = action.input.checked;
|
|
336
323
|
}
|
|
337
324
|
},
|
|
338
325
|
|
|
339
|
-
deleteTodoItemOperation
|
|
326
|
+
// REMARKS: The deleteTodoItemOperation removes an item from the list.
|
|
327
|
+
deleteTodoItemOperation(state, action, dispatch) {
|
|
328
|
+
// REMARKS: Before removing the item, we find it to determine its checked status.
|
|
329
|
+
// This is necessary to correctly decrement our statistics.
|
|
340
330
|
const item = state.items.find((item) => item.id === action.input.id);
|
|
341
331
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
332
|
+
// REMARKS: We always decrement the total count.
|
|
333
|
+
state.stats.total -= 1;
|
|
334
|
+
|
|
335
|
+
// REMARKS: We then decrement the 'checked' or 'unchecked' count based on the item's status.
|
|
336
|
+
if (item?.checked) {
|
|
337
|
+
// This is shorthand for item?.checked === true
|
|
338
|
+
state.stats.checked -= 1;
|
|
339
|
+
}
|
|
340
|
+
if (item?.checked === false) {
|
|
341
|
+
state.stats.unchecked -= 1;
|
|
349
342
|
}
|
|
350
343
|
|
|
344
|
+
// REMARKS: Finally, we create a new 'items' array that excludes the deleted item.
|
|
345
|
+
// Assigning to 'state.items' is handled by Powerhouse to produce a new immutable state.
|
|
351
346
|
state.items = state.items.filter((item) => item.id !== action.input.id);
|
|
352
347
|
},
|
|
353
348
|
};
|