Example Workflow: Adding a New Field to Invoice Extraction #
This comprehensive guide walks through the complete process of adding a new field to the BonsAI invoice extraction system. We’ll use a realistic example: adding a title field to invoice_data extraction.
Note: This field already exists in the codebase, but we’ll use it as an example to demonstrate the complete workflow. When implementing your own features, replace “title” with your field name.
Overview #
When adding a new field to invoice extraction, you’ll need to update:
- OpenAPI Schema - Define the field in the API contract
- Generated Code - Run codegen to generate TypeScript and Python models
- Database Schema - Add the column to the database
- BonsAPI (Rust) - Update backend logic to handle the field
- Bonsai-Invoice (Python) - Update extraction logic to extract the field
- Webapp (Next.js) - Update UI to display and edit the field
Time Estimate: 2-4 hours for a simple field addition
1. Environment Setup #
Connect to Your Coder Workspace #
# SSH into your Coder workspace
coder ssh coder bonsai
# Navigate to the project directory
cd ~/bonsai
Create Feature Branch #
Before making any changes, create a feature branch from main using Linear:
- Open your Linear issue (e.g.,
ENG-1234) - Click the “Copy Git Branch Name” icon (top-right)
- Change the issue status to “In Progress”
# Pull latest from main
git checkout main
git pull origin main
# Create your feature branch (paste the Linear branch name)
git checkout -b tofie/eng-1234-add-title-field-to-invoice
Best Practice: Always use the Linear branch name format:
username/eng-####-description
2. Update OpenAPI Schema #
The OpenAPI schema is the source of truth for the API contract. All code generation starts here.
Location #
/home/coder/bonsai/apps/bonsapi/docs/openapi/index.yaml
Find the InvoiceData Schema #
The InvoiceData schema is located around line 6762 in the OpenAPI file:
# Find the exact line number
grep -n "InvoiceData:" /home/coder/bonsai/apps/bonsapi/docs/openapi/index.yaml
Add the Title Field #
Open /home/coder/bonsai/apps/bonsapi/docs/openapi/index.yaml and locate the InvoiceData schema under components.schemas:
Before:
InvoiceData:
type: object
properties:
id:
type: string
format: uuid
entity_id:
type: string
format: uuid
invoice_id:
type: string
format: uuid
description:
type: string
language_id:
type: string
invoice_number:
type: string
# ... other fields
After:
InvoiceData:
type: object
properties:
id:
type: string
format: uuid
entity_id:
type: string
format: uuid
invoice_id:
type: string
format: uuid
title:
type: string
nullable: true
description:
type: string
language_id:
type: string
invoice_number:
type: string
# ... other fields
Key Points:
- Add the field in a logical location (e.g., near related fields like
description)- Set
nullable: trueif the field is optional- Use appropriate OpenAPI types (
string,number,boolean,integer, etc.)- Add a
descriptionfield to document the field’s purpose (recommended)
Field Type Mapping #
| OpenAPI Type | TypeScript Type | Rust Type | Python Type | PostgreSQL Type |
|---|---|---|---|---|
string |
string |
String |
str |
text or varchar |
integer |
number |
i64 |
int |
bigint |
number |
number |
f64 |
float |
decimal |
boolean |
boolean |
bool |
bool |
bool |
string (format: uuid) |
string |
Uuid |
str |
uuid |
3. Generate TypeScript and Python Code #
BonsAI uses code generation to ensure consistency across the stack. The codegen process reads the OpenAPI schema and generates TypeScript and Python models.
Run Codegen #
mise run codegen
What this does:
- Filters OpenAPI spec - Separates internal and external API specs
- Generates TypeScript types - Creates types in
/apps/webapp/src/shared/lib/api/_generated/ - Generates Python models - Creates models in
/libs/python/bonsai-model/bonsai_model/
Expected Output #
β Filtering external OpenAPI spec
β Generating TypeScript types from OpenAPI spec
β Generating Python models from OpenAPI spec
Verify Generated Files #
TypeScript (Frontend):
# Check the generated schema file
cat /home/coder/bonsai/apps/webapp/src/shared/lib/api/_generated/bonsAPI.schemas.ts | grep -A5 "InvoiceData"
You should see the title field added to the InvoiceData interface:
export interface InvoiceData {
id: string;
entity_id: string;
invoice_id: string;
title?: string; // β New field added
description?: string;
language_id?: string;
invoice_number?: string;
// ... other fields
}
Python (Bonsai-Invoice):
# Check the Python model
grep -A20 "class InvoiceData" /home/coder/bonsai/libs/python/bonsai-model/bonsai_model/openapi.py
The generated Python model will include the title field:
class InvoiceData(BaseModel):
id: str
entity_id: str
invoice_id: str
title: Optional[str] = None # β New field added
description: Optional[str] = None
language_id: Optional[str] = None
invoice_number: Optional[str] = None
# ... other fields
Important: Never manually edit generated files. They are overwritten on each codegen run. If you see issues, fix them in the OpenAPI schema and re-run codegen.
4. Update Database Schema #
Now we need to add the title column to the invoice_data table in PostgreSQL.
Location #
/home/coder/bonsai/tools/database/schema.hcl
Add the Column to schema.hcl #
Open /home/coder/bonsai/tools/database/schema.hcl and find the invoice_data table (around line 724):
Before:
table "invoice_data" {
schema = schema.public
comment = "Stores extracted invoice data"
column "id" {
type = uuid
null = false
}
column "entity_id" {
type = uuid
null = false
}
column "invoice_id" {
type = uuid
null = false
}
column "description" {
type = text
null = true
comment = "Additional description or memo for the invoice"
}
# ... other columns
}
After:
table "invoice_data" {
schema = schema.public
comment = "Stores extracted invoice data"
column "id" {
type = uuid
null = false
}
column "entity_id" {
type = uuid
null = false
}
column "invoice_id" {
type = uuid
null = false
}
column "title" {
type = text
null = true
comment = "Title of the invoice document"
}
column "description" {
type = text
null = true
comment = "Additional description or memo for the invoice"
}
# ... other columns
}
Key Points:
- Place the column near related fields (before
descriptionmakes logical sense)- Set
null = truefor optional fields- Add a comment to document the field’s purpose
- Use the appropriate HCL type (see mapping table above)
Generate Migration #
Use Atlas to generate a migration file based on the schema changes:
# Generate the migration diff
mise run db-migrate-diff add_title_to_invoice_data
Expected Output:
Diff changes detected, generating migration file...
-- Modify "invoice_data" table
ALTER TABLE "public"."invoice_data" ADD COLUMN "title" text NULL;
Migration file created: tools/database/migrations/20251021120000_add_title_to_invoice_data.sql
Generate Migration Hash #
Atlas uses hashes to track which migrations have been applied:
mise run db-migrate-hash
Expected Output:
β Generated migration hash
Apply Migration Locally #
Apply the migration to your local development database:
mise run db-migrate-apply
Expected Output:
Applying migration 20251021120000_add_title_to_invoice_data.sql...
-- Modify "invoice_data" table
ALTER TABLE "public"."invoice_data" ADD COLUMN "title" text NULL;
β Migration applied successfully
Verify the Migration #
Check that the column was added:
# Connect to the local database and verify
docker exec -it bonsai-postgres-1 psql -U bonsai -d bonsai -c "\d invoice_data" | grep title
You should see the title column listed in the table structure.
Troubleshooting: If the migration fails, check:
- Is the Docker database container running? (
docker ps | grep postgres)- Is the schema.hcl syntax correct?
- Are there any conflicting migrations?
5. Update BonsAPI Backend (Rust) #
Now we need to update the Rust backend to handle the new title field. This involves updating SQL queries, models, repositories, services, and controllers.
A. Update SQL Queries #
The repository layer contains raw SQL queries that need to be updated to include the title field.
Location: Invoice Data Repository #
/home/coder/bonsai/libs/rust/bonsai-database/src/repository/invoice_data.rs
Update SELECT Queries #
Find all SELECT queries that fetch invoice_data and add the title column:
Example - Before:
SELECT
id,
entity_id,
invoice_id,
description,
language_id,
invoice_number,
-- ... other fields
FROM invoice_data
WHERE id = $1
Example - After:
SELECT
id,
entity_id,
invoice_id,
title,
description,
language_id,
invoice_number,
-- ... other fields
FROM invoice_data
WHERE id = $1
Update INSERT Queries #
Find INSERT queries and add the title field:
Example - Before:
INSERT INTO invoice_data (
id,
entity_id,
invoice_id,
description,
language_id,
invoice_number,
-- ... other fields
) VALUES (
$1, $2, $3, $4, $5, $6, -- ... other params
)
Example - After:
INSERT INTO invoice_data (
id,
entity_id,
invoice_id,
title,
description,
language_id,
invoice_number,
-- ... other fields
) VALUES (
$1, $2, $3, $4, $5, $6, $7, -- ... other params (note: param count increases)
)
Important: When adding a field to an INSERT query, you must also add the corresponding parameter binding (e.g.,
.bind(&invoice_data.title)).
B. Update Rust Models #
The BonsAI Rust models are typically auto-generated or manually maintained in the bonsai-model crate.
Location: Bonsai Model #
/home/coder/bonsai/libs/rust/bonsai-model/src/invoice_data.rs
Update the InvoiceData Struct #
Add the title field to the InvoiceData struct:
Example - Before:
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvoiceData {
pub id: Uuid,
pub entity_id: Uuid,
pub invoice_id: Uuid,
pub description: Option<String>,
pub language_id: Option<String>,
pub invoice_number: Option<String>,
// ... other fields
}
Example - After:
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvoiceData {
pub id: Uuid,
pub entity_id: Uuid,
pub invoice_id: Uuid,
pub title: Option<String>,
pub description: Option<String>,
pub language_id: Option<String>,
pub invoice_number: Option<String>,
// ... other fields
}
Coding Standard: Always import types at the top of the file instead of using fully qualified paths. Group imports: external crates first, then internal crates, then standard library.
C. Update Service Layer #
Services contain the business logic. Update them to pass the title field through the layers.
Location: Invoice Data Service #
/home/coder/bonsai/apps/bonsapi/src/service/invoice_data/create.rs
Update Service Logic #
The service layer typically doesn’t need extensive changes for simple field additions, as it often just passes data through. However, ensure:
- Validation logic (if needed) is added for the
titlefield - Business rules are implemented if the field requires special handling
Example - Adding Validation:
// In the create service
pub async fn create(
mut self,
req: CreateInvoiceDataRequest,
qs: CreateInvoiceDataQs,
api_user: User,
) -> Result<InvoiceDataResponse> {
// Validate title if provided
if let Some(title) = &req.title {
if title.len() > 500 {
return Err(ApiErrorResponse::BadRequest(
"Title must be 500 characters or less".to_string(),
));
}
}
// ... rest of the service logic
}
D. Update Controller to Pass Data to Bonsai-Invoice #
When an invoice is created or updated, BonsAPI sends a message to bonsai-invoice for extraction. Ensure the title field is included in this message.
Location: Document Service (RabbitMQ Message) #
/home/coder/bonsai/apps/bonsapi/src/service/document/create.rs
Update the Job Message #
When sending a job to bonsai-invoice, include the title field in the message payload:
Example - Before:
let job_message = DocumentJobMessage {
invoice_id: invoice.id(),
entity_id: invoice.entity_id(),
organization_id: api_user.organization_id(),
extraction_type: ExtractionType::Full,
document: Some(document_info),
document_pages: vec![],
trace_id: Some(trace_id),
};
Example - After:
// The DocumentJobMessage may need to be updated in the bonsai-mq crate
// to include the title field if it's needed for extraction context
let job_message = DocumentJobMessage {
invoice_id: invoice.id(),
entity_id: invoice.entity_id(),
organization_id: api_user.organization_id(),
extraction_type: ExtractionType::Full,
document: Some(document_info),
document_pages: vec![],
title: invoice_data.title.clone(), // Add title if needed
trace_id: Some(trace_id),
};
Note: In many cases, the extraction service fetches invoice data directly from the database, so you may not need to modify the message payload. Check the extraction flow in your specific implementation.
E. Build and Check for Errors #
After making changes, build the Rust code to check for compilation errors:
# Check for compilation errors
cargo check
# Run clippy for linting
cargo clippy --all-targets --all-features -- -D warnings
# Format the code
cargo fmt --all
Expected Output:
β Checking bonsapi v0.1.0
β Checking bonsai-model v0.1.0
β Checking bonsai-database v0.1.0
β Finished dev [unoptimized + debuginfo] target(s) in 12.34s
6. Update Bonsai-Invoice (Python) #
The bonsai-invoice service is responsible for extracting data from invoice documents using OCR and ML models. We need to update it to extract the title field.
Location #
/home/coder/bonsai/apps/bonsai-invoice/bonsai_invoice/jobs/document.py
A. Update Extraction Logic #
The extraction logic is typically handled by the Hinoki library, which processes invoice documents and extracts fields.
Example: Extracting Title from Invoice #
Location of Extraction:
/home/coder/bonsai/libs/python/bonsai-hinoki/hinoki/modules/invoice_extraction_processor.py
Example - Adding Title Extraction:
async def extract_invoice_information(self) -> Optional[ExtractionResult]:
"""
Extract invoice information from the document.
"""
# ... existing extraction logic
# Extract title (this is a simplified example)
# In reality, you would use ML models or OCR to extract this
title = await self._extract_title_from_document()
# Add title to the extraction result
extraction_result = ExtractionResult(
invoice_number=invoice_number,
invoice_date=invoice_date,
due_date=due_date,
title=title, # β New field
contact_name=contact_name,
total_amount=total_amount,
# ... other fields
)
return extraction_result
Example - Title Extraction Helper:
async def _extract_title_from_document(self) -> Optional[str]:
"""
Extract the title from the invoice document.
This could come from the document header, metadata, or OCR.
"""
# Example: Look for title in the document header
# This is a simplified implementation
ocr_text = await self._get_ocr_text()
# Simple heuristic: First line of the document
lines = ocr_text.split('\n')
if lines:
# Use the first non-empty line as the title
for line in lines:
if line.strip():
return line.strip()
return None
B. Update the BonsAPI Request #
After extraction, bonsai-invoice sends the extracted data back to BonsAPI. Ensure the title field is included:
Location:
/home/coder/bonsai/libs/python/bonsai-hinoki/hinoki/utils/bonsapi.py
Example - Before:
async def create_invoice_data(
self,
invoice_id: str,
extraction_result: ExtractionResult,
) -> InvoiceData:
"""
Create invoice data in BonsAPI.
"""
payload = {
"entity_id": self.entity_id,
"invoice_id": invoice_id,
"invoice_number": extraction_result.invoice_number,
"invoice_date": extraction_result.invoice_date,
"description": extraction_result.description,
"contact_name": extraction_result.contact_name,
# ... other fields
}
response = await self.client.post(
f"/api/v1/invoice-data",
json=payload,
)
return InvoiceData(**response.json())
Example - After:
async def create_invoice_data(
self,
invoice_id: str,
extraction_result: ExtractionResult,
) -> InvoiceData:
"""
Create invoice data in BonsAPI.
"""
payload = {
"entity_id": self.entity_id,
"invoice_id": invoice_id,
"invoice_number": extraction_result.invoice_number,
"invoice_date": extraction_result.invoice_date,
"title": extraction_result.title, # β New field
"description": extraction_result.description,
"contact_name": extraction_result.contact_name,
# ... other fields
}
response = await self.client.post(
f"/api/v1/invoice-data",
json=payload,
)
return InvoiceData(**response.json())
C. Update Python Models (if needed) #
If you manually maintain Python models (outside of the generated bonsai_model package), update them:
Location:
/home/coder/bonsai/libs/python/bonsai-hinoki/hinoki/types/response.py
Example:
from typing import Optional
from pydantic import BaseModel
class ExtractionResult(BaseModel):
"""
Result of invoice extraction.
"""
invoice_number: Optional[str] = None
invoice_date: Optional[int] = None
due_date: Optional[int] = None
title: Optional[str] = None # β New field
description: Optional[str] = None
contact_name: Optional[str] = None
# ... other fields
D. Run Python Formatting and Linting #
# Format Python code
mise run python-format-fix
# Run Python type checking and linting
mise run python-ci
Expected Output:
β Running ruff format
β Running mypy type checking
β Running ruff linting
β All checks passed
7. Update Webapp (Next.js/TypeScript) #
Finally, we need to update the frontend to display and allow editing of the title field.
A. Update Invoice Form Schema #
The form schema defines validation rules for the invoice form.
Location #
/home/coder/bonsai/apps/webapp/src/features/review/components/extracted-data/form/invoice-schema.ts
Update the Schema #
The title field should already be in the generated types, but we need to add validation rules:
Before:
export const invoiceInputSchema = z.object({
description: z.string().nullable().optional(),
invoice_number: z.string(),
invoice_date: z.string(),
due_date: z.string(),
currency_id: z.string().nullable(),
supplier_name: z.string(),
// ... other fields
});
After:
export const invoiceInputSchema = z.object({
title: z
.string()
.max(500, "Title must be 500 characters or less")
.nullable()
.optional(),
description: z.string().nullable().optional(),
invoice_number: z.string(),
invoice_date: z.string(),
due_date: z.string(),
currency_id: z.string().nullable(),
supplier_name: z.string(),
// ... other fields
});
Validation Best Practices:
- Add
.max()to limit string length- Add
.min()if there’s a minimum requirement- Use
.email()for email fields- Use
.regex()for pattern matching- Use
.refine()for custom validation logic
B. Add Title Input to Invoice Form #
Add an input field for the title in the invoice form UI.
Location #
/home/coder/bonsai/apps/webapp/src/features/review/components/extracted-data/form/invoice-form.tsx
Add the Title Field #
Example - Before:
export function InvoiceForm() {
const form = useInvoiceForm();
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Description field */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Other fields */}
</form>
</Form>
);
}
Example - After:
export function InvoiceForm() {
const form = useInvoiceForm();
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Title field - NEW */}
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input
{...field}
placeholder="Enter invoice title (optional)"
maxLength={500}
/>
</FormControl>
<FormDescription>
A descriptive title for this invoice document
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{/* Description field */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Other fields */}
</form>
</Form>
);
}
C. Update Form Initialization #
Ensure the form is initialized with the title value from the API response.
Location #
/home/coder/bonsai/apps/webapp/src/features/review/hooks/use-invoice-form.tsx
Update Default Values #
Example:
export function useInvoiceForm(invoiceData?: InvoiceData) {
const form = useForm<InvoiceFormValues>({
resolver: zodResolver(invoiceInputSchema),
defaultValues: {
title: invoiceData?.title ?? '', // β Initialize from API data
description: invoiceData?.description ?? '',
invoice_number: invoiceData?.invoice_number ?? '',
invoice_date: invoiceData?.invoice_date ?? '',
// ... other fields
},
});
return form;
}
D. Update API Mutation #
Ensure the title field is included when creating or updating invoice data.
Location #
/home/coder/bonsai/apps/webapp/src/features/review/hooks/use-invoice-form.tsx
Update the Mutation #
Example:
const mutation = useMutation({
mutationFn: async (values: InvoiceFormValues) => {
return await createInvoiceData({
entity_id: entityId,
invoice_id: invoiceId,
title: values.title, // β Include title
description: values.description,
invoice_number: values.invoice_number,
invoice_date: values.invoice_date,
// ... other fields
});
},
});
E. Run Frontend Checks #
# Run type checking and linting
pnpm webapp check
# Auto-fix linting issues
pnpm webapp check:fix
Expected Output:
β Type checking passed
β ESLint passed
β All checks passed
8. Testing & Code Quality #
Before committing, ensure all checks pass and the feature works correctly.
A. Start Development Environment #
# Start all services
mise run dev
Wait for all services to start (this takes 2-3 minutes):
β webapp - http://localhost:3000
β bonsapi - http://localhost:8080
β bonsai-invoice - Running
β postgres - Running
β redis - Running
β rabbitmq - Running
B. Manual Testing #
- Open the webapp at http://localhost:3000
- Upload an invoice document
- Verify the extraction:
- Check that the
titlefield appears in the form - Try entering a title manually
- Save the invoice and verify it persists
- Check that the
- Check the database:
docker exec -it bonsai-postgres-1 psql -U bonsai -d bonsai -c "SELECT id, title FROM invoice_data ORDER BY created_at DESC LIMIT 5;"
C. Run Code Quality Checks #
# Auto-fix formatting and linting
mise run fix
# Run all CI checks (linting, type checking, tests)
mise run ci
Expected Output:
β Webapp type checking
β Webapp linting
β Rust formatting check
β Rust clippy
β Python formatting
β Python type checking
β Python linting
β All checks passed
D. Fix Any Issues #
If there are errors:
- TypeScript errors: Check the generated types match your usage
- Rust errors: Check SQL queries have the correct number of parameters
- Python errors: Check imports and type hints
- Linting errors: Run
mise run fixto auto-fix most issues
9. Release Notes & Commit #
BonsAI uses Hasami for release management. You must create release notes before committing your changes.
A. Analyze Your Changes #
# See what files changed
git status
# See the diff
git diff main
B. Create Release Notes with Hasami #
You can use Hasami interactively or via command:
Interactive Mode:
hasami
Follow the prompts:
- Project: Select the affected projects (e.g.,
webapp,bonsapi,bonsai-invoice) - Type: Choose
feature(new functionality),bugfix(fix), ormisc(other) - Bump: Choose
patch(0.0.x),minor(0.x.0), ormajor(x.0.0) - Description: Write release notes
Command Mode (Faster):
# For BonsAPI changes
hasami add -p bonsapi -t feature -b minor "Added title field to invoice_data extraction"
# For Bonsai-Invoice changes
hasami add -p bonsai-invoice -t feature -b minor "Added title extraction from invoice documents"
# For Webapp changes
hasami add -p webapp -t feature -b minor "Added title field to invoice form UI"
Best Practices:
- Create separate release notes for each affected project
- Write user-facing descriptions (not technical details)
- Use
minorbump for new features,patchfor small fixes- Use
majorbump for breaking changes
Generated Files:
Hasami creates markdown files in .hasami/:
.hasami/
βββ 20251021-bonsapi-feature-minor.md
βββ 20251021-bonsai-invoice-feature-minor.md
βββ 20251021-webapp-feature-minor.md
C. Commit Your Changes #
# Stage all changes
git add .
# Commit with a clear message
git commit -m "feat: add title field to invoice extraction
- Updated OpenAPI schema with title field
- Added title column to invoice_data table
- Updated BonsAPI to handle title field
- Updated bonsai-invoice extraction to extract title
- Added title input to webapp invoice form
- Generated migration for database schema change"
Commit Message Format:
- Use conventional commit format:
feat:,fix:,chore:, etc.- First line: Brief summary (50 chars or less)
- Body: Detailed bullet points of what changed
D. Push to GitHub #
# Push your branch
git push -u origin koki/eng-1234-add-title-field-to-invoice
10. Create Pull Request #
A. Create PR on GitHub #
- Go to GitHub Pull Requests
- Click “New pull request”
- Select your branch:
koki/eng-1234-add-title-field-to-invoice - Click “Create pull request”
B. Fill Out PR Description #
Title:
feat: Add title field to invoice extraction
Description Template:
## Summary
Added a `title` field to invoice data extraction. This allows users to view and edit a descriptive title for each invoice document.
## Changes
### Backend (BonsAPI)
- Added `title` field to `InvoiceData` schema in OpenAPI spec
- Updated database schema with `title` column in `invoice_data` table
- Generated and applied database migration
- Updated Rust models and SQL queries to include `title`
### Extraction (Bonsai-Invoice)
- Updated extraction logic to extract title from invoice documents
- Added title extraction helper function
- Updated API payload to include title field
### Frontend (Webapp)
- Added `title` input field to invoice form
- Added validation for title field (max 500 characters)
- Updated form schema and initialization
## Testing
- [x] Manually tested invoice upload and extraction
- [x] Verified title field displays in UI
- [x] Verified title persists to database
- [x] Ran `mise run ci` successfully
- [x] No TypeScript errors
- [x] No Rust clippy warnings
## Screenshots
[Add screenshots if applicable]
## Related Issues
- Closes ENG-1234
C. Post to Slack #
Post the PR link to #eng-pr-review on Slack:
π PR ready for review: Add title field to invoice extraction
https://github.com/tofu2-limited/bonsai/pull/1234
This adds a new `title` field to invoice extraction, allowing users to view and edit invoice titles.
D. Wait for Review #
- Code owners will automatically be assigned as reviewers
- If your Linear issue has a
previewlabel, a preview environment will be automatically deployed - Address any review comments
- Once approved, squash merge your PR into
main
11. Tips & Common Issues #
Common Pitfalls #
1. Forgetting to Run Codegen #
Symptom: TypeScript or Python types are missing the new field
Solution:
mise run codegen
2. SQL Query Parameter Mismatch #
Symptom: Rust compiler error about mismatched number of parameters
error: this function takes 7 parameters but 6 parameters were supplied
Solution: Check that your SQL query has the same number of placeholders ($1, $2, etc.) as .bind() calls:
// Query has 7 placeholders
sqlx::query("INSERT INTO invoice_data (id, entity_id, invoice_id, title, ...) VALUES ($1, $2, $3, $4, ...)")
.bind(&id) // $1
.bind(&entity_id) // $2
.bind(&invoice_id) // $3
.bind(&title) // $4 - Don't forget this!
// ... more binds
3. Migration Already Applied #
Symptom: Migration fails because the column already exists
ERROR: column "title" of relation "invoice_data" already exists
Solution: Your database already has the column. Either:
- Drop the migration file if it hasn’t been pushed
- Create a new migration to revert it
- Manually remove the column from your local DB
# Manually remove the column (local only!)
docker exec -it bonsai-postgres-1 psql -U bonsai -d bonsai -c "ALTER TABLE invoice_data DROP COLUMN title;"
4. Field Not Appearing in UI #
Symptom: The field doesn’t show up in the form
Solution: Check these things in order:
- Did you run
mise run codegen? - Did you add the field to
invoice-schema.ts? - Did you add a
<FormField>ininvoice-form.tsx? - Did you restart the webapp? (
docker compose restart webapp) - Did you clear your browser cache?
5. Field Not Persisting to Database #
Symptom: The field is entered in the UI but doesn’t save
Solution: Check:
- Is the field in the API request payload? (Check browser DevTools Network tab)
- Is the field in the Rust
CreateInvoiceDataRequestmodel? - Is the field in the SQL INSERT query?
- Is the field being bound in the repository layer?
Debugging Tips #
Check What’s Running #
# Check Docker containers
docker ps
# Check logs for a specific service
docker logs -f bonsai-webapp-1
docker logs -f bonsai-bonsapi-1
docker logs -f bonsai-invoice-1
Check Database State #
# Connect to PostgreSQL
docker exec -it bonsai-postgres-1 psql -U bonsai -d bonsai
# View table structure
\d invoice_data
# Query recent records
SELECT id, title, invoice_number, created_at FROM invoice_data ORDER BY created_at DESC LIMIT 10;
# Exit PostgreSQL
\q
Check API Requests #
Use browser DevTools:
- Open DevTools (F12)
- Go to Network tab
- Filter by Fetch/XHR
- Look for requests to
/api/v1/invoice-data - Check the Payload tab to see what’s being sent
Check Rust Compilation #
# Verbose build
cargo build --verbose
# Check a specific package
cargo check -p bonsapi
# Run tests
cargo test
Performance Considerations #
Database Indexing #
If the title field will be frequently searched or filtered, consider adding an index:
// In schema.hcl
index "invoice_data_title_idx" {
columns = [column.title]
type = GIN // Use GIN for text search
}
Then regenerate the migration:
mise run db-migrate-diff add_title_index
mise run db-migrate-hash
mise run db-migrate-apply
Extraction Performance #
If title extraction is slow:
- Cache OCR results to avoid re-processing
- Use simpler heuristics for title detection (e.g., first line)
- Run extraction in parallel with other fields
Where to Find Help #
- BonsAI Internal Docs: https://doc.internal.gotofu.com
- API Documentation: http://localhost:8001 (Swagger UI)
- Slack Channels:
#eng-general- General engineering questions#eng-pr-review- PR review requests
- Linear: Link your issue for context
Useful Commands Reference #
# Development
mise run dev # Start all services
mise run down # Stop all services
mise run codegen # Generate types from OpenAPI
# Database
mise run db-migrate-diff add_my_field # Create migration
mise run db-migrate-hash # Hash migrations
mise run db-migrate-apply # Apply migrations
mise run db-migrate-status # Check migration status
# Code Quality
mise run fix # Auto-fix formatting and linting
mise run ci # Run all CI checks
mise run lint # Lint code
mise run format # Format code
# Release Management
hasami # Interactive release notes
hasami add -p PROJECT -t TYPE -b BUMP "description" # Quick release note
# Git
git status # See changes
git diff main # See diff from main
git add . # Stage all changes
git commit -m "message" # Commit changes
git push -u origin BRANCH # Push to GitHub
Summary Checklist #
Use this checklist to ensure you’ve completed all steps:
-
Environment Setup
- SSH into Coder workspace
- Pull latest from main
- Create feature branch from Linear
-
OpenAPI Schema
- Add field to
InvoiceDataschema - Set appropriate type and nullable
- Add field to
-
Code Generation
- Run
mise run codegen - Verify TypeScript types generated
- Verify Python models generated
- Run
-
Database Schema
- Add column to
invoice_datatable inschema.hcl - Run
mise run db-migrate-diff - Run
mise run db-migrate-hash - Run
mise run db-migrate-apply - Verify migration applied
- Add column to
-
BonsAPI (Rust)
- Update SQL SELECT queries
- Update SQL INSERT queries
- Update Rust models
- Update repository bindings
- Run
cargo checkandcargo clippy
-
Bonsai-Invoice (Python)
- Update extraction logic
- Update API payload
- Run
mise run python-ci
-
Webapp (TypeScript)
- Update form schema with validation
- Add input field to form UI
- Update form initialization
- Update API mutation
- Run
pnpm webapp check
-
Testing
- Manual testing in UI
- Verify database persistence
- Run
mise run ci
-
Release Notes & Commit
- Create release notes with Hasami
- Commit changes with clear message
- Push to GitHub
-
Pull Request
- Create PR on GitHub
- Fill out description
- Post to Slack
- Address review comments
- Merge when approved
π€ Automating with Cursor Commands #
You can automate much of steps 8-9 using Cursor’s built-in commands:
# After staging your changes:
git add <files>
# In Cursor, run these commands:
/release-note # Automatically generates Hasami release notes
/commit # Automatically creates conventional commit message
# Then push:
git push -u origin <branch-name>
See the Cursor IDE Guide for complete documentation on automation commands.
For more information, see:
- Development Workflow - Complete development process
- Cursor IDE Guide - Automation commands and best practices
- Release Management with Hasami - Release note workflow
- Database Migrations - Atlas migration guide
- API Documentation - API reference and endpoints