buster/packages/database/.cursor/global.mdc

704 lines
19 KiB
Plaintext

## Overview
The database package provides centralized database utilities, query builders, and shared types for consistent data access patterns across the application.
## 🚨 CARDINAL RULE
**ALL DATABASE INTERACTIONS MUST GO THROUGH THE QUERIES FOLDER**
Direct database access outside of the `src/queries/` directory is strictly prohibited. This ensures:
- Consistent error handling
- Input validation with Zod
- Proper type safety
- Centralized business logic
- Easier testing and maintenance
## Directory Structure
```
database/
├── src/
│ ├── queries/ # ALL database interactions go here
│ │ ├── shared-types/ # Reusable query utilities and types
│ │ │ ├── pagination.types.ts # Pagination type definitions
│ │ │ ├── with-pagination.ts # Pagination query utilities
│ │ │ └── index.ts # Exports all shared types
│ │ ├── chats/ # Chat-related queries
│ │ │ ├── chats.ts # Chat query functions
│ │ │ └── index.ts # Exports chat queries
│ │ ├── users/ # User-related queries
│ │ │ ├── user.ts # User query functions
│ │ │ ├── users-to-organizations.ts
│ │ │ ├── users-to-organizations.test.ts
│ │ │ └── index.ts # Exports user queries
│ │ ├── organizations/ # Organization-related queries
│ │ ├── messages/ # Message-related queries
│ │ ├── assets/ # Asset-related queries
│ │ ├── dataSources/ # Data source queries
│ │ ├── metadata/ # Metadata queries
│ │ └── index.ts # Exports all query modules
│ ├── schema.ts # Database schema definitions
│ ├── schema-types/ # TypeScript types for JSONB columns
│ ├── connection.ts # Database connection management
│ └── index.ts # Main package exports
└── drizzle/ # Migration files
```
## Core Query Patterns
### 1. Input Validation with Zod
**Every query function MUST validate its inputs using Zod:**
```typescript
// ✅ ALWAYS define input schema
const GetUserInputSchema = z.object({
userId: z.string().uuid('User ID must be a valid UUID'),
includeDeleted: z.boolean().optional().default(false),
});
type GetUserInput = z.infer<typeof GetUserInputSchema>;
// ✅ ALWAYS validate at the start of the function
export async function getUser(params: GetUserInput) {
const validated = GetUserInputSchema.parse(params);
// Use validated input, not raw params
}
```
### 2. Error Handling Pattern
**Always differentiate between validation errors and database errors:**
```typescript
export async function queryFunction(params: Input) {
try {
// Validate input
const validated = InputSchema.parse(params);
// Execute query
const result = await db.select()...;
if (!result.length || !result[0]) {
throw new Error('Resource not found');
}
return result[0];
} catch (error) {
// ✅ Handle Zod validation errors separately
if (error instanceof z.ZodError) {
throw new Error(
`Invalid input: ${error.errors.map((e) => e.message).join(', ')}`
);
}
// ✅ Log with context
console.error('Error in queryFunction:', {
params,
error: error instanceof Error ? error.message : error,
});
// ✅ Re-throw known errors
if (error instanceof Error) {
throw error;
}
// ✅ Wrap unknown errors
throw new Error('Failed to execute query');
}
}
```
### 3. Naming Conventions
**Use consistent prefixes for all query functions:**
- `get*` - For SELECT queries (single or multiple records)
- `create*` - For INSERT operations
- `update*` - For UPDATE operations
- `delete*` - For DELETE operations (soft or hard)
- `upsert*` - For INSERT ... ON CONFLICT UPDATE operations
```typescript
// ✅ Good naming
export async function getUser(id: string) {}
export async function getUsersByOrganization(orgId: string) {}
export async function createUser(data: CreateUserInput) {}
export async function updateUser(id: string, data: UpdateUserInput) {}
export async function deleteUser(id: string) {}
```
### 4. Update Operations Pattern
**Build update objects dynamically to handle optional fields:**
```typescript
const UpdateOrganizationInputSchema = z.object({
organizationId: z.string().uuid(),
name: z.string().optional(),
settings: z.object({...}).optional(),
colorPalettes: z.array(ColorPaletteSchema).optional(),
});
export async function updateOrganization(params: UpdateOrganizationInput) {
const validated = UpdateOrganizationInputSchema.parse(params);
// ✅ Build update data dynamically
const updateData: Partial<Organization> = {
updatedAt: new Date().toISOString(),
};
// ✅ Only include fields that were provided
if (validated.name !== undefined) {
updateData.name = validated.name;
}
if (validated.settings !== undefined) {
updateData.settings = validated.settings;
}
if (validated.colorPalettes !== undefined) {
updateData.organizationColorPalettes = validated.colorPalettes;
}
// ✅ Ensure we have something to update
if (Object.keys(updateData).length === 1) { // Only updatedAt
throw new Error('No fields to update');
}
await db
.update(organizations)
.set(updateData)
.where(eq(organizations.id, validated.organizationId));
}
```
## Query Organization
The `queries/` directory is organized into domain-specific folders, each containing multiple query files related to that domain:
- **`shared-types/`** - Reusable utilities like pagination, sorting, and filtering that can be used across all domains
- **Domain folders** (`chats/`, `users/`, `organizations/`, etc.) - Each contains:
- Multiple `.ts` files with query functions specific to that domain
- Optional `.test.ts` files for testing query logic
- An `index.ts` file that exports all queries from that domain
- **Root `index.ts`** - Exports all query modules for easy importing
### Domain Query Patterns
Each domain folder should follow this structure:
```typescript
// Example: queries/chats/chats.ts
export async function getChatById(id: string) {
// Chat-specific query logic
}
export async function getChatsForUser(userId: string) {
// More chat queries
}
// queries/chats/index.ts
export * from './chats';
```
---
# Part 1: Building Shared Utilities
This section covers creating reusable utilities in the `shared-types/` folder.
## Pagination Patterns
### Type Definitions (`pagination.types.ts`)
**Always use these standard pagination types:**
```typescript
// ✅ Use the standard pagination input schema
import { PaginationInputSchema } from '@/database/queries/shared-types';
// Validate pagination inputs
const validatedInput = PaginationInputSchema.parse({ page: 1, page_size: 10 });
// ✅ Use the standard response type
import type { PaginatedResponse } from '@/database/queries/shared-types';
function getUsers(): Promise<PaginatedResponse<User>> {
// Returns { data: User[], pagination: PaginationMetadata }
}
```
**Type Hierarchy:**
- `PaginationInput` - For API input validation (page, page_size)
- `PaginationMetadata` - For response metadata (page, page_size, total, total_pages)
- `PaginatedResponse<T>` - Generic response wrapper with data + pagination
- `WithPagination<T>` - Type helper for adding pagination to existing types
### Query Utilities (`with-pagination.ts`)
**Use `withPagination()` for Drizzle queries:**
```typescript
import { withPagination } from '@/database/queries/shared-types';
// ✅ Apply pagination to any Drizzle query
const query = db.select().from(users).$dynamic();
const paginatedQuery = withPagination(query, users.createdAt, page, pageSize);
const results = await paginatedQuery;
```
**Use `createPaginatedResponse()` for custom data:**
```typescript
import { createPaginatedResponse } from '@/database/queries/shared-types';
// ✅ When you already have data and count from separate queries
// Use Promise.all for parallel execution when queries are independent
const [users, total] = await Promise.all([
customUserQuery(),
customCountQuery()
]);
return createPaginatedResponse({
data: users.map(transformUser),
page: 1,
page_size: 10,
total
});
```
> **🚀 Performance Note**: Using `Promise.all()` is crucial here because the user data query and count query are independent operations that can run in parallel. This can reduce response time by up to 50% compared to sequential `await` calls, especially with network latency to the database. Always use `Promise.all()` when you have multiple independent async operations.
## Shared Utilities Best Practices
### 1. File Organization
- **`shared-types/`** - Place reusable query utilities and type definitions here
- **Export pattern** - Always export through `index.ts` for clean imports
- **Naming convention** - Use descriptive names that indicate the utility purpose
### 2. Type Safety Rules
```typescript
// ✅ Always use proper typing with generics
export function withSorting<T extends PgSelect>(
qb: T,
sortColumn?: PgColumn | SQL | SQL.Aliased | null
): T {
// Implementation
}
// ✅ Provide clear type constraints
export interface FilterOptions<TTable> {
where?: PgColumn<TTable> | SQL;
limit?: number;
}
// ❌ Avoid 'any' types in shared utilities
export function badUtility(query: any): any {
// This defeats type safety
}
```
### 3. Schema Validation
```typescript
// ✅ Always provide Zod schemas for input validation
export const SortInputSchema = z.object({
column: z.string(),
direction: z.enum(['asc', 'desc']).default('asc'),
});
export type SortInput = z.infer<typeof SortInputSchema>;
// ✅ Use schemas in your utilities
export function withSorting<T>(qb: T, sort: SortInput): T {
const validatedSort = SortInputSchema.parse(sort);
// Implementation
}
```
### 4. Documentation Standards
```typescript
/**
* Brief description of the utility function
*
* @example
* ```typescript
* // Show realistic usage example
* const query = db.select().from(users).$dynamic();
* const result = withUtility(query, options);
* ```
*
* @param qb - The Drizzle query builder
* @param options - Configuration options
* @returns Modified query builder
*/
export function withUtility<T>(qb: T, options: Options): T {
// Implementation
}
```
## Common Utility Patterns
### 1. Query Builder Extensions
```typescript
// ✅ Create composable query utilities
export function withFilters<T extends PgSelect>(
qb: T,
filters: FilterOptions
): T {
let query = qb;
if (filters.where) {
query = query.where(filters.where);
}
return query;
}
// ✅ Chain utilities together
const results = await withPagination(
withFilters(
db.select().from(users).$dynamic(),
{ where: eq(users.active, true) }
),
users.createdAt,
page,
pageSize
);
```
### 2. Type-Safe Response Builders
```typescript
// ✅ Create helpers for consistent API responses
export function createSuccessResponse<T>(data: T): ApiResponse<T> {
return {
success: true,
data,
error: null,
};
}
export function createErrorResponse(error: string): ApiResponse<never> {
return {
success: false,
data: null,
error,
};
}
```
### 3. Reusable Type Transformers
```typescript
// ✅ Create helpers for common transformations
export type WithTimestamps<T> = T & {
created_at: Date;
updated_at: Date;
};
export type WithId<T> = T & {
id: string;
};
// ✅ Use Pick for type-safe selections
export type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
```
## Anti-Patterns for Shared Utilities
```typescript
// ❌ Don't use 'any' types in shared utilities
export function badQuery(options: any): any { }
// ❌ Don't hard-code pagination limits
export function badPagination(page: number) {
return query.limit(10); // Should be configurable
}
// ❌ Don't create utilities without proper generics
export function badUtility(query: PgSelect) {
// Loses type information
}
// ❌ Don't mix pagination logic in domain-specific queries
export function getUsersWithPagination() {
// Should use withPagination utility instead
}
// ❌ Don't use sequential await for independent operations
export async function badParallelQueries() {
const users = await getUsersQuery(); // Waits unnecessarily
const count = await getCountQuery(); // Could run in parallel
return { users, count };
}
// ✅ Use Promise.all for independent operations
export async function goodParallelQueries() {
const [users, count] = await Promise.all([
getUsersQuery(),
getCountQuery()
]);
return { users, count };
}
```
## When to Add New Shared Types
Add new utilities to `shared-types/` when:
- ✅ The pattern is used in 3+ different queries
- ✅ The utility provides type safety benefits
- ✅ The pattern has reusable business logic
- ✅ It improves consistency across the codebase
Don't add to `shared-types/` when:
- ❌ The utility is domain-specific to one feature
- ❌ It's a simple wrapper without added value
- ❌ The pattern is unlikely to be reused
---
# Part 2: Writing Domain Queries
This section covers best practices for writing query functions in domain folders (like `users/`, `chats/`, `organizations/`, etc.).
## Type Safety and Schema Validation
**Always use type-safe schema types:**
```typescript
// ✅ Use InferSelectModel for type safety
import { type InferSelectModel } from 'drizzle-orm';
type User = InferSelectModel<typeof users>;
type UserToOrganization = InferSelectModel<typeof usersToOrganizations>;
// ✅ Use Pick for type-safe selections instead of full types
type OrganizationUser = Pick<User, 'id' | 'name' | 'email' | 'avatarUrl'> &
Pick<UserToOrganization, 'role' | 'status'>;
```
**Always validate input with Zod schemas:**
```typescript
// ✅ Define input validation schema
const GetUserToOrganizationInputSchema = z.object({
userId: z.string().uuid('User ID must be a valid UUID'),
page: z.number().optional().default(1),
page_size: z.number().optional().default(250),
user_name: z.string().optional(),
email: z.string().optional(),
});
type GetUserToOrganizationInput = z.infer<typeof GetUserToOrganizationInputSchema>;
```
## Query Performance Patterns
**Use Promise.all for independent operations:**
```typescript
// ✅ Execute data and count queries in parallel
const [data, totalResult] = await Promise.all([getData, getTotal]);
```
**Use shared utilities for common patterns:**
```typescript
// ✅ Use withPagination for consistent pagination
const getData = withPagination(
db.select({...}).from(users).$dynamic(),
asc(users.name),
page,
page_size
);
// ✅ Use createPaginatedResponse for consistent API responses
return createPaginatedResponse({
data,
page,
page_size,
total,
});
```
## Error Handling and Logging
**Always wrap queries in try/catch blocks:**
```typescript
// ✅ Proper error handling
try {
const [data, totalResult] = await Promise.all([getData, getTotal]);
return createPaginatedResponse({ data, page, page_size, total });
} catch (error) {
console.error('Error fetching organization users:', error);
throw new Error('Failed to fetch organization users');
}
```
## Documentation Standards
**Document all exported functions with JSDoc:**
```typescript
/**
* Get paginated list of users in the same organization as the requesting user
* with optional filtering by name or email
*/
export const getUserToOrganization = async (
params: GetUserToOrganizationInput
): Promise<PaginatedResponse<OrganizationUser>> => {
// Implementation
};
```
## Query Building Best Practices
**Use dynamic query building with proper conditions:**
```typescript
// ✅ Build where conditions safely
const whereConditions = and(
eq(usersToOrganizations.organizationId, organizationId),
isNull(usersToOrganizations.deletedAt),
user_name ? like(users.name, `%${user_name}%`) : undefined,
email ? like(users.email, `%${email}%`) : undefined,
role ? inArray(usersToOrganizations.role, role) : undefined,
);
```
---
# Import Guidelines
```typescript
// ✅ Use clean imports from shared-types
import {
withPagination,
createPaginatedResponse,
type PaginatedResponse,
type PaginationInput
} from '@/database/queries/shared-types';
// ✅ Import types with 'type' keyword for tree-shaking
import type { PaginationMetadata } from '@/database/queries/shared-types';
```
Don't add to `shared-types/` when:
- ❌ The utility is domain-specific to one feature
- ❌ It's a simple wrapper without added value
- ❌ The pattern is unlikely to be reused # Database Package Cursor Rules
Don't add to `shared-types/` when:
- ❌ The utility is domain-specific to one feature
- ❌ It's a simple wrapper without added value
- ❌ The pattern is unlikely to be reused # Database Package Cursor Rules
---
# Database Migrations
## Running Migrations with pnpm
```bash
# Generate migration from schema changes
pnpm drizzle-kit generate:pg
# Apply migrations to database
pnpm run migrations
# Push schema changes directly (development only)
pnpm drizzle-kit push:pg
# Drop all tables (careful!)
pnpm drizzle-kit drop
# Check migration status
pnpm drizzle-kit check:pg
```
## Migration Best Practices
1. **Always generate migrations for schema changes** - Don't use push:pg in production
2. **Review generated SQL** - Check the generated migration files before applying
3. **Test migrations** - Run migrations on a test database first
4. **Keep migrations small** - One logical change per migration
5. **Never edit applied migrations** - Create new migrations to fix issues
---
# Testing Query Functions
## Integration Testing Pattern
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { getUserOrganization } from './organizations';
import { testDb } from '@/test-utils';
describe('getUserOrganization', () => {
let testUserId: string;
beforeEach(async () => {
// Setup test data
testUserId = await testDb.createTestUser();
});
afterEach(async () => {
// Cleanup test data
await testDb.cleanup();
});
it('should return user organization data', async () => {
const result = await getUserOrganization({ userId: testUserId });
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');
});
it('should return null for non-existent user', async () => {
const result = await getUserOrganization({
userId: '00000000-0000-0000-0000-000000000000'
});
expect(result).toBeNull();
});
});
```
## Unit Testing with Mocks
```typescript
import { vi } from 'vitest';
import { db } from '../../connection';
// Mock the database connection
vi.mock('../../connection', () => ({
db: {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockReturnThis(),
}
}));
describe('getUser (unit)', () => {
it('should validate input', async () => {
// Test pure validation logic without database
});
});
```