Example Workflow: Adding a New Field

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:

  1. OpenAPI Schema - Define the field in the API contract
  2. Generated Code - Run codegen to generate TypeScript and Python models
  3. Database Schema - Add the column to the database
  4. BonsAPI (Rust) - Update backend logic to handle the field
  5. Bonsai-Invoice (Python) - Update extraction logic to extract the field
  6. 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:

  1. Open your Linear issue (e.g., ENG-1234)
  2. Click the “Copy Git Branch Name” icon (top-right)
  3. 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: true if the field is optional
  • Use appropriate OpenAPI types (string, number, boolean, integer, etc.)
  • Add a description field 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:

  1. Filters OpenAPI spec - Separates internal and external API specs
  2. Generates TypeScript types - Creates types in /apps/webapp/src/shared/lib/api/_generated/
  3. 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 description makes logical sense)
  • Set null = true for 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:

  1. Validation logic (if needed) is added for the title field
  2. 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 #

  1. Open the webapp at http://localhost:3000
  2. Upload an invoice document
  3. Verify the extraction:
    • Check that the title field appears in the form
    • Try entering a title manually
    • Save the invoice and verify it persists
  4. 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:

  1. TypeScript errors: Check the generated types match your usage
  2. Rust errors: Check SQL queries have the correct number of parameters
  3. Python errors: Check imports and type hints
  4. Linting errors: Run mise run fix to 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:

  1. Project: Select the affected projects (e.g., webapp, bonsapi, bonsai-invoice)
  2. Type: Choose feature (new functionality), bugfix (fix), or misc (other)
  3. Bump: Choose patch (0.0.x), minor (0.x.0), or major (x.0.0)
  4. 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 minor bump for new features, patch for small fixes
  • Use major bump 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 #

  1. Go to GitHub Pull Requests
  2. Click “New pull request”
  3. Select your branch: koki/eng-1234-add-title-field-to-invoice
  4. 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 preview label, 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:

  1. Did you run mise run codegen?
  2. Did you add the field to invoice-schema.ts?
  3. Did you add a <FormField> in invoice-form.tsx?
  4. Did you restart the webapp? (docker compose restart webapp)
  5. 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:

  1. Is the field in the API request payload? (Check browser DevTools Network tab)
  2. Is the field in the Rust CreateInvoiceDataRequest model?
  3. Is the field in the SQL INSERT query?
  4. 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:

  1. Open DevTools (F12)
  2. Go to Network tab
  3. Filter by Fetch/XHR
  4. Look for requests to /api/v1/invoice-data
  5. 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:

  1. Cache OCR results to avoid re-processing
  2. Use simpler heuristics for title detection (e.g., first line)
  3. 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 InvoiceData schema
    • Set appropriate type and nullable
  • Code Generation

    • Run mise run codegen
    • Verify TypeScript types generated
    • Verify Python models generated
  • Database Schema

    • Add column to invoice_data table in schema.hcl
    • Run mise run db-migrate-diff
    • Run mise run db-migrate-hash
    • Run mise run db-migrate-apply
    • Verify migration applied
  • BonsAPI (Rust)

    • Update SQL SELECT queries
    • Update SQL INSERT queries
    • Update Rust models
    • Update repository bindings
    • Run cargo check and cargo 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: