Initial commit: YouTube Studio Flow (backend, frontend, infrastructure, docs)
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user