@powerhousedao/academy 5.1.0-dev.1 → 5.1.0-dev.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/blog/BeyondCommunication-ABlueprintForDevelopment.md +2 -1
  3. package/blog/TheChallengeOfChange.md +1 -0
  4. package/docs/academy/01-GetStarted/00-ExploreDemoPackage.mdx +24 -27
  5. package/docs/academy/01-GetStarted/01-CreateNewPowerhouseProject.md +118 -9
  6. package/docs/academy/01-GetStarted/02-DefineToDoListDocumentModel.md +110 -28
  7. package/docs/academy/01-GetStarted/03-ImplementOperationReducers.md +191 -145
  8. package/docs/academy/01-GetStarted/04-WriteDocumentModelTests.md +378 -0
  9. package/docs/academy/01-GetStarted/05-BuildToDoListEditor.md +560 -0
  10. package/docs/academy/01-GetStarted/_04-BuildToDoListEditor +1 -1
  11. package/docs/academy/02-MasteryTrack/01-BuilderEnvironment/03-BuilderTools.md +2 -2
  12. package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/05-VetraStudio.md +48 -6
  13. package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/02-SpecifyTheStateSchema.md +75 -44
  14. package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/03-SpecifyDocumentOperations.md +28 -22
  15. package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/04-UseTheDocumentModelGenerator.md +28 -31
  16. package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/05-ImplementDocumentReducers.md +211 -206
  17. package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/06-ImplementDocumentModelTests.md +176 -62
  18. package/docs/academy/02-MasteryTrack/02-DocumentModelCreation/07-ExampleToDoListRepository.md +21 -0
  19. package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/01-BuildingDocumentEditors.md +309 -319
  20. package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/00-DocumentToolbar.mdx +4 -0
  21. package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/01-OperationHistory.md +4 -0
  22. package/docs/academy/02-MasteryTrack/05-Launch/04-ConfigureEnvironment.md +1 -1
  23. package/docs/academy/03-ExampleUsecases/Chatroom/02-CreateNewPowerhouseProject.md +111 -35
  24. package/docs/academy/03-ExampleUsecases/Chatroom/03-DefineChatroomDocumentModel.md +255 -76
  25. package/docs/academy/03-ExampleUsecases/Chatroom/04-ImplementOperationReducers.md +281 -160
  26. package/docs/academy/03-ExampleUsecases/Chatroom/05-ImplementChatroomEditor.md +188 -35
  27. package/docs/academy/03-ExampleUsecases/Chatroom/06-LaunchALocalReactor.md +95 -7
  28. package/docs/academy/03-ExampleUsecases/Chatroom/_category_.json +1 -1
  29. package/docs/academy/03-ExampleUsecases/TodoList/00-GetTheStarterCode.md +24 -0
  30. package/docs/academy/03-ExampleUsecases/TodoList/01-GenerateTodoListDocumentModel.md +211 -0
  31. package/docs/academy/03-ExampleUsecases/TodoList/02-ImplementTodoListDocumentModelReducerOperationHandlers.md +171 -0
  32. package/docs/academy/03-ExampleUsecases/TodoList/03-AddTestsForTodoListActions.md +462 -0
  33. package/docs/academy/03-ExampleUsecases/TodoList/04-GenerateTodoListDocumentEditor.md +45 -0
  34. package/docs/academy/03-ExampleUsecases/TodoList/05-ImplementTodoListDocumentEditorUIComponents.md +422 -0
  35. package/docs/academy/03-ExampleUsecases/TodoList/06-GenerateTodoDriveExplorer.md +61 -0
  36. package/docs/academy/03-ExampleUsecases/TodoList/07-AddSharedComponentForShowingTodoListStats.md +384 -0
  37. package/docs/academy/03-ExampleUsecases/TodoList/_category_.json +8 -0
  38. package/docs/academy/03-ExampleUsecases/VetraPackageLibrary/VetraPackageLibrary.md +7 -0
  39. package/docs/academy/03-ExampleUsecases/VetraPackageLibrary/_category_.json +9 -0
  40. package/docs/academy/04-APIReferences/00-PowerhouseCLI.md +6 -2
  41. package/docs/academy/04-APIReferences/01-ReactHooks.md +2 -2
  42. package/docs/academy/04-APIReferences/06-VetraRemoteDrive.md +160 -0
  43. package/docs/academy/04-APIReferences/renown-sdk/00-Overview.md +316 -0
  44. package/docs/academy/04-APIReferences/renown-sdk/01-Authentication.md +672 -0
  45. package/docs/academy/04-APIReferences/renown-sdk/02-APIReference.md +957 -0
  46. package/docs/academy/04-APIReferences/renown-sdk/_category_.json +8 -0
  47. package/docs/academy/05-Architecture/00-PowerhouseArchitecture.md +7 -39
  48. package/docs/academy/06-ComponentLibrary/00-DocumentEngineering.md +72 -24
  49. package/docs/academy/08-Glossary.md +7 -0
  50. package/docs/academy/10-TodoListTutorial/02-ImplementTodoListDocumentModelReducerOperationHandlers.md +171 -0
  51. package/docs/academy/10-TodoListTutorial/03-AddTestsForTodoListActions.md +462 -0
  52. package/docs/academy/10-TodoListTutorial/05-ImplementTodoListDocumentEditorUIComponents.md +422 -0
  53. package/docs/academy/10-TodoListTutorial/07-AddSharedComponentForShowingTodoListStats.md +370 -0
  54. package/docusaurus.config.ts +28 -3
  55. package/package.json +1 -1
  56. package/sidebars.ts +49 -12
  57. package/src/css/custom.css +26 -18
  58. package/static/img/Vetra-logo-dark.svg +11 -0
  59. package/static/img/vetra-logo-light.svg +11 -0
  60. package/docs/academy/00-EthereumArgentinaHackathon.md +0 -207
  61. package/docs/academy/01-GetStarted/04-BuildToDoListEditor.md +0 -218
  62. package/docs/academy/01-GetStarted/06-ReactorMCP.md +0 -58
  63. package/docs/academy/05-Architecture/02-ReferencingMonorepoPackages +0 -65
  64. package/docs/academy/05-Architecture/04-MovingBeyondCRUD +0 -61
  65. /package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/images/Modules.png +0 -0
  66. /package/docs/academy/{01-GetStarted → 02-MasteryTrack/01-BuilderEnvironment}/images/VetraStudioDrive.png +0 -0
  67. /package/docs/academy/02-MasteryTrack/03-BuildingUserExperiences/06-DocumentTools/{02-RevisionHistoryTimeline → 02-RevisionHistoryTimeline/360/237/232/247"} +0 -0
  68. /package/docs/academy/05-Architecture/05-DocumentModelTheory/{01-WhatIsADocumentModel → 360/237/232/247 /01-WhatIsADocumentModel"} +0 -0
  69. /package/docs/academy/05-Architecture/05-DocumentModelTheory/{02-DAOandDocumentsModelsQ+A → 360/237/232/247 /02-DAOandDocumentsModelsQ+A"} +0 -0
  70. /package/docs/academy/05-Architecture/05-DocumentModelTheory/{02-domain-modeling → 360/237/232/247 /02-domain-modeling"} +0 -0
  71. /package/docs/academy/05-Architecture/05-DocumentModelTheory/{03-BenefitsOfDocumentModels → 360/237/232/247 /03-BenefitsOfDocumentModels"} +0 -0
  72. /package/docs/academy/05-Architecture/05-DocumentModelTheory/{04-UtilitiesAndTips → 360/237/232/247 /04-UtilitiesAndTips"} +0 -0
  73. /package/docs/academy/05-Architecture/05-DocumentModelTheory/{05-best-practices → 360/237/232/247 /05-best-practices"} +0 -0
  74. /package/docs/academy/05-Architecture/05-DocumentModelTheory/{_category_.json → 360/237/232/247 /_category_.json"} +0 -0
  75. /package/docs/academy/05-Architecture/05-DocumentModelTheory/{three-data-layers.png → 360/237/232/247 /three-data-layers.png"} +0 -0
@@ -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., `ToDoListState`, `ToDoItem`).
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.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`).
15
+ 3. **Code Generation**: You used `ph generate <YourModelName.phd>` to create the necessary TypeScript types, action creators, and, crucially, the skeleton file for your reducers (typically `document-models/<your-model-name>/src/reducers/todos.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., `{ id: '1', 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., `{ 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,204 +39,211 @@ 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. We'll explore techniques for immutability shortly.
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
+ :::
43
47
 
44
48
  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.
45
49
 
46
50
  4. **Delegation to specific operation handlers**:
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:
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/todo-list/src/reducers/todos.ts`, you'll find an object structure like this:
48
52
 
49
53
  ```typescript
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
54
+ import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
52
55
 
53
- export const reducer: ToDoListToDoListOperations = {
54
- addTodoItemOperation(state: ToDoListState, action, dispatch) {
56
+ export const todoListTodosOperations: TodoListTodosOperations = {
57
+ addTodoItemOperation(state, action) {
55
58
  // Your logic for ADD_TODO_ITEM
56
- // ...
57
- return newState;
58
59
  },
59
- updateTodoItemOperation(state: ToDoListState, action, dispatch) {
60
+ updateTodoItemOperation(state, action) {
60
61
  // Your logic for UPDATE_TODO_ITEM
61
- // ...
62
- return newState;
63
62
  },
64
- deleteTodoItemOperation(state: ToDoListState, action, dispatch) {
63
+ deleteTodoItemOperation(state, action) {
65
64
  // Your logic for DELETE_TODO_ITEM
66
- // ...
67
- return newState;
68
65
  },
69
- // ... other operations
70
66
  };
71
67
  ```
72
68
 
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.
74
-
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.
69
+ The `TodoListTodosOperations` type 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.
76
70
 
77
71
  ## Implementing reducer logic: A practical guide
78
72
 
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.
73
+ Let's use our familiar `TodoList` example to illustrate common patterns.
80
74
 
81
- Our `ToDoListState` now looks like this:
75
+ ### Basic implementation (matching Get Started)
82
76
 
83
- ```typescript
84
- interface ToDoItem {
85
- id: string;
86
- text: string;
87
- checked: boolean;
88
- }
89
-
90
- interface ToDoListStats {
91
- total: number;
92
- checked: number;
93
- unchecked: number;
94
- }
77
+ The basic implementation matches what you built in the Get Started tutorial:
95
78
 
96
- interface ToDoListState {
97
- items: ToDoItem[];
98
- stats: ToDoListStats;
99
- }
79
+ ```typescript
80
+ import { generateId } from "document-model/core";
81
+ import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
82
+
83
+ export const todoListTodosOperations: TodoListTodosOperations = {
84
+ addTodoItemOperation(state, action) {
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
+ };
100
109
  ```
101
110
 
102
- And our action creators (from `../../gen/creators` or `../../gen/operations.js`) provide actions like:
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
+ :::
103
114
 
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' })`
115
+ ### Advanced implementation (with statistics tracking)
107
116
 
108
- ### 1. Adding an item (e.g., `addTodoItemOperation`)
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
+ :::
109
120
 
110
- To add a new item to the `items` array immutably:
121
+ For the advanced version with `stats`, we need to update the statistics whenever items are added, updated, or deleted:
111
122
 
112
123
  ```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
- ```
124
+ import { generateId } from "document-model/core";
125
+ import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
126
+
127
+ export const todoListTodosOperations: TodoListTodosOperations = {
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;
127
135
 
128
- **Explanation**:
136
+ // Add the new item to the state
137
+ state.items.push({
138
+ id,
139
+ text: action.input.text,
140
+ checked: false, // New items always start as unchecked
141
+ });
142
+ },
129
143
 
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`.
144
+ updateTodoItemOperation(state, action) {
145
+ // Find the specific item we want to update
146
+ const item = state.items.find((item) => item.id === action.input.id);
132
147
 
133
- ### 2. Updating an item (e.g., `updateTodoItemOperation`)
148
+ if (!item) {
149
+ throw new Error(`Item with id ${action.input.id} not found`);
150
+ }
134
151
 
135
- To update an existing item in the `items` array immutably:
152
+ // Update text if provided
153
+ if (action.input.text !== undefined) {
154
+ item.text = action.input.text;
155
+ }
136
156
 
137
- ```typescript
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
- };
157
+ // Handle checked status changes and update stats
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;
153
165
  }
154
- // This is not the item we're looking for, return it unchanged.
155
- return item;
156
- }),
157
- };
158
- }
159
- ```
166
+ item.checked = action.input.checked;
167
+ }
168
+ },
169
+
170
+ deleteTodoItemOperation(state, action) {
171
+ // Find the item to determine its checked status for stats
172
+ const item = state.items.find((item) => item.id === action.input.id);
173
+
174
+ if (item) {
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
+ }
160
183
 
161
- **Explanation**:
184
+ // Remove the item from the list
185
+ state.items = state.items.filter((item) => item.id !== action.input.id);
186
+ },
187
+ };
188
+ ```
162
189
 
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.
190
+ ### Common patterns explained
167
191
 
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:
192
+ #### 1. Adding an item
169
193
 
170
194
  ```typescript
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;
195
+ addTodoItemOperation(state, action) {
196
+ const id = generateId(); // Generate unique ID
197
+ state.items.push({ ...action.input, id, checked: false });
178
198
  }
179
- // ... proceed with map
180
199
  ```
181
200
 
182
- ### 3. Deleting an item (e.g., `deleteTodoItemOperation`)
201
+ - We use `generateId()` to create a unique identifier
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
183
204
 
184
- To remove an item from the `items` array immutably:
205
+ #### 2. Updating an item
185
206
 
186
207
  ```typescript
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
- };
208
+ updateTodoItemOperation(state, action) {
209
+ const item = state.items.find((item) => item.id === action.input.id);
210
+ if (!item) return;
211
+
212
+ item.text = action.input.text ?? item.text;
213
+ item.checked = action.input.checked ?? item.checked;
195
214
  }
196
215
  ```
197
216
 
198
- **Explanation**:
217
+ - We find the item by ID
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`)
220
+
221
+ #### 3. Deleting an item
222
+
223
+ ```typescript
224
+ deleteTodoItemOperation(state, action) {
225
+ state.items = state.items.filter((item) => item.id !== action.input.id);
226
+ }
227
+ ```
199
228
 
200
- - We use the `filter` array method, which returns a _new_ array containing only the elements for which the callback function returns `true`.
229
+ - We use `filter` to create a new array without the deleted item
230
+ - Immer handles making this immutable
201
231
 
202
232
  ## Leveraging generated types
203
233
 
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`).
234
+ 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`).
205
235
 
206
236
  **Always use these generated types in your reducer implementations!**
207
237
 
208
238
  ```typescript
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
- };
239
+ import { generateId } from "document-model/core";
240
+ import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
241
+
242
+ export const todoListTodosOperations: TodoListTodosOperations = {
243
+ addTodoItemOperation(state, action) {
244
+ // TypeScript knows action.input has { text: string }
245
+ const id = generateId();
246
+ state.items.push({ id, text: action.input.text, checked: false });
240
247
  },
241
248
  // ... other reducers
242
249
  };
@@ -248,101 +255,99 @@ Using these types provides:
248
255
  - **Autocompletion and IntelliSense**: Improved developer experience in your IDE.
249
256
  - **Clearer code**: Types serve as documentation for the expected data structures.
250
257
 
251
- ## Practical implementation: Writing the `ToDoList` reducers
258
+ ## Practical implementation: Writing the `TodoList` reducers
252
259
 
253
- Now that you understand the principles, let's put them into practice by implementing the reducers for our `ToDoList` document model.
260
+ Now that you understand the principles, let's put them into practice by implementing the reducers for our `TodoList` document model.
254
261
 
255
262
  <details>
256
- <summary>Tutorial: Implementing the ToDoList reducers</summary>
263
+ <summary>Tutorial: Implementing the TodoList reducers</summary>
257
264
 
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.
265
+ This tutorial assumes you have followed the steps in the previous chapters, especially using `ph generate TodoList.phd` to scaffold your document model's code.
259
266
 
260
267
  ### Implement the operation reducers
261
268
 
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.
269
+ Navigate to `document-models/todo-list/src/reducers/todos.ts`. The generator will have created a skeleton file. Replace its contents with the following logic.
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):**
263
298
 
264
299
  ```typescript
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.
300
+ import { generateId } from "document-model/core";
301
+ import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list";
302
+
303
+ export const todoListTodosOperations: TodoListTodosOperations = {
304
+ addTodoItemOperation(state, action) {
305
+ const id = generateId();
306
+
279
307
  state.stats.total += 1;
280
308
  state.stats.unchecked += 1;
281
309
 
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.
284
310
  state.items.push({
285
- id: action.input.id,
311
+ id,
286
312
  text: action.input.text,
287
- checked: false, // New items always start as unchecked.
313
+ checked: false,
288
314
  });
289
315
  },
290
316
 
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.
317
+ updateTodoItemOperation(state, action) {
295
318
  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.
298
319
  if (!item) {
299
320
  throw new Error(`Item with id ${action.input.id} not found`);
300
321
  }
301
322
 
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) {
323
+ if (action.input.text !== undefined) {
305
324
  item.text = action.input.text;
306
325
  }
307
326
 
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;
327
+ if (action.input.checked !== undefined && action.input.checked !== item.checked) {
328
+ if (action.input.checked) {
329
+ state.stats.unchecked -= 1;
330
+ state.stats.checked += 1;
331
+ } else {
332
+ state.stats.unchecked += 1;
333
+ state.stats.checked -= 1;
334
+ }
322
335
  item.checked = action.input.checked;
323
336
  }
324
337
  },
325
338
 
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.
339
+ deleteTodoItemOperation(state, action) {
330
340
  const item = state.items.find((item) => item.id === action.input.id);
331
341
 
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;
342
+ if (item) {
343
+ state.stats.total -= 1;
344
+ if (item.checked) {
345
+ state.stats.checked -= 1;
346
+ } else {
347
+ state.stats.unchecked -= 1;
348
+ }
342
349
  }
343
350
 
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.
346
351
  state.items = state.items.filter((item) => item.id !== action.input.id);
347
352
  },
348
353
  };