# Vss Add Nest Module

> Scaffolds and wires a new NestJS service/module for the Video Search & Summarization sample app's pipeline-manager using the repo's real conventions. Use when asked to add a new service/module to pipeline-manager, extend the VSS orchestrator, add a new endpoint/queue/event to pipeline-manager, or scaffold a NestJS module the repo way for the video-search-and-summarization sample app.

- Skill: `open-edge-platform/vss-add-nest-module` (Agent Skill, multi-file: 21 files)
- Install (CLI): `npx skillmds@latest add open-edge-platform/vss-add-nest-module`
- Raw SKILL.md: https://api.skillmd.com/api/skills/open-edge-platform/vss-add-nest-module/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: open-edge-platform (https://skillmd.com/u/open-edge-platform)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/open-edge-platform/vss-add-nest-module

---


# Add a VSS pipeline-manager NestJS module

Use this when adding a new `pipeline-manager/src/<feature>/` capability. First read nearby modules again (`summary`, `search`, `video-upload`, `state-manager`) because this repo has concrete patterns that matter more than generic NestJS advice.

## Environment setup (run first)

This skill drives the Video Search & Summarization app through its real source
files, so the VSS application must be present and you must run commands from its
app root. **Do this before anything else**, and it works whether or not the VSS
source is already in your workspace.

Run the bundled bootstrap. It first tries to find an existing VSS checkout -
walking up from the current directory and inspecting the enclosing git repo - and
reuses it **without ever re-cloning**. Only when no checkout is found does it do a
shallow, single-branch, sparse checkout of just
`sample-applications/video-search-and-summarization` from `main`. It prints the
resolved app root on stdout:

```bash
# SKILL_DIR is THIS skill's own directory (shown to you when the skill loads);
# in-repo it is .github/skills/vss-add-nest-module. Works the same if the skill is installed standalone.
SKILL_DIR=".github/skills/vss-add-nest-module"
APP_ROOT="$(bash "$SKILL_DIR/scripts/vss-bootstrap.sh")"
cd "$APP_ROOT"
```

Every command below assumes the working directory is this `APP_ROOT`. To pull
from a fork/branch or reuse a specific checkout dir, override `VSS_REPO_URL`,
`VSS_REPO_BRANCH`, or `VSS_CLONE_DIR` before running it.

## 1. Match the repo layout

Most feature modules use this shape:

```text
src/<feature>/
  <feature>.module.ts
  controllers/<feature>.controller.ts
  services/<feature>.service.ts
  services/<feature>-db.service.ts        # only when TypeORM-backed
  queues/<feature>-queue.service.ts       # only when tick/event queued
  models/<feature>.model.ts
  models/<feature>.entity.ts              # only when persisted
```

Why: controllers stay thin, services hold orchestration, `*-db.service.ts` isolates TypeORM repository access, queues are event-driven processors, and `models/` keeps DTO/interfaces/entities close to the feature.

Real examples:

```ts
// summary/summary.module.ts
@Module({
  imports: [VideoUploadModule, StateManagerModule],
  controllers: [SummaryController],
  providers: [SummaryService],
})
export class SummaryModule {}
```

```ts
// search/search.module.ts
@Module({
  providers: [SearchStateService, SearchDbService, SearchShimService],
  controllers: [SearchController],
  imports: [HttpModule, TypeOrmModule.forFeature([SearchEntity]), VideoUploadModule],
  exports: [],
})
export class SearchModule {}
```

## 2. Create model/DTO files first

Use interfaces for internal shapes and Swagger classes for request/response documentation, as in `summary/models/summary-pipeline.model.ts` and `search/model/search.model.ts`.

```ts
export class SearchQueryDTO {
  @ApiProperty({ description: 'Search query string', example: 'person walking' })
  query: string;

  @ApiPropertyOptional({ description: 'Comma-separated tags to filter by' })
  tags?: string;
}
```

Why: controllers can type request bodies while Swagger gets concrete classes with decorators.

## 3. Add TypeORM only if the feature persists data

Entity pattern:

```ts
@Entity('search')
export class SearchEntity {
  @PrimaryGeneratedColumn()
  dbId?: number;

  @Column({ unique: true })
  queryId: string;

  @Column('jsonb', { nullable: true })
  results: SearchResult[];
}
```

Repository pattern:

```ts
@Injectable()
export class SearchDbService {
  constructor(
    @InjectRepository(SearchEntity)
    private searchRepo: Repository<SearchEntity>,
  ) {}

  async read(queryId: string): Promise<SearchEntity | null> {
    const search = await this.searchRepo.findOne({ where: { queryId } });
    return search ?? null;
  }
}
```

Wire the entity in two places:

1. Feature module: `TypeOrmModule.forFeature([NewEntity])`.
2. Root `app.module.ts`: add `NewEntity` to `TypeOrmModule.forRoot({ entities: [...] })`.

Why: `forFeature` enables repository injection in the feature; the root entity list lets TypeORM create/sync the table.

## 4. Keep controllers thin and Swagger-tagged

Controllers inject services using the repo's `$name` property style and expose REST routes with `@ApiTags`, `@ApiOperation`, `@ApiBody`, `@ApiParam`, and response decorators.

```ts
@ApiTags('Search')
@Controller('search')
export class SearchController {
  constructor(private $search: SearchStateService) {}

  @Post('')
  @ApiOperation({ summary: 'Add a new search query' })
  @ApiBody({ type: SearchQueryDTO })
  async addQuery(@Body() reqBody: SearchQueryDTO) {
    return await this.$search.newQuery(reqBody.query);
  }
}
```

Why: validation/errors and orchestration live in services; controllers translate HTTP to typed service calls.

## 5. Use DI through modules, not ad hoc construction

Import modules that export the services you need. Examples:

- `SummaryModule` imports `VideoUploadModule` and `StateManagerModule` to inject `VideoService`, `AppConfigService`, `StateService`, `UiService`.
- `VideoUploadModule` exports `AppConfigService`, `FeaturesService`, `VideoService`, `VideoDbService`.
- `StateManagerModule` exports `StateService`, `UiService`, `AudioQueueService`.

If another module must inject your service, add it to `exports`.

Why: Nest resolves providers only across module boundaries through `imports`/`exports`.

## 6. Add events using the central enums

Define new event names in `src/events/Pipeline.events.ts`, `src/events/app.events.ts`, or `src/events/socket.events.ts`. Use dot-delimited names because root wiring uses:

```ts
EventEmitterModule.forRoot({ delimiter: '.', maxListeners: 5 })
```

Emit and listen like existing services:

```ts
this.$emitter.emit(SearchEvents.RUN_QUERY, res.queryId);

@OnEvent(SearchEvents.RUN_QUERY)
async reRunQuery(queryId: string) { ... }
```

Pipeline examples include `pipeline.summary.start`, `pipeline.chunking.complete`, and `pipeline.summary.stream`. App tick events are emitted by `AppService` and consumed by queues.

Why: event enums prevent string drift and decouple controllers/services/queues/gateways.

## 7. Use the repo queue pattern for background work

Queue processors are injectable services under `state-manager/queues` or a feature `queues/` folder. They keep in-memory `waiting`/`processing` state, enqueue on a domain event, process on `AppEvents.FAST_TICK`, emit progress/completion events, and remove stale work on `AppEvents.SUMMARY_REMOVED`.

```ts
@Injectable()
export class SummaryQueueService {
  waiting: SummaryQueueItem[] = [];
  processing: SummaryQueueItem[] = [];

  @OnEvent(PipelineEvents.SUMMARY_TRIGGER)
  streamTrigger({ stateId }: PipelineDTOBase) {
    this.waiting.push({ stateId, taskType: 'videoSummary' });
  }

  @OnEvent(AppEvents.FAST_TICK)
  processQueue() {
    if (this.waiting.length > 0 && this.$inferenceCount.hasLlmSlots()) {
      const queueItem = this.waiting.shift()!;
      this.processing.push(queueItem);
      this.startVideoSummary(queueItem);
    }
  }
}
```

Why: long-running work is throttled centrally and does not block HTTP requests.

## 8. Send UI updates through SocketEvent + EventsGateway

Add socket event enum values to `src/events/socket.events.ts`, emit them from state/services, and add `@OnEvent(...)` handlers in `sockets/events.gateway.ts`.

```ts
@OnEvent(SocketEvent.SEARCH_UPDATE)
searchUpdate(payload: SearchQuery) {
  this.server.emit('search:update', payload);
}
```

For state-specific updates, join clients to a state room and emit names like `summary:sync/${stateId}/status`.

Why: services remain transport-agnostic; only the gateway knows Socket.IO event names and rooms.

## 9. Read configuration through ConfigService

Add config keys in `src/config/configuration.ts`, then inject `ConfigService`:

```ts
constructor(private $config: ConfigService) {}

const endPoint: string = this.$config.get('search.endpoint')!;
const maxConcurrent = this.$config.get<number>('openai.vlmCaptioning.concurrent')!;
```

Why: this app centralizes environment parsing and nested config names (`search.endpoint`, `datastore.bucketName`, `tick.fastInterval`).

## 10. Register the module in root wiring

In `src/app.module.ts`:

```ts
import { NewFeatureModule } from './new-feature/new-feature.module';
import { NewFeatureEntity } from './new-feature/models/new-feature.entity';

@Module({
  imports: [
    // existing imports...
    TypeOrmModule.forRoot({
      // existing options...
      entities: [StateEntity, VideoEntity, SearchEntity, TagEntity, NewFeatureEntity],
    }),
    NewFeatureModule,
  ],
})
export class AppModule {}
```

Place `EventEmitterModule.forRoot(...)` once only; do not add another root event emitter.

## 11. Validate

From `pipeline-manager/`, run existing checks only:

```bash
npm run build
npm test -- --runInBand
npm run lint
```

If changing only docs or templates, no app build is required. If adding entities, verify migrations/sync behavior is acceptable for this sample (`synchronize: true`, `migrationsRun: true` are already set).

See [references/nest-conventions.md](references/nest-conventions.md) for a catalog of actual repo conventions and file references. Use [assets/module-template/](assets/module-template/) as a starting scaffold, then rename `example` to the real feature.

