update rules

This commit is contained in:
Nate Kelley 2025-07-17 13:12:11 -06:00
parent 1ae52ec603
commit b8de7a5c51
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
5 changed files with 322 additions and 53 deletions

View File

@ -4,12 +4,22 @@ import type { User } from '@supabase/supabase-js';
declare module 'hono' {
interface ContextVariableMap {
supabaseUser: User;
busterUser: BusterUser;
// This is the user's organization ID and role. It is set in the requireOrganizationAdmin and requireOrganization middleware. YOU MUST SET THIS IN THE MIDDLEWARE IF YOU USE THIS CONTEXT VARIABLE.
userOrganizationInfo: {
organizationId: string;
role: OrganizationRole;
/**
* The authenticated Supabase user. This object is readonly to prevent accidental mutation.
*/
readonly supabaseUser: User;
/**
* The Buster user object from the database. This object is readonly to ensure immutability.
*/
readonly busterUser: BusterUser;
/**
* This is the user's organization ID and role. It is set in the requireOrganizationAdmin and requireOrganization middleware.
* YOU MUST SET THIS IN THE MIDDLEWARE IF YOU USE THIS CONTEXT VARIABLE.
* The object and its properties are readonly to prevent mutation.
*/
readonly userOrganizationInfo: {
readonly organizationId: string;
readonly role: OrganizationRole;
};
}
}

View File

@ -1,5 +1,6 @@
import type { OrganizationRole } from '@buster/server-shared/organization';
import { OrganizationRoleEnum } from '@buster/server-shared/organization';
export const isOrganizationAdmin = (role: OrganizationRole) => {
return role === 'workspace_admin' || role === 'data_admin';
return role === OrganizationRoleEnum.workspace_admin || role === OrganizationRoleEnum.data_admin;
};

View File

@ -1,40 +1,177 @@
---
globs: src/*
alwaysApply: false
description: Global rules for the database directory
---
## 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/
│ │ ├── shared-types/ # Reusable query utilities and types
│ ├── 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
│ │ │ └── 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/ # Database schema definitions
│ └── index.ts # Main package exports
└── drizzle/ # Migration files
│ │ │ └── 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
@ -457,3 +594,110 @@ 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
});
});
```

View File

@ -1,37 +1,65 @@
---
globs: src/schema.ts
alwaysApply: true
---
## Database Schema Definition Rules
The database schema is defined using Drizzle ORM in `src/schema.ts`. All table definitions and modifications should be made in this file.
## Adding a New JSONB Column
When adding a new JSONB column to the database schema, it is important to ensure type safety and maintainability by defining the corresponding TypeScript type. Follow these steps:
1. **Define the TypeScript Type**:
- Navigate to the `@/schema-types` directory.
- Navigate to the `src/schema-types` directory.
- Create a new file for your entity if it doesn't exist (e.g., `newEntity.ts`).
- Define the TypeScript type for the JSONB column. For example:
```typescript
export type NewEntityConfig = {
key: string;
value: any; // Use specific types instead of 'any' for better type safety
value: string; // Use specific types instead of 'any' for better type safety
// Add proper typing for all fields
};
// For arrays of objects
export type NewEntityConfigs = NewEntityConfig[];
```
2. **Export the Type**:
- Ensure the new type is exported from the `index.ts` file in the `@/schema-types` directory.
- Ensure the new type is exported from the `index.ts` file in the `src/schema-types` directory:
```typescript
export * from './newEntity';
```
3. **Consume the Type in Schema**:
- In the `src/schema.ts` file, use the defined type for the JSONB column. For example:
- In the `src/schema.ts` file, use the defined type for the JSONB column with the `.$type<T>()` method:
```typescript
import type { NewEntityConfig } from './schema-types';
export const newEntities = pgTable('new_entities', {
// ... other columns
config: jsonb('config').$type<NewEntityConfig>().default(sql`'{}'::jsonb`).notNull(),
config: jsonb('config')
.$type<NewEntityConfig>()
.default(sql`'{}'::jsonb`)
.notNull(),
// For nullable JSONB columns
metadata: jsonb('metadata')
.$type<SomeMetadata>()
.default(sql`null`),
});
```
By following these steps, you ensure that the JSONB column is type-safe and maintainable, leveraging TypeScript's compile-time checks and IntelliSense support.
## Type Inference from Schema
Always use `InferSelectModel` for deriving types from table definitions:
```typescript
import { type InferSelectModel } from 'drizzle-orm';
export type User = InferSelectModel<typeof users>;
```
This ensures types stay in sync with the database schema automatically.
By following these steps, you ensure that the JSONB column is type-safe and maintainable, leveraging TypeScript's compile-time checks and IntelliSense support.

View File

@ -31,17 +31,3 @@ export type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T exte
* type _Check = Expect<Equal<MyType, DatabaseType>>; // Errors if types don't match exactly
*/
export type Expect<T extends true> = T;
/**
* Variable assignment approach for type equality (most reliable)
* Copy this pattern for guaranteed type checking:
*
* @example
* const _check1: MyType = {} as DatabaseType;
* const _check2: DatabaseType = {} as MyType;
*/
/**
* Legacy type equality checker - use Equal + Expect pattern instead
*/
export type IsEqual<T, U> = Equal<T, U>;