mirror of https://github.com/buster-so/buster.git
claude rules and schema tweak
This commit is contained in:
parent
5941d6975d
commit
8e9e9a087e
39
CLAUDE.md
39
CLAUDE.md
|
@ -122,3 +122,42 @@ pnpm run test:watch
|
||||||
- Implement comprehensive unit tests for error scenarios
|
- Implement comprehensive unit tests for error scenarios
|
||||||
- Log errors strategically for effective debugging
|
- Log errors strategically for effective debugging
|
||||||
- Avoid over-logging while ensuring sufficient context for troubleshooting
|
- Avoid over-logging while ensuring sufficient context for troubleshooting
|
||||||
|
|
||||||
|
## Hono API Development Guidelines
|
||||||
|
|
||||||
|
### API Structure and Organization
|
||||||
|
- **Version-based organization** - APIs are organized under `/api/v2/` directory
|
||||||
|
- **Feature-based folders** - Each feature gets its own folder (e.g., `chats/`, `security/`)
|
||||||
|
- **Separate handler files** - Each endpoint handler must be in its own file
|
||||||
|
- **Functional handlers** - All handlers should be pure functions that accept request data and return response data
|
||||||
|
|
||||||
|
### Request/Response Type Safety
|
||||||
|
- **Use shared types** - All request and response types must be defined in `@buster/server-shared`
|
||||||
|
- **Zod schemas** - Define schemas in server-shared and export both the schema and inferred types
|
||||||
|
- **zValidator middleware** - Always use `zValidator` from `@hono/zod-validator` for request validation
|
||||||
|
- **Type imports** - Import types from server-shared packages for consistency
|
||||||
|
|
||||||
|
### Handler Pattern
|
||||||
|
```typescript
|
||||||
|
// Handler file (e.g., get-workspace-settings.ts)
|
||||||
|
import type { GetWorkspaceSettingsResponse } from '@buster/server-shared/security';
|
||||||
|
import type { User } from '@buster/database';
|
||||||
|
|
||||||
|
export async function getWorkspaceSettingsHandler(
|
||||||
|
user: User
|
||||||
|
): Promise<GetWorkspaceSettingsResponse> {
|
||||||
|
// Implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route definition (index.ts)
|
||||||
|
.get('/workspace-settings', async (c) => {
|
||||||
|
const user = c.get('busterUser');
|
||||||
|
const response = await getWorkspaceSettingsHandler(user);
|
||||||
|
return c.json(response);
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication and User Context
|
||||||
|
- **Use requireAuth middleware** - Apply to all protected routes
|
||||||
|
- **Extract user context** - Use `c.get('busterUser')` to get the authenticated user
|
||||||
|
- **Type as User** - Import `User` type from `@buster/database` for handler parameters
|
|
@ -0,0 +1,356 @@
|
||||||
|
# Hono Server Development Guidelines
|
||||||
|
|
||||||
|
This document provides specific guidelines for developing the Hono-based backend server in this monorepo.
|
||||||
|
|
||||||
|
## API Development Standards
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
```
|
||||||
|
apps/server/src/api/
|
||||||
|
├── v2/ # Current API version
|
||||||
|
│ ├── chats/ # Feature folder
|
||||||
|
│ │ ├── index.ts # Route definitions
|
||||||
|
│ │ ├── handler.ts # Main handler (if single)
|
||||||
|
│ │ ├── create-chat.ts # Individual handlers
|
||||||
|
│ │ └── services/ # Business logic
|
||||||
|
│ └── security/ # Another feature
|
||||||
|
│ ├── index.ts
|
||||||
|
│ ├── get-workspace-settings.ts
|
||||||
|
│ └── update-workspace-settings.ts
|
||||||
|
└── healthcheck.ts # Non-versioned endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
### Route Definition Pattern
|
||||||
|
|
||||||
|
Always follow this pattern in `index.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { RequestSchema } from '@buster/server-shared/feature';
|
||||||
|
import { zValidator } from '@hono/zod-validator';
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { requireAuth } from '../../../middleware/auth';
|
||||||
|
import '../../../types/hono.types';
|
||||||
|
import { HTTPException } from 'hono/http-exception';
|
||||||
|
import { handlerFunction } from './handler-file';
|
||||||
|
|
||||||
|
const app = new Hono()
|
||||||
|
// Apply authentication middleware
|
||||||
|
.use('*', requireAuth)
|
||||||
|
|
||||||
|
// GET endpoint (no body validation)
|
||||||
|
.get('/endpoint', async (c) => {
|
||||||
|
const user = c.get('busterUser');
|
||||||
|
const response = await handlerFunction(user);
|
||||||
|
return c.json(response);
|
||||||
|
})
|
||||||
|
|
||||||
|
// POST/PATCH/PUT endpoint (with body validation)
|
||||||
|
.post('/endpoint', zValidator('json', RequestSchema), async (c) => {
|
||||||
|
const request = c.req.valid('json');
|
||||||
|
const user = c.get('busterUser');
|
||||||
|
const response = await handlerFunction(request, user);
|
||||||
|
return c.json(response);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Error handling
|
||||||
|
.onError((e, c) => {
|
||||||
|
if (e instanceof HTTPException) {
|
||||||
|
return e.getResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HTTPException(500, {
|
||||||
|
message: 'Internal server error',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handler Function Pattern
|
||||||
|
|
||||||
|
Each handler must:
|
||||||
|
1. Be in its own file
|
||||||
|
2. Be a pure async function
|
||||||
|
3. Accept typed parameters
|
||||||
|
4. Return typed responses
|
||||||
|
5. Handle business logic or delegate to services
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import type {
|
||||||
|
FeatureRequest,
|
||||||
|
FeatureResponse
|
||||||
|
} from '@buster/server-shared/feature';
|
||||||
|
import type { User } from '@buster/database';
|
||||||
|
|
||||||
|
export async function featureHandler(
|
||||||
|
request: FeatureRequest,
|
||||||
|
user: User
|
||||||
|
): Promise<FeatureResponse> {
|
||||||
|
// TODO: Implement business logic
|
||||||
|
|
||||||
|
// For complex logic, delegate to service functions
|
||||||
|
// const result = await featureService.process(request, user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// Response data matching FeatureResponse type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Safety Requirements
|
||||||
|
|
||||||
|
1. **Request/Response Types**: Define all types in `@buster/server-shared`
|
||||||
|
```typescript
|
||||||
|
// In @buster/server-shared/feature/requests.ts
|
||||||
|
export const FeatureRequestSchema = z.object({
|
||||||
|
field: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FeatureRequest = z.infer<typeof FeatureRequestSchema>;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Validation**: Always use `zValidator` for request body validation
|
||||||
|
```typescript
|
||||||
|
.post('/endpoint', zValidator('json', RequestSchema), async (c) => {
|
||||||
|
const request = c.req.valid('json'); // Fully typed
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **User Type**: Always use `User` from `@buster/database`
|
||||||
|
```typescript
|
||||||
|
import type { User } from '@buster/database';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Pattern
|
||||||
|
|
||||||
|
1. Apply `requireAuth` middleware to all protected routes
|
||||||
|
2. Extract user with `c.get('busterUser')`
|
||||||
|
3. Pass user to handler functions
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const app = new Hono()
|
||||||
|
.use('*', requireAuth) // Protects all routes in this app
|
||||||
|
.get('/protected', async (c) => {
|
||||||
|
const user = c.get('busterUser'); // Type: User from @buster/database
|
||||||
|
// Use user in handler
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
1. **Custom Errors**: Create domain-specific error classes
|
||||||
|
```typescript
|
||||||
|
export class FeatureError extends Error {
|
||||||
|
constructor(
|
||||||
|
public code: string,
|
||||||
|
message: string,
|
||||||
|
public statusCode: number
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Error Responses**: Handle in route error handler
|
||||||
|
```typescript
|
||||||
|
.onError((e, c) => {
|
||||||
|
if (e instanceof FeatureError) {
|
||||||
|
return c.json({ error: e.message, code: e.code }, e.statusCode);
|
||||||
|
}
|
||||||
|
// Default error handling
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Guidelines
|
||||||
|
|
||||||
|
1. **Unit Tests**: Test handlers in isolation
|
||||||
|
```typescript
|
||||||
|
// handler.test.ts
|
||||||
|
describe('featureHandler', () => {
|
||||||
|
it('should process valid request', async () => {
|
||||||
|
const mockUser = { id: '123' } as User;
|
||||||
|
const request = { field: 'value' };
|
||||||
|
|
||||||
|
const result = await featureHandler(request, mockUser);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
// Expected response
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Integration Tests**: Test full API routes
|
||||||
|
```typescript
|
||||||
|
// index.test.ts
|
||||||
|
import { testClient } from 'hono/testing';
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Separation of Concerns**
|
||||||
|
- Routes: Handle HTTP concerns only
|
||||||
|
- Handlers: Coordinate business logic
|
||||||
|
- Services: Implement business logic
|
||||||
|
- Repositories: Handle data access
|
||||||
|
|
||||||
|
2. **Consistent Naming**
|
||||||
|
- Handler files: `verb-resource.ts` (e.g., `get-user.ts`, `update-settings.ts`)
|
||||||
|
- Handler functions: `verbResourceHandler` (e.g., `getUserHandler`)
|
||||||
|
- Service files: `resource-service.ts`
|
||||||
|
|
||||||
|
3. **Response Validation**
|
||||||
|
- Consider validating responses in development
|
||||||
|
- Use `.safeParse()` for critical endpoints
|
||||||
|
|
||||||
|
4. **Logging**
|
||||||
|
- Log errors with context
|
||||||
|
- Use appropriate log levels
|
||||||
|
- Include user context where relevant
|
||||||
|
|
||||||
|
5. **Performance**
|
||||||
|
- Keep handlers lightweight
|
||||||
|
- Delegate heavy computation to background jobs
|
||||||
|
- Monitor response times
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Pagination
|
||||||
|
```typescript
|
||||||
|
// In request schema
|
||||||
|
export const ListRequestSchema = z.object({
|
||||||
|
page: z.number().min(1).default(1),
|
||||||
|
limit: z.number().min(1).max(100).default(20),
|
||||||
|
});
|
||||||
|
|
||||||
|
// In handler
|
||||||
|
const offset = (request.page - 1) * request.limit;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Query Parameters
|
||||||
|
```typescript
|
||||||
|
// Define query schema in server-shared
|
||||||
|
export const ListItemsQuerySchema = z.object({
|
||||||
|
search: z.string().optional(),
|
||||||
|
status: z.enum(['active', 'inactive', 'all']).default('all'),
|
||||||
|
page: z.coerce.number().min(1).default(1),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(20),
|
||||||
|
sortBy: z.enum(['created_at', 'name', 'updated_at']).default('created_at'),
|
||||||
|
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ListItemsQuery = z.infer<typeof ListItemsQuerySchema>;
|
||||||
|
|
||||||
|
// Route definition with query validation
|
||||||
|
.get('/items', zValidator('query', ListItemsQuerySchema), async (c) => {
|
||||||
|
const query = c.req.valid('query'); // Fully typed as ListItemsQuery
|
||||||
|
const user = c.get('busterUser');
|
||||||
|
|
||||||
|
const response = await listItemsHandler(query, user);
|
||||||
|
return c.json(response);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handler receives typed query
|
||||||
|
export async function listItemsHandler(
|
||||||
|
query: ListItemsQuery,
|
||||||
|
user: User
|
||||||
|
): Promise<ListItemsResponse> {
|
||||||
|
const offset = (query.page - 1) * query.limit;
|
||||||
|
// Use query.search, query.status, etc. with full type safety
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Path Parameters
|
||||||
|
```typescript
|
||||||
|
// Define param schema
|
||||||
|
export const ItemParamsSchema = z.object({
|
||||||
|
item_id: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ItemParams = z.infer<typeof ItemParamsSchema>;
|
||||||
|
|
||||||
|
// Route with path param validation
|
||||||
|
.get('/items/:item_id', zValidator('param', ItemParamsSchema), async (c) => {
|
||||||
|
const params = c.req.valid('param'); // Typed as ItemParams
|
||||||
|
const user = c.get('busterUser');
|
||||||
|
|
||||||
|
const response = await getItemHandler(params.item_id, user);
|
||||||
|
return c.json(response);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handler receives typed param
|
||||||
|
export async function getItemHandler(
|
||||||
|
itemId: string,
|
||||||
|
user: User
|
||||||
|
): Promise<ItemResponse> {
|
||||||
|
// itemId is guaranteed to be a valid UUID
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Combined Parameters (Path + Query + Body)
|
||||||
|
```typescript
|
||||||
|
// Define schemas
|
||||||
|
export const UpdateItemParamsSchema = z.object({
|
||||||
|
item_id: z.string().uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateItemQuerySchema = z.object({
|
||||||
|
validate: z.coerce.boolean().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UpdateItemBodySchema = z.object({
|
||||||
|
name: z.string().min(1).max(255),
|
||||||
|
description: z.string().optional(),
|
||||||
|
status: z.enum(['active', 'inactive']).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route with multiple validations
|
||||||
|
.patch(
|
||||||
|
'/items/:item_id',
|
||||||
|
zValidator('param', UpdateItemParamsSchema),
|
||||||
|
zValidator('query', UpdateItemQuerySchema),
|
||||||
|
zValidator('json', UpdateItemBodySchema),
|
||||||
|
async (c) => {
|
||||||
|
const params = c.req.valid('param');
|
||||||
|
const query = c.req.valid('query');
|
||||||
|
const body = c.req.valid('json');
|
||||||
|
const user = c.get('busterUser');
|
||||||
|
|
||||||
|
const response = await updateItemHandler(
|
||||||
|
params.item_id,
|
||||||
|
body,
|
||||||
|
{ validate: query.validate },
|
||||||
|
user
|
||||||
|
);
|
||||||
|
return c.json(response);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler with all parameters typed
|
||||||
|
export async function updateItemHandler(
|
||||||
|
itemId: string,
|
||||||
|
data: UpdateItemBody,
|
||||||
|
options: { validate: boolean },
|
||||||
|
user: User
|
||||||
|
): Promise<ItemResponse> {
|
||||||
|
if (options.validate) {
|
||||||
|
// Perform validation
|
||||||
|
}
|
||||||
|
// Update logic
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Important Notes on Type Coercion
|
||||||
|
- Use `z.coerce.number()` for numeric query params (they come as strings)
|
||||||
|
- Use `z.coerce.boolean()` for boolean query params
|
||||||
|
- Path params are always strings, validate format with `.uuid()`, `.regex()`, etc.
|
||||||
|
- Query arrays need special handling: `ids: z.array(z.string()).or(z.string()).transform(v => Array.isArray(v) ? v : [v])`
|
||||||
|
|
||||||
|
### Background Jobs
|
||||||
|
```typescript
|
||||||
|
// Queue job without waiting
|
||||||
|
import { tasks } from '@trigger.dev/sdk/v3';
|
||||||
|
|
||||||
|
await tasks.trigger('job-name', { data });
|
||||||
|
// Return immediately, don't await job completion
|
||||||
|
```
|
|
@ -5,4 +5,4 @@ ALTER TABLE "asset_permissions" ALTER COLUMN "role" SET DATA TYPE "public"."asse
|
||||||
ALTER TABLE "messages" ADD COLUMN "post_processing_message" jsonb;--> statement-breakpoint
|
ALTER TABLE "messages" ADD COLUMN "post_processing_message" jsonb;--> statement-breakpoint
|
||||||
ALTER TABLE "organizations" ADD COLUMN "domains" text[];--> statement-breakpoint
|
ALTER TABLE "organizations" ADD COLUMN "domains" text[];--> statement-breakpoint
|
||||||
ALTER TABLE "organizations" ADD COLUMN "restrict_new_user_invitations" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
ALTER TABLE "organizations" ADD COLUMN "restrict_new_user_invitations" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||||
ALTER TABLE "organizations" ADD COLUMN "defaultRole" "user_organization_role_enum" DEFAULT 'restricted_querier' NOT NULL;
|
ALTER TABLE "organizations" ADD COLUMN "default_role" "user_organization_role_enum" DEFAULT 'restricted_querier' NOT NULL;
|
|
@ -1020,7 +1020,7 @@ export const organizations = pgTable(
|
||||||
paymentRequired: boolean('payment_required').default(false).notNull(),
|
paymentRequired: boolean('payment_required').default(false).notNull(),
|
||||||
domains: text('domains').array(),
|
domains: text('domains').array(),
|
||||||
restrictNewUserInvitations: boolean('restrict_new_user_invitations').default(false).notNull(),
|
restrictNewUserInvitations: boolean('restrict_new_user_invitations').default(false).notNull(),
|
||||||
defaultRole: userOrganizationRoleEnum().default('restricted_querier').notNull(),
|
defaultRole: userOrganizationRoleEnum('default_role').default('restricted_querier').notNull(),
|
||||||
},
|
},
|
||||||
(table) => [unique('organizations_name_key').on(table.name)]
|
(table) => [unique('organizations_name_key').on(table.name)]
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue