mirror of https://github.com/buster-so/buster.git
update rules
This commit is contained in:
parent
1ae52ec603
commit
b8de7a5c51
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
||||
```
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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>;
|
||||
|
|
Loading…
Reference in New Issue