From 8e9e9a087ea64698dccc998edca01c7909e2f4e9 Mon Sep 17 00:00:00 2001 From: dal Date: Wed, 9 Jul 2025 14:15:46 -0600 Subject: [PATCH] claude rules and schema tweak --- CLAUDE.md | 41 +- apps/server/CLAUDE.md | 356 ++++++++++++++++++ .../database/drizzle/0076_tired_madripoor.sql | 2 +- packages/database/src/schema.ts | 2 +- 4 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 apps/server/CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md index ed87ce25f..cb6b602e5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -121,4 +121,43 @@ pnpm run test:watch - Consider the state errors put the system into - Implement comprehensive unit tests for error scenarios - Log errors strategically for effective debugging - - Avoid over-logging while ensuring sufficient context for troubleshooting \ No newline at end of file + - 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 { + // 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 \ No newline at end of file diff --git a/apps/server/CLAUDE.md b/apps/server/CLAUDE.md new file mode 100644 index 000000000..f96d4b963 --- /dev/null +++ b/apps/server/CLAUDE.md @@ -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 { + // 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; + ``` + +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; + +// 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 { + 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; + +// 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 { + // 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 { + 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 +``` \ No newline at end of file diff --git a/packages/database/drizzle/0076_tired_madripoor.sql b/packages/database/drizzle/0076_tired_madripoor.sql index cc9a23cb5..4b930502b 100644 --- a/packages/database/drizzle/0076_tired_madripoor.sql +++ b/packages/database/drizzle/0076_tired_madripoor.sql @@ -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 "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 "defaultRole" "user_organization_role_enum" DEFAULT 'restricted_querier' NOT NULL; \ No newline at end of file +ALTER TABLE "organizations" ADD COLUMN "default_role" "user_organization_role_enum" DEFAULT 'restricted_querier' NOT NULL; \ No newline at end of file diff --git a/packages/database/src/schema.ts b/packages/database/src/schema.ts index 9b3ba6727..e85531aa9 100644 --- a/packages/database/src/schema.ts +++ b/packages/database/src/schema.ts @@ -1020,7 +1020,7 @@ export const organizations = pgTable( paymentRequired: boolean('payment_required').default(false).notNull(), domains: text('domains').array(), 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)] );