210 lines
6.3 KiB
Markdown
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.
|