# Powerhouse Academy - Complete Documentation > Generated: 2026-05-29T19:48:11.049Z > Total Documents: 87 > Source: https://powerhouse.academy/llms-full.txt # Get Started ## Explore the demo package > Source: https://powerhouse.academy/academy/GetStarted/ExploreDemoPackage ## Let's get started To give you a quick idea of how the Powerhouse Vetra builder platform operates on document models and packages, why don't you try installing a package? We will show you how to install the Powerhouse command-line tool `ph-cmd` and then use it to install a pre-built demo package containing a document model, an editor, and a Drive-app. ## Step 1: Install the Powerhouse CLI You will use the Powerhouse CLI to launch a local environment with a "To-do List Demo Package" installed. This is also the package that you'll recreate during the tutorials and gets you familiar with Powerhouse Vetra. ```bash pnpm install -g ph-cmd ``` Verify the installation: ```bash ph --version ``` ## Step 2: Initialize a new project Now use the `ph init` command to initialize a new project and install a Powerhouse package inside the project. You'll be asked to name your project. Afterwards, move inside your project with `cd project-name` ## Step 3: Install the to-do list demo package Now, use the `ph install` command to install the demo package inside the project. ```bash # Install the package ph install @powerhousedao/todo-demo ``` This command downloads and sets up the `@powerhousedao/todo-demo`, making its features available in your Powerhouse project.
Expected CLI result ```bash installing dependencies ๐Ÿ“ฆ ... โ€‰WARNโ€‰ 19 deprecated subdependencies found: @esbuild-kit/core-utils@3.3.2, @esbuild-kit/esm-loader@2.6.5, @npmcli/move-file@1.1.2, @paulmillr/qr@0.2.1, are-we-there-yet@3.0.1, gauge@4.0.4, glob@7.2.3, graphql-language-service-interface@2.10.2, graphql-language-service-parser@1.10.4, graphql-language-service-types@1.8.7, graphql-language-service-utils@2.7.1, inflight@1.0.6, multibase@4.0.6, multicodec@3.2.1, node-domexception@1.0.0, npmlog@6.0.2, rimraf@2.7.1, rimraf@3.0.2, sudo-prompt@8.2.5 Packages: +1 + Progress: resolved 2277, reused 2106, downloaded 1, added 1, done โ€‰WARNโ€‰ Issues with peer dependencies found . โ”œโ”€โ”ฌ @powerhousedao/reactor-browser 3.1.0 โ”‚ โ””โ”€โ”ฌ @powerhousedao/analytics-engine-browser 0.6.0 โ”‚ โ””โ”€โ”€ โœ• unmet peer @powerhousedao/analytics-engine-knex@0.5.1: found 0.6.0 โ”œโ”€โ”ฌ @types/react-dom 19.1.6 โ”‚ โ””โ”€โ”€ โœ• unmet peer @types/react@^19.0.0: found 18.3.23 โ””โ”€โ”ฌ react-native 0.80.0 โ”œโ”€โ”€ โœ• unmet peer @types/react@^19.1.0: found 18.3.23 โ”œโ”€โ”€ โœ• unmet peer react@^19.1.0: found 18.3.1 โ””โ”€โ”ฌ @react-native/virtualized-lists 0.80.0 โ””โ”€โ”€ โœ• unmet peer @types/react@^19.0.0: found 18.3.23 dependencies: + @powerhousedao/todo-demo-package 1.1.1 โ•ญ Warning โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ โ”‚ โ”‚ โ”‚ Ignored build scripts: @acaldas/graphql-codegen-typescript-validation-schema, @apollo/protobufjs, โ”‚ โ”‚ @datadog/pprof, @ipshipyard/node-datachannel, @parcel/watcher, @prisma/client, @prisma/engines, โ”‚ โ”‚ @tailwindcss/oxide, bufferutil, esbuild, keccak, prisma, sqlite3, utf-8-validate. โ”‚ โ”‚ Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts. โ”‚ โ”‚ โ”‚ โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ Done in 6s using pnpm v10.9.0 Dependency installed successfully ๐ŸŽ‰ โš™๏ธ Updating powerhouse config file... Config file updated successfully ๐ŸŽ‰ ```
You have now successfully installed `ph-cmd`, initalized a product project and added your first package! ## Step 4: Run the Connect host application To run the package locally in Connect, run: ```bash ph connect ``` Click the returned localhost URL and you should see Connect appear in your browser. **INFO:** **Connect** is the Powerhouse host applicationโ€”a container that runs all your apps, editors, and drives. Think of it as the browser for your Powerhouse ecosystem. **Vetra Studio** (which you'll use for building) runs inside Connect, just like how a web app runs inside a browser.
Connect Home
The Powerhouse Connect interface.
When you click the settings wheel in the bottom right corner, you'll get access to the **Package Manager**. Here, you'll see that you've installed the `@powerhousedao/todo-demo`, which contains not only a **Document Model** and its accompanying editor but also a **Drive-app** specific to the todo-list document model.
Package Manager
The Package Manager showing the installed todo-demo-package.
## Step 5: Create a todo list document **TIP:** A **drive** is a folder to store and organize your documents in. Powerhouse offers the ability to build customized Drive-apps for your documents. Think of a Drive-app as a specialized lensโ€”it offers **different ways to visualize, organize, and interact with** the data stored within a drive, making it more intuitive and efficient for specific use cases. To learn more, visit [Building a Drive-app](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) **TIP:** An **editor** is a UI component for viewing and modifying a single document. A **Drive-app** is a custom interface for managing multiple documents within a driveโ€”providing aggregated views, progress tracking, and specialized workflows across your document collection. ### 5.1 Create a local todo-list app drive First, let's create a dedicated drive for your to-do lists: - Click the new drive icon in the interface - In the **Drive-app** field, select 'todo-list Drive-app' - This creates a specialized drive that's optimized for to-do list documents ### 5.2 Create a todo-list document Now move into the drive you've just created: - Click the button at the bottom of the page to create a new to-do list document - This opens the to-do list editor where you can start managing your tasks ### 5.3 Add a few todos and inspect the document history - Add a few to-dos that are on your mind - You'll see a statistics widget that counts the open to-dos - After closing the document, look at the todo-list Drive-app interfaceโ€”you'll see that it tracks your tasks and displays a progress bar This is an example of the **usefulness and impact of Drive-apps**. They offer a customized interface that works well with the different documents inside your drive. Read more about Drive-apps in the Mastery Track: [Drive-apps](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer). A key feature of Connect is the **Operations History**. Every change to a document is stored as an individual operation, creating an immutable and replayable history. This provides complete auditability and transparency, as you can inspect each revision, its details, and any associated signatures. For example, you can see a chronological list of all modifications, along with who made them and when.
Operations History Button
You can find the button to visit the operations history in the document model toolbar
Operations History
Example of the operations history for a document, showing all modifications made to it in a list.{" "}
Learn more about the [Operations History](../docs/MasteryTrack/BuildingUserExperiences/DocumentTools/OperationHistory) and other document tools you get for free. ## Step 6: Enable operation signing and verification through Renown Renown is Powerhouse's **decentralized identity and reputation system** designed to address the challenge of trust within open organizations, where contributors often operate under pseudonyms. In traditional organizations, personal identity and reputation are key to establishing trust and accountability. Renown replicates this dynamic in the digital space, allowing contributors to earn experience and build reputation without revealing their real-world identities. **TIP:** When signing in with Renown, use an Ethereum or blockchain address that can function as your 'identity', as this address will accrue more experience and history over time. ### 6.1 Click the renown icon and connect your Ethereum identity "**Log in with Renown**" is a decentralized authentication flow that enables you to log into applications by signing a credential with your Ethereum wallet. Upon signing in, a Decentralized Identifier (DID) is created based on your Ethereum key.
Renown Login
The Renown login screen, prompting for a signature from a wallet.
### 6.2 Authorize Connect to sign document edits on your behalf This DID is then associated with a credential that authorizes a specific Connect instance to act on your behalf. That credential is stored securely on Ceramic, a decentralized data network. When you perform actions through the Powerhouse Connect interface, those operations are signed with the DID and transmitted to Switchboard, which serves as the verifier.
Connect Address for DID
A newly generated DID and address shown within the Connect interface.
Renown Login Complete
Confirmation of a successful login with Renown.
### 6.3 Verify the signatures of new operations in the todo list By leveraging this system, every operation or modification made to a document is cryptographically signed by the contributor's Renown identity. This ensures that each change is verifiable, traceable, and attributable to a specific pseudonymous user, providing a robust audit trail for all document activity. Now, return to your to-do list and make some additional changes. You'll notice that these operations are now signed with your Renown identity, making every action traceable and verifiable in the operations history.
Operation History Signature
Your DID is now signing the operations that are being added to the history.
## Step 7: Export a document Export the document as a `.phd` (Powerhouse Document) file using the export button in the document toolbar at the top. In this toolbar, you will find all available functionality for your documents. The `.phd` file can be sent through any of your preferred channels to other users on your network. ### Up next Now that you have explored a Powerhouse package and discovered its basic functionalities, it is time to start building your own. Our next tutorial focuses on creating a simple to-do list document and will introduce you to the world of **Document Models**โ€”the foundation of **Specification Driven Design & Development**, where structured specs become the shared language between you and AI agents. --- ## Create a new to-do list document > Source: https://powerhouse.academy/academy/GetStarted/CreateNewPowerhouseProject **TIP:** ๐Ÿ“ฆ **Reference Code**: [step-1-initialize-with-ph-init](https://github.com/powerhouse-inc/todo-tutorial/tree/step-1-initialize-with-ph-init) This tutorial step has a corresponding branch in the repository. You can: - View the complete code for this step - Clone and checkout the branch to see the result - Compare your implementation using `git diff` ::: ## Overview This tutorial guides you through creating a simplified version of a 'Powerhouse project' for a **todo-list**. A Powerhouse project primarily consists of a document model and its editor. As your projects use-case expands you can add data-integrations or a specific Drive-app as seen in the demo package. For today's purpose, you'll be using **Vetra Studio**, the builder platform through which developers can access and manage specifications of your project. Vetra Studio runs inside **Connect**, the Powerhouse host application that serves as a container for all Powerhouse apps and drives. ## Prerequisites - Powerhouse CLI installed: `pnpm install -g ph-cmd` or `npm install -g ph-cmd` - Node.js 24 and a package manager (pnpm or npm) installed - Visual Studio Code (or your preferred IDE) - Terminal/Command Prompt access If you need help with installing the prerequisites you can visit our page [prerequisites](/academy/MasteryTrack/BuilderEnvironment/Prerequisites)
๐Ÿ“– How to use this tutorial This tutorial is designed for you to **build your own project from scratch** while having access to reference code at each step. ### Setup: Create your project and connect to tutorial repo 1. **Create your project** following the tutorial: ```bash mkdir ph-projects cd ph-projects ph init # When prompted, enter project name: todo-tutorial cd todo-tutorial ``` 2. **Add the tutorial repository as a remote** to access reference branches: ```bash git remote add tutorial https://github.com/powerhouse-inc/todo-tutorial.git git fetch tutorial --prune ``` 3. **Create your own branch** to keep your work organized: ```bash git checkout -b my-todo-project ``` Now you have access to all tutorial step branches while working on your own code! ### Compare your work with reference steps At any point, compare what you've built with a tutorial step: ```bash # Compare your current work with step-1 git diff tutorial/step-1-initialize-with-ph-init # See what changed between tutorial steps git diff tutorial/step-1-initialize-with-ph-init..tutorial/step-2-generate-todo-list-document-model # Compare specific files git diff tutorial/step-1-initialize-with-ph-init -- package.json ``` ### Visual diff with GitHub Desktop For a more visual comparison, use GitHub Desktop: 1. **First, make your initial commit** (GitHub Desktop won't show your branch until you have at least one commit): ```bash git add . git commit -m "Initial project setup" ``` 2. **Open GitHub Desktop** and open your repository 3. **Compare branches visually**: - Click on **Branch** menu in the top menu bar - Select **"Compare to Branch..."** - Choose the tutorial branch you want to compare with (e.g., `tutorial/step-1-initialize-with-ph-init`) - GitHub Desktop will show you all file differences in a visual interface 4. **Review the differences**: - Click on any file to see side-by-side or unified diff view - See exactly what's different between your code and the reference **Tip**: You can also use VS Code's Git Graph extension or the command palette โ†’ "Git: Compare with Branch" ### If you get stuck Reset your code to match a tutorial step: ```bash # Reset to step-2 (WARNING: loses your changes) git reset --hard tutorial/step-2-generate-todo-list-document-model ```
## Quick start Create a new Powerhouse project with a single command: ```bash ph init ``` ## Before you begin 1. Open your terminal (either your system terminal or IDE's integrated terminal) 2. Optionally, create a folder first to keep your Powerhouse projects: ```bash mkdir ph-projects cd ph-projects ``` 3. Ensure you're in the correct directory before running the `ph init` command. In the terminal, you will be asked to enter the project name. Fill in the project name and press Enter. ````bash you@yourmachine:~/ph-projects % ph init ? What is the project name? โ€ฃ todo-tutorial ``` ```` Once the project is created, you will see output like: ```bash ๐Ÿš€ Initializing a new project... โ–ถ๏ธ Creating directory for project "todo-tutorial"... โœ… Project directory created โ–ถ๏ธ Initializing git repository... โœ… Git repository initialized โ–ถ๏ธ Creating project boilerplate files... โœ… Project boilerplate files created โ–ถ๏ธ Installing project dependencies with npm... โœ… Project dependencies installed โ–ถ๏ธ Formatting boilerplate project files... โœ… Boilerplate files formatted ๐ŸŽ‰ Successfully created project "todo-tutorial" ๐ŸŽ‰ ``` Navigate to the newly created project directory: `bash cd todo-tutorial ` ## Develop a single document model in Vetra Studio **Vetra Studio** is the builder's orchestration hub for assembling all specifications needed for your package. It provides a **Vetra Studio Drive** to access, manage, and share document model specifications, editors, and data integrationsโ€”all through a visual interface. For deeper coverage, see the [Vetra Studio documentation](/academy/MasteryTrack/BuilderEnvironment/VetraStudio). Once in the project directory, run the `ph vetra --watch` command to start a Vetra Studio Drive where you'll be defining your specifications. This is the preferred way to launch your development environment. **INFO:** You'll notice "reactor-api" in the terminal output. A **Reactor** is the Powerhouse back-end service that hosts your drives, handles document synchronization, and provides the GraphQL API. When you run `ph vetra --watch`, a local Reactor starts automatically to power your development environment. ```bash ph vetra --watch ``` The host application for Vetra Studio will start and you will see the following output: ```bash โ„น [reactor-api] [package-manager] Loading packages: @powerhousedao/vetra 14:44:19 โ„น [reactor-api] [server] WebSocket server available at /graphql/subscriptions 14:44:22 โ„น [reactor-api] [graphql-manager] Registered /graphql/system subgraph. 14:44:22 โ„น [reactor-api] [graphql-manager] Registered /graphql/analytics subgraph. 14:44:22 โ„น [reactor-api] [graphql-manager] Registered /d/:drive subgraph. 14:44:22 โ„น [reactor-api] [graphql-manager] Registered /graphql supergraph 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/document-editor subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/vetra-package subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/subgraph-module subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/processor-module subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/app-module subgraph. 14:44:23 โ„น [reactor-api] [graphql-manager] Registered /graphql/vetra-read-model subgraph. 14:44:23 โ„น [reactor-api] [server] MCP server available at http://localhost:4001/mcp 14:44:24 Switchboard initialized 14:44:24 โžœ Drive URL: http://localhost:4001/d/vetra-bac239dd 14:44:24 2:44:24 PM [vite] (client) Re-optimizing dependencies because vite config has changed 14:44:24 Port 3000 is in use, trying another one... 14:44:24 โžœ Local: http://localhost:3000/ 14:44:24 โžœ Network: use --host to expose 14:44:24 โžœ press h + enter to show help ```` A new browser window will open when visiting localhost and you will see the Vetra Studio Drive
Vetra Studio Drive
The Vetra Studio Drive, a builder app that collects all of the specification of a package.
Create a new document model by clicking the Document Models 'Add new specification' button. Name your document TodoList (PascalCase, no spaces or hyphens). If you've followed the steps correctly, you'll have an empty TodoList document where you can define the 'Document Specifications' in the next step.
Alternatively: Develop a single document model in Connect (legacy) **NOTE:** The `ph connect` command is a legacy feature. We recommend using `ph vetra --watch` for all new development, as it provides better tooling and automatic code generation. Once in the project directory, run the `ph connect` command to start a local instance of the Connect application. This allows you to start your document model specification document. Run the following command to start the Connect application: ```bash ph connect ``` The Connect application will start and you will see the following output: ```bash โžœ Local: http://localhost:3000/ โžœ Network: http://192.168.5.110:3000/ โžœ press h + enter to show help ``` A new browser window will open and you will see the Connect application. If it doesn't open automatically, you can open it manually by navigating to `http://localhost:3000/` in your browser. You will see your local drive and a button to create a new drive. **TIP:** If you local drive is not present navigate into Settings in the bottom left corner. Settings > Danger Zone > Clear Storage. Clear the storage of your localhost application as it might has an old session cached. 4. Move into your local drive. Create a new document model by clicking the `DocumentModel` button, found in the 'New Document' section at the bottom of the page. Name your document `TodoList` (PascalCase, no spaces or hyphens). If you've followed the steps correctly, you'll have an empty `TodoList` document where you can define the **'Document Specifications'**.
## Verify your setup At this point, your project structure should match the `step-1-initialize-with-ph-init` branch. You should have: - Empty `document-models/`, `editors/`, `processors/`, and `subgraphs/` directories - Configuration files: `powerhouse.config.json`, `powerhouse.manifest.json` - Package management files: `package.json`, `pnpm-lock.yaml` - Build configuration: `tsconfig.json`, `vite.config.ts`, `vitest.config.ts` ### Compare with reference implementation Verify your initial setup matches the tutorial: ```bash # Compare your project structure with step-1 git diff tutorial/step-1-initialize-with-ph-init # List files in the tutorial's step-1 git ls-tree -r --name-only tutorial/step-1-initialize-with-ph-init # View a specific config file from step-1 git show tutorial/step-1-initialize-with-ph-init:package.json ```` ## ph install / ph add ```bash ph install [dependencies...] [--registry ] [--local] [--allow-build ] ``` Aliases: `ph add`, `ph i` The install command adds Powerhouse dependencies to your project. By default it only registers the package in `powerhouse.config.json` with provider `"registry"` โ€” Connect will load it from the registry CDN at runtime. With `--local`, the package is also installed into `node_modules` and marked as provider `"local"` โ€” it will be bundled into `ph connect build` so the preview works without the registry being reachable. Resolution order for the registry URL: `--registry` flag > `PH_REGISTRY_URL` env > `powerhouse.config.json` > default ## Up next In the next tutorials, you will learn how to specify, add code and build an editor for your document model and export it to be used in your Powerhouse package. --- ## Write the document specification > Source: https://powerhouse.academy/academy/GetStarted/DefineToDoListDocumentModel **TIP:** ๐Ÿ“ฆ **Reference Code**: [step-2-generate-todo-list-document-model](https://github.com/powerhouse-inc/todo-tutorial/tree/step-2-generate-todo-list-document-model) This tutorial step has a corresponding branch. After completing this step, your project will have a generated document model with: - Document model specification files (`todo-list.json`, `schema.graphql`) - Auto-generated TypeScript types and action creators - Reducer scaffolding ready for implementation :::
๐Ÿ“– How to use this tutorial **Prerequisites**: Complete step 1 and set up the tutorial remote (see previous step). ### Compare your generated code After running `ph generate document-model --document TodoList.phdm.zip`, compare with the reference: ```bash # Compare all generated files with step-2 git diff tutorial/step-2-generate-todo-list-document-model # Compare specific directory git diff tutorial/step-2-generate-todo-list-document-model -- document-models/todo-list/ ``` ### See what was generated View the complete step-2 reference code: ```bash # List files in the tutorial's step-2 git ls-tree -r --name-only tutorial/step-2-generate-todo-list-document-model document-models/ # View a specific file from step-2 git show tutorial/step-2-generate-todo-list-document-model:document-models/todo-list/schema.graphql ``` ### Visual comparison with GitHub Desktop After making a commit, use GitHub Desktop for visual diff: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/step-2-generate-todo-list-document-model` 3. Review all file differences in the visual interface See step 1 for detailed GitHub Desktop instructions.
In this tutorial, you will learn how to define the specifications for a **todo-list** document model within Vetra Studio using its GraphQL schema, and then export the resulting document model specification document for your Powerhouse project. If you don't have a document specification file created yet, have a look at the previous step of this tutorial to create a new document specification. ## TodoList document specification We'll continue with this project to teach you how to create a document model specification and later an editor for your document model. We use the **GraphQL Schema Definition Language** (SDL) to define the schema for the document model. Below, you can see the SDL for the `TodoList` document model. **INFO:** This schema defines the **data structure** of the document model and the types involved in its operations, which are detailed further as input types. Documents in Powerhouse leverage **event sourcing principles**, where every state transition is represented by an operation. GraphQL input types describe operations, ensuring that user intents are captured effectively. These operations detail the parameters needed for state transitions. The use of GraphQL aligns these transitions with explicit, validated, and reproducible commands. This is the essence of **Specification Driven Design & Development**: your schema serves as a machine-readable specification that both humans and AI agents can understand and executeโ€”turning your intent into precise, maintainable functionality.
State schema of our simplified TodoList ```graphql # The state of our TodoList - contains an array of todo items type TodoListState { items: [TodoItem!]! } # A single to-do item with its properties type TodoItem { id: OID! # Unique identifier for each to-do item text: String! # The text description of the to-do item checked: Boolean! # Status of the to-do item (checked/unchecked) } ```
Operations schema of our simplified TodoList ```graphql # Defines a GraphQL input type for adding a new to-do item # Only text is required - ID is generated automatically, checked defaults to false input AddTodoItemInput { text: String! # The text for the new todo item } # Defines a GraphQL input type for updating a to-do item # ID is required to identify which item to update # text and checked are optional - only provided fields will be updated input UpdateTodoItemInput { id: OID! # Required: which item to update text: String # Optional: new text value checked: Boolean # Optional: new checked state } # Defines a GraphQL input type for deleting a to-do item input DeleteTodoItemInput { id: OID! # The ID of the item to delete } ```
## Define the document model specification ### The steps below show you how to do this: 1. In Vetra Studio, click on **'document model'** to open the document model specification editor. 2. Name your document model `TodoList` (PascalCase, no spaces or hyphens) in the Connect application. **Pay close attention to capitalization, as it influences code generation.** 3. You'll be presented with a form to fill in metadata about the document model. Fill in the details in the respective fields. In the **Document Type** field, type `powerhouse/todo-list` (lowercase with hyphen). This defines the new type of document that will be created with this document model specification. ![TodoList Document Model Form Metadata](../docs/docs/images/DocumentModelHeader.png) 4. In the code editor, you can see the SDL for the document model. Replace the existing SDL template with the SDL defined in the [State Schema](#state-schema) section. Only copy and paste the types, leaving the inputs for the next step. You can, however, already press the 'Sync with schema' button to set the initial state of your document model specification based on your Schema Definition Language. 5. Below the editor, find the input field `Add module`. You'll use this to create and name a module for organizing your input operations. In this case, we will name the module `todos`. Press enter. 6. Now there is a new field, called `Add operation`. Here you will have to add each input operation to the module, one by one. 7. Inside the `Add operation` field, type `ADD_TODO_ITEM` and press enter. A small editor will appear underneath it, with an empty input type that you have to fill. Copy the first input type from the [Operations Schema](#operations-schema) section and paste it in the editor. The editor should look like this: ```graphql input AddTodoItemInput { text: String! } ``` 8. Repeat the process from step 7 for the other input operations: `UPDATE_TODO_ITEM` and `DELETE_TODO_ITEM`. You may have noticed that you only need to add the name of the operation (e.g., `UPDATE_TODO_ITEM`, `DELETE_TODO_ITEM`) without the `Input` suffix. It will then be generated once you press enter. 9. In the meantime Vetra has been keeping an eye on your inputs and started code generation in your directory. In your terminal you will also find any validation errors that help you to identify missing specifications. Check below screenshot for the complete implementation: ![ToDoList Document Model](../docs/docs/images/DocumentModelOperations.png) ## Verify your document model generation If you have been watching the terminal in your IDE you will see that Vetra has been tracking your changes and scaffolding your directory. It will mention: ``` โ„น [Vetra] Document model TodoList is valid, proceeding with code generation ``` Your project should have the following structure in `document-models/todo-list/`: ``` document-models/todo-list/ โ”œโ”€โ”€ index.ts โ”œโ”€โ”€ todo-list.json # Document model specification โ”œโ”€โ”€ v1/ โ”‚ โ”œโ”€โ”€ index.ts โ”‚ โ”œโ”€โ”€ actions.ts โ”‚ โ”œโ”€โ”€ hooks.ts โ”‚ โ”œโ”€โ”€ module.ts โ”‚ โ”œโ”€โ”€ schema.graphql # GraphQL schema โ”‚ โ”œโ”€โ”€ utils.ts โ”‚ โ”œโ”€โ”€ gen/ # Auto-generated code (don't edit) โ”‚ โ”‚ โ”œโ”€โ”€ schema/ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ index.ts โ”‚ โ”‚ โ”œโ”€โ”€ actions.ts โ”‚ โ”‚ โ”œโ”€โ”€ controller.ts โ”‚ โ”‚ โ”œโ”€โ”€ creators.ts โ”‚ โ”‚ โ”œโ”€โ”€ document-model.ts โ”‚ โ”‚ โ”œโ”€โ”€ document-schema.ts โ”‚ โ”‚ โ”œโ”€โ”€ document-type.ts โ”‚ โ”‚ โ”œโ”€โ”€ index.ts โ”‚ โ”‚ โ”œโ”€โ”€ ph-factories.ts โ”‚ โ”‚ โ”œโ”€โ”€ reducer.ts โ”‚ โ”‚ โ”œโ”€โ”€ types.ts โ”‚ โ”‚ โ”œโ”€โ”€ utils.ts โ”‚ โ”‚ โ””โ”€โ”€ todos/ # Per-module generated files โ”‚ โ”‚ โ”œโ”€โ”€ actions.ts โ”‚ โ”‚ โ”œโ”€โ”€ creators.ts โ”‚ โ”‚ โ”œโ”€โ”€ error.ts โ”‚ โ”‚ โ””โ”€โ”€ operations.ts โ”‚ โ”œโ”€โ”€ src/ # Your custom implementation โ”‚ โ”‚ โ”œโ”€โ”€ index.ts โ”‚ โ”‚ โ”œโ”€โ”€ utils.ts โ”‚ โ”‚ โ””โ”€โ”€ reducers/ โ”‚ โ”‚ โ””โ”€โ”€ todos.ts โ”‚ โ””โ”€โ”€ tests/ โ”‚ โ”œโ”€โ”€ document-model.test.ts โ”‚ โ””โ”€โ”€ todos.test.ts โ””โ”€โ”€ upgrades/ โ”œโ”€โ”€ index.ts โ”œโ”€โ”€ upgrade-manifest.ts โ”œโ”€โ”€ v1.ts โ””โ”€โ”€ versions.ts ``` **TIP:** To make sure everything works as expected: ```bash # Check types compile correctly pnpm tsc # Check linting passes pnpm lint # Compare your generated files with step-2 git diff tutorial/step-2-generate-todo-list-document-model -- document-models/todo-list/ ``` ### Up next: reducers Up next, you'll learn how to implement the runtime logic and components that will use the `TodoList` document model specification you've just created and exported. --- ## Implement the document model reducers > Source: https://powerhouse.academy/academy/GetStarted/ImplementOperationReducers **TIP:** ๐Ÿ“ฆ **Reference Code**: [step-3-implement-reducer-operation-handlers](https://github.com/powerhouse-inc/todo-tutorial/tree/step-3-implement-reducer-operation-handlers) This step focuses on implementing the reducer logic for add, update, and delete operations.
๐Ÿ“– How to use this tutorial ### Compare your reducer implementation After implementing your reducers: ```bash # Compare your reducers with the reference git diff tutorial/step-3-implement-reducer-operation-handlers -- document-models/todo-list/src/reducers/ # View the reference reducer implementation git show tutorial/step-3-implement-reducer-operation-handlers:document-models/todo-list/src/reducers/todos.ts ``` ### Visual comparison with GitHub Desktop After committing your work, compare visually: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/step-3-implement-reducer-operation-handlers` 3. Review differences in the visual interface ### If you get stuck View or reset to a specific step: ```bash # View the reducer code git show tutorial/step-3-implement-reducer-operation-handlers:document-models/todo-list/src/reducers/todos.ts # Reset to this step (WARNING: loses your changes) git reset --hard tutorial/step-3-implement-reducer-operation-handlers ```
In this section, we will implement the operation reducers for the **todo-list** document model. In the previous step Vetra imported our document specification and scaffolded our code and directory through live code generation. **INFO:** Reducers are a core concept in Powerhouse document models. They implement the state transition logic for each operation defined in your schema. **Connection to schema definition language (SDL)**: The reducers directly implement the operations you defined in your SDL. Remember how we defined `AddTodoItemInput`, `UpdateTodoItemInput`, and `DeleteTodoItemInput` in our schema? The reducers provide the actual implementation of what happens when those operations are performed. ## Explore the generated reducer file Navigate to `/document-models/todo-list/src/reducers/todos.ts` and open it. You should see scaffolding code that needs to be filled for the three operations you specified: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // TODO: Implement "addTodoItemOperation" reducer throw new Error('Reducer "addTodoItemOperation" not yet implemented'); }, updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, }; ``` ## Implement the operation reducers Let's implement each reducer one by one. ### Step 1: Add the import First, add the `generateId` import at the top of the file: ```typescript // added-line ``` ### Step 2: Implement addTodoItemOperation Replace the boilerplate `addTodoItemOperation` with the actual implementation: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { // removed-start addTodoItemOperation(state, action) { // TODO: Implement "addTodoItemOperation" reducer throw new Error('Reducer "addTodoItemOperation" not yet implemented'); }, // removed-end // added-start addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, // added-end updateTodoItemOperation(state, action) { // ... }, deleteTodoItemOperation(state, action) { // ... }, }; ``` **What's happening here:** - We generate a unique ID using `generateId()` from `document-model/core` - We push a new item to the `items` array with the input text, new ID, and `checked: false` - Under the hood, Powerhouse uses Immer.js, so this "mutation" is actually immutable ### Step 3: Implement updateTodoItemOperation Replace the boilerplate `updateTodoItemOperation`: ```typescript // removed-start updateTodoItemOperation(state, action) { // TODO: Implement "updateTodoItemOperation" reducer throw new Error('Reducer "updateTodoItemOperation" not yet implemented'); }, // removed-end // added-start updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, // added-end ``` **What's happening here:** - We find the item by its ID - We return early if the item is not found - We use nullish coalescing (`??`) to only update fields that are provided ### Step 4: Implement deleteTodoItemOperation Replace the boilerplate `deleteTodoItemOperation`: ```typescript // removed-start deleteTodoItemOperation(state, action) { // TODO: Implement "deleteTodoItemOperation" reducer throw new Error('Reducer "deleteTodoItemOperation" not yet implemented'); }, // removed-end // added-start deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, // added-end ``` **What's happening here:** - We filter out the item with the matching ID - This creates a new array without the deleted item ## Complete reducer file Here's the complete implementation:
Complete todos.ts ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ```
**TIP:** To make sure everything works as expected: ```bash # Check types compile correctly pnpm tsc # Check linting passes pnpm lint # Compare with reference implementation git diff tutorial/step-3-implement-reducer-operation-handlers -- document-models/todo-list/src/reducers/ ``` ## Up next: Writing tests In the next chapter, you'll write comprehensive tests to verify your reducer implementations work correctly. --- ## Write document model tests > Source: https://powerhouse.academy/academy/GetStarted/WriteDocumentModelTests **TIP:** ๐Ÿ“ฆ **Reference Code**: [step-4-implement-tests-for-todos-operations](https://github.com/powerhouse-inc/todo-tutorial/tree/step-4-implement-tests-for-todos-operations) This step focuses on writing comprehensive tests for the reducers you implemented in the previous step.
๐Ÿ“– How to use this tutorial ### Compare your tests After writing tests: ```bash # Compare your tests with the reference git diff tutorial/step-4-implement-tests-for-todos-operations -- document-models/todo-list/src/tests/ # View the reference test implementation git show tutorial/step-4-implement-tests-for-todos-operations:document-models/todo-list/src/tests/todos.test.ts ``` ### Visual comparison with GitHub Desktop After committing your work, compare visually: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/step-4-implement-tests-for-todos-operations` 3. Review differences in the visual interface
In order to make sure the operation reducers are working as expected, you need to write tests for them. When you generated your document model code, we created some boilerplate tests for you. Now we'll enhance them to properly verify our reducer logic. ## Understanding the generated test file Navigate to `/document-models/todo-list/src/tests/todos.test.ts`. You will see that we have some basic "sanity check" style tests already. These make sure that your operations at least result in a valid document model state. ```typescript reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { // The `createDocument` utility function from your document model creates // a new empty document, i.e. one with your default initial state const document = utils.createDocument(); // The generateMock function takes one of your generated input schemas // and creates an object populated with random values for each field const input = generateMock(AddTodoItemInputSchema()); // We call your document model's reducer with the new document we just created // and the action we want to test, `addTodoItem` in this case. // The reducer returns a new object, which is the document with the action applied. // If successful, there will be an operation which corresponds to this action // in the updated document's operations list. const updatedDocument = reducer(document, addTodoItem(input)); // When you generate a document model, we give you validation utilities like // `isTodoListDocument` which confirms the document is of the correct form // in a way that TypeScript recognizes expect(isTodoListDocument(updatedDocument)).toBe(true); // At the start a document will have 0 operations, so after applying this action // there should now be one operation expect(updatedDocument.operations.global).toHaveLength(1); // The operation added to the list should correspond to the correct action type expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); }); it("should handle updateTodoItem operation", () => { // ... boilerplate test }); it("should handle deleteTodoItem operation", () => { // ... boilerplate test }); }); ``` ## Enhance the tests The boilerplate tests check that operations are applied, but they don't verify the **actual results**. Let's write more comprehensive tests. ### Test 1: Update the addTodoItem test The add test is already fairly complete. We just need to add type annotations: ```typescript it("should handle addTodoItem operation", () => { const document = utils.createDocument(); // added-line const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema()); const updatedDocument = reducer(document, addTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); ``` ### Test 2: Replace the updateTodoItem test Delete the existing boilerplate and add two separate tests - one for updating text, one for updating the checked state: ```typescript // removed-start it("should handle updateTodoItem operation", () => { const document = utils.createDocument(); const input = generateMock(UpdateTodoItemInputSchema()); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); // ... }); // removed-end ``` **Add the new test for updating text:** ```typescript it("should handle updateTodoItem operation to update text", () => { // We need there to already be a todo item in the document, // since we want to test updating an existing item const mockItem = generateMock(TodoItemSchema()); // We also need to generate a mock input for the update operation we are testing const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema()); // Since the mocks are generated with random values, we need to set the `id` // on our mock input to match the `id` of the existing mock item input.id = mockItem.id; // We want to easily check if the item's text was updated to our new value, // so we assign a variable and use that for the mock input's text field const newText = "new text"; input.text = newText; // We are only testing updating the text here, so we want the checked field // on the input to be undefined, i.e. it should not change anything on the existing item input.checked = undefined; // We can pass a different initial state to the `createDocument` utility, // so in this case we pass in an `items` array with our existing item already in it const document = utils.createDocument({ global: { items: [mockItem], }, }); // Create an updated document by applying the reducer with the action and input const updatedDocument = reducer(document, updateTodoItem(input)); // Use our validator to check that the document conforms to the document model schema expect(isTodoListDocument(updatedDocument)).toBe(true); // There should now be one operation in the operations list expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // Find the updated item in the items list by its `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // The item's text should now be updated to be our new text expect(updatedItem?.text).toBe(newText); // The item's `checked` field should be unchanged expect(updatedItem?.checked).toBe(mockItem.checked); }); ``` **Add the new test for updating checked state:** ```typescript it("should handle updateTodoItem operation to update checked", () => { // Generate a mock existing item const mockItem = generateMock(TodoItemSchema()); // Generate a mock input const input: UpdateTodoItemInput = generateMock(UpdateTodoItemInputSchema()); // Set the mock input's `id` to the mock item's `id` input.id = mockItem.id; // We want the new `checked` field value to be the opposite of the // randomly generated value from the mock const newChecked = !mockItem.checked; input.checked = newChecked; // Leave the `text` field unchanged input.text = undefined; // Create a document with the existing item in it const document = utils.createDocument({ global: { items: [mockItem], }, }); // Apply the reducer with the action and the mock input const updatedDocument = reducer(document, updateTodoItem(input)); // Validate your document expect(isTodoListDocument(updatedDocument)).toBe(true); // Get the updated item by its `id` const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); // The item's `text` field should remain unchanged expect(updatedItem?.text).toBe(mockItem.text); // The item's `checked` field should be updated to our new checked value expect(updatedItem?.checked).toBe(newChecked); }); ``` ### Test 3: Update the deleteTodoItem test The boilerplate delete test passes even without an existing item to delete. Let's fix that: ```typescript it("should handle deleteTodoItem operation", () => { // removed-start const document = utils.createDocument(); const input = generateMock(DeleteTodoItemInputSchema()); // removed-end // added-start // Create an existing item to delete const mockItem = generateMock(TodoItemSchema()); const document = utils.createDocument({ global: { items: [mockItem], }, }); const input: DeleteTodoItemInput = generateMock(DeleteTodoItemInputSchema()); input.id = mockItem.id; // added-end const updatedDocument = reducer(document, deleteTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); // added-start // Verify the item was actually removed const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); // added-end }); ``` ## Complete test file Here's the complete test file with all updates. Don't forget to add the missing imports:
Complete todos.test.ts ```typescript AddTodoItemInput, DeleteTodoItemInput, UpdateTodoItemInput, } from "todo-tutorial/document-models/todo-list"; reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, TodoItemSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { const document = utils.createDocument(); const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema()); const updatedDocument = reducer(document, addTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation to update text", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newText = "new text"; input.text = newText; input.checked = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(newText); expect(updatedItem?.checked).toBe(mockItem.checked); }); it("should handle updateTodoItem operation to update checked", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newChecked = !mockItem.checked; input.checked = newChecked; input.text = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(mockItem.text); expect(updatedItem?.checked).toBe(newChecked); }); it("should handle deleteTodoItem operation", () => { const mockItem = generateMock(TodoItemSchema()); const document = utils.createDocument({ global: { items: [mockItem], }, }); const input: DeleteTodoItemInput = generateMock( DeleteTodoItemInputSchema(), ); input.id = mockItem.id; const updatedDocument = reducer(document, deleteTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); }); }); ```
**TIP:** To make sure everything works as expected: ```bash # Check types compile correctly pnpm tsc # Check linting passes pnpm lint # Run tests pnpm test # Compare with reference implementation git diff tutorial/step-4-implement-tests-for-todos-operations -- document-models/todo-list/src/tests/ ``` Expected test output: ```bash โœ“ document-models/todo-list/src/tests/document-model.test.ts (3 tests) 1ms โœ“ document-models/todo-list/src/tests/todos.test.ts (4 tests) 8ms Test Files 2 passed (2) Tests 7 passed (7) ``` ## Up next: Building the editor In the next chapter, you'll learn how to implement a user interface (editor) for your document model so you can interact with it visually. --- ## Build a to-do list editor > Source: https://powerhouse.academy/academy/GetStarted/BuildToDoListEditor **TIP:** ๐Ÿ“ฆ **Reference Code**: - **Editor Scaffolding**: [step-5-generate-todo-list-document-editor](https://github.com/powerhouse-inc/todo-tutorial/tree/step-5-generate-todo-list-document-editor) - **Complete Editor UI**: [step-6-add-basic-todo-editor-ui-components](https://github.com/powerhouse-inc/todo-tutorial/tree/step-6-add-basic-todo-editor-ui-components) This tutorial covers two steps: 1. **Step 5**: Generating the editor template with Vetra Studio 2. **Step 6**: Building a complete, interactive UI with components for adding, editing, and deleting todos Compare implementations: `git diff step-5-generate-todo-list-document-editor step-6-add-basic-todo-editor-ui-components`
๐Ÿ“– How to use this tutorial This tutorial shows building from **generated scaffolding** (step-5) to **complete UI** (step-6). ### Compare your generated editor After running `ph generate editor`: ```bash # Compare generated scaffolding with step-5 git diff tutorial/step-5-generate-todo-list-document-editor -- editors/ # View the generated editor template git show tutorial/step-5-generate-todo-list-document-editor:editors/todo-list-editor/editor.tsx ``` ### Compare your custom components After building your UI: ```bash # Compare your complete editor with step-6 git diff tutorial/step-6-add-basic-todo-editor-ui-components -- editors/ # See what was added from scaffolding to complete UI git diff tutorial/step-5-generate-todo-list-document-editor..tutorial/step-6-add-basic-todo-editor-ui-components ``` ### Browse the complete implementation Explore the production-ready component structure: ```bash # List all components in step-6 git ls-tree -r --name-only tutorial/step-6-add-basic-todo-editor-ui-components editors/todo-list-editor/components/ # View a specific component git show tutorial/step-6-add-basic-todo-editor-ui-components:editors/todo-list-editor/components/TodoList.tsx ``` ### Visual comparison with GitHub Desktop After committing your editor code: 1. **Branch** menu โ†’ **"Compare to Branch..."** 2. Select `tutorial/step-5-generate-todo-list-document-editor` or `tutorial/step-6-add-basic-todo-editor-ui-components` 3. See all your custom components vs. the reference implementation See step 1 for detailed GitHub Desktop instructions.
In this chapter we will continue with the interface or editor implementation of the **todo-list** document model. This means you will create a simple user interface for the **todo-list** document model which will be used inside Connect to create, update and delete your todo-list items. ## Add a document editor specification in Vetra Studio. Go back to Vetra Studio and click the 'Add new specification' button in the User Experiences column under 'Editors'. This will create an editor template for your document model. Give the editor the name `todo-list-editor` and select the correct document model. In our case that's the `powerhouse/todo-list` ### Editor implementation options When building your editor component within the Powerhouse ecosystem, you have several options for styling, allowing you to leverage your preferred methods: 1. **Default HTML Styling:** Standard HTML tags (`

`, `

`, ` ); } ``` **What's happening here:** - We use a form with `onSubmit` handler for better UX (Enter key support) - We extract the text value from the input field - We dispatch the `addTodoItem` action (generated from our SDL) - We reset the form after submission ### Step 4: Create the Todos list component Create `editors/todo-list-editor/components/Todos.tsx` to render the list of todos: ```tsx type Props = { todos: TodoItem[]; }; /** Shows a list of the todo items in the selected todo list */ export function Todos({ todos }: Props) { const hasTodos = todos.length > 0; if (!hasTodos) { return

Start adding things to your todo list

; } return (
    {todos.map((todo) => (
  • ))}
); } ``` **What's happening here:** - We accept `todos` as a prop (passed from `TodoList` parent) - We show a helpful message if the list is empty - We map over todos and render a `Todo` component for each item ### Step 5: Create the Todo item component Create `editors/todo-list-editor/components/Todo.tsx` for individual todo items with edit and delete functionality: ```tsx useState, type ChangeEventHandler, type FormEventHandler, type MouseEventHandler, } from "react"; deleteTodoItem, updateTodoItem, } from "todo-tutorial/document-models/todo-list"; type Props = { todo: TodoItem; }; /** Displays a single todo item in the selected todo list * * Allows checking/unchecking the todo item. * Allows editing the todo item text. * Allows deleting the todo item. */ export function Todo({ todo }: Props) { const [isEditing, setIsEditing] = useState(false); // Even though this component is for a single todo item and not the whole list, // we can use the exact same hook for dispatching updates to it. // The dispatch function works for any action supported by a TodoList document. const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; const todoId = todo.id; const todoText = todo.text; const todoChecked = todo.checked; const onSubmitUpdateTodoText: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const textInput = form.elements.namedItem("todoText") as HTMLInputElement; const text = textInput.value; if (!text) return; // We can use the dispatch function for any of the actions // supported by a TodoList document dispatch(updateTodoItem({ id: todo.id, text })); setIsEditing(false); }; const onChangeTodoChecked: ChangeEventHandler = (event) => { dispatch( updateTodoItem({ id: todo.id, checked: event.target.checked, }), ); }; const onClickDeleteTodo: MouseEventHandler = () => { dispatch(deleteTodoItem({ id: todoId })); }; const onClickEditTodo: MouseEventHandler = () => { setIsEditing(true); }; const onClickCancelEditTodo: MouseEventHandler = () => { setIsEditing(false); }; if (isEditing) return (
); return (
{todoText}
); } ``` **What's happening here:** - We use local state (`isEditing`) to toggle between view and edit modes - We dispatch `updateTodoItem` for both checking and text editing - We dispatch `deleteTodoItem` to remove items - We use TypeScript event handlers for type safety ### Step 6: Create the TodoListName component Finally, create `editors/todo-list-editor/components/TodoListName.tsx` for displaying and editing the document name: ```tsx /** Allows editing the name of the selected todo list */ export function TodoListName() { const [isEditing, setIsEditing] = useState(false); const [selectedTodoList, dispatch] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; const documentName = selectedTodoList.name; const onSubmitEditName: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const nameInput = form.elements.namedItem("name") as HTMLInputElement; const name = nameInput.value; if (name) { dispatch(setName(name)); setIsEditing(false); } }; if (isEditing) { return (
); } return (

setIsEditing(true)} > {documentName}

); } ``` **What's happening here:** - We use the `setName` action from `document-model/document` (a built-in action) - We toggle between viewing and editing the name - Click the name to edit it ## Test your editor Now you can run the Vetra Studio Preview and see the **todo-list** editor in action: ```bash ph vetra --watch ``` In the bottom right corner you'll find a new Document Model that you can create: **todo-list**. Click on it to create a new todo-list document. **INFO:** The editor will update dynamically as you make changes, so you can experiment with styling and functionality while seeing your results appear in Vetra Studio in real-time. **Try it out:** 1. Add some todo items using the input form 2. Click on the document name to edit it 3. Check/uncheck items to mark them as complete 4. Click "Edit" on any item to modify its text 5. Click "Delete" to remove items Congratulations! ๐ŸŽ‰ If you managed to follow this tutorial until this point, you have successfully implemented the **todo-list** document model with its reducer operations and editor. ## Compare with the reference implementation The tutorial repository's step-6 branch includes additional enhancements you can explore: **Additional components in step-6:** ``` editors/todo-list-editor/components/ โ”œโ”€โ”€ CloseButton.tsx # Editor close functionality โ”œโ”€โ”€ UndoRedoButtons.tsx # Operation history navigation โ””โ”€โ”€ Stats.tsx # Display metadata (creation/modification times) ``` **View individual components from the reference:** ```bash # See the enhanced TodoList component with all features git show tutorial/step-6-add-basic-todo-editor-ui-components:editors/todo-list-editor/components/TodoList.tsx # Explore the UndoRedoButtons component git show tutorial/step-6-add-basic-todo-editor-ui-components:editors/todo-list-editor/components/UndoRedoButtons.tsx # Compare your implementation with the reference git diff tutorial/step-6-add-basic-todo-editor-ui-components -- editors/todo-list-editor/ ``` **TIP:** To make sure everything works as expected: ```bash # Check types compile correctly pnpm tsc # Check linting passes pnpm lint # Run tests pnpm test # Test in Vetra Studio ph vetra --watch # Compare with reference implementation git diff tutorial/step-6-add-basic-todo-editor-ui-components -- editors/todo-list-editor/ ``` In Connect, you should be able to: - Create a new todo-list document - Add, edit, and delete todo items - Check/uncheck items to mark them complete ## Key concepts learned In this tutorial you've learned: โœ… **Component-based architecture** - Breaking down complex UIs into reusable components โœ… **Document model hooks** - Using `useSelectedTodoListDocument` to connect React to your document state โœ… **Action dispatching** - How to dispatch operations (`addTodoItem`, `updateTodoItem`, `deleteTodoItem`) from your UI โœ… **Type-safe development** - Leveraging TypeScript with generated types from your SDL โœ… **Form handling** - Using React forms with proper event handlers โœ… **Local vs. document state** - When to use React `useState` vs. document model state ### Up next: Mastery Track In the [Mastery Track chapter: Document Model Creation](/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel) we guide you through the theoretics of the previous steps while creating a more advanced version of the todo-list. You will learn: - The ins and outs of a document model. - How to use UI & Scalar components from the Document Engineering system. - How to build Custom drive-apps or Drive Explorers. --- # Mastery Track ## Prerequisites > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/Prerequisites Let's set up your machine to start building your first Document Model. Don't worry if this is your first time setting up a development environment - we'll guide you through each step! **INFO:** If you've already set up **Git, Node.js 24, and a package manager (pnpm or npm)**, your most important step is to install the **Powerhouse CLI** with the command: `pnpm install -g ph-cmd` or `npm install -g ph-cmd`. A global install is recommended if you want to use the command from any directory as a power user. The Powerhouse CLI is used to create, build, and run your Document Models and gives you direct access to a series of Powerhouse Builder Tools. Move to the end of this page to [verify your installation.](#verify-installation) --- ## Overview Before we begin building our Document Model, we need to install some software on your machine. We'll need three main tools: - Node.js 24, which helps us run our code. - Visual Studio Code (VS Code), which is where we'll write our code - Git, which helps us manage our code. Follow the steps below based on your computer's operating system. ### Windows Users: Consider Using WSL If you're on Windows, we recommend using **Windows Subsystem for Linux (WSL)** for the best development experience. WSL lets you run a full Linux environment directly on Windows without the overhead of a virtual machine. This gives you access to Linux command-line tools and utilities, which are often preferred for modern web development workflows. **To install WSL:** 1. Open PowerShell as Administrator (right-click and select "Run as administrator") 2. Run the following command: ```powershell wsl --install ``` 3. Restart your computer when prompted 4. After restart, a terminal will open asking you to create a Linux username and password Once WSL is set up, you can follow the **Linux (Ubuntu/Debian)** instructions below for installing Node.js, Git, and other tools. Your Linux environment will be accessible through the Windows Terminal or by typing `wsl` in PowerShell. **TIP:** Using WSL provides a consistent development experience that matches most production environments and online tutorials. You can still use VS Code on Windows โ€” it integrates seamlessly with WSL through the [Remote - WSL extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-wsl). For more details, see the official [WSL installation guide](https://learn.microsoft.com/en-us/windows/wsl/install). --- ### Installing Node.js 24 Node.js 24 is a tool that lets us run our application. Let's install it step by step. #### For Windows: 1. **Set up PowerShell for running commands:** - Press the Windows key - Type "PowerShell" - Right-click on "Windows PowerShell" and select "Run as administrator" - In the PowerShell window, type this command and press Enter: ```powershell Set-ExecutionPolicy RemoteSigned -Scope CurrentUser ``` - Type 'A' when prompted to confirm - You can now close this window and open PowerShell normally for the remaining steps 2. **Install Node.js 24:** - Visit the [Node.js official website](https://nodejs.org/) - Click the big green button that says "LTS" (this means Long Term Support - it's the most stable version) - Once the installer downloads, double-click it to start installation - Click "Next" through the installation wizard, leaving all settings at their defaults 3. **Install a package manager (pnpm or npm):** - Open PowerShell (no need for admin mode) - For pnpm (recommended), type this command and press Enter: ```powershell npm install -g pnpm ``` - Note: Node.js comes with npm by default, so npm is already available after installing Node.js 4. **Verify Installation:** - Open PowerShell (no need for admin mode) - Type these commands one at a time and press Enter after each: ```powershell node --version pnpm --version # or npm --version ``` - You should see version numbers appear after each command (e.g., v24.x.x for Node.js). If you do, congratulations - Node.js and your package manager are installed! > **Note**: If Node.js commands don't work in VS Code, restart VS Code to refresh environment variables. #### For macOS: 1. **Install Homebrew:** - Open Terminal (press Command + Space and type "Terminal") - Copy and paste this command into Terminal and press Enter: ```bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" ``` - Follow any additional instructions that appear 2. **Install Node.js 24:** - In the same Terminal window, type this command and press Enter: ```bash brew install node@24 ``` - Then, optionally install pnpm (npm comes with Node.js): ```bash brew install pnpm ``` 3. **Verify Installation:** - In Terminal, type these commands one at a time and press Enter after each: ```bash node --version pnpm --version # or npm --version ``` - If you see version numbers, you've successfully installed Node.js and your package manager! #### For Linux (Ubuntu/Debian): 1. **Open Terminal:** - Press Ctrl + Alt + T on your keyboard, or - Click the Activities button and type "Terminal" 2. **Update Package List:** ```bash sudo apt update ``` 3. **Install Node.js 24 and optionally pnpm:** ```bash sudo apt install nodejs # Optionally install pnpm (npm comes with Node.js) sudo apt install pnpm ``` 4. **Verify Installation:** - Type these commands one at a time and press Enter after each: ```bash node --version pnpm --version # or npm --version ``` - If you see version numbers, you're all set! ### Installing Visual Studio Code VS Code is the editor we'll use to write our code. Here's how to install it: #### For Windows: 1. Visit the [Visual Studio Code website](https://code.visualstudio.com/) 2. Click the blue "Download for Windows" button 3. Once the installer downloads, double-click it 4. Accept the license agreement and click "Next" 5. Leave the default installation location and click "Next" 6. In the Select Additional Tasks window, make sure "Add to PATH" is checked 7. Click "Next" and then "Install" 8. When installation is complete, click "Finish" #### For macOS: 1. Visit the [Visual Studio Code website](https://code.visualstudio.com/) 2. Click the blue "Download for Mac" button 3. Once the .zip file downloads, double-click it to extract 4. Drag Visual Studio Code.app to the Applications folder 5. Double-click the app to launch it 6. To make VS Code available in your terminal: - Open VS Code - Press Command + Shift + P - Type "shell command" and select "Shell Command: Install 'code' command in PATH" #### For Linux (Ubuntu/Debian): 1. Open Terminal (Ctrl + Alt + T) 2. First, update the packages list: ```bash sudo apt update ``` 3. Install the dependencies needed to add Microsoft's repository: ```bash sudo apt install software-properties-common apt-transport-https wget ``` 4. Import Microsoft's GPG key: ```bash wget -q https://packages.microsoft.com/keys/microsoft.asc -O- | sudo apt-key add - ``` 5. Add the VS Code repository: ```bash sudo add-apt-repository "deb [arch=amd64] https://packages.microsoft.com/repos/vscode stable main" ``` 6. Install VS Code: ```bash sudo apt install code ``` 7. Once installed, you can launch VS Code by: - Typing `code` in the terminal, or - Finding it in your Applications menu ### Install Git #### For Windows 1. Open PowerShell (press Windows key, type "PowerShell", and press Enter) 2. Visit the [Git website](https://git-scm.com/) 3. Download the latest version for Windows 4. Run the installer and use the recommended settings 5. Verify installation by opening PowerShell: ```powershell git --version ``` #### For macOS 1. Install using Homebrew: ```bash brew install git ``` 2. Verify installation: ```bash git --version ``` #### For Linux (Ubuntu/Debian) 1. Update package list: ```bash sudo apt update ``` 2. Install Git: ```bash sudo apt install git ``` 3. Verify installation: ```bash git --version ``` ### Configure Git (All Systems) After installation, set up your identity: ```bash git config --global user.name "Your Name" git config --global user.email "your.email@example.com" ``` ### Install the Powerhouse CLI The Powerhouse CLI (installed via the `ph-cmd` package) is a command-line interface tool. It provides the `ph` command, which is essential for managing Powerhouse projects. You can get access to the Powerhouse Ecosystem tools by installing them globally using: ```bash pnpm install -g ph-cmd ``` Or if you're using npm: ```bash npm install -g ph-cmd ``` Key commands include: - `ph connect` for running the Connect application locally - `ph switchboard` or `ph reactor` for starting the API service - `ph init` to start a new project and build a document model - `ph help` to get an overview of all the available commands This tool will be fundamental on your journey when creating, building, and running Document Models.
How to use different branches? When installing or using the Powerhouse CLI commands you can use the dev & staging branches. These branches contain more experimental features than the latest stable release the PH CLI uses by default. They can be used to get access to a bug fix or features under development. | Command | Description | | ---------------------------------- | ------------------------------------------------- | | **pnpm install -g ph-cmd** | Install latest stable version | | **pnpm install -g ph-cmd@dev** | Install development version | | **pnpm install -g ph-cmd@staging** | Install staging version | | **ph init** | Use latest stable version of the boilerplate | | **ph init --dev** | Use development version of the boilerplate | | **ph init --staging** | Use staging version of the boilerplate | | **ph use latest** | Switch all dependencies to latest stable versions | | **ph use dev** | Switch all dependencies to development versions | | **ph use staging** | Switch all dependencies to staging versions | Please be aware that these versions can contain bugs and experimental features that aren't fully tested.
Managing ph-cmd Versions and Package Information ### Switching Between Versions To change to a different version of `ph-cmd`, reinstall it globally with your package manager: ```bash # Install latest version with a specific tag npm install -g ph-cmd@staging pnpm install -g ph-cmd@dev # Install a specific version number npm install -g ph-cmd@1.2.3-staging.10 pnpm install -g ph-cmd@6.0.0-dev.33 ``` **Important:** Always use the same package manager you used for the original global install to avoid conflicting installations. ### Checking Your Installation Use the `which` command to see where your global install is located: ```bash which ph # Example output: /Users/username/Library/pnpm/ph ``` This shows which package manager was used (in this case, pnpm). ### Viewing Available Versions Use `npm view` to see all available versions and tags: ```bash npm view ph-cmd ``` **Example dist-tags output:** ``` dist-tags: latest: 5.3.0 dev: 6.0.0-dev.33 staging: 5.3.0-staging.24 test: 2.5.0-test.0 ``` ### Best Practices - Use specific version numbers instead of tags when you need exact version consistency - Check `npm view ph-cmd` before switching to see the latest available versions - Remember that without specifying a version, `@latest` is installed by default
### Verify Installation Open your terminal (command prompt) and run the following commands to verify your setup: ```bash node --version pnpm --version git --version ph --version ``` You should see version numbers displayed for all commands, similar to the example output below (your versions might be higher). The output for `ph --version` includes its version and may also show additional messages if further setup like `ph setup-globals` is needed. You're now ready to start building your first Document Model! ```bash % node --version v24.13.0 % pnpm --version 10.10.0 % git --version git version 2.39.3 % ph --version PH CMD version: 0.43.18 ------------------------------------- PH CLI is not available, please run `ph setup-globals` to generate the default global project ``` --- ## Vetra Studio > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/VetraStudio ## Introducing Vetra Studio Vetra Studio is the builder environment where you create, manage, and collaborate on Powerhouse packages. It consists of two main components: - **Vetra Studio Drive**: Serves as a hub for developers to access, manage & share specifications through a remote Vetra drive. It functions as the orchestration hub where you as a builder assemble all the necessary specifications for your intended use-case, software solution, or package. Each specification document corresponds to a **module** โ€” a distinct building block of your package (such as a document model, editor, or data integration). - **Vetra Package Library**: Store, publish, and fork git repositories of packages in the Vetra Package Library. Visit the [Vetra Package Library here](https://vetra.io/packages) **INFO:** What is a Specification Document? A **specification document** is a configuration file that defines how a specific module in your package should behave. Think of it as a blueprint โ€” it describes the structure, rules, and relationships that Powerhouse uses to generate the actual code for that module. These specification documents unlock **Specification Driven Design & Development**โ€”enabling you to communicate your solution and intent through a structured framework designed for AI collaboration. Specs serve as a shared language that enables precise, iterative editsโ€”turning messy intent into clean execution, and turning business needs into maintainable functionality. As Vetra Studio matures, each of these specification documents will offer an interface by which you as a builder get more control over the modules that make up your package. For now, the specification documents offer you a template for code generation.
Modules
The list of available modules color coded according to the 3 categories.
### Module Categories ### 1. Document Models A **document model** is a structured data type that defines what information your application can store and how it can be modified. Unlike traditional databases, document models use **operations** (actions like "add item" or "update title") rather than direct data manipulation, making them ideal for collaborative and auditable applications. - **Document model specification**: Defines the structure and operations of a document model using [GraphQL SDL](https://graphql.org/learn/schema/) (Schema Definition Language), ensuring consistent data management and processing. โ†’ [Learn more about Document Models](/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel) ### 2. User Experiences - **Editor specification**: Outlines the interface and functionalities of a document model editor, allowing users to interact with and modify document data. - **Drive-app specification**: Specifies the UI and interactions for managing documents within a drive, providing tailored views and functionalities. โ†’ [Learn more about Building Document Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) โ†’ [Learn more about Building a Drive Explorer](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) ### 3. Data Integrations - **Subgraph specification**: Details the connections and relationships within a subgraph (a subset of your data exposed via a GraphQL API), facilitating efficient data querying and manipulation. - **Codegen Processor Specification**: Describes the process for automatically generating code from document model specifications, ensuring alignment with intended architecture. - **RelationalDb Processor Specification**: Defines how relational databases are structured and queried, supporting efficient data management and retrieval. โ†’ [Learn more about Using Subgraphs](/academy/MasteryTrack/WorkWithData/UsingSubgraphs) โ†’ [Learn more about Relational DB Processor](/academy/MasteryTrack/WorkWithData/RelationalDbProcessor)
Vetra Studio Drive
The Vetra Studio Drive, a builder app that collects all of the specifications of a package.
### Configure a Vetra Drive in Your Project You can connect to a remote Vetra drive instead of using the local one auto-generated when you run `ph vetra` (where `ph` is short for "powerhouse", the CLI tool and the Organization behind Vetra). - **Without** the `--remote-drive` option: Vetra will create a local drive for you that lives in your browser's local storage. This is useful for solo development or experimentation. - **With** the `--remote-drive` argument: Vetra will connect to a remote drive instead of creating a local one. The remote drive can be hosted wherever you want (e.g., on your own server or a shared team environment). The Powerhouse config includes a Vetra URL for consistent project configuration across different environments. ```typescript vetra: { driveId: string; driveUrl: string; } ``` Imagine you are a builder and want to work on, or continue with a set of specifications from your teammates. You could then add the specific remote Vetra drive to your Powerhouse configuration in the `powerhouse.config.json` file to get going: ```json "vetra": { "driveId": "bai-specifications", "driveUrl": "https://switchboard.staging.vetra.io/d/bai-specifications" } ``` An example of a builder team building on the Powerhouse Vetra Ecosystem and its complementary Vetra Studio Drive specifications for the different packages can be found [here](https://vetra.io/builders/bai). ### Connect Claude to the Reactor MCP Claude can connect directly to your running Reactor via the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), giving it live access to your drives, documents, and document model operations. Add the following configuration to your Claude MCP settings (e.g. `~/.claude/mcp.json` for Claude Code, or the MCP servers section in Claude Desktop): ```json { "mcpServers": { "reactor-mcp": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:4001/mcp"] } } } ``` This connects Claude to the Reactor running at `http://localhost:4001`. Make sure `ph vetra --watch` (or `ph reactor`) is running before starting a Claude session that uses the MCP. โ†’ See [Connecting Claude with Reactor MCP](/academy/Cookbook#connecting-claude-with-reactor-mcp) for a step-by-step walkthrough.
๐Ÿ“ฆ Vetra Remote Drive Commands Remote drives enable collaborative development by syncing specifications across team members. **Key Commands:** - `ph init --remote-drive ` - Create or connect to a project using a remote drive - `ph vetra --watch` - Start development with a preview drive for testing local changes **Workflows:** - **Project Owner**: `ph init --remote-drive` โ†’ Create GitHub repo โ†’ Push โ†’ `ph vetra --watch` to configure - **Collaborator**: `ph init --remote-drive ` โ†’ `ph vetra --watch` to start developing **Preview Drive (`--watch` mode):** The preview drive allows you to safely test changes before they affect the shared remote drive. - The main **"Vetra" drive** syncs with the remote and contains the stable package configuration. - The **"Vetra Preview" drive** is created locally for testing document models and editors before syncing your changes to the team. - When restarting Vetra, always use `ph vetra --watch` so it loads your local documents and editors. โ†’ [Full Vetra Remote Drive Reference](/academy/APIReferences/VetraRemoteDrive)
--- ## Create a package with Vetra > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/CreateAPackageWithVetra **INFO:** A **Powerhouse Package** is a distributable unit that bundles one or more document models, editors, and optional reactor modules into a single installable artifact. Once published to the Vetra registry, a package can be installed in any Vetra Cloud environment โ€” extending Connect with new document types and editors, and extending Switchboard with the corresponding reactor logic.
Create a Package
The five steps to build and publish a package to the Vetra ecosystem.
On Vetra Cloud you'll find these five steps as a quick reference for creating and publishing a package. This guide walks you through each step in detail โ€” from installing the CLI and scaffolding your project, to building document models and editors, and finally publishing your package to the Vetra registry so others can install it in their environments. **WARNING:** **This tutorial is a summary for builders that are already familiar with building document models**. It provides a summary from initial setup up to publishing a distributable package. Please start with the [**Get Started**](/) Chapter or [**Document Model Creation**](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema) section if you are unfamiliar with building a document model.
Key commands that you'll use in this flow - `pnpm install -g ph-cmd` or `npm install -g ph-cmd`: Installs the Powerhouse CLI globally. - `ph init`: Initializes a new Powerhouse project or sets up the local environment. - `ph vetra --interactive`: Launches Vetra Studio in interactive mode for package development. - `ph vetra --interactive --watch`: Launches Vetra Studio with dynamic reloading for document-models and editors. - `ph build`: Builds the project for production. - `pnpm run test` or `npm test`: Runs unit tests. - `ph publish`: Publishes your package to the Vetra registry. - `ph install @your-org-ph/your-package-name`: Installs a published package into a local Powerhouse environment.
## Phase 1: Setup and initialization ### 1.1. Install Powerhouse CLI Ensure you have the Powerhouse Command Line Interface (`ph-cmd`) installed. This tool is crucial for managing your Powerhouse projects. ```bash pnpm install -g ph-cmd ``` Or if you're using npm: ```bash npm install -g ph-cmd ``` **INFO:** See the [Prerequisites](/academy/MasteryTrack/BuilderEnvironment/Prerequisites) guide for detailed installation instructions for Node.js 24, package managers (pnpm or npm), and Git if you haven't set them up yet. ### 1.2. Initialize your project environment Before creating a specific project, or if you're setting up your environment for the first time, initialize the Powerhouse environment. This command prepares your local setup, including a local Reactor configuration. ```bash ph init my-package ``` Replace `my-package` with your desired package name. If you run `ph init` without a name you will be prompted for one.
How to make use of different branches? When installing or using the Powerhouse CLI commands you are able to make use of the dev & staging branches. These branches contain more experimental features than the latest stable release the PH CLI uses by default. They can be used to get access to a bugfix or features under development. | Command | Description | | ----------------------------------------------------------------------- | ------------------------------------------------- | | **pnpm install -g ph-cmd** or **npm install -g ph-cmd** | Install latest stable version | | **pnpm install -g ph-cmd@dev** or **npm install -g ph-cmd@dev** | Install development version | | **pnpm install -g ph-cmd@staging** or **npm install -g ph-cmd@staging** | Install staging version | | **ph init** | Use latest stable version of the boilerplate | | **ph init --dev** | Use development version of the boilerplate | | **ph init --staging** | Use staging version of the boilerplate | | **ph use latest** | Switch all dependencies to latest stable versions | | **ph use dev** | Switch all dependencies to development versions | | **ph use staging** | Switch all dependencies to staging versions | Please be aware that these versions can contain bugs and experimental features that aren't fully tested.
### 1.3. Launch Vetra Studio You can launch Vetra Studio in two modes: #### Interactive Mode (Recommended for Development) ```bash ph vetra --interactive ``` In interactive mode: - You'll receive confirmation prompts before any code generation - Changes require explicit confirmation before being processed - Provides better control and visibility over document changes #### Watch Mode ```bash ph vetra --interactive --watch ``` In watch mode: - Adding `--watch` to your command enables dynamic loading for document-models and editors in Vetra studio and switchboard. - When enabled, the system will watch for changes in these directories and reload them dynamically. **WARNING:** When you are building your document model the code can break the Vetra Studio environment. A full overview of the Vetra Studio commands can be found in the [Powerhouse CLI](/academy/APIReferences/PowerhouseCLI#vetra) #### Standard Mode ```bash ph vetra ``` In standard mode: - Changes are processed automatically with 1-second debounce - Multiple changes are batched and processed together - Uses the latest document state for processing
Alternatively: Use Connect Connect is your local development hub. Running it in Studio Mode spins up a local instance with a local Reactor, allowing you to define, build, and test document models. ```bash ph connect ``` This command typically opens Connect in your browser at `http://localhost:3000/`. **INFO:** **Powerhouse Reactors** are essential nodes in the Powerhouse network. They store documents, manage versions, resolve conflicts, and verify document operation histories by rerunning them. Reactors can be configured for local storage (as in Studio Mode), centralized cloud storage, or decentralized storage networks.
### 1.4. Launch Claude with Reactor-MCP Vetra Studio integrates deeply with Claude through MCP (Model Context Protocol). This is where AI comes into the mix and you get the chance to have greater control and direction over what your LLM is coding for you. **INFO:** Vetra embraces **Specification Driven Design & Development** โ€”an approach where your structured specification documents become the shared language between you and AI agents. You communicate intent through precise specs that are machine-readable and executable.
Explainer: Specification Driven AI In the **'Get Started'** chapter we've been making use of strict schema definition principles to communicate the intended use case of our document models. The **schema definition language** is not only a shared language that bridges the gap between developer, designer and analyst but also the gap between builder and AI-agent through **specification driven AI control**. - Communicate your solution and intent through a structured specification framework designed for AI collaboration. - Specifications enable precise, iterative edits, since all our specification documents are machine-readable and executable. #### Key Reactor MCP Features **Reactor-mcp** is a Model Context Protocol (MCP) server that bridges AI agents with Powerhouse document operations. - It supports automatic document model creation from natural language descriptions - It implements a smart editor based on the underlying document models - It automatically triggers code generation when documents reach valid state - The MCP server enables the agent to work with both existing and newly created document models - Vetra supports integration with custom remote drives, allowing users to create, share and manage documents within these drives **Document Operations:** - `createDocument` / `getDocument` / `deleteDocument` - Manage documents - `addActions` - Modify document state through operations **Drive Operations:** - `getDrives` / `addDrive` / `getDrive` - Manage document drives - `addRemoteDrive` - Connect to remote drives **Document Model Operations:** - `getDocumentModels` - List available document model types - `getDocumentModelSchema` - Get schema for specific models **Document Model Agent:** A specialized AI agent that guides users through document model creation with requirements gathering, design confirmation, and implementation including state schema definition, operation creation, and code generation.
#### 0. Configure the Reactor MCP Add the following to your Claude MCP settings (`~/.claude/mcp.json` for Claude Code, or the MCP servers section in Claude Desktop): ```json { "mcpServers": { "reactor-mcp": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:4001/mcp"] } } } ``` This only needs to be done once. Make sure `ph vetra` is running before starting a Claude session that uses the MCP. #### 1. Start the Reactor MCP: Make sure you are in the same directory as your project. Claude will automatically recognize the necessary files and MCP tools. ```bash claude ``` Since you're interacting with an LLM it has a high capacity for interpreting your intentions. Similar natural language commands will work as well. ```bash connect to the reactor mcp ``` #### 2. Verify MCP connection: - Check that the Reactor MCP is available. - Confirm Vetra Studio shows "Connected to Reactor MCP" ```bash Connected to MCP successfully! I can see there's a "vetra-4de7fa45" drive available. The reactor-mcp server is running and ready for document model operations. or Connected to reactor MCP. You have access to 1 drive: vetra+a049e1b1== ``` ## Phase 2: Package Creation ### 2.1. Set Package Description (Required) 1. Provide a name for your package 2. Add a meaningful description 3. Add keywords to add search terms to your package 4. Confirm changes when prompted in interactive mode ### 2.2. Define Document Model (Required) You can create document models in two ways: #### Manual Creation - Define document schema with fields and types as in the **'Get Started'** chapter - Create the necessary operations - Add the required modules to your package - The document model creation chapter in the Mastery track provides in depth support [here](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema) โ†’ [Learn more about Document Models](/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel) #### Using MCP (AI-Assisted) - Describe your package, it's functionality and your needs in natural language in great detail. - Claude will: - Generate an appropriate schema in the document model - Create the necessary operations - Implement the required reducers - Place the document in the Vetra drive - Claude will also add the necessary interface in the form of a [document editor](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) and scaffold the [drive-app functionality](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) when specified. โ†’ [Learn more about Building Document Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) โ†’ [Learn more about Building a Drive Explorer](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer)
Alternatively: Use Connect Within Connect Studio Mode, navigate to the Document Model Editor. Here, you'll specify the structure of your document model using GraphQL Schema Definition Language (SDL). - **State Schema:** Describes the data fields and types within your document (e.g., `ToDoItem` with `id`, `text`, `checked` fields). - This schema is the blueprint for your document model's data. In the same editor, specify the operations (state transitions) for your document model. These are also defined using GraphQL, specifically input types. - **Operations Schema:** Specifies the actions that can be performed on the document (e.g., `AddTodoItemInput`, `UpdateTodoItemInput`, `DeleteTodoItemInput`). - Each input type details the parameters required for an operation. - **Best Practices:** - Clearly define operations (often aligning with CRUD principles). - Use GraphQL input types for operation parameters. - Ensure operations reflect user intent for a clean API. Once your schema and operations are defined in Connect, export the specification. This will download a `.phdm.zip` file (e.g., `YourModelName.phdm.zip`). Save this file in the root of your Powerhouse project directory. Use the Powerhouse CLI to process an exported `.phdm.zip` file and generate the necessary boilerplate code for your document model. ```bash ph generate document-model --document YourModelName.phdm.zip ``` This command creates a new directory under `document-models/YourModelName/` containing: - A JSON file with the document model specification. - A GraphQL file with the state and operation schemas. - A `gen/` folder with autogenerated TypeScript types, action creators, and utility functions based on your schema. - A `src/` folder where you'll implement your custom logic (reducers, utils).
### 2.3. Add Document Editor (Required) #### Manual Creation - Select your target document model - Configure the currently limited editor properties - Add the editor specification to Vetra Studio drive - The system will generate scaffolding code #### Using MCP (AI-Assisted) - Request Claude to create an editor for your document. Do this with the help of a detailed description of the user interface, user experience and logic that you wish to generate. Make sure to reference operations from the document model to get the best results - Claude will: - Generate editor components - Implement necessary hooks - Create required UI elements โ†’ [Learn more about Building Document Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors)
Alternatively: Generate command A document editor provides the user interface for interacting with your document model. Generate an editor template: ```bash ph generate editor --name YourModelName --document-type powerhouse/YourModelName ``` - The `--name YourModelName` flag specifies the document model this editor is for. - The `--document-type powerhouse/YourModelName` flag links the editor to the specific document type defined in your model specification. This creates a template file, typically at `editors/your-model-name/editor.tsx`. - Customize this React component to build your UI. - You can use standard HTML, Tailwind CSS (available in Connect), or import custom CSS. - Utilize components from `@powerhousedao/document-engineering` for consistency and rapid development. Learn more at [Document-Engineering](/academy/ComponentLibrary/DocumentEngineering)
## Phase 3: Implementation and testing ### 3.1. Implement reducer logic Reducers are pure functions that implement the state transition logic for each operation defined in your schema. Navigate to `document-models/YourModelName/src/reducers/to-do-list.ts` (or similar, based on your model name). - Implement the functions for each operation (e.g., `addTodoItemOperation`, `updateTodoItemOperation`). - These functions take the current state and an action (containing input data) and return the new state. - Powerhouse handles immutability behind the scenes. ### 3.2. Write unit tests for reducers It's crucial to test your reducer logic. Write unit tests in the `document-models/YourModelName/src/reducers/tests/` directory. - Verify that each operation correctly transforms the document state. - Use the auto-generated action creators from the `gen/` folder to create operation actions for your tests. Run tests using: ```bash pnpm run test ``` Or with npm: ```bash npm test ``` ### 3.3. Test the editor Test your editor in Vetra Studio by creating a new document of your defined type. Interact with your editor, test all functionalities, and ensure it correctly dispatches actions to the reducers and reflects state changes.
Alternatively: Use Connect Run Connect locally to see your editor in action: ```bash ph connect ``` Create a new document of your defined type. Interact with your editor, test all functionalities, and ensure it correctly dispatches actions to the reducers and reflects state changes.
**TIP:** **Working with MCP and Claude** 1. Provide clear, specific instructions. 2. Ask for clarifying questions to be answered before code generation. 3. Review generated schemas before confirmation. 4. Work in layers instead of 'one-shotting' your code. 5. Verify implementation details in generated code before continuing. 6. Always double-check proposed next actions.
Complete Guide: Tips for Working with Claude in Vetra Studio ## Before You Start **Setup Requirements:** 1. Run `ph vetra --interactive --watch` in one terminal first 2. Start Claude in a separate terminal from your project directory 3. Connect with: `claude` or `connect to the reactor mcp` 4. Verify you see the confirmation message with your drive name ## Communication Best Practices ### 1. Always Review Before Implementation **CRITICAL**: Claude will present a proposal before creating anything. You'll see: - Proposed document model structure (state schema, operations, modules) - How data will be organized - What actions users can perform **Always review and confirm** before Claude proceeds. This is your chance to adjust the design. ### 2. Be Specific and Detailed When describing what you need, include: **For Document Models:** - Purpose of the document (what problem does it solve?) - All data fields and their types (strings, numbers, dates, etc.) - What operations users should be able to perform - Any relationships between data - Business rules or constraints **For Document Editors:** - Which document model it's for - UI layout and components you want - User interactions and workflows - Specific operations to use (by name from your document model) - Any styling preferences ### 3. Use Clear Examples Good prompt for a document model: ``` Create a document model for expense tracking with: - Each expense has: amount (number), description (text), category (expense type), date, and receipt URL (optional) - Users can: add expenses, edit expense details, delete expenses, and categorize by type - Track total amount automatically ``` Good prompt for an editor: ``` Create an editor for the expense tracker with: - A form to add new expenses (amount, description, category, date) - A table showing all expenses with sort by date - Each row has edit and delete buttons - Show total at the bottom - Use the addExpense, updateExpense, and deleteExpense operations ``` ### 4. Work in Layers (Don't "One-Shot") Instead of asking for everything at once: - โœ… Start with the core document model - โœ… Test it works - โœ… Then add the editor - โœ… Then add advanced features This approach catches issues early and gives you better results. ### 5. Interactive Mode Benefits Using `ph vetra --interactive` gives you confirmation prompts: - Schema changes - Operation definitions - Code generation **Review each step** before confirming - it's easier to adjust now than later. ### 6. What to Expect After Implementation Claude will automatically: - Run TypeScript checks (`npm run tsc`) - Run linting (`npm run lint:fix`) - Report any errors found - Fix issues if needed You'll see confirmation when everything compiles successfully. ### 7. Common Issues and How to Avoid Them **Issue**: Generated model doesn't match expectations - **Solution**: Provide more detailed requirements upfront. Ask clarifying questions. **Issue**: Operations don't work as expected - **Solution**: Be explicit about all actions and their parameters. Use real-world examples. **Issue**: Editor UI doesn't look right - **Solution**: Describe the UI in detail (layout, components, interactions). Reference similar interfaces if helpful. ## Key Concepts to Know - **Document Model**: The template/blueprint for your documents (like a database schema) - **Document**: An actual instance with real data (like a database record) - **Operations**: Actions users can perform (like "add expense", "update status") - **Editor**: The user interface to interact with your documents - **Drive**: A collection that holds your documents (like a folder) ## Quick Tips 1. **Be specific**: More detail = better results 2. **Review proposals**: Always check before confirming 3. **Work incrementally**: Build in layers, not all at once 4. **Use operation names**: Reference them when describing editor functionality 5. **Ask questions**: If unsure, ask Claude to clarify or suggest options 6. **Test as you go**: Create model first, test it, then add the editor ## What Claude Can Do For You - Generate complete document models from natural language - Create all necessary operations automatically - Build React editor interfaces with your specifications - Handle all the TypeScript and boilerplate code - Fix type errors and linting issues - Add demo documents to test your models ## What You Should Focus On - Clearly describing your business requirements - Defining what data you need to track - Specifying what actions users should perform - Reviewing and confirming proposals - Testing the generated results **Remember**: Claude works best with clear, detailed requirements. Take time to explain what you want - it's faster than multiple iterations to fix misunderstandings.
## Phase 4: Packaging and publishing Once your document model and editor are implemented and tested, you can package them for distribution. A Powerhouse Package is a modular unit that can group document models, editors, scripts, and processors. ### 4.1. Prepare project for packaging If you didn't initialize your project with `ph init` intending it as a package, ensure your project structure is set up correctly. The `ph init` command is designed to create this structure. - `document-models/`: Contains your document models. - `editors/`: Contains your editor components. - `src/`: Often used for shared utilities or can be part of the model/editor structure. - (Optional) `processors/`, `scripts/` for advanced functionalities. ### 4.2. Specify package details in `package.json` Navigate to the `package.json` file in your project root. This file is crucial for NPM publishing. - **`name`**: Follow a scoped naming convention, e.g., `@your-org-ph/your-package-name`. The `-ph` suffix helps identify Powerhouse ecosystem packages. - **`version`**: Use semantic versioning (e.g., `1.0.0`). - **`author`**: Your name or organization. - **`license`**: e.g., `AGPL-3.0-only`. - **`main`**: The entry point of your package (e.g., `index.js` or `dist/index.js`). - **`publishConfig`**: For scoped packages intended to be public, add: ```json "publishConfig": { "access": "public" } ``` Example `package.json` snippet: ```json { "name": "@your-org-ph/your-package-name", "version": "1.0.0", "author": "Your Name", "license": "AGPL-3.0-only", "main": "index.js", "files": [ // Ensure your build output and necessary files are included "dist", "manifest.json", "document-models", "editors" ], "publishConfig": { "access": "public" } } ``` ### 4.3. Add a manifest file (`manifest.json`) Create a `manifest.json` file in your project root. This file describes your package's contents (document models, editors) and helps host applications like Connect understand and integrate your package. Example `manifest.json`: ```json { "name": "@yourorg-ph/your-package-name", // it's recommended to use an organization-specific NPM account (e.g., `yourorg-ph`). "description": "A brief description of your package and its document models.", "category": "your-category", // e.g., "Finance", "People Ops", "Legal" "publisher": { "name": "your-publisher-name", // Your organization or name "url": "your-publisher-url" // Link to your website or repository }, "documentModels": [ { "id": "powerhouse/YourModelName", // Document type string as defined in Connect "name": "YourModelName" // Human-readable name } ], "editors": [ { "id": "your-editor-id", // A unique ID for the editor component "name": "YourModelName Editor", // Human-readable name "documentTypes": ["powerhouse/YourModelName"] // Links editor to document type(s) } ] } ``` Update your project's main `index.js` or entry point to export your document models and editors so they can be discovered by Powerhouse applications. ### 4.4. Build your project Compile and optimize your project for production: ```bash ph build ``` This command typically creates a `dist/` or `build/` directory with the compiled assets. Ensure your `package.json`'s `files` array includes this directory and other necessary assets like `manifest.json`, `document-models`, and `editors` if they are not part of the build output but need to be in the package. ### 4.5. Version control Store your project in a Git repository for versioning and collaboration. ```bash git init git add . git commit -m "Initial commit of document model package" # git remote add origin # git push -u origin main ``` ### 4.6. Publish to NPM To share your package with others or deploy it to different environments, publish it to the NPM registry. 1. **Login to NPM:** If you haven't already, log into your NPM account. It's recommended to use an organization-specific NPM account (e.g., `yourorg-ph`). ```bash npm login ``` Follow the prompts in your terminal or browser. 2. **Publish the package:** ```bash ph publish ``` This publishes your package to the Vetra registry, making it available to install in any Vetra Cloud environment. ### 4.7. Using your published package Once published, your package can be installed and used in any Powerhouse environment (like another local Connect instance or a deployed Reactor setup). ```bash ph install @your-org-ph/your-package-name ``` This command makes the document models and editors defined in your package available within that Powerhouse instance. Congratulations! You've successfully created, packaged, and published a Powerhouse Document Model. This enables modularity, reusability, and collaboration within the Powerhouse ecosystem. --- ## Vetra Cloud > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/VetraCloud Vetra Cloud is the hosted infrastructure layer for Powerhouse applications. It lets you spin up a personal cloud environment that runs **Powerhouse Connect** (the user-facing document editor) and **Powerhouse Switchboard** (the backend GraphQL server) โ€” and extend them with packages from the Vetra registry. --- ## Getting Started ### 1. Connect Your Wallet Navigate to the **Cloud** section in Vetra and authenticate with your Ethereum wallet via **Renown**. Renown lets Connect sign operations on behalf of your on-chain identity without exposing your private key.
Connect Wallet via Renown
Connect your Ethereum wallet via Renown to authenticate with Vetra.
--- ### 2. Create an Environment After authenticating, create a new environment by giving it a name. Each environment gets its own subdomain (e.g. `my-env.vetra.io`) and runs its own isolated set of services.
Create Environment
Give your environment a name to get a dedicated subdomain and isolated set of services.
--- ### 3. Configure Services Inside your environment you can enable and configure three services: | Service | Description | | -------------------------- | -------------------------------------------------------------- | | **Powerhouse Connect** | The document-editor UI served at `connect..vetra.io` | | **Powerhouse Switchboard** | The GraphQL API server at `switchboard..vetra.io/graphql` | | **Powerhouse Fusion** | Optional data-fusion layer (disabled by default) | Use the **Size** dropdown to pick the compute tier for each service and toggle it on or off with the switch on the right.
Services overview
Enable and configure Connect, Switchboard, and Fusion services for your environment.
--- ### 4. Select a Version Each service shows its currently pinned version. Click **Change version** (or **Update All** when updates are available) to pin Connect and Switchboard to a specific release.
Available updates
Pin each service to a specific version or apply all available updates at once.
--- ### 5. Install Packages Packages extend Connect and Switchboard with additional document models, editors, and reactor modules. Click **+ Add package** in the **Installed Packages** section, then search for a package by name. Available packages include community and official Powerhouse packages such as: - `@powerhousedao/builder-profile` - `@powerhousedao/contributor-billing` - `@powerhousedao/knowledge-note` - `@arbitrum/arbgrants`
Add Package
Search and select a package from the Vetra registry to install into your environment.
Once you select a package, a **Pending change** banner appears at the bottom. Click **Approve** to apply the change and redeploy your environment. --- ## Next Steps Once your environment is running you can point your local `ph` CLI at it, or share the Connect URL with collaborators so they can start working with your document models right away. To publish your own packages to the registry, see the [Publishing Packages](../docs/02-PublishingPackages/index.md) guide. --- ## Vetra builder tooling > Source: https://powerhouse.academy/academy/MasteryTrack/BuilderEnvironment/BuilderTools This page provides an overview of all the builder tooling offered in the Vetra ecosystem by Powerhouse. This list will be maintained and updated as our toolkit grows. ## Powerhouse command line interface --- ### Installing the Powerhouse CLI **TIP:** The Powerhouse CLI tool is the only essential tool to install on this page. Once you've installed it with the command below you can continue to the next steps. The Powerhouse CLI (`ph-cmd`) is a command-line interface tool that provides essential commands for managing Powerhouse projects. You can get access to the Powerhouse ecosystem tools by installing them globally using: ```bash pnpm install -g ph-cmd ``` Or if you're using npm: ```bash npm install -g ph-cmd ``` Key commands include: - `ph connect` for running the Connect application locally - `ph switchboard` or `ph reactor` for starting the API service - `ph init` to start a new project and build a Document Model - `ph help` to get an overview of all the available commands This tool will be fundamental on your journey when creating, building, and running Document Models
How to make use of different branches? When installing or using the Powerhouse CLI commands you are able to make use of the dev & staging branches. These branches contain more experimental features then the latest stable release the PH CLI uses by default. They can be used to get access to a bugfix or features under development. | Command | Description | | ----------------------------------------------------------------------- | ------------------------------------------------- | | **pnpm install -g ph-cmd** or **npm install -g ph-cmd ** | Install latest stable version | | **pnpm install -g ph-cmd@dev** or **npm install -g ph-cmd@dev** | Install development version | | **pnpm install -g ph-cmd@staging** or **npm install -g ph-cmd@staging** | Install staging version | | **ph init** | Use latest stable version of the boilerplate | | **ph init --dev** | Use development version of the boilerplate | | **ph init --staging** | Use staging version of the boilerplate | | **ph use latest** | Switch all dependencies to latest stable versions | | **ph use dev** | Switch all dependencies to development versions | | **ph use staging** | Switch all dependencies to staging versions | Please be aware that these versions can contain bugs and experimental features that aren't fully tested.
How to clean your system of the Powerhouse CLI? ### Cleaning and updating ph-cmd If you need to perform a clean reinstallation of the Powerhouse CLI (`ph-cmd`), follow these steps: 1. First, uninstall the global ph-cmd package: ```bash pnpm uninstall -g ph-cmd # or with npm npm uninstall -g ph-cmd ``` 2. Remove the Powerhouse configuration directory: ```bash rm -rf ~/.ph ``` 3. Reinstall the CLI tool (choose one): ```bash # For the latest stable version pnpm install -g ph-cmd # or with npm npm install -g ph-cmd # For the staging version pnpm install -g ph-cmd@staging # or with npm npm install -g ph-cmd@staging # For a specific version pnpm install -g ph-cmd@ # or with npm npm install -g ph-cmd@ ``` This process ensures a clean slate by removing both the CLI tool and its configuration files before installing the desired version. It's particularly useful when: - Troubleshooting CLI issues - Upgrading to a new version - Switching between stable and staging versions - Resolving configuration conflicts
### The use command The use command allows you to switch between different environments for your Powerhouse project dependencies. ```bash ph use ``` **Available Tags** - latest - Uses the latest stable version of all Powerhouse packages. - dev - Uses development versions of the packages. - staging - Uses staging versions of the packages. **Examples** #### Switch to latest stable versions ```bash ph use latest ``` #### Switch to development versions ```bash ph use dev ``` #### Use local monorepo packages from a specific path ```bash ph use-local /path/to/local/packages ``` #### Use a specific package manager ```bash ph use latest --package-manager pnpm ``` ### The update command The update command allows you to update your Powerhouse dependencies to their latest versions based on the version ranges specified in your package.json. ```bash ph update [options] ``` **Examples** #### Update dependencies based on package.json ranges ```bash ph update ``` #### Force update to latest dev versions ```bash ph update --force dev ``` #### Force update to latest stable versions ```bash ph update --force prod ``` #### Use a specific package manager ```bash ph update --package-manager pnpm ``` ## **Key differences** ### **Use command** - For switching between different **environments**. - Requires you to specify an environment. - Can work with **local packages**. ### **Update command** - Updating **dependencies** within your current environment. - Optional with its parameters. - Focused on updating **remote package** versions. Both commands support multiple package managers (npm, yarn, pnpm, and bun) and will automatically detect your project's package manager based on the lockfile present in your project directory. ## Boilerplate --- The Document Model Boilerplate is a foundational template that is used for code generation when scaffolding your editors and models. It ensures compatibility with host applications like Connect and Switchboard for seamless Document Model and editor integration. After installing `ph-cmd`, you will run `ph init` to initialize a project directory and structure. This initialization command makes use of the boilerplate. The boilerplate includes essential commands with NPM/PNPM scripts for: - Generating code - Linting - Formatting - Building - Testing ## Design system --- The Powerhouse Design System is a collection of reusable front-end components based on GraphQL scalars, including custom scalars specific to the web3 ecosystem. It provides: - Consistent UI components across Powerhouse applications - Automatic inclusion as a dependency in new Document Model projects - Customization options using CSS variables We cover some of these topics in our design system documentation. Read more about the [design system here](/academy/ComponentLibrary/DocumentEngineering) ## Reactor libraries --- Reactors are the nodes in the Powerhouse network that handle document storage, conflict resolution, and operation verification. The Reactor Libraries include: ### API - **Subgraphs**: Modular GraphQL services that connect to the Reactor for structured data access - **Processors**: Event-driven components that react to document changes and process data ### Browser Handles client-side interactions ### Local Manages local storage and offline functionality ### drive-app Handles document organization and storage management, but can also be customized to offer specific functionality, categorization, or tailored interfaces for your documents. ## Code generators --- Powerhouse provides several code generation tools to streamline development: ### Document model scaffolding Generates the basic structure for new Document Models with the command `ph init` based on the boilerplate. ### Editor generator Creates template code for Document Model editors with the command `ph generate editor --name --document-type ` ### Subgraph generator Creates GraphQL subgraph templates for data access automatically upon `ph reactor` ### Processor generator Generates processor templates for event handling automatically upon `ph reactor` ### Analytics processor generator Creates specialized processors for analytics tracking ## Analytics engine --- The Analytics Engine is a system that allows tracking and analyzing operations and state changes on Document Models. Features include: - Custom dashboard and report generation - Document Model-specific analytics - Metric and dimension tracking - Data combination from multiple Document Models Generate an analytics processor using: ```bash ph generate processor --type analytics ``` --- ## What is a document model? > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/WhatIsADocumentModel **TIP:** This chapter on **Document Model Creation** will help you with an in-depth practical understanding while building an **advanced to-do list** document model. If you have completed the [Get Started](/academy/GetStarted/CreateNewPowerhouseProject) tutorial (which includes creating a simple to-do list document model), you will revisit familiar topics and can enhance your existing document model with the advanced features covered in this Mastery Track, such as statistics tracking. Although not required, completing the Get Started tutorial first is highly recommended as it provides a hands-on foundation for the concepts explored in depth here. **INFO:** A Document Model is a programmable document structure that defines how data is stored, changed, and interpreted in a decentralized system. It acts like a living blueprintโ€”capturing state, tracking changes, and enabling interaction through defined operations. For instance, an invoice document model might define fields like _issuer_, _lineItems_, and _status_, with operations such as _AddLineItem_ and _MarkAsPaid_. Document models are central to **Specification Driven Design & Development**โ€”an approach where you communicate your solution and intent through structured specification documents. These specs are machine-readable and executable, serving as a shared language between developers, designers, and AI agents. This enables precise, iterative development and lays the groundwork for AI-assisted workflows. A Document Model can be understood as: - A structured software framework that represents and **manages business logic** within a digital environment. - A sophisticated template that **encapsulates the essential aspects of a digital process or a set of data**. - A blueprints that define how data is **captured, manipulated, and visualised** within a system. - A **standardized way to store, modify, and query data** in scalable, decentralized applications. ### **How does a document model function?** #### **Structure and composition** Each document model consists of three key components: 1. **State Schema** โ€“ Defines the structure of the document. 2. **Document Operations** โ€“ Defines how the document can be modified. 3. **Event History** โ€“ Maintains an append-only log of changes. Document models leverage **event sourcing, CQRS (Command Query Responsibility Segregation), and an append-only architecture** to ensure immutability, auditability, and scalability. --- ## **1. Structure of a document model** A document model consists of the following key components: ### **1.1 State schema** The **state schema** defines the structure of the document, including its fields and data types. It serves as a blueprint for how data is stored and validated. Example of a **GraphQL-like state schema** for an invoice document: ```graphql type InvoiceState { id: OID! # Unique identifier for the invoice issuer: OID! # Reference to the issuing entity recipient: OID! # Reference to the recipient entity status: String # (value: "DRAFT") # Invoice status dueDate: DateTime # Payment due date lineItems: [LineItem!]! # List of line items totalAmount: Currency # Computed field for total invoice value } type LineItem { id: OID! description: String quantity: Int unitPrice: Currency } ``` ### **State schema features:** - Uses **GraphQL-like definitions** for a **clear, structured schema**. - Supports **custom scalar types** like `OID`, `Currency`, and `DateTime`. Or other Web3 specific scalars - Defines **relationships** using object references (`OID!`). The state schema acts as a **template** for document instances. Every new invoice created will follow this structure. --- ### **1.2 Document operations** Document models are **append-only**, meaning changes are not made directly to the document state. Instead, **document operations** define valid state transitions. Example operations for modifying an invoice: ```graphql input AddLineItemInput { invoiceId: OID! description: String quantity: Int unitPrice: Currency } input UpdateRecipientInput { invoiceId: OID! newRecipient: OID! } input MarkAsPaidInput { invoiceId: OID! } ``` Each operation **modifies the document state** without altering past data. Instead, a new event is appended to the document history. --- ### **1.3 Event history (append-only log)** Every operation applied to a document is **stored as an event** in an append-only log. #### **Example event log for an invoice document:** ```json [ { "timestamp": 1700000001, "operation": "CREATE_INVOICE", "data": { "id": "inv-001", "issuer": "company-123", "recipient": "client-456" } }, { "timestamp": 1700000100, "operation": "ADD_LINE_ITEM", "data": { "description": "Software Development", "quantity": 10, "unitPrice": 100 } }, { "timestamp": 1700000200, "operation": "MARK_AS_PAID", "data": {} ``` ### **Event history benefits:** - Provides a **transparent audit trail** of changes. - Enables **time travel debugging** by reconstructing past states. - Supports **event sourcing**, allowing developers to **replay events** to restore state. --- ## **2. How document models work technically** Document models in Powerhouse rely on **event-driven architecture, event sourcing, and CQRS principles**. Here's a step-by-step breakdown: ### **2.1 Document creation** 1. A user (or system) **submits an operation** to create a new document. 2. The document model **validates** the input data against the state schema. 3. The system **appends the operation** as an event in the document history. 4. The **initial state is computed** by applying the recorded events. **Example:** ```json { "operation": "CREATE_INVOICE", "data": { "id": "inv-001", "issuer": "company-123", "recipient": "client-456" } } ``` --- ### **2.2 Document modification** 1. A user submits an **operation** (e.g., `ADD_LINE_ITEM`). 2. The **event is appended** to the document history. 3. The **state transition logic updates the computed state**. 4. The UI re-renders the updated document. **Example:** Adding a line item: ```json { "operation": "ADD_LINE_ITEM", "data": { "description": "Software Development", "quantity": 10, "unitPrice": 100 } } ``` Since changes are **not applied directly**, this model is **highly scalable and auditable**. --- ### **2.3 Querying document models** Powerhouse uses **GraphQL queries** to fetch document states efficiently. Because documents store structured data, developers can instantly query: ```graphql query { invoice(id: "inv-001") { issuer recipient status lineItems { description quantity unitPrice } } } ``` This removes the need for **complex database joins** and allows for **fast, structured access to data**. --- ### **What do document models unlock?** Document Models offer a range of features that can be leveraged to create sophisticated, automated, and data-driven solutions: - **Automation**: Automate workflows using consistent, structured document logic. - **Auditability**: Maintain a full history of changes for compliance and transparency. - **API Integration**: Seamlessly connect with Switchboard or external APIs for data exchange. - **Data Analysis**: Enable real-time and historical insights through structured read models. - **Version Control**: Track and compare document states over time, similar to Git. - **Collaboration**: Empower decentralized teams to build, modify, and share documents asynchronously. - **Extensibility**: Add new fields, operations, and integrations over time without rewriting logic. - **Schema Evolution**: Evolve document models with [versioning](/academy/MasteryTrack/DocumentModelCreation/DocumentModelVersioning)โ€”upgrade schemas while maintaining backward compatibility with existing documents. Document Models are a powerful primitive within the Powerhouse vision, offering a flexible, structured, and efficient way to manage business logic and data. ### Up next: How to build a document model In the next chapters, we'll teach you how to build a To-do List document model while explaining all of the theory that is involved. --- ## Specify the state schema > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema The state schema is the backbone of your document model. It defines the structure, data types, and relationships of the information your document will hold. In Powerhouse, we use the **GraphQL Schema Definition Language (SDL)** to define this schema. A well-defined state schema is crucial for ensuring data integrity, consistency, and for enabling powerful querying and manipulation capabilities. **TIP:** Your state schema is more than just a data structureโ€”it's a **specification** that enables **Specification Driven Design & Development**. This schema becomes a machine-readable blueprint that AI agents can interpret and execute, enabling precise collaboration between you and AI throughout the development process. ## Core concepts ### Types At the heart of GraphQL SDL are **types**. Types define the shape of your data. You can define custom object types that represent the entities in your document. For example, in a `TodoList` document, you might have a `TodoListState` type and a `TodoItem` type. - **`TodoListState`**: This could be the root type representing the overall state of the to-do list. It might contain a list of `TodoItem` objects. - **`TodoItem`**: This type would represent an individual to-do item, with properties like an `id`, `text` (the task description), and `checked` (a boolean indicating if the task is completed). ### Fields Each type has **fields**, which represent the properties of that type. Each field has a name and a type. For instance, the `TodoItem` type would have an `id` field of type `OID!`, a `text` field of type `String!`, and a `checked` field of type `Boolean!`. ### Scalars GraphQL has a set of built-in **scalar types**: - `String`: A UTFโ€8 character sequence. - `Int`: A signed 32โ€bit integer. - `Float`: A signed double-precision floating-point value. - `Boolean`: `true` or `false`. - `ID`: A unique identifier, often used as a key for a field. It is serialized in the same way as a String; however, it is not intended to be human-readable. In addition to these standard types, the Powerhouse Document-Engineering system introduces custom scalars that are linked to reusable front-end components. These scalars are tailored for the web3 ecosystem and will be explored in the Component Library section of the documentation. **TIP:** Powerhouse provides the `OID` (Object ID) scalar type, which is a custom scalar specifically designed for unique identifiers in document models. It provides automatic ID generation capabilities when used with the `generateId()` function from the document-model core library. ### Lists and non-null You can modify types using lists and non-null indicators: - **Lists**: To indicate that a field will return a list of a certain type, you wrap the type in square brackets, e.g., `[TodoItem!]!`. This means the field `items` in `TodoListState` will be a list of `TodoItem` objects. - **Non-Null**: To indicate that a field cannot be null, you add an exclamation mark `!` after the type name, e.g., `String!`. This means that the `text` field of a `TodoItem` must always have a value. The outer `!` in `[TodoItem!]!` means the list itself cannot be null (it must be at least an empty list), and the inner `!` on `TodoItem!` means that every item within that list must also be non-null. ## Example: TodoList state schema Let's revisit the `TodoList` example from the "Define the TodoList document specification" tutorial in Get Started. ### Basic schema (matching Get Started tutorial) This is the same schema you built in the Get Started tutorial: ```graphql # The state of our TodoList type TodoListState { items: [TodoItem!]! } # A single to-do item type TodoItem { id: OID! text: String! checked: Boolean! } ``` ### Advanced schema (with statistics tracking) **INFO:** In this Mastery Track, we'll extend the basic schema with a `stats` field to demonstrate how you can add computed statistics to your document model. This is an **optional enhancement** that builds on the foundation from Get Started. ```graphql # The state of our TodoList (advanced version with stats) type TodoListState { items: [TodoItem!]! stats: TodoListStats! } # A single to-do item type TodoItem { id: OID! text: String! checked: Boolean! } # The statistics on our to-do's (advanced feature) type TodoListStats { total: Int! checked: Int! unchecked: Int! } ``` ### Breakdown: - **`TodoListState` type**: - `items: [TodoItem!]!`: This field defines that our `TodoListState` contains a list called `items`. - `[TodoItem!]`: This signifies that `items` is a list of `TodoItem` objects. - `TodoItem!`: The `!` after `TodoItem` means that no item in the list can be null. Each entry must be a valid `TodoItem`. - The final `!` after `[TodoItem!]` means that the `items` list itself cannot be null. It can be an empty list `[]`, but it cannot be absent. - `stats: TodoListStats!` _(advanced)_: Holds aggregated statistics about the to-do items. - **`TodoItem` type**: - `id: OID!`: Each `TodoItem` has a unique identifier using Powerhouse's custom `OID` scalar. This is crucial for referencing specific items, for example, when updating or deleting them. - `text: String!`: The textual description of the to-do item. It cannot be null, ensuring every to-do has a description. - `checked: Boolean!`: Indicates whether the to-do item is completed. It defaults to a boolean value (true or false) and cannot be null. - **`TodoListStats` type** _(advanced)_: This type holds the summary statistics for the to-do list. - `total: Int!`: The total count of all to-do items. This field must be an integer and cannot be null. - `checked: Int!`: The number of to-do items that are marked as completed. This must be an integer and cannot be null. - `unchecked: Int!`: The number of to-do items that are still pending. This also must be an integer and cannot be null. ## Best practices for designing your state schema 1. **Start Simple, Iterate**: Begin with the core entities and properties. You can always expand and refine your schema as your understanding of the document's requirements grows. 2. **Clarity and Explicitness**: Name your types and fields clearly and descriptively. This makes the schema easier to understand and maintain. 3. **Use Non-Null Wisely**: Enforce data integrity by using non-null (`!`) for fields that must always have a value. However, be mindful not to over-constrain if a field can genuinely be optional. 4. **Normalize vs. Denormalize**: - **Normalization**: Similar to relational databases, you can normalize your data by having distinct types and linking them via IDs. This can reduce data redundancy. For example, if you had `User` and `TodoItem` and wanted to assign tasks, you might have an `assigneeId` field in `TodoItem` that links to a `User`'s `id`. - **Denormalization**: Sometimes, for performance or simplicity, you might embed data directly. For instance, if user information associated with a to-do item was very simple and only used in that context, you might embed user fields directly in `TodoItem`. - The choice depends on your specific use case, query patterns, and how data is updated. 5. **Consider Future Needs**: While you shouldn't over-engineer, think a little about potential future enhancements. For example, adding a `createdAt: String` or `dueDate: String` field to `TodoItem` might be useful later. 6. **Root State Type**: It's a common pattern to have a single root type for your document state (e.g., `TodoListState`). This provides a clear entry point for accessing all document data. By carefully defining your state schema, you lay a solid foundation for your Powerhouse document model, making it robust, maintainable, and easy to work with. The schema dictates not only how data is stored but also how it can be queried and mutated through operations, which will be covered in the next section. ## Practical implementation: defining the state schema in Vetra Studio Now that you understand the concepts behind the state schema, let's put it into practice. This section will guide you through creating a document model specification for the TodoList example discussed above.
Tutorial: The state schema specification ### Prerequisites - You have a Powerhouse project set up. If not, please follow the [Create a new Powerhouse Project](../docs/../GetStarted/CreateNewPowerhouseProject) tutorial. - Vetra Studio is running. If not, navigate to your project directory in the terminal and run `ph vetra --watch`. ### Steps 1. **Create a New Document Model**: - With Vetra Studio open in your browser, you'll see the Vetra Studio Drive. - Click the **Document Models 'Add new specification'** button to create a new document model specification. 2. **Define Document Metadata**: - **Name**: Give your document model a descriptive name: `TodoList`. **Pay close attention to capitalization, as it influences our code generation.** - **Document Type**: In the 'Document Type' field, enter a unique identifier for this document type: `powerhouse/todo-list`. 3. **Specify the State Schema**: - In the code editor provided, you'll see a template for a GraphQL schema. - Replace the entire content of the editor with the advanced `TodoList` schema we've designed in this chapter: ```graphql # The state of our TodoList (advanced version with stats) type TodoListState { items: [TodoItem!]! stats: TodoListStats! } # A single to-do item type TodoItem { id: OID! text: String! checked: Boolean! } # The statistics on our to-do's (advanced feature) type TodoListStats { total: Int! checked: Int! unchecked: Int! } ``` 4. **Sync Schema and View Initial State**: - After pasting the schema, click the **'Sync with schema'** button. - This action processes your schema and generates an initial JSON state for your document model based on the `TodoListState` type. You can view this initial state, which helps you verify that your schema is structured correctly. For now, you can ignore the "Modules & Operations" section. We will define and implement the operations that modify this state in the upcoming sections of this Mastery Track. By completing these steps, you have successfully specified the data structure for the advanced TodoList document model. The next step is to define the operations that will allow users to interact with and change this state.
Alternatively: Define the state schema in Connect ### Prerequisites - You have a Powerhouse project set up. If not, please follow the [Create a new Powerhouse Project](../docs/../GetStarted/CreateNewPowerhouseProject) tutorial. - Connect is running. If not, navigate to your project directory in the terminal and run `ph connect`. ### Steps 1. **Create a New Document Model**: - With Connect open in your browser, navigate into your local drive. - At the bottom of the page in the 'New Document' section, click the `DocumentModel` button to create a new document model specification. 2. **Define Document Metadata**: - **Name**: Give your document model a descriptive name: `TodoList`. **Pay close attention to capitalization, as it influences our code generation.** - **Document Type**: In the 'Document Type' field, enter a unique identifier for this document type: `powerhouse/todo-list`. 3. **Specify the State Schema**: - In the code editor provided, you'll see a template for a GraphQL schema. - Replace the entire content of the editor with the advanced `TodoList` schema we've designed in this chapter: ```graphql # The state of our TodoList (advanced version with stats) type TodoListState { items: [TodoItem!]! stats: TodoListStats! } # A single to-do item type TodoItem { id: OID! text: String! checked: Boolean! } # The statistics on our to-do's (advanced feature) type TodoListStats { total: Int! checked: Int! unchecked: Int! } ``` 4. **Sync Schema and View Initial State**: - After pasting the schema, click the **'Sync with schema'** button. - This action processes your schema and generates an initial JSON state for your document model based on the `TodoListState` type. You can view this initial state, which helps you verify that your schema is structured correctly. For now, you can ignore the "Modules & Operations" section. We will define and implement the operations that modify this state in the upcoming sections of this Mastery Track. By completing these steps, you have successfully specified the data structure for the advanced TodoList document model. The next step is to define the operations that will allow users to interact with and change this state.
For a complete, working example, you can always have a look at the [Example TodoList Repository](/academy/MasteryTrack/DocumentModelCreation/ExampleToDoListRepository) which contains the full implementation of the concepts discussed in this Mastery Track. --- ## Specify document operations > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/SpecifyDocumentOperations In the previous section, we defined the state schema for our document model. Now, we turn our attention to a critical aspect of document model creation: **specifying document operations**. These operations are the heart of your document's behavior, dictating how its state can be modified. ## What are document operations? In Powerhouse, document models adhere to event sourcing principles. This means that every change to a document's state is the result of a sequence of operations (or events). Instead of directly mutating the state, you define specific, named operations that describe the intended change. For example, in our `TodoList` document model, operations might include: - `ADD_TODO_ITEM`: To add a new task. - `UPDATE_TODO_ITEM`: To modify an existing task (e.g., change its text or mark it as completed). - `DELETE_TODO_ITEM`: To remove a task. Each operation acts as a command that, when applied, transitions the document from one state to the next. The complete history of these operations defines the document's journey to its current state. ## Connecting operations to the schema In the "Define TodoList Document Model" chapter in the "Get Started" guide, we used GraphQL `input` types to define the structure of the data required for each operation. Let's revisit that: ```graphql # Defines a GraphQL input type for adding a new to-do item input AddTodoItemInput { text: String! } # Defines a GraphQL input type for updating a to-do item input UpdateTodoItemInput { id: OID! text: String checked: Boolean } # Defines a GraphQL input type for deleting a to-do item input DeleteTodoItemInput { id: OID! } ``` These `input` types are not just abstract definitions; they are the **specifications for our document operations**. - **`AddTodoItemInput`** specifies that to execute an `ADD_TODO_ITEM` operation, we only need the `text` for the new item. The `id` is automatically generated by the reducer using Powerhouse's `generateId()` function. - **`UpdateTodoItemInput`** specifies that for an `UPDATE_TODO_ITEM` operation, we need the `id` of the item to update, and optionally new `text` or a `checked` status. - **`DeleteTodoItemInput`** specifies that a `DELETE_TODO_ITEM` operation requires the `id` of the item to be removed. **TIP:** Notice that `AddTodoItemInput` only requires `text` โ€” not an `id`. This is because the ID is generated automatically in the reducer using `generateId()` from `document-model/core`. This ensures unique, consistent IDs and follows the pattern used in the [todo-demo repository](https://github.com/powerhouse-inc/todo-demo). Vetra Studio uses these GraphQL input types when you define operations within a module (e.g., the `todos` module with operations `ADD_TODO_ITEM`, `UPDATE_TODO_ITEM`, `DELETE_TODO_ITEM`). ## Designing effective document operations Careful design of your document operations is crucial for a robust and maintainable document model. Here are some key considerations: ### 1. Granularity Operations should be granular enough to represent distinct user intentions or logical changes. - **Too coarse:** An operation like `MODIFY_TODOLIST` that takes a whole new list of items would be too broad. It would be hard to track specific changes and could lead to complex reducer logic. - **Too fine:** While possible, having separate operations like `SET_TODO_ITEM_TEXT` and `SET_TODO_ITEM_CHECKED_STATUS` might be overly verbose if these are often updated together. `UPDATE_TODO_ITEM` with optional fields offers a good balance. - **Just right:** The `ADD_TODO_ITEM`, `UPDATE_TODO_ITEM`, and `DELETE_TODO_ITEM` operations for our `TodoList` are good examples. They represent clear, atomic changes. ### 2. Naming conventions Clear and consistent naming makes your operations understandable. A common convention is `VERB_NOUN` or `VERB_NOUN_SUBJECT`. - Examples: `ADD_ITEM`, `UPDATE_USER_PROFILE`, `ASSIGN_TASK_TO_USER`. - In our case: `ADD_TODO_ITEM`, `UPDATE_TODO_ITEM`, `DELETE_TODO_ITEM`. The name you provide in Vetra Studio (or Connect) (e.g., `ADD_TODO_ITEM`) directly corresponds to the operation type that will be recorded and that your reducers will handle. ### 3. Input types (payloads) The input type for an operation (its payload) should contain all the necessary information to perform that operation, and nothing more. - **Completeness:** If an operation needs a user ID to authorize a change, include it in the input. - **Conciseness:** Avoid including data that isn't directly used by the operation. - **Clarity:** Use descriptive field names within your input types. `action.input.text` is clearer than `action.input.t`. The GraphQL `input` types we defined earlier (`AddTodoItemInput`, `UpdateTodoItemInput`, `DeleteTodoItemInput`) serve precisely this purpose. They ensure that whoever triggers an operation provides the correct data in the correct format. ### 4. Immutability and pure functions While not specified in the operation definition itself, remember that the _implementation_ of these operations (the reducers) should treat state as immutable and behave as pure functions. The operation specification (input type) provides the data for these pure functions. ## Role in event sourcing and CQRS - **Events:** Each successfully executed operation is recorded as an event in the document's history. This history provides an audit trail and allows for replaying events to reconstruct state, which is invaluable for debugging and understanding how a document evolved. - **Commands:** Document operations are essentially "commands" in a Command Query Responsibility Segregation (CQRS) pattern. They represent an intent to change the state. The processing of this command (by the reducer) leads to one or more events being stored and the state being updated. ## From specification to implementation Specifying your document operations is the bridge between defining your data structure (the state schema) and implementing the logic that changes that data (the reducers). 1. **You define the state schema** (e.g., `TodoListState`, `TodoItem`). 2. **You specify the operations** that can alter this state, along with their required input data (e.g., `ADD_TODO_ITEM` with `AddTodoItemInput`). 3. **Next, you will implement reducers** for each specified operation. Each reducer will take the current state and an operation's input, and produce a new state. The generated code from `ph generate` (as seen in `03-ImplementOperationReducers.md`) will create a structure for your reducers based on the operations you specified in the Connect application (which, in turn, were based on your GraphQL input types). For example, the `TodoListTodosOperations` type generated by Powerhouse will expect methods corresponding to `addTodoItemOperation`, `updateTodoItemOperation`, and `deleteTodoItemOperation`. ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // Implementation uses action.input which matches AddTodoItemInput }, updateTodoItemOperation(state, action) { // Implementation uses action.input which matches UpdateTodoItemInput }, deleteTodoItemOperation(state, action) { // Implementation uses action.input which matches DeleteTodoItemInput }, }; ``` ## Practical implementation: Defining operations in Vetra Studio Now that you understand the theory, let's walk through the practical steps of defining these operations for our `TodoList` document model within Vetra Studio.
Tutorial: Specifying TodoList operations Assuming you have already defined the state schema for the `TodoList` as covered in the previous section, follow these steps to add the operations: 1. **Create a Module for Operations:** Below the schema editor in Vetra Studio, find the input field labeled `Add module`. Modules help organize your operations. - Type `todos` into the field and press Enter. 2. **Add the `ADD_TODO_ITEM` Operation:** A new field, `Add operation`, will appear under your new module. - Type `ADD_TODO_ITEM` into this field and press Enter. - An editor will appear for the operation's input type. You need to define the data required for this operation. Paste the following GraphQL `input` definition into the editor: ```graphql # Defines a GraphQL input type for adding a new to-do item input AddTodoItemInput { text: String! } ``` :::info Notice we don't include `id` in the input โ€” the reducer will generate it automatically using `generateId()` from `document-model/core`. ::: 3. **Add the `UPDATE_TODO_ITEM` Operation:** - In the `Add operation` field again, type `UPDATE_TODO_ITEM` and press Enter. - Paste the corresponding `input` definition into its editor: ```graphql # Defines a GraphQL input type for updating a to-do item input UpdateTodoItemInput { id: OID! text: String checked: Boolean } ``` 4. **Add the `DELETE_TODO_ITEM` Operation:** - Finally, type `DELETE_TODO_ITEM` in the `Add operation` field and press Enter. - Paste its `input` definition: ```graphql # Defines a GraphQL input type for deleting a to-do item input DeleteTodoItemInput { id: OID! } ``` 5. **Review:** After adding all three operations, your document model specification in Vetra Studio is complete for now. You can see how each operation (`ADD_TODO_ITEM`, etc.) is now explicitly linked to an input type that defines its payload. Vetra Studio automatically tracks your document model specifications. In the next chapter, we will see how the generator is used to create code for our reducers.
Alternatively: Define operations in Connect Assuming you have already defined the state schema for the `TodoList` as covered in the previous section, follow these steps to add the operations in Connect: 1. **Create a Module for Operations:** Below the schema editor in Connect, find the input field labeled `Add module`. Modules help organize your operations. - Type `todos` into the field and press Enter. 2. **Add the `ADD_TODO_ITEM` Operation:** A new field, `Add operation`, will appear under your new module. - Type `ADD_TODO_ITEM` into this field and press Enter. - An editor will appear for the operation's input type. You need to define the data required for this operation. Paste the following GraphQL `input` definition into the editor: ```graphql # Defines a GraphQL input type for adding a new to-do item input AddTodoItemInput { text: String! } ``` :::info Notice we don't include `id` in the input โ€” the reducer will generate it automatically using `generateId()` from `document-model/core`. ::: 3. **Add the `UPDATE_TODO_ITEM` Operation:** - In the `Add operation` field again, type `UPDATE_TODO_ITEM` and press Enter. - Paste the corresponding `input` definition into its editor: ```graphql # Defines a GraphQL input type for updating a to-do item input UpdateTodoItemInput { id: OID! text: String checked: Boolean } ``` 4. **Add the `DELETE_TODO_ITEM` Operation:** - Finally, type `DELETE_TODO_ITEM` in the `Add operation` field and press Enter. - Paste its `input` definition: ```graphql # Defines a GraphQL input type for deleting a to-do item input DeleteTodoItemInput { id: OID! } ``` 5. **Review and Export:** After adding all three operations, your document model specification in Connect is complete for now. You can see how each operation (`ADD_TODO_ITEM`, etc.) is now explicitly linked to an input type that defines its payload. The next step in a real project would be to click the `Export` button to save this specification file. In the next chapter, we will see how this exported file is used to generate code for our reducers.
## Conclusion Specifying document operations is a foundational step in building robust and predictable document models in Powerhouse. By clearly defining the **"what" (the operation and its input)** before implementing the **"how" (the reducer logic)**, you create a clear contract for state transitions. This approach enhances type safety, testability, and the overall maintainability of your document model. In the next section, we will dive deeper into the implementation of the reducer functions for these specified operations. --- ## Use the Document Model Generator > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/UseTheDocumentModelGenerator When building document models with **Vetra Studio**, code generation happens automatically. As you add and update specification documents in your Vetra Studio Drive, Vetra monitors your changes and generates the necessary scaffolding in real-time. You'll receive updates directly in your terminal as Vetra processes your specifications. This article covers the **manual code generation method** using the `ph generate` commandโ€”an alternative approach that remains available when working with **Connect** and exported `.phd` specification files. While Vetra Studio is the recommended workflow for most developers, understanding the generator command provides useful context for how the scaffolding works under the hood. ## When to Use Manual Generation | Workflow | Code Generation | | ---------------- | -------------------------------------------------------------------------- | | **Vetra Studio** | Automaticโ€”Vetra watches your specifications and generates code as you work | | **Connect** | Manualโ€”Export a `.phd` file and run `ph generate` | If you're using Vetra Studio with `ph vetra --interactive`, you don't need to run any generation commands. Vetra handles everything for you, prompting for confirmation before processing changes. ## Prerequisites (Connect Workflow Only) If you're using the Connect workflow and need to manually generate code: 1. **Powerhouse CLI (`ph-cmd`) Installed:** The generator is part of the Powerhouse CLI. If you haven't installed it, refer to the [Builder Tools documentation](/academy/MasteryTrack/BuilderEnvironment/BuilderTools#installing-the-powerhouse-cli). 2. **Exported `.phd` File:** You must have exported your document model specification from Connect as a `.phd` file (e.g., `TodoList.phd`). ## The Generate Command The core command to invoke the Document Model Generator is: ```bash ph generate document-model --document ``` Replace `` with the actual filename of your exported document model specification. For instance, if your exported file is named `TodoList.phd`, the command would be: ```bash ph generate document-model --document TodoList.phd ``` When executed, this command reads and parses the specification file and generates a set of files and directories within your Powerhouse project. ## Understanding the Generated Artifacts Whether generated automatically by Vetra or manually via `ph generate`, the output structure is the same. Understanding these artifacts helps you work effectively with your document model. The generator creates a new directory specific to your document model, located at: `document-models//` For example, using `TodoList.phd` would result in a directory structure under `document-models/todo-list/`. Inside this directory, you will find: ### 1. Specification Files - **`todo-list.json`**: A JSON representation of your document model specification containing the parsed schema, operation definitions, document type, and metadata. - **`schema.graphql`**: The raw GraphQL Schema Definition Language (SDL) for both the state and operationsโ€”a human-readable reference of your schema. ### 2. The `gen/` Directory (Auto-Generated Code) This directory houses all code automatically generated from your specification. **Do not manually edit files within the `gen/` directory**โ€”they will be overwritten when the model is regenerated. Key files include: - **`types.ts`**: TypeScript interfaces derived from your GraphQL schema, including types for your document's state (e.g., `TodoListState`), complex types (e.g., `TodoItem`), and operation inputs (e.g., `AddTodoItemInput`). - **`creators.ts`**: Action creator functions for each operation. Instead of manually constructing action objects, you use functions like `addTodoItem({ text: 'Buy groceries' })`. - **`utils.ts`**: Utility functions including helpers to create initial document instances (e.g., `utils.createDocument()`). - **`reducer.ts`**: A TypeScript interface defining the expected shape of your reducer implementation. ### 3. The `src/` Directory (Your Implementation) This is where you write custom logic. Unlike `gen/`, these files are meant for manual editing. - **`reducers/`**: Contains skeleton reducer files (e.g., `todos.ts`) with function stubs for each operation that you implement with state transition logic. - **`tests/`**: Test files for your reducer logic. ## Versioned Document Models When your document model needs to evolve over timeโ€”adding new fields, operations, or changing behaviorโ€”you can use **versioning**. This allows multiple versions of the same document model to coexist, with automatic upgrade paths between them. ### Enabling Versioning Versioning is enabled by default. Simply run: ```bash ph generate document-model --document TodoList.phd ``` ### Versioned Folder Structure With versioning enabled, the generator creates a different structure: ``` document-models/ โ””โ”€โ”€ todo/ โ”œโ”€โ”€ v1/ # Version 1 code โ”‚ โ”œโ”€โ”€ gen/ # Auto-generated v1 types and utilities โ”‚ โ”œโ”€โ”€ src/reducers/ # V1 reducer implementations โ”‚ โ””โ”€โ”€ module.ts # Exports DocumentModelModule with version: 1 โ”œโ”€โ”€ v2/ # Version 2 code โ”‚ โ”œโ”€โ”€ gen/ โ”‚ โ”œโ”€โ”€ src/reducers/ โ”‚ โ””โ”€โ”€ module.ts # Exports DocumentModelModule with version: 2 โ”œโ”€โ”€ upgrades/ # Migration logic โ”‚ โ”œโ”€โ”€ versions.ts # Supported versions list โ”‚ โ”œโ”€โ”€ v2.ts # Upgrade reducer: v1 โ†’ v2 โ”‚ โ””โ”€โ”€ upgrade-manifest.ts โ””โ”€โ”€ document-models.ts # Exports all versions + manifests ``` ### Key Differences with Versioning | Standard Generation | Versioned Generation | | ------------------------------- | ----------------------------------------- | | Single `gen/` and `src/` folder | Separate `v1/`, `v2/` folders per version | | One reducer implementation | Version-specific reducers | | No upgrade logic | `upgrades/` folder with manifests | | Direct module export | Exports all versions + upgrade manifests | For comprehensive documentation on implementing versioning, including upgrade reducers and integration with Connect and Switchboard, see [Document Model Versioning](/academy/MasteryTrack/DocumentModelCreation/DocumentModelVersioning). ## Benefits of Generated Scaffolding The generation processโ€”whether automatic via Vetra or manual via `ph generate`โ€”provides: 1. **Reduced Boilerplate:** Automates creation of type definitions, action creators, and utilities. 2. **Type Safety:** TypeScript types from your GraphQL schema catch errors at compile-time. 3. **Consistency:** Standardized project structure across document models. 4. **Accelerated Development:** Focus on business logic instead of foundational plumbing. 5. **Ecosystem Alignment:** Generated code integrates seamlessly with the Powerhouse ecosystem. 6. **Single Source of Truth:** Code stays synchronized with your specification. 7. **Version Support:** Optional versioning enables safe schema evolution over time. ## Practical Examples ### Using Vetra Studio (Recommended) When using Vetra Studio, code generation is automatic: 1. **Start Vetra in Interactive Mode:** ```bash ph vetra --interactive ``` 2. **Create Your Document Model:** Define your `TodoList` document model in the Vetra Studio Driveโ€”either manually or with AI assistance through Claude and the Reactor MCP. 3. **Watch the Terminal:** As you add specifications, Vetra automatically detects changes and generates scaffolding. In interactive mode, you'll be prompted to confirm before generation proceeds. 4. **Explore Generated Files:** Once complete, find your generated files at `document-models/todo-list/`: - `todo-list.json` and `schema.graphql`: Your model definition - `gen/`: Type-safe generated code - `src/reducers/todos.ts`: Skeleton reducer functions ready for implementation ### Using Connect (Alternative Method)
Tutorial: Manual Generation with Connect This approach is useful when working with Connect's Document Model Editor or when you need explicit control over the generation process. #### Prerequisites - **`TodoList.phd` file**: Your document model specification exported from Connect. #### Steps 1. **Place the Specification File in Your Project:** Navigate to your Powerhouse project root and copy your `TodoList.phd` file there. 2. **Run the Generator Command:** ```bash ph generate document-model --document TodoList.phd ``` 3. **Explore the Generated Files:** After the command completes, find the new directory at `document-models/todo-list/`: - `todo-list.json` and `schema.graphql`: The definition of your model - `gen/`: Type-safe generated code including `types.ts`, `creators.ts`, etc. - `src/`: Implementation skeleton, including `src/reducers/todos.ts` with empty functions for `addTodoItemOperation`, `updateTodoItemOperation`, and `deleteTodoItemOperation`
## Next Steps With your document model scaffolded, the next step is implementing the reducer logic in `document-models/todo-list/src/reducers/todos.ts`. Each reducer function takes the current state and action input, returning the new document state. Subsequently, write unit tests for your reducers to ensure they behave correctly. This cycle of defining, generating, implementing, and testing forms the core loop of document model development in Powerhouse. --- ## Implement document reducers > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/ImplementDocumentReducers ## The heart of document logic In our journey through Powerhouse Document Model creation, we've defined the "what" โ€“ the structure of our data ([State Schema](02-SpecifyTheStateSchema.md)) and the ways it can be changed ([Document Operations](03-SpecifyDocumentOperations.md)). We've also seen how the [Document Model Generator](04-UseTheDocumentModelGenerator.md) translates these specifications into a coded scaffold. Now, we arrive at the "how": implementing **Document Reducers**. Reducers are the core logic units of your document model. They are the functions that take the current state of your document and an operation (an "action"), and then determine the _new_ state of the document. They are the embodiment of your business rules and the engine that drives state transitions in a predictable, auditable, and immutable way. ## Recap: The journey to reducer implementation Before diving into the specifics of writing reducers, let's recall the preceding steps: 1. **State Schema Definition**: You designed the GraphQL `type` definitions for your document's data structure (e.g., `TodoListState`, `TodoItem`). 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. 3. **Code Generation**: You used `ph generate ` to create the necessary TypeScript types, action creators, and, crucially, the skeleton file for your reducers (typically `document-models//src/reducers/todos.ts`). 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. ## What is a reducer? ### The core principles In the context of Powerhouse and inspired by patterns like Redux, a reducer is a **pure function** with the following signature (conceptually): `(currentState, action) => newState` Let's break down its components and principles: - **`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**. - **`action`**: This is an object describing the operation to be performed. It typically has: - A `type` property: A string identifying the operation (e.g., `'ADD_TODO_ITEM'`). - 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`). - **`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. ### Key principles guiding reducer implementation: 1. **Purity**: - **Deterministic**: Given the same `currentState` and `action`, a reducer must _always_ produce the same `newState`. - **No Side Effects**: Reducers must not perform any side effects. This means no API calls, no direct DOM manipulation, no `Math.random()` (unless seeded deterministically for specific testing scenarios), and no modification of variables outside their own scope. Their sole job is to compute the next state. 2. **Immutability**: - **Never Mutate `currentState`**: You must never directly modify the `currentState` object or any of its nested properties. - **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. - This is fundamental to Powerhouse's event sourcing architecture, enabling time travel, efficient change detection, and a clear audit trail. :::tip Powerhouse uses Immer.js 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. ::: 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. 4. **Delegation to specific operation handlers**: 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: ```typescript import type { TodoListTodosOperations } from "todo-tutorial/document-models/todo-list"; export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // Your logic for ADD_TODO_ITEM }, updateTodoItemOperation(state, action) { // Your logic for UPDATE_TODO_ITEM }, deleteTodoItemOperation(state, action) { // Your logic for DELETE_TODO_ITEM }, }; ``` 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. ## Implementing reducer logic: A practical guide Let's use our familiar `TodoList` example to illustrate common patterns. ### Basic implementation (matching Get Started) The basic implementation matches what you built in the Get Started tutorial: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // Generate a unique ID for the new todo item const id = generateId(); // Add the new item to the state (Immer handles immutability) state.items.push({ ...action.input, id, checked: false }); }, updateTodoItemOperation(state, action) { // Find the item to update by its ID const item = state.items.find((item) => item.id === action.input.id); // Return early if item not found if (!item) return; // Update only the fields that are provided (partial update) item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, deleteTodoItemOperation(state, action) { // Filter out the item with the matching ID state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ``` **INFO:** 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. ### Advanced implementation (with statistics tracking) **INFO:** 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. For the advanced version with `stats`, we need to update the statistics whenever items are added, updated, or deleted: ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // Generate a unique ID for the new todo item const id = generateId(); // Update statistics state.stats.total += 1; state.stats.unchecked += 1; // Add the new item to the state state.items.push({ id, text: action.input.text, checked: false, // New items always start as unchecked }); }, updateTodoItemOperation(state, action) { // Find the specific item we want to update const item = state.items.find((item) => item.id === action.input.id); if (!item) { throw new Error(`Item with id ${action.input.id} not found`); } // Update text if provided if (action.input.text !== undefined) { item.text = action.input.text; } // Handle checked status changes and update stats if ( action.input.checked !== undefined && action.input.checked !== item.checked ) { if (action.input.checked) { state.stats.unchecked -= 1; state.stats.checked += 1; } else { state.stats.unchecked += 1; state.stats.checked -= 1; } item.checked = action.input.checked; } }, deleteTodoItemOperation(state, action) { // Find the item to determine its checked status for stats const item = state.items.find((item) => item.id === action.input.id); if (item) { // Update statistics state.stats.total -= 1; if (item.checked) { state.stats.checked -= 1; } else { state.stats.unchecked -= 1; } } // Remove the item from the list state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ``` ### Common patterns explained #### 1. Adding an item ```typescript addTodoItemOperation(state, action) { const id = generateId(); // Generate unique ID state.items.push({ ...action.input, id, checked: false }); } ``` - We use `generateId()` to create a unique identifier - We spread `action.input` to get the text, add the generated ID and default `checked: false` - With Immer, this "mutation" is actually immutable #### 2. Updating an item ```typescript updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; } ``` - We find the item by ID - We use nullish coalescing (`??`) to only update fields that were provided - This allows partial updates (e.g., just changing `checked` without touching `text`) #### 3. Deleting an item ```typescript deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); } ``` - We use `filter` to create a new array without the deleted item - Immer handles making this immutable ## Leveraging generated types 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`). **Always use these generated types in your reducer implementations!** ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { // TypeScript knows action.input has { text: string } const id = generateId(); state.items.push({ id, text: action.input.text, checked: false }); }, // ... other reducers }; ``` Using these types provides: - **Compile-time safety**: Catch errors related to incorrect property names or data types before runtime. - **Autocompletion and IntelliSense**: Improved developer experience in your IDE. - **Clearer code**: Types serve as documentation for the expected data structures. ## Practical implementation: Writing the `TodoList` reducers Now that you understand the principles, let's put them into practice by implementing the reducers for our `TodoList` document model.
Tutorial: Implementing the TodoList reducers 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. ### Implement the operation reducers 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. **Basic version (without stats):** ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.items.push({ ...action.input, id, checked: false }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) return; item.text = action.input.text ?? item.text; item.checked = action.input.checked ?? item.checked; }, deleteTodoItemOperation(state, action) { state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ``` **Advanced version (with stats):** ```typescript export const todoListTodosOperations: TodoListTodosOperations = { addTodoItemOperation(state, action) { const id = generateId(); state.stats.total += 1; state.stats.unchecked += 1; state.items.push({ id, text: action.input.text, checked: false, }); }, updateTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (!item) { throw new Error(`Item with id ${action.input.id} not found`); } if (action.input.text !== undefined) { item.text = action.input.text; } if ( action.input.checked !== undefined && action.input.checked !== item.checked ) { if (action.input.checked) { state.stats.unchecked -= 1; state.stats.checked += 1; } else { state.stats.unchecked += 1; state.stats.checked -= 1; } item.checked = action.input.checked; } }, deleteTodoItemOperation(state, action) { const item = state.items.find((item) => item.id === action.input.id); if (item) { state.stats.total -= 1; if (item.checked) { state.stats.checked -= 1; } else { state.stats.unchecked -= 1; } } state.items = state.items.filter((item) => item.id !== action.input.id); }, }; ```
## Reducers and the event sourcing model Every time a reducer processes an operation and returns a new state, Powerhouse records the original operation (the "event") in an append-only log associated with the document instance. The current state of the document is effectively a "fold" or "reduction" of all past events, applied sequentially by the reducers. This is why purity and immutability are so critical: - **Purity** ensures that replaying the same sequence of events will always yield the exact same final state. - **Immutability** ensures that each event clearly defines a discrete state transition, making it easy to audit changes and understand the document's history. ## Conclusion Implementing document reducers is where you breathe life into your document model's specification. By adhering to the principles of purity and immutability, and by leveraging the type safety provided by Powerhouse's code generation, you can build predictable, testable, and maintainable business logic. These reducers form the immutable backbone of your document's state management, perfectly aligning with the event sourcing architecture that underpins Powerhouse. With your reducers implemented, your document model is now functionally complete from a data manipulation perspective. The next chapter covers how to write tests for this logic to ensure its correctness and reliability. --- ## Implement document model tests > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/ImplementDocumentModelTests ## Ensuring robustness and reliability In the previous chapter, we implemented the core reducer logic for our document model. Now, we reach a critical stage that underpins the reliability and correctness of our entire model: **Implementing Document Model Tests**. Testing is not an afterthought; it's an integral part of the development lifecycle, especially in systems like Powerhouse where data integrity and predictable state transitions are paramount. Well-crafted tests serve as a safety net, allowing you to refactor and extend your document model with confidence. This document provides a practical, hands-on tutorial for testing the `TodoList` document model reducers you have just built. ## Practical implementation: Writing and running the TodoList tests This tutorial assumes you have implemented the `TodoList` reducers as described in the previous chapter and that the code generator has created a test file skeleton at `document-models/todo-list/src/tests/todos.test.ts`.
Tutorial: Implementing and running the TodoList reducer tests ### 1. Implement the reducer tests With the reducer logic in place, it's critical to test it. Navigate to the generated test file at `document-models/todo-list/src/tests/todos.test.ts` and replace its contents with comprehensive tests. This suite tests each operation, verifying not only that the `items` array is correct, but also that the operation itself is recorded properly in the document's history. **Basic tests (matching Get Started):** ```typescript AddTodoItemInput, DeleteTodoItemInput, UpdateTodoItemInput, } from "todo-tutorial/document-models/todo-list"; reducer, utils, isTodoListDocument, addTodoItem, AddTodoItemInputSchema, updateTodoItem, UpdateTodoItemInputSchema, deleteTodoItem, DeleteTodoItemInputSchema, TodoItemSchema, } from "todo-tutorial/document-models/todo-list"; describe("Todos Operations", () => { it("should handle addTodoItem operation", () => { const document = utils.createDocument(); const input: AddTodoItemInput = generateMock(AddTodoItemInputSchema()); const updatedDocument = reducer(document, addTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); // Verify the operation was recorded expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "ADD_TODO_ITEM", ); expect(updatedDocument.operations.global[0].action.input).toStrictEqual( input, ); expect(updatedDocument.operations.global[0].index).toEqual(0); }); it("should handle updateTodoItem operation to update text", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newText = "new text"; input.text = newText; input.checked = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); // Verify the operation was recorded expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "UPDATE_TODO_ITEM", ); // Verify the state was updated correctly const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(newText); expect(updatedItem?.checked).toBe(mockItem.checked); }); it("should handle updateTodoItem operation to update checked", () => { const mockItem = generateMock(TodoItemSchema()); const input: UpdateTodoItemInput = generateMock( UpdateTodoItemInputSchema(), ); input.id = mockItem.id; const newChecked = !mockItem.checked; input.checked = newChecked; input.text = undefined; const document = utils.createDocument({ global: { items: [mockItem], }, }); const updatedDocument = reducer(document, updateTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); const updatedItem = updatedDocument.state.global.items.find( (item) => item.id === input.id, ); expect(updatedItem?.text).toBe(mockItem.text); expect(updatedItem?.checked).toBe(newChecked); }); it("should handle deleteTodoItem operation", () => { const mockItem = generateMock(TodoItemSchema()); const document = utils.createDocument({ global: { items: [mockItem], }, }); const input: DeleteTodoItemInput = generateMock( DeleteTodoItemInputSchema(), ); input.id = mockItem.id; const updatedDocument = reducer(document, deleteTodoItem(input)); expect(isTodoListDocument(updatedDocument)).toBe(true); // Verify the operation was recorded expect(updatedDocument.operations.global).toHaveLength(1); expect(updatedDocument.operations.global[0].action.type).toBe( "DELETE_TODO_ITEM", ); // Verify the item was removed from state const updatedItems = updatedDocument.state.global.items; expect(updatedItems).toHaveLength(0); }); }); ``` **Advanced tests (with stats verification):** **INFO:** If you implemented the advanced version with statistics tracking, add these additional tests to verify the stats are updated correctly. ```typescript describe("Todos Operations with Stats", () => { it("should update stats when adding a todo item", () => { const document = utils.createDocument(); const input = { text: "Buy milk" }; const updatedDocument = reducer(document, addTodoItem(input)); expect(updatedDocument.state.global.items).toHaveLength(1); expect(updatedDocument.state.global.stats.total).toBe(1); expect(updatedDocument.state.global.stats.unchecked).toBe(1); expect(updatedDocument.state.global.stats.checked).toBe(0); }); it("should update stats when checking a todo item", () => { const document = utils.createDocument(); // Add an item first const addedDocument = reducer(document, addTodoItem({ text: "Buy milk" })); const itemId = addedDocument.state.global.items[0].id; // Now check it const updatedDocument = reducer( addedDocument, updateTodoItem({ id: itemId, checked: true }), ); expect(updatedDocument.state.global.stats.total).toBe(1); expect(updatedDocument.state.global.stats.unchecked).toBe(0); expect(updatedDocument.state.global.stats.checked).toBe(1); }); it("should update stats when deleting an unchecked todo item", () => { const document = utils.createDocument(); // Add an item const addedDocument = reducer(document, addTodoItem({ text: "Buy milk" })); const itemId = addedDocument.state.global.items[0].id; // Delete it const updatedDocument = reducer( addedDocument, deleteTodoItem({ id: itemId }), ); expect(updatedDocument.state.global.items).toHaveLength(0); expect(updatedDocument.state.global.stats.total).toBe(0); expect(updatedDocument.state.global.stats.unchecked).toBe(0); expect(updatedDocument.state.global.stats.checked).toBe(0); }); it("should update stats when deleting a checked todo item", () => { const document = utils.createDocument(); // Add and check an item const addedDocument = reducer(document, addTodoItem({ text: "Buy milk" })); const itemId = addedDocument.state.global.items[0].id; const checkedDocument = reducer( addedDocument, updateTodoItem({ id: itemId, checked: true }), ); // Delete it const updatedDocument = reducer( checkedDocument, deleteTodoItem({ id: itemId }), ); expect(updatedDocument.state.global.items).toHaveLength(0); expect(updatedDocument.state.global.stats.total).toBe(0); expect(updatedDocument.state.global.stats.checked).toBe(0); }); }); ``` ### 2. Run the tests Now, run the tests from your project's root directory to verify your implementation. ```bash pnpm run test ``` Or with npm: ```bash npm test ``` If all tests pass, you have successfully verified the core logic of your `TodoList` document model. This ensures that the reducers you wrote behave exactly as expected.
## Best practices for document model tests While the tutorial provides a concrete example, keep these general best practices in mind when writing your tests: - **Isolate Tests**: Each `it` block should ideally test one specific aspect or scenario. `beforeEach` is crucial for resetting state between tests. - **Descriptive Names**: Name your `describe` and `it` blocks clearly so they explain what's being tested. - **AAA Pattern (Arrange, Act, Assert)**: - **Arrange**: Set up the initial state and any required test data (e.g., using `utils.createDocument()` and defining `input` objects). - **Act**: Execute the operation by calling the `reducer` with an action from a `creator`. - **Assert**: Check if the outcome is as expected using `expect()`. - **Test Immutability**: A key assertion is to ensure the state is not mutated directly. You can check that a new array or object was created: `expect(newState.items).not.toBe(oldState.items);`. - **Cover Edge Cases**: Test what happens when an operation receives invalid input (e.g., trying to update an item that doesn't exist). Your test should confirm the reducer either throws an error or returns the state unchanged, depending on your implementation. - **Run Tests Frequently**: Integrate testing into your development workflow. Run tests after making changes to ensure you haven't broken anything. The `pnpm run test` (or `npm test`) command is your friend. ## Conclusion: The payoff of diligent testing Implementing comprehensive tests for your document model reducers is an investment that pays dividends in the long run. It leads to: - **Higher Quality Models**: More reliable and robust document models with fewer bugs. - **Increased Confidence**: Ability to make changes and refactor code without fear of breaking existing functionality. - **Easier Debugging**: When tests fail, they pinpoint the exact operation and scenario that's problematic. - **Better Collaboration**: Tests clarify the intended behavior of the document model for all team members. By following the tutorial and applying these best practices, you can build a strong suite of tests that safeguard the integrity and functionality of your document models. This diligence is a hallmark of a "Mastery Track" developer, ensuring that the solutions you build are not just functional but also stable, maintainable, and trustworthy. ## Up next In the next chapter of the Mastery Track - Building User Experiences you will learn how to implement an [editor](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) for your document model so you can see a simple user interface for the **TodoList** document model in action. For a complete, working example, you can always have a look at the [Example TodoList Repository](/academy/MasteryTrack/DocumentModelCreation/ExampleToDoListRepository) which contains the full implementation of the concepts discussed in this Mastery Track. --- ## Example: Todo-demo-package > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/ExampleToDoListRepository **INFO:** The Todo-demo is maintained by the Powerhouse Team and serves as a reference for testing and introducing new features. It will be continuously updated alongside the accompanying documentation. https://github.com/powerhouse-inc/todo-demo There are several ways to explore this package: ### Option 1: Rebuild the Todo-demo The Todo-demo and repository are your main reference points during the Mastery Track. Follow the steps in the "Mastery Track โ€“ Document Model Creation" chapters to build along with the examples. Key patterns used in the repository: - **Naming convention**: `TodoList`, `TodoItem`, `TodoListState` (one word, PascalCase) - **Document type**: `powerhouse/todo-list` - **Module name**: `todos` - **ID generation**: Uses `generateId()` from `document-model/core` in the reducer - **Hooks**: Uses `useSelectedTodoListDocument` for state management in the editor ### Option 2: Clone and run the code locally The package includes: - The Document Model - Reducer Code - Reducer Tests - Editor Code - Drive-app Code You can clone the repository and run Vetra Studio to see all the code in action: ```bash git clone https://github.com/powerhouse-inc/todo-demo cd todo-demo pnpm install ph vetra --watch ```
Alternatively: Run with Connect ```bash git clone https://github.com/powerhouse-inc/todo-demo cd todo-demo pnpm install ph connect ```
### Option 3: Install the todo demo package in a (local) host app Alternatively, you can install this package in a Powerhouse project or in your deployed host apps: ```bash ph install @powerhousedao/todo-demo ``` ## Comparing Get Started vs Mastery Track | Aspect | Get Started | Mastery Track (Advanced) | | ------------------ | -------------------------- | ------------------------------------ | | Schema | Basic `items` array only | Includes `stats` object for tracking | | Reducer complexity | Simple CRUD operations | Includes statistics updates | | Editor | Component-based with hooks | Same approach + stats display | | Tests | Basic operation tests | Includes stats verification tests | Both approaches use the same naming conventions and patterns โ€” the Mastery Track simply extends the foundation with additional features to demonstrate more advanced concepts. --- ## Document Model Versioning > Source: https://powerhouse.academy/academy/MasteryTrack/DocumentModelCreation/DocumentModelVersioning **TIP:** This chapter covers **advanced document model versioning**โ€”a system for evolving document schemas and operations while maintaining backward compatibility with existing documents. This is essential when your document models need to change over time in production environments. ## Why Versioning? Document models in Powerhouse are **event-sourced**. Once a document is created with a certain schema (v1), and operations are applied to it, you can't simply change the schema without breaking existing documents. Versioning solves this problem by allowing you to: - **Add new fields** to the state schema - **Add new operations** to the document model - **Modify reducer logic** for new documents - **Automatically upgrade** old documents to new versions when needed **INFO:** Document Model Versioning is a system that allows multiple versions of the same document model to coexist. Each version has its own schema, operations, and reducers. Documents created with older versions continue to work with their original reducers, while new documents use the latest version. Upgrade manifests define how to migrate documents between versions. --- ## How Versioning Works ### The Problem It Solves Consider a simple Todo document model: **Version 1 State:** ```graphql type TodoState { todos: [Todo!]! } ``` Now you want to add a `title` field to track the list's name: **Version 2 State:** ```graphql type TodoState { title: String todos: [Todo!]! } ``` Without versioning, existing v1 documents would break because they don't have a `title` field. With versioning: - V1 documents continue to work with the v1 reducer - New documents are created with v2 - V1 documents can be **upgraded** to v2 when needed ### Key Components Document model versioning consists of four key components: | Component | Purpose | | ----------------------- | ------------------------------------------------------------------ | | **Version Folders** | Separate `v1/`, `v2/` directories containing version-specific code | | **DocumentModelModule** | Each version exports a module with explicit `version` number | | **Upgrade Manifest** | Declares supported versions and upgrade paths | | **Upgrade Reducer** | Transforms document state from one version to another | --- ## Folder Structure When versioning is enabled, the document model generator creates a versioned folder structure: ``` document-models/ โ””โ”€โ”€ todo/ โ”œโ”€โ”€ v1/ # Version 1 โ”‚ โ”œโ”€โ”€ gen/ # Auto-generated code (DO NOT EDIT) โ”‚ โ”‚ โ”œโ”€โ”€ reducer.ts โ”‚ โ”‚ โ”œโ”€โ”€ creators.ts โ”‚ โ”‚ โ”œโ”€โ”€ types.ts # V1 TypeScript types โ”‚ โ”‚ โ””โ”€โ”€ ... โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ”‚ โ””โ”€โ”€ reducers/ # Your v1 reducer implementations โ”‚ โ”‚ โ””โ”€โ”€ todo-operations.ts โ”‚ โ””โ”€โ”€ module.ts # Exports DocumentModelModule with version: 1 โ”‚ โ”œโ”€โ”€ v2/ # Version 2 โ”‚ โ”œโ”€โ”€ gen/ โ”‚ โ”‚ โ””โ”€โ”€ types.ts # V2 TypeScript types (includes 'title') โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ”‚ โ””โ”€โ”€ reducers/ โ”‚ โ””โ”€โ”€ module.ts # Exports DocumentModelModule with version: 2 โ”‚ โ”œโ”€โ”€ upgrades/ # Migration logic โ”‚ โ”œโ”€โ”€ versions.ts # Supported versions list โ”‚ โ”œโ”€โ”€ v2.ts # Upgrade reducer: v1 โ†’ v2 โ”‚ โ””โ”€โ”€ upgrade-manifest.ts # Ties everything together โ”‚ โ””โ”€โ”€ document-models.ts # Exports all versions + manifests ``` --- ## Core Type Definitions Understanding the underlying types helps you implement versioning correctly: ### UpgradeTransition Defines a single version upgrade: ```typescript type UpgradeTransition = { toVersion: number; upgradeReducer: UpgradeReducer; description?: string; }; ``` ### UpgradeManifest Declares all supported versions and their upgrade paths: ```typescript type UpgradeManifest = { documentType: string; latestVersion: number; supportedVersions: TVersions; upgrades: { // Keys are "v2", "v3", etc. (never "v1" - nothing to upgrade from) [V in Exclude, 1> as `v${V}`]: UpgradeTransition; }; }; ``` --- ## Implementation Guide ### Step 1: Enable Versioning in Code Generation Versioning is enabled by default when using the Powerhouse CLI: ```bash ph generate TodoList.phd ``` Or when using Vetra Studio, versioning support is configured in your project settings. ### Step 2: Version Configuration Files **versions.ts** - Declare supported versions: ```typescript // upgrades/versions.ts export const supportedVersions = [1, 2] as const; export const latestVersion = supportedVersions[1]; // 2 ``` ### Step 3: Document Model Module Each version exports a `DocumentModelModule` with an explicit version number: ```typescript // v1/module.ts export const Todo: DocumentModelModule = { version: 1, // Explicit version number reducer, actions, utils, documentModel: createState(defaultBaseState(), documentModel), }; ``` ```typescript // v2/module.ts export const Todo: DocumentModelModule = { version: 2, // Different version reducer, actions, utils, documentModel: createState(defaultBaseState(), documentModel), }; ``` ### Step 4: Upgrade Reducer The upgrade reducer transforms a document from one version to the next: ```typescript // upgrades/v2.ts function upgradeReducer( document: PHDocument, action: Action, ): PHDocument { return { ...document, state: { ...document.state, global: { ...document.state.global, title: "", // Initialize the new field }, }, initialState: { ...document.initialState, global: { ...document.initialState.global, title: "", // Also in initial state }, }, }; } export const v2: UpgradeTransition = { toVersion: 2, upgradeReducer, description: "Add title field to global state", }; ``` ### Step 5: Upgrade Manifest Tie everything together in the manifest: ```typescript // upgrades/upgrade-manifest.ts export const upgradeManifest: UpgradeManifest = { documentType: "my-org/todo", latestVersion, supportedVersions, upgrades: { v2 }, }; ``` ### Step 6: Export All Versions ```typescript // document-models.ts export const documentModels: DocumentModelModule[] = [TodoV1, TodoV2]; export const upgradeManifests: UpgradeManifest[] = [ todoUpgradeManifest, ]; ``` --- ## Integration with Connect and Switchboard ### How Connect Loads Versioned Documents Connect automatically loads all document model versions and upgrade manifests from your Vetra packages: ```typescript // Simplified view of Connect's reactor setup const documentModelModules = vetraPackages.flatMap( (pkg) => pkg.modules.documentModelModules, ); const upgradeManifests = vetraPackages.flatMap((pkg) => pkg.upgradeManifests); const reactor = await createBrowserReactor( documentModelModules, upgradeManifests, renown, ); ``` ### Creating Documents at Specific Versions By default, new documents are created at the **latest version**. You can optionally specify a version: ```typescript // Create at latest version (default) const doc = await client.createEmpty("my-org/todo"); // doc.state.document.version === 2 (latest) // Create at specific version const v1Doc = await client.createEmpty("my-org/todo", { documentModelVersion: 1, }); // v1Doc.state.document.version === 1 ``` ### Querying Documents Documents can be queried regardless of version: ```typescript // Find all todo documents (both v1 and v2) const result = await client.find({ type: "my-org/todo" }); ``` --- ## Use Cases ### 1. Adding a New Field **Scenario:** Your Todo document needs a `title` field. **Solution:** 1. Create v2 with the new field in the state schema 2. Implement upgrade reducer that sets `title: ""` 3. New documents get v2; existing v1 documents can be upgraded ### 2. Adding New Operations **Scenario:** V1 has `ADD_TODO`, `REMOVE_TODO`. V2 adds `EDIT_TITLE`. **How it works:** - V2 module includes the new operation - V1 documents don't have access to `EDIT_TITLE` until upgraded - The upgrade manifest handles the migration ### 3. Changing Reducer Behavior **Scenario:** V2 items should include an `addedAt` timestamp. ```typescript // V1 reducer - no timestamp function v1StateReducer(state, action) { if (action.type === "ADD_ITEM") { return { ...state, global: { items: [ ...state.global.items, { id: action.input.id, name: action.input.name, }, ], }, }; } } // V2 reducer - adds timestamp field function v2StateReducer(state, action) { if (action.type === "ADD_ITEM") { return { ...state, global: { items: [ ...state.global.items, { id: action.input.id, name: action.input.name, addedAt: action.input.addedAt, // New field from input }, ], }, }; } } ``` --- ## Best Practices ### Upgrade Reducer Guidelines 1. **Always handle both `state` and `initialState`** - Both need to be migrated 2. **Provide sensible defaults** for new fields 3. **Never lose data** - Transform existing data, don't delete it 4. **Keep upgrade reducers pure** - No side effects or async operations ### Version Compatibility - **Don't remove operations** from newer versions unless absolutely necessary - **Don't change existing operation input schemas** - add new operations instead - **Document breaking changes** in the upgrade transition description ### Testing Upgrades Test your upgrade reducers thoroughly: ```typescript it("should upgrade v1 document to v2", () => { const v1Doc = createV1Document(); v1Doc.state.global.todos = [{ id: "1", title: "Test", completed: false }]; const v2Doc = upgradeReducer(v1Doc, {} as Action); expect(v2Doc.state.global.title).toBe(""); // New field initialized expect(v2Doc.state.global.todos).toEqual(v1Doc.state.global.todos); // Data preserved }); ``` --- ## Summary | Concept | Description | | -------------------------- | ----------------------------------------------------------- | | **Version Folders** | `v1/`, `v2/` directories with version-specific code | | **DocumentModelModule** | Exports with explicit `version` field | | **UpgradeTransition** | Defines how to migrate from one version to the next | | **UpgradeManifest** | Declares all versions and upgrade paths for a document type | | **Backward Compatibility** | Old documents work with their original reducers | | **Automatic Upgrades** | Reactor handles version detection and migration | Document model versioning enables your applications to evolve safely while preserving the integrity of existing data. By following these patterns, you can confidently add new features, modify schemas, and improve your document models over time. --- ## Related Documentation - [Use the Document Model Generator](/academy/MasteryTrack/DocumentModelCreation/UseTheDocumentModelGenerator) - [Implement Document Reducers](/academy/MasteryTrack/DocumentModelCreation/ImplementDocumentReducers) - [Powerhouse CLI Reference](/academy/APIReferences/PowerhouseCLI) --- ## Build document editors > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors ## Build with React on Powerhouse At Powerhouse, frontend development for document editors follows a simple and familiar flow, leveraging the power and flexibility of React. ### Development environment **Vetra Studio** is your primary tool for builder workflows and editor development. When you run `ph vetra --watch`, it provides a dynamic, local environment where you can define and preview your document models and their editors live. This replaces the need for tools like Storybook for editor development, though Storybook remains invaluable for exploring the [Powerhouse Component Library](#powerhouse-component-library). #### Key aspects of the Powerhouse development environment: - **React Foundation**: Build your editor UIs using React components, just as you would in any standard React project. - **Automatic Build Processes**: Tailwind CSS is installed by default and fully managed by Vetra Studio. There's no need to manually configure or run Tailwind or other build processes during development. Vetra Studio handles CSS generation and other necessary build steps automatically, especially when you publish a package. - **Styling Flexibility**: You are not limited to Tailwind. Regular CSS (`.css` files), inline styles, and any React-compatible styling method work exactly as you would expect. #### Powerhouse aims to keep your developer experience clean, familiar, and focused: - Build React components as you normally would. - Use styling approaches you're comfortable with. - Trust Vetra Studio to handle the setup and build processes for you.
Alternatively: Use Connect for development You can also use **Connect** as your development environment by running `ph connect`. Connect provides a similar dynamic local environment where you can preview your document models and their editors live. The development experience is essentially the same, with Connect also handling Tailwind CSS and build processes automatically.
### Generating your editor template When using **Vetra Studio**, editor generation is automatic and integrated into your workflow. Simply create an **Editor specification document** in your Vetra Studio Drive, and Vetra will automatically generate the editor template code for you. #### With Vetra Studio (Recommended) 1. Open Vetra Studio (`ph vetra --watch`) 2. In your Vetra Studio Drive, click **"Add new specification"** in the Editors section 3. Select your document model (e.g., `TodoList`) to link the editor to 4. Name your editor (e.g., `todo-list-editor`) 5. Vetra automatically generates the `editors/todo-list-editor/editor.tsx` template That's it! No manual commands needed. Vetra watches your specifications and generates code as you work.
Alternatively: Manual generation with ph generate If you're using Connect or prefer manual control, you can use the `ph generate` command to create an editor template: ```bash ph generate --editor todo-list-editor --document-types powerhouse/todo-list ``` This will create the template in the `editors/todo-list-editor/editor.tsx` folder. If you want a refresher on how to define your document model specification please read the chapter on [specifying the State Schema](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema)
### Styling your editor You have several options for styling your editor components: 1. **Default HTML Styling**: Standard HTML tags (`

`, `

`, ` ); } ``` **Why hooks are recommended:** - โœ… **Self-contained components**: Each component gets its own connection to the document - โœ… **Less boilerplate**: No need to pass props through multiple levels - โœ… **Easier refactoring**: Move components around without rewiring props - โœ… **Modern React pattern**: Follows React's recommended approach for state management ### Method 2: Using Props ๐Ÿ“ฆ The **props-based approach** receives the document and dispatch function as properties passed from a parent component. ```typescript export type IProps = EditorProps; export default function Editor(props: IProps) { const { document, dispatch } = props; const state = document.state.global; // Now you'd pass state and dispatch to child components as props return (

); } ``` **When props might be useful:** - When you need strict control over which components can access state - When building components that should work outside of Powerhouse context - For testing purposes where you want to inject mock state ### Which should you use? | Scenario | Recommended Approach | | ------------------------------------------------- | -------------------- | | Building a standard Powerhouse editor | **Hooks** ๐Ÿช | | Component needs document state | **Hooks** ๐Ÿช | | Building reusable UI components (buttons, inputs) | **Props** ๐Ÿ“ฆ | | Need to test components in isolation | **Props** ๐Ÿ“ฆ | **Bottom line**: Use hooks for most Powerhouse editor development. It's simpler, cleaner, and matches the patterns used in the [todo-demo repository](https://github.com/powerhouse-inc/todo-demo). ### Additional hooks for editors Beyond the document-specific hooks (like `useSelectedTodoListDocument`), Powerhouse provides a comprehensive set of hooks from the reactor-browser package that you can use in your editors: | Hook | Description | | --------------------------- | --------------------------------------------- | | `useSelectedDocument` | Returns the currently selected document | | `useSelectedDocumentId` | Returns just the ID of the selected document | | `useDocumentById` | Returns a document by its ID | | `useSelectedDrive` | Returns the currently selected drive | | `useRevisionHistoryVisible` | Check and control revision history visibility | | `usePHModal` | Manage modals in your editor | **Example: Using `useDocumentById` to reference another document** ```typescript export function RelatedDocument({ documentId }: { documentId: string }) { const relatedDoc = useDocumentById(documentId); if (!relatedDoc) return Loading...; return (

Related: {relatedDoc.name}

{/* Display related document info */}
); } ``` **Example: Showing a modal from your editor** ```typescript export function CreateNewButton({ documentType }: { documentType: string }) { return ( ); } ``` For a complete list of all available hooks, see the [React Hooks API Reference](/academy/APIReferences/ReactHooks). ## Local vs. Global State When building editors, you'll work with two types of state: - **Global Document State**: Data that is part of the document itself and should be saved. This is accessed via hooks (`useSelectedTodoListDocument`) or props (`document.state.global`). You modify it by dispatching actions. - **Local Component State**: UI-specific state that doesn't need to be saved (e.g., "is the dropdown open?", "what's in the input field before submission?"). Use React's `useState` hook for this. ```typescript export function AddTodo() { // Local state - just for this component's UI const [inputValue, setInputValue] = useState(''); // Global document state - saved in the document const [todoList, dispatch] = useSelectedTodoListDocument(); const handleSubmit = () => { if (inputValue.trim()) { dispatch(addTodoItem({ text: inputValue })); // Updates global state setInputValue(''); // Clears local state } }; return (
setInputValue(e.target.value)} />
); } ``` ## Handling dispatch errors When dispatching actions to a document, you may want to handle errors that occur during action execution. The `dispatch` function accepts an optional `onErrors` callback as its second parameter, which is invoked with any errors thrown by the reducers when processing the actions. ```typescript useSelectedTodoListDocument, addTodoItem, } from "todo-tutorial/document-models/todo-list"; export function AddTodo() { const [todoList, dispatch] = useSelectedTodoListDocument(); const handleAdd = (text: string) => { dispatch(addTodoItem({ text }), (errors) => { // Handle errors - e.g., show a toast notification console.error("Failed to add todo:", errors); alert(`Error: ${errors[0]?.message}`); }); }; // ... rest of component } ``` This pattern is useful when you need to: - Display error messages to users - Log errors for debugging - Trigger recovery actions when an operation fails ## Powerhouse component library Powerhouse provides a rich set of reusable UI components through the **`@powerhousedao/document-engineering/scalars`** package. These components are designed for consistency, efficiency, and seamless integration with the Powerhouse ecosystem, with many based on GraphQL scalar types. For more information read our chapter on the [Component Library](/academy/ComponentLibrary/DocumentEngineering) ### Exploring components You can explore available components, see usage examples, and understand their properties (props) using our Storybook instance: [https://storybook.powerhouse.academy](https://storybook.powerhouse.academy) Storybook allows you to: - Visually inspect each component. - Interact with different states and variations. - View code snippets for basic implementation. - Consult the props table for detailed configuration options. ### Using components 1. **Import**: Add an import statement at the top of your editor file: ```typescript import { Checkbox, StringField, Form, } from "@powerhousedao/document-engineering/scalars"; ``` 2. **Implement**: Use the component in your JSX, configuring it with props: ```typescript // Example using StringField for an input
{ /* Handle submission */ }}> setTaskText(e.target.value)} /> ```
Tutorial: Implementing the TodoList Editor ## Build a TodoList editor In this final part of our tutorial we will continue with the interface or editor implementation of the **TodoList** document model. This means you will create a simple user interface for the **TodoList** document model which will be used inside the Connect app to create, update and delete your TodoList items, and also display the statistics we've implemented in our reducers (if you followed the advanced version). ## Generate the editor template ### Using Vetra Studio (Recommended) With Vetra Studio running (`ph vetra --watch`), simply create an Editor specification: 1. In your Vetra Studio Drive, click **"Add new specification"** in the Editors section 2. Select the **TodoList** document model to link the editor to 3. Name your editor `todo-list-editor` 4. Vetra automatically generates `editors/todo-list-editor/editor.tsx` Once complete, navigate to the `editors/todo-list-editor/editor.tsx` file and open it in your IDE.
Alternatively: Manual generation with ph generate If you're not using Vetra Studio, run the command below to generate the editor template: ```bash ph generate --editor todo-list-editor --document-types powerhouse/todo-list ``` This command reads the **TodoList** document model definition from the `document-models` folder and generates the editor template in the `editors/todo-list-editor` folder as `editor.tsx`. Notice the `--editor` flag which specifies the editor name, and the `--document-types` flag defines the document type `powerhouse/todo-list`.
### Editor implementation options When building your editor component within the Powerhouse ecosystem, you have several options for styling, allowing you to leverage your preferred methods: 1. **Default HTML Styling:** Standard HTML tags (`

`, `

`, ` ); } ``` ### Todo item component ```typescript // editors/todo-list-editor/components/Todo.tsx type Props = { todo: TodoItem; }; export function Todo({ todo }: Props) { const [isEditing, setIsEditing] = useState(false); const [todoList, dispatch] = useSelectedTodoListDocument(); if (!todoList) return null; const onChangeTodoChecked: ChangeEventHandler = (event) => { dispatch(updateTodoItem({ id: todo.id, checked: event.target.checked })); }; const onClickDeleteTodo: MouseEventHandler = () => { dispatch(deleteTodoItem({ id: todo.id })); }; const onSubmitUpdateTodoText: FormEventHandler = (event) => { event.preventDefault(); const form = event.currentTarget; const textInput = form.elements.namedItem("todoText") as HTMLInputElement; const text = textInput.value; if (!text) return; dispatch(updateTodoItem({ id: todo.id, text })); setIsEditing(false); }; if (isEditing) { return (

); } return (
{todo.text}
); } ``` --- ## Advanced: Adding stats display **INFO:** If you implemented the advanced version with statistics tracking, you can add a stats component to display the todo counts. ```typescript // Add to TodoList.tsx export function TodoList() { const [selectedTodoList] = useSelectedTodoListDocument(); if (!selectedTodoList) return null; const { items, stats } = selectedTodoList.state.global; return (

TodoList

{/* Stats section (only show if there are items) */} {items.length >= 2 && (
Total
{stats.total}
Completed
{stats.checked}
Remaining
{stats.unchecked}
)}
); } ``` --- ## Test your editor Now you can run Vetra Studio and see the **TodoList** editor in action: ```bash ph vetra --watch ``` In Vetra Studio, you'll be able to create and test your **TodoList** documents. Click on the Document Models section and create a new TodoList document. **TIP:** The editor will update dynamically, so you can play around with your editor styling while seeing your results appear in Vetra Studio.
Alternatively: Test with Connect You can also run the Connect app to see the **TodoList** editor in action: ```bash ph connect ``` In Connect, in the bottom right corner you'll find a new Document Model that you can create: **TodoList**. Click on it to create a new TodoList document. The editor will update dynamically, so you can play around with your editor styling while seeing your results appear in Connect.

Congratulations! If you managed to follow this tutorial until this point, you have successfully implemented the **TodoList** document model with its reducer operations and editor. ## Up Next Now you can move on to creating a [custom Drive-app](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) for your TodoList document. Imagine you have many TodoLists sitting in a drive. A custom Drive-app will allow you to organize and track them at a glance, opening up a new world of possibilities to increase the functionality of your documents! ### Further Reading - [React Hooks API Reference](/academy/APIReferences/ReactHooks) โ€” Complete reference for all available Powerhouse hooks - [Component Library](/academy/ComponentLibrary/DocumentEngineering) โ€” Pre-built UI components for your editors --- ## Build a Drive-app > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer **Drive-apps** enhance how contributors and organizations interact with document models. They create an 'app-like' experience by providing a **custom interface** for exploring and interacting with the contents of a drive. **TIP:** A Drive-app offers a tailored application designed around its document models. Think of a Drive-app as a specialized lensโ€”it offers **different ways to visualize, organize, and interact with** the data stored within a drive, making it more intuitive and efficient for specific use cases. ### Drive-apps are purpose-built Organizations typically build Drive-apps for specific use cases, often packaging them with a corresponding document model. This allows for customized user experiences, streamlined workflows, and maximized efficiency for contributors. Drive-apps **bridge the gap between raw data and usability**, unlocking the full potential of document models within the Powerhouse framework. ### Key features of Drive-apps - **Custom Views & Organization** โ€“ Drive-apps can present data in formats like Kanban boards, list views, or other structured layouts to suit different workflows. - **Aggregated Insights** โ€“ They can provide high-level summaries of important details across document models, enabling quick decision-making. - **Enhanced Interactivity** โ€“ Drive-apps can include widgets, data processors, or read models to process and display document data dynamically. ## Build a Drive-app Drive-apps provide custom interfaces for interacting with the contents of a drive. Let's start with a **quick overview** of the steps for building a Drive-app. We will then apply these steps to create our **todo-list Drive-app**. ### Step 1. Generate the scaffolding code When using **Vetra Studio**, Drive-app generation is automatic. Simply create a **Drive-app specification document** in your Vetra Studio Drive: 1. Open Vetra Studio (`ph vetra --watch`) 2. In your Vetra Studio Drive, click **"Add new specification"** in the Apps section 3. Name your Drive-app (e.g., `todo-list-drive-explorer`) 4. Vetra automatically generates the Drive-app template code
Alternatively: Manual generation with ph generate If you're not using Vetra Studio, use the `generate drive editor` command to create the basic template structure: ```bash ph generate --drive-editor ```
### Step 2. Update the manifest file After creating your Drive-app, you need to update its `powerhouse.manifest.json` file. This file identifies your project and its components within the Powerhouse ecosystem. ### Step 3. Customize the Drive-app Review the generated template and modify it to better suit your document model: 1. Remove unnecessary files and components 2. Add custom views specific to your data model 3. Implement specialized interactions for your use case ### About the Drive-app template The default template provides a solid foundation. It contains: - A tree structure navigation panel - Basic file/folder operations - Standard layout components But the real power comes from tailoring the interface to your specific document models. Now, let's implement a specific example for the to-do list we've been working on throughout this guide. ## Implementation example: todo-list Drive-app This example demonstrates how to create a todo-list Drive-app application using the Powerhouse platform. The application allows users to create and manage to-do lists with a visual progress indicator. **WARNING:** If you've been following the Mastery Track, you can continue with the to-do list document model and Powerhouse project you've created. For more details, you can refer to the [Document Model Creation guide](/academy/MasteryTrack/DocumentModelCreation/SpecifyTheStateSchema). If not, you can follow the shortened guide below to prepare your project for this tutorial.
Prepare your Powerhouse Project to create a custom drive ### 1. Create a To-do document model: - Initialize a new project with `ph init` and give it a project name. - Start by running Vetra Studio locally with `ph vetra --watch` - Follow the [Get Started guide](/academy/GetStarted/DefineToDoListDocumentModel) to create your TodoList document model specification. - Drop the downloaded file in the Vetra Studio drive. You'll find it under document models. Vetra should now automatically generate the necessary code for your project ### 2. Add the reducer code: - Copy the code from [`todos.ts`](https://github.com/powerhouse-inc/todo-tutorial/blob/step-3-complete-implement-todo-list-document-model-reducer-operation-handlers/document-models/todo-list/src/reducers/todos.ts) - Paste it into `document-models/todo-list/src/reducers/todos.ts` ### 3. Generate a document editor: In Vetra Studio, create an Editor specification: 1. Click **"Add new specification"** in the Editors section 2. Select the **TodoList** document model 3. Name your editor `TodoList` 4. Vetra automatically generates the editor template
Alternatively: Manual generation ```bash ph generate --editor TodoList --document-types powerhouse/todo-list ```
### 4. Add the editor code: - Follow the [Build TodoList Editor guide](/academy/GetStarted/BuildToDoListEditor) to implement your editor components.
Alternatively: Use Connect instead of Vetra Studio You can also start by running Connect locally with `ph connect` instead of Vetra Studio. The workflow is the same, with Connect providing a similar development environment.
## Generate the Drive-app ### 1. Generate a Drive-app: With Vetra Studio running (`ph vetra --watch`), create a Drive-app specification: 1. In your Vetra Studio Drive, click **"Add new specification"** in the Apps section 2. Name your Drive-app `todo-list-drive-explorer` 3. Vetra automatically generates the Drive-app template in `editors/todo-list-drive-explorer/`
Alternatively: Manual generation with ph generate ```bash ph generate --drive-editor todo-list-drive-explorer ```
### 2. Update the `powerhouse.manifest.json` file: - The manifest file contains metadata for your package that is displayed when other users install it. Update the manifest to register your new Drive-app: ```json { "name": "To-do List Package", "description": "A simple todo-list with a dedicated Drive-app", "category": "Productivity", "publisher": { "name": "Powerhouse", "url": "https://www.powerhouse.inc/" }, "documentModels": [ { "id": "todo-list", "name": "TodoList" } ], "editors": [ { "id": "todo-list-editor", "name": "TodoList Editor", "documentTypes": ["todo-list"] } ], "apps": [ { "id": "todo-list-drive-explorer", "name": "TodoList drive-app", "driveEditor": "todo-list-drive-explorer" } ], "subgraphs": [], "importScripts": [] } ``` ### 3. Remove Unnecessary Default Components: - First, let's remove some default template files that we won't need for this specific demo. If you want to see what the default template looks like before removing files, you can run `ph connect` at any time. ```bash rm -rf editors/todo-list-drive-explorer/hooks rm -rf editors/todo-list-drive-explorer/components/FileItemsGrid.tsx rm -rf editors/todo-list-drive-explorer/components/FolderItemsGrid.tsx rm -rf editors/todo-list-drive-explorer/components/FolderTree.tsx ``` ### 4. Create custom components for your Drive-app: - Next, create the following files. These will define the data types for our todo-list items and provide the custom React components for our Drive-app.
Create `editors/todo-list-drive-explorer/types/todo.ts` This file defines the TypeScript type `ToDoState`. It specifies the shape of todo-list document data within the Drive-app, combining the document's revision information with its global state. This ensures that our components work with a predictable and strongly-typed data structure. ```typescript import type { TodoListDocument } from "todo-tutorial/document-models/todo-list"; export type TodoState = { documentType: string; revision: { global: number; local: number; }; global: TodoListDocument["state"]["global"]; }; ```
Create `editors/todo-list-drive-explorer/components/ProgressBar.tsx` This is a simple React component that renders a visual progress bar. It takes a `value` and `max` number to calculate the percentage of completed tasks. It also displays the percentage and has a special state for when there are no tasks. ```tsx import type { FC } from 'react'; interface ProgressBarProps { value: number; max: number; } export const ProgressBar: FC = ({ value, max }) => { if (max === 0) { return (
No tasks
); } const percentage = Math.min(100, (value / max) * 100); return (
{Math.round(percentage)}%
); }; ```
Update `editors/todo-list-drive-explorer/components/DriveExplorer.tsx` This is the main component of our Drive-app. It fetches all `powerhouse/todo-list` documents from the drive, displays them in a table with their progress, and allows a user to click on a document to open it in the `EditorContainer`. It also includes a button to create new documents. ```typescript type TodoState = { documentType: string; revision: { global: number; local: number; }; global: TodoListDocument["state"]["global"]; }; interface DriveExplorerProps { driveId: string; nodes: Node[]; onAddFolder: (name: string, parentFolder?: string) => void; onDeleteNode: (nodeId: string) => void; renameNode: (nodeId: string, name: string) => void; onCopyNode: (nodeId: string, targetName: string, parentId?: string) => void; context: DriveEditorContext; } export function DriveExplorer({ driveId, nodes, context, }: DriveExplorerProps) { const { getDocumentRevision } = context; const [activeDocumentId, setActiveDocumentId] = useState< string | undefined >(); const [openModal, setOpenModal] = useState(false); const selectedDocumentModel = useRef(null); const { addDocument, documentModels, useDriveDocumentStates } = useDriveContext(); const [state, fetchDocuments] = useDriveDocumentStates({ driveId }); useEffect(() => { fetchDocuments(driveId).catch(console.error); }, [activeDocumentId]); const { todoNodes } = useMemo(() => { return Object.keys(state).reduce( (acc, curr) => { const document = state[curr]; if (document.documentType === "powerhouse/todo-list") { acc.todoNodes[curr] = document as TodoState; } return acc; }, { todoNodes: {} as Record, }, ); }, [state]); const handleEditorClose = useCallback(() => { setActiveDocumentId(undefined); }, []); const onCreateDocument = useCallback( async (fileName: string) => { setOpenModal(false); const documentModel = selectedDocumentModel.current; if (!documentModel) return; const node = await addDocument( driveId, fileName, documentModel.documentModel.id, ); selectedDocumentModel.current = null; setActiveDocumentId(node.id); }, [addDocument, driveId], ); const onSelectDocumentModel = useCallback( (documentModel: DocumentModelModule) => { selectedDocumentModel.current = documentModel; setOpenModal(true); }, [], ); const onGetDocumentRevision = useCallback( (options?: GetDocumentOptions) => { if (!activeDocumentId) return; return getDocumentRevision?.(activeDocumentId, options); }, [getDocumentRevision, activeDocumentId], ); const filteredDocumentModels = documentModels; const fileNodes = nodes.filter((node) => node.kind === "file") as FileNode[]; // Get the active document info from nodes const activeDocument = activeDocumentId ? fileNodes.find((file) => file.id === activeDocumentId) : undefined; const documentModelModule = activeDocument ? context.getDocumentModelModule(activeDocument.documentType) : null; const editorModule = activeDocument ? context.getEditor(activeDocument.documentType) : null; return (
{/* Main Content */}
{activeDocument && documentModelModule && editorModule ? ( ) : ( <>

ToDos:

{Object.entries(todoNodes).map(([documentId, todoNode]) => ( ))}
Document ID Document Type Tasks Completed Progress
setActiveDocumentId(documentId)} className="text-blue-600 hover:text-blue-800 cursor-pointer" > {documentId}
{todoNode.documentType} {todoNode.global.stats.total} {todoNode.global.stats.checked}
{/* Create Document Section */} )}
{/* Create Document Modal */} setOpenModal(open)} open={openModal} />
); } ```
Update `editors/todo-list-drive-explorer/components/EditorContainer.tsx` This component acts as a wrapper for the document editor. When a user selects a document in `DriveExplorer.tsx`, this component mounts the appropriate editor (`todo-list-editor` in this case) and provides it with the necessary context and properties to function. It also renders the `DocumentToolbar` which provides actions like closing, exporting, and viewing revision history. ```typescript useDriveContext, type User, type DriveEditorContext, } from "@powerhousedao/reactor-browser"; documentModelDocumentModelModule, type DocumentModelModule, type EditorContext, type EditorProps, type PHDocument, type EditorModule, type Operation, } from "document-model"; DocumentToolbar, RevisionHistory, DefaultEditorLoader, generateLargeTimeline, type TimelineItem, } from "@powerhousedao/design-system"; export interface EditorContainerProps { driveId: string; documentId: string; documentType: string; onClose: () => void; title: string; context: Omit & Pick; documentModelModule: DocumentModelModule; editorModule: EditorModule; } export const EditorContainer: React.FC = (props) => { const { driveId, documentId, documentType, onClose, title, context, documentModelModule, editorModule } = props; const [selectedTimelineItem, setSelectedTimelineItem] = useState(null); const [showRevisionHistory, setShowRevisionHistory] = useState(false); const { useDocumentEditorProps } = useDriveContext(); const user = context.user as User | undefined; const timelineItems = useTimelineItems(documentId); const { dispatch, error, document } = useDocumentEditorProps({ documentId, documentType, driveId, documentModelModule, user, }); const loadingContent = (
); if (!document) return loadingContent; const moduleWithComponent = editorModule as EditorModule; const EditorComponent = moduleWithComponent.Component; return showRevisionHistory ? ( setShowRevisionHistory(false)} /> ) : ( {}} /> ); }; ```
- In case you are getting stuck and want to verify your progress with the reference repository you can find the example repository of the [Todo Tutorial here](https://github.com/powerhouse-inc/todo-tutorial) ### 3. Run the application: - With the code for our Drive-app in place, it's time to see it in action. If you've been running Vetra Studio with `ph vetra --watch`, your Drive-app is already available. Otherwise, run Connect: ```bash ph connect ``` ### Now it's your turn! Start building your own Drive-apps or experiences. Congratulations on completing this tutorial! You've successfully built a custom drive explorer, enhancing the way users interact with document models. Now, take a moment to think about the possibilities! - What **unique Drive Experiences** could you create for your own projects? - How can you tailor interfaces and streamline workflows to unlock the full potential of your document models? The Powerhouse platform provides the tools. It's time to start building! --- ## CSS Customization for Connect Integration > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/CSSCustomization When your editor runs inside Connect, it's rendered within a specific container hierarchy. Understanding this structure allows you to customize your editor's appearance to match your application's design requirements. ## Understanding the Editor Container Hierarchy Connect wraps your editor component in two key containers that you can target for styling: ```
โ””โ”€โ”€ โ””โ”€โ”€
โ””โ”€โ”€ ``` ### Container Details | Container ID | Default Classes | Data Attributes | Purpose | | ---------------------------- | ----------------- | ----------------------------------- | ----------------------------------------------------------- | | `#document-editor-container` | `flex-1` | `data-document-type` | Outermost wrapper, controls overall editor space allocation | | `#document-editor-context` | `relative h-full` | `data-editor`, `data-document-type` | Inner context, provides positioning context and full height | These containers are defined in Connect's source: - [`document-editor-container.tsx`](https://github.com/powerhouse-inc/ph-monorepo/blob/main/apps/connect/src/components/document-editor-container.tsx) (line 94) - [`editors.tsx`](https://github.com/powerhouse-inc/ph-monorepo/blob/main/apps/connect/src/components/editors.tsx) (line 173) ## Customizing Your Editor's Appearance ### Method 1: Inline Styles in Your Editor Component (Recommended) The simplest and most maintainable approach is to apply styles directly to your editor's root element. This keeps your styling self-contained within your editor. ```tsx export function Editor() { return (
{/* Your editor content */}
); } ``` **TIP:** Using `height: "100%"` ensures your editor fills the available vertical space within Connect's container hierarchy. ### Method 2: CSS File with Container Selectors For more complex customizations or when you need to override Connect's default container styles, you can target the container IDs directly in a CSS file: **DANGER:** Targeting container IDs directly will apply styles to **ALL** editors in your Connect application. For editor-specific styling, use [Method 3: Scoped Styling with Data Attributes](#method-3-scoped-styling-with-data-attributes) instead. ```css /* editors/my-editor/editor.css */ #document-editor-container { /* Customize the outer container */ background-color: #f8fafc; } #document-editor-context { /* Customize the inner context */ max-width: 1200px; margin: 0 auto; } /* Scope styles to your editor within the context */ #document-editor-context .my-editor-root { padding: 2rem; } ``` **WARNING:** Remember to import styles in your `styles.css` file rather than directly in `.tsx` files. Direct imports work in development but won't be included in production builds. ```css /* styles.css */ @import "./editors/my-editor/editor.css"; ``` ### Method 3: Scoped Styling with Data Attributes Connect adds `data-editor` and `data-document-type` attributes to editor containers, allowing you to scope CSS rules to specific editors without affecting others. ```css /* Only applies to a specific editor */ #document-editor-context[data-editor="document-editor-editor"] { background-color: #f0f9ff; } /* Only applies to a specific document type */ #document-editor-context[data-document-type="powerhouse/document-editor"] { max-width: 900px; margin: 0 auto; } /* Combine both for precise targeting */ #document-editor-context[data-editor="document-editor-editor"][data-document-type="powerhouse/document-editor"] { padding: 1rem; } ``` #### Finding Your Editor ID The `data-editor` value comes from the `id` property in your editor module configuration: ```typescript // editors/my-editor/module.ts export const MyEditor: EditorModule = { config: { id: "my-custom-editor", // <-- This becomes the data-editor value documentTypes: ["my-org/my-document-model"], }, Component: MyEditorComponent, }; ``` #### Common Editor IDs | Editor ID | Document Type | Description | | ------------------------ | ---------------------------- | --------------- | | `document-editor-editor` | `powerhouse/document-editor` | Document Editor | | `vetra-drive-app` | `powerhouse/document-drive` | Drive Explorer | | `app-editor` | `powerhouse/app` | App Editor | **TIP:** You can inspect the `data-editor` and `data-document-type` attributes in your browser's developer tools when editing a document to find the exact values for your target editor. ## Reference Implementation: Vetra Drive App The Vetra Drive App provides a real-world example of CSS customization. Here's how it styles its editor wrapper: ```tsx // From: packages/vetra/editors/vetra-drive-app/editor.tsx
``` This example demonstrates: - **`height: "100%"`** - Ensures the editor fills the full container height - **`bg-gray-50`** - Applies a light gray background color - **`p-6`** - Adds consistent padding around the content - **`after:*` pseudo-element** - Creates a visual effect layer for transitions ## Common Use Cases ### Full-Height Editor with Scrollable Content When your editor needs a fixed header/toolbar with scrollable main content: ```tsx export function Editor() { return (

Document Title

{/* Toolbar buttons */}
{/* Scrollable content area */}
); } ``` ### Custom Background and Theming Apply custom backgrounds or gradients to match your application's theme: ```tsx export function Editor() { return (
{/* Themed content */}
); } ``` ### Centered Content with Max Width Constrain content width for better readability: ```tsx export function Editor() { return (
{/* Centered, width-constrained content */}
); } ``` ## Troubleshooting ### Editor Doesn't Fill Available Height **Problem:** Your editor content appears squished or doesn't use the full height. **Solution:** Ensure your root element has `height: "100%"` or uses flex utilities: ```tsx // Option 1: Inline style
// Option 2: Tailwind class
``` ### Styles Not Applied in Production **Problem:** Styles work in development but not in production builds. **Solution:** Move style imports from `.tsx` files to your `styles.css` file: ```css /* styles.css - correct location for imports */ @import "./editors/my-editor/editor.css"; ``` ### Z-Index Conflicts **Problem:** Overlays, modals, or dropdowns appear behind other elements. **Solution:** The `#document-editor-context` has `position: relative`. Use this as your positioning context: ```tsx
{/* Your content */}
{/* Positioned overlay */}
``` ### Content Overflows Container **Problem:** Content extends beyond the editor boundaries. **Solution:** Add overflow handling to your root element: ```tsx
{/* Scrollable when content overflows */}
// Or hide overflow
{/* Content is clipped */}
``` ## Further Reading - [Building Document Editors](/academy/MasteryTrack/BuildingUserExperiences/BuildingDocumentEditors) - Fundamentals of editor development including basic styling - [Building a Drive Explorer](/academy/MasteryTrack/BuildingUserExperiences/BuildingADriveExplorer) - Creating custom drive apps with styling --- ## Document Toolbar > Source: https://powerhouse.academy/academy/MasteryTrack/BuildingUserExperiences/DocumentTools/DocumentToolbar The `DocumentToolbar` renders a default set of document controls for common document actions: undo, redo, download, document name editing, revision history, opening in Switchboard, and closing the current document view. The toolbar can be customized by enabling or disabling built-in controls, replacing individual controls, adding custom controls to toolbar slots, replacing the toolbar containers, applying custom styles, or replacing the entire toolbar contents with `children`.
Document Toolbar
The Document Toolbar can be found at the top of any generic document.
## Basic usage ```tsx export function MyDocumentPage({ document }) { return ; } ``` When no `document` prop is provided, the toolbar falls back to the currently selected document. ```tsx ``` ## Built-in controls The default toolbar includes these controls: ```ts ["undo", "redo", "download", "name", "history", "switchboard", "close"] ``` The controls are arranged into three slots: ```ts { first: ["undo", "redo", "download"], second: ["name"], third: ["history", "switchboard", "close"], } ``` ## Disabling controls Use `disabledControls` to remove specific built-in controls from the default toolbar. ```tsx ``` ## Enabling only specific controls Use `enabledControls` to render only a subset of the built-in controls. ```tsx ``` ## Combining enabled and disabled controls A control renders only when it is included in `enabledControls` and absent from `disabledControls`. When a control appears in both lists, `disabledControls` takes precedence. ```tsx ``` In this example, only `switchboard` and `close` render. ## Adding a custom control to a slot Use `customControls` to add controls to a specific toolbar slot. Custom controls receive the current document when available. ```tsx ( ), }, }} /> ``` ## Adding a custom control at the end of a slot By default, custom controls render at the start of their slot. Use `position: "end"` to render a custom control after the built-in controls in that slot. ```tsx ( ), }, }} /> ``` ## Adding multiple custom controls to a slot A slot can receive a list of custom controls. Each item needs a `key`. ```tsx ( ), }, { key: "custom-two", position: "end", component: ({ document }) => ( ), }, ], }} /> ``` ## Replacing the toolbar container Use `toolbarContainer` to replace the outer toolbar container. The custom toolbar container receives the toolbar children and the resolved `toolbarClassName`. ```tsx (
{children}
)} /> ``` ## Replacing the controls container Use `controlsContainer` to replace the container used for each toolbar slot. The custom controls container receives the slot children and the resolved `controlsContainerClassName`. ```tsx (
{children}
)} /> ``` ## Replacing a built-in control Use `componentOverrides` to replace individual built-in controls while keeping the default toolbar structure. This is useful when you want to reuse the built-in control behavior or styling but change part of the rendering or click behavior. ```tsx ( alert(props.document?.header.name)} > Download?? ), }} /> ``` The `name` control can also be replaced through `componentOverrides`. ```tsx (