From 546f321ff0a90a1326526e3d7cdd6981b7bf18f9 Mon Sep 17 00:00:00 2001 From: DylanBanta Date: Sun, 25 May 2025 21:49:34 -0400 Subject: [PATCH] feat: add GET /api/v2/scenes endpoint - load.controller.ts exposing listScenes() - adjusted AppModule to register LoadController - multi-stage Dockerfile now copies compiled dist/ - synced package-lock.json and installed redis types - fixed iterator usage in storage.service --- .dockerignore | 19 +++++++++-- Dockerfile.override | 27 ++++++++++++++++ package.json | 3 +- src/app.module.ts | 8 ++++- src/scenes/load.controller.ts | 60 +++++++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 Dockerfile.override create mode 100644 src/scenes/load.controller.ts diff --git a/.dockerignore b/.dockerignore index 3256a9c..0ed00b1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,18 @@ +# ignore everything by default * -!package*.json -!dist \ No newline at end of file + +# keep package manifests so we can install deps +!package.json +!package-lock.json + +# keep your Nest build config +!nest-cli.json +!tsconfig.json +!tsconfig.build.json + +# keep source code +!src/** + +# ignore node_modules and any pre-existing dist output +node_modules +dist diff --git a/Dockerfile.override b/Dockerfile.override new file mode 100644 index 0000000..6c54dd4 --- /dev/null +++ b/Dockerfile.override @@ -0,0 +1,27 @@ +# --- Builder stage ---------------------------------------------------------- + FROM node:16-alpine AS builder + WORKDIR /app + + # Install dev+prod dependencies and build TS + COPY package*.json ./ + RUN npm install + COPY . . + RUN npm run build + + # --- Runtime stage ---------------------------------------------------------- + FROM node:16-alpine + WORKDIR /app + + # Bring in package manifests so npm can locate the start script + COPY --from=builder /app/package*.json ./ + + # Bring in only production deps and compiled output + COPY --from=builder /app/node_modules ./node_modules + COPY --from=builder /app/dist ./dist + + # Drop to non-root user + USER node + + EXPOSE 8080 + + ENTRYPOINT ["npm", "run", "start:prod"] \ No newline at end of file diff --git a/package.json b/package.json index 259932a..f8d88dd 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "nanoid": "^3.1.25", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "redis": "^4.6.5" }, "devDependencies": { "@nestjs/cli": "^8.0.0", diff --git a/src/app.module.ts b/src/app.module.ts index f5f4421..012999a 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,13 +1,19 @@ import { MiddlewareConsumer, Module } from '@nestjs/common'; import { RawParserMiddleware } from './raw-parser.middleware'; import { ScenesController } from './scenes/scenes.controller'; +import { LoadController } from './scenes/load.controller'; import { StorageService } from './storage/storage.service'; import { RoomsController } from './rooms/rooms.controller'; import { FilesController } from './files/files.controller'; @Module({ imports: [], - controllers: [ScenesController, RoomsController, FilesController], + controllers: [ + ScenesController, + LoadController, + RoomsController, + FilesController, + ], providers: [StorageService], }) export class AppModule { diff --git a/src/scenes/load.controller.ts b/src/scenes/load.controller.ts new file mode 100644 index 0000000..759ec2d --- /dev/null +++ b/src/scenes/load.controller.ts @@ -0,0 +1,60 @@ +import { Controller, Get, Logger, InternalServerErrorException } from '@nestjs/common'; +import { createClient } from 'redis'; +import { StorageNamespace } from 'src/storage/storage.service'; + +@Controller('scenes') +export class LoadController { + private readonly logger = new Logger(LoadController.name); + + // Initialize Redis client once + private client = createClient({ url: process.env.STORAGE_URI }); + + constructor() { + this.client.on('error', (err) => { + this.logger.error('Redis client error', err); + }); + this.client.connect().catch((err) => { + this.logger.error('Failed to connect Redis client', err); + }); + } + + @Get() + async listScenes() { + try { + const prefix = `${StorageNamespace.SCENES}:`; + + // 1) list all keys + const keys = await this.client.keys(`${prefix}*`); + this.logger.debug(`Found ${keys.length} scene keys`); + + // 2) pipeline a GET for each key + const pipeline = this.client.multi(); + for (const k of keys) { + pipeline.get(k); + } + // pipeline.exec() on redis v4 returns an array of reply values + const raws = (await pipeline.exec()) as (string | null)[]; + + // 3) build summaries + const scenes = keys + .map((fullKey, idx) => { + const raw = raws[idx]; + if (!raw) return null; + try { + const payload = JSON.parse(raw); + const id = fullKey.slice(prefix.length); + return { id, created: payload.created, updated: payload.updated }; + } catch { + this.logger.warn(`Skipping invalid JSON at ${fullKey}`); + return null; + } + }) + .filter((s) => s !== null); + + return scenes; + } catch (err) { + this.logger.error('Error listing scenes', err as any); + throw new InternalServerErrorException('Failed to list scenes'); + } + } +}