Files

210 lines
6.3 KiB
Markdown

# Recipes
Step-by-step instructions for common development tasks. For module structure conventions and shared service patterns, see the [[02 - Backend]] architecture reference.
---
## Recipe: Add a New Backend Module
1. Create the module directory: `backend/src/modules/my-feature/`
2. Create `my-feature.module.ts`:
```typescript
import { Module } from '@nestjs/common';
import { PrismaModule } from '../../shared/prisma/prisma.module';
import { AuditModule } from '../../shared/audit/audit.module';
import { MyFeatureController } from './my-feature.controller';
import { MyFeatureService } from './my-feature.service';
@Module({
imports: [PrismaModule, AuditModule],
controllers: [MyFeatureController],
providers: [MyFeatureService],
})
export class MyFeatureModule {}
```
3. Create `my-feature.controller.ts` with the required decorators:
```typescript
import { Controller, UseGuards, Request } from '@nestjs/common';
import { ApiTags, ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@ApiTags('my-feature')
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Controller('my-feature')
export class MyFeatureController {
constructor(private readonly myFeatureService: MyFeatureService) {}
}
```
4. Create `my-feature.service.ts` with `PrismaService` injection. Always scope DB queries to `req.user.teamId`:
```typescript
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../shared/prisma/prisma.service';
@Injectable()
export class MyFeatureService {
constructor(private readonly prisma: PrismaService) {}
}
```
5. Register the module in `AppModule` imports array at `backend/src/app.module.ts`
6. If the worker also needs access to this module's services (e.g. for use in a queue processor), register it in `WorkerModule` at `backend/src/worker.module.ts` as well
> **Audit logging:** If the module performs user-facing mutations (create/update/delete), it must import `AuditModule` and inject `AuditService`. Call `AuditService.log(actorId, entityType, entityId, action, before, after)` in every mutation. See [[04 - Backend Architecture Patterns]] for the full audit pattern.
---
## Recipe: Add a New Lint Rule
1. Create `backend/src/modules/linting/rules/my-rule.rule.ts`:
```typescript
import { LintRule, LintIssue } from './base.rule';
import { LintSeverity } from '@prisma/client';
export class MyRule implements LintRule {
code = 'MY_RULE_CODE';
severity = LintSeverity.WARNING;
check(video: any): LintIssue | null {
if (/* condition */) {
return {
targetField: 'title',
message: 'Describe the problem clearly',
fixSuggestion: 'Explain how to fix it',
};
}
return null;
}
}
```
2. Open `backend/src/modules/linting/linting.service.ts`, import the new rule class, and add an instance to the `rules[]` array:
```typescript
private readonly rules: LintRule[] = [
new ExistingRule(),
new MyRule(), // add here
];
```
3. The rule runs automatically on all subsequent lint jobs. It can be disabled per-team in the Settings page (lint rule disabling is stored on the `Team` model). See [[03 - Metadata Linting]] for the full lint system overview.
---
## Recipe: Add a New API Endpoint to the Frontend
1. If the endpoint returns a new response shape, add a TypeScript interface to `frontend/src/lib/api.ts`:
```typescript
export interface MyResponse {
id: string;
name: string;
// ...
}
```
2. Add the API function in the same file:
```typescript
export const myNewAction = (id: string, data: MyData): Promise<MyResponse> =>
apiClient.post<MyResponse>(`/my-feature/${id}/action`, data).then(r => r.data);
```
3. Use it in a component with TanStack Query:
```typescript
const qc = useQueryClient(); // must be called at component level, not inside a callback
const mut = useMutation({
mutationFn: () => myNewAction(id, data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['affected-key'] });
},
});
```
> See [[03 - Component Patterns]] for TanStack Query conventions, including `placeholderData` usage and multi-key invalidation.
---
## Recipe: Add a New Sidebar Navigation Item
1. Open `frontend/src/components/shared/Sidebar.tsx`
2. Import the icon from `lucide-react`:
```typescript
import { MyIcon } from 'lucide-react';
```
3. Add an entry to the `navItems` array:
```typescript
{ label: 'My Page', href: '/my-page', icon: MyIcon, group: 'WORKSPACE' },
```
Available groups and their current members:
| Group | Used for |
|---|---|
| `WORKSPACE` | Overview, Videos, Calendar |
| `OPERATIONS` | Bulk Jobs, Import/Export |
| `PEOPLE` | Collaborators, Teams |
| `LOGGING` | Audit Log, Quota |
4. Create the corresponding route at `frontend/src/app/(dashboard)/my-page/page.tsx`
---
## Recipe: Add a New Prisma Field
1. Add the field to the appropriate model in `backend/prisma/schema.prisma`
2. Stop the backend (and worker if running)
3. Create and apply the migration:
```bash
npx prisma migrate dev --name add-my-field
```
4. Regenerate the Prisma client:
```bash
npx prisma generate
```
5. Restart the backend and worker
> **Never remove enum values** from the Prisma schema. PostgreSQL enum removal requires a raw SQL migration and risks data loss if any rows reference the removed value. Mark deprecated values in the UI instead. See [[04 - Gotchas]] for detail.
---
## Recipe: Add a New System Token (e.g. `{video.myField}`)
System tokens are built-in substitution tokens like `{video.title}` or `{collab.youtube}` that are handled by dedicated resolvers rather than team variable lookup.
1. Add the new token definition to `SYSTEM_VARIABLES[]` in:
`backend/src/shared/system-variables/system-variables.registry.ts`
2. Add the token string to the `SYSTEM_VARIABLE_TOKENS` Set in the same file:
```typescript
export const SYSTEM_VARIABLE_TOKENS = new Set([
// existing tokens...
'{video.myField}',
]);
```
3. Implement the resolution logic in `RenderEngineService` (`backend/src/shared/render-engine/render-engine.service.ts`) in the appropriate resolver method
> **Critical:** Without step 2, `resolveVariables()` will treat the token as a team variable key, find nothing, and silently produce an empty string in the rendered description. This is a common source of invisible rendering bugs. See [[07 - Render Engine]] for the full token resolution pipeline.