- Introduced a new streaming mechanism for generating TODO lists, allowing for real-time updates and improved user experience. - Added context handling for datasets in both `AnalystAgent` and `ThinkAndPrepAgent`, ensuring relevant data is included in system messages. - Implemented new helper functions for managing TODO state and reasoning messages, improving code organization and clarity. - Updated integration tests to validate the new streaming functionality and ensure consistent message roles. These changes significantly enhance the functionality and maintainability of the TODO creation process within the agent workflow. |
||
---|---|---|
.. | ||
.cursor | ||
drizzle | ||
scripts | ||
src | ||
supabase | ||
.env.example | ||
.gitignore | ||
CLAUDE.md | ||
README.md | ||
biome.json | ||
drizzle.config.ts | ||
env.d.ts | ||
package.json | ||
tsconfig.json | ||
turbo.json | ||
vitest.config.ts |
README.md
@buster/database
A TypeScript database library using Drizzle ORM with PostgreSQL connection pooling. This library provides a centralized database connection and schema management for the Buster application.
Architecture & Patterns
Schema Definition (src/schema.ts
)
The database schema is defined using Drizzle ORM. All table definitions and modifications should be made in this file.
JSONB Columns with Type Safety
For JSONB columns, we use a specific pattern to ensure type safety:
// 1. Define the type in src/schema-types/[entity].ts
export type OrganizationColorPalette = {
id: string | number;
colors: string[]; // Hex color codes
};
export type OrganizationColorPalettes = OrganizationColorPalette[];
// 2. Export from src/schema-types/index.ts
export * from './organization';
// 3. Use in schema.ts with .$type<T>()
import type { OrganizationColorPalettes } from './schema-types';
export const organizations = pgTable('organizations', {
// ... other columns
organizationColorPalettes: jsonb('organization_color_palettes')
.$type<OrganizationColorPalettes>()
.default(sql`'[]'::jsonb`)
.notNull(),
});
Query Organization (src/queries/
)
ALL DATABASE INTERACTIONS MUST GO THROUGH THE QUERIES FOLDER. Direct database access outside of this folder is not allowed.
Directory Structure
queries/
├── shared-types/ # Reusable query utilities and types
│ ├── pagination.types.ts
│ ├── with-pagination.ts
│ └── index.ts
├── organizations/ # Domain-specific queries
│ ├── organizations.ts
│ ├── update-organization.ts
│ └── index.ts
├── users/
│ ├── user.ts
│ ├── users-to-organizations.ts
│ └── index.ts
└── index.ts # Exports all query modules
Query Function Pattern
Every query function follows this pattern:
import { z } from 'zod';
import { type InferSelectModel } from 'drizzle-orm';
import { db } from '../../connection';
import { tableName } from '../../schema';
// 1. Type inference from schema
type TableType = InferSelectModel<typeof tableName>;
// 2. Input validation schema using Zod
const GetSomethingInputSchema = z.object({
id: z.string().uuid('ID must be a valid UUID'),
// ... other fields
});
type GetSomethingInput = z.infer<typeof GetSomethingInputSchema>;
// 3. Query function with proper error handling
export async function getSomething(params: GetSomethingInput): Promise<TableType> {
try {
// Validate input
const validated = GetSomethingInputSchema.parse(params);
// Execute query
const result = await db
.select()
.from(tableName)
.where(/* conditions */)
.limit(1);
if (!result.length || !result[0]) {
throw new Error('Resource not found');
}
return result[0];
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Invalid input: ${error.errors.map((e) => e.message).join(', ')}`
);
}
throw error;
}
}
Pagination Pattern
For paginated queries, use the shared pagination utilities:
import { withPagination, createPaginatedResponse, type PaginatedResponse } from '../shared-types';
const GetUsersInputSchema = z.object({
page: z.number().optional().default(1),
page_size: z.number().optional().default(250),
// ... other filters
});
export async function getUsers(params: GetUsersInput): Promise<PaginatedResponse<User>> {
const { page, page_size } = GetUsersInputSchema.parse(params);
// Build base query
const baseQuery = db
.select()
.from(users)
.where(/* conditions */)
.$dynamic(); // Required for withPagination
// Apply pagination
const getData = withPagination(baseQuery, users.name, page, page_size);
// Get total count
const getTotal = db
.select({ count: count() })
.from(users)
.where(/* same conditions */);
// Execute in parallel
const [data, totalResult] = await Promise.all([getData, getTotal]);
return createPaginatedResponse({
data,
page,
page_size,
total: totalResult[0]?.count ?? 0,
});
}
Type Safety Patterns
Using Pick for Specific Types
When you need only specific fields from multiple tables:
// Type-safe subset using Pick
type OrganizationUser = Pick<User, 'id' | 'name' | 'email' | 'avatarUrl'> &
Pick<UserToOrganization, 'role' | 'status'>;
Update Operations
For update operations, validate input and build update objects dynamically:
const UpdateOrganizationInputSchema = z.object({
organizationId: z.string().uuid(),
organizationColorPalettes: z.array(ColorPaletteSchema).optional(),
});
export async function updateOrganization(params: UpdateOrganizationInput): Promise<void> {
const validated = UpdateOrganizationInputSchema.parse(params);
// Build update data dynamically
const updateData: Partial<Organization> = {
updatedAt: new Date().toISOString(),
};
if (validated.organizationColorPalettes !== undefined) {
updateData.organizationColorPalettes = validated.organizationColorPalettes;
}
await db
.update(organizations)
.set(updateData)
.where(eq(organizations.id, validated.organizationId));
}
Best Practices
- Always use Zod for input validation - Parse inputs before using them in queries
- Export types alongside functions - Make types available for consumers
- Use InferSelectModel for type safety - Derive types from schema definitions
- Handle errors explicitly - Differentiate between validation errors and database errors
- Use transactions for multi-step operations - Ensure data consistency
- Parallel queries when possible - Use
Promise.all()
for independent queries - Consistent naming - Use
get
,create
,update
,delete
prefixes
Adding New Queries
- Create a new folder under
queries/
for your domain if it doesn't exist - Create query files following the pattern above
- Export all functions from an
index.ts
in your domain folder - Add the export to the main
queries/index.ts
Running Migrations
# Generate migration from schema changes
pnpm drizzle-kit generate:pg
# Apply migrations
pnpm run migrations
# Push schema changes directly (development only)
pnpm drizzle-kit push:pg
Testing
Query functions should be tested with integration tests that use a test database. Unit tests can mock the database connection for isolated testing.
// Example test structure
describe('getUserOrganization', () => {
it('should return user organization data', async () => {
const result = await getUserOrganization({ userId: 'valid-uuid' });
expect(result).toMatchObject({
organizationId: expect.any(String),
role: expect.any(String),
});
});
it('should throw on invalid UUID', async () => {
await expect(getUserOrganization({ userId: 'invalid' }))
.rejects.toThrow('User ID must be a valid UUID');
});
});