Forráskód Böngészése

feat(stats): enforce required channelId and machine fields in event payload validation

Dave 2 hónapja
szülő
commit
c2316036ae

+ 34 - 6
PHASE4_REFACTOR_SUMMARY.md

@@ -1,6 +1,7 @@
 # Phase 4: Enforce Required channelId and machine Fields
 
 ## Overview
+
 This phase makes `channelId` and `machine` required (non-optional) throughout the entire stats pipeline, enforcing data integrity from the client through to the database layer.
 
 ## Changes Made
@@ -8,12 +9,14 @@ This phase makes `channelId` and `machine` required (non-optional) throughout th
 ### 1. Prisma Models (mongo-stats schema)
 
 **Updated Files:**
+
 - `prisma/mongo-stats/schema/user.prisma`
 - `prisma/mongo-stats/schema/user-login-history.prisma`
 - `prisma/mongo-stats/schema/ads-click-history.prisma`
 - `prisma/mongo-stats/schema/events.prisma`
 
 **Changes:**
+
 - Removed `?` (optional) marker from `channelId` field in:
   - `User` model
   - `UserLoginHistory` model
@@ -31,6 +34,7 @@ This phase makes `channelId` and `machine` required (non-optional) throughout th
   - `AdImpressionEvents` model
 
 **Indexes Added:**
+
 - Added composite index `@@index([channelId, createAt])` to `User` for channel-based queries
 - Added composite index `@@index([channelId, createAt])` to `UserLoginHistory` for channel-based reporting
 - Added composite index `@@index([channelId, uid, clickAt])` to `AdsClickHistory` for channel+user analytics
@@ -41,32 +45,36 @@ This phase makes `channelId` and `machine` required (non-optional) throughout th
 ### 2. Event Payload Interfaces
 
 **Updated File:**
+
 - `libs/common/src/events/user-login-event.dto.ts`
 
 **Changes:**
+
 ```typescript
 // Before
 export interface UserLoginEventPayload {
-  channelId?: string;  // optional
-  machine?: string;    // optional
+  channelId?: string; // optional
+  machine?: string; // optional
 }
 
 // After
 export interface UserLoginEventPayload {
-  channelId: string;  // required
-  machine: string;    // required
+  channelId: string; // required
+  machine: string; // required
 }
 ```
 
 ### 3. DTOs (API Input Validation)
 
 **Updated Files:**
+
 - `apps/box-app-api/src/feature/auth/login.dto.ts`
 - `apps/box-app-api/src/feature/ads/dto/ad-click.dto.ts`
 - `apps/box-app-api/src/feature/ads/dto/ad-impression.dto.ts`
 
 **Changes:**
-- `LoginDto`: 
+
+- `LoginDto`:
   - `channelId` changed from `@IsOptional()` to `@IsNotEmpty()` @IsString()`
   - `machine` changed from `@IsOptional()` to `@IsNotEmpty() @IsString()`
 
@@ -81,13 +89,14 @@ export interface UserLoginEventPayload {
 ### 4. Service Methods
 
 **Updated File:**
+
 - `apps/box-stats-api/src/feature/user-login/user-login.service.ts`
 
 **Changes:**
+
 - `createUser()` method:
   - Removed `?? null` coalescing for `channelId` and `machine`
   - Now directly assigns values without null fallback (since they're required)
-  
 - `recordLogin()` method:
   - Removed `?? null` coalescing for `channelId` and `machine`
   - Now directly assigns values
@@ -95,9 +104,11 @@ export interface UserLoginEventPayload {
 ### 5. Stats Event Consumer
 
 **Updated File:**
+
 - `apps/box-stats-api/src/feature/stats-events/stats-events.consumer.ts`
 
 **Changes:**
+
 - `handleAdClick()`:
   - Validation now checks: `!payload.channelId || !payload.machine`
   - Removed `?? null` coalescing, now directly assigns values
@@ -114,11 +125,14 @@ export interface UserLoginEventPayload {
 ### 6. Channel Management Updates
 
 **Updated Files:**
+
 - `apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.dto.ts`
 - `apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.service.ts`
 
 **Changes:**
+
 - Added required `channelId` field to `CreateChannelDto`:
+
   ```typescript
   @IsNotEmpty()
   @IsString()
@@ -198,19 +212,23 @@ StatsEventsConsumer.handleAdClick()
 ## Validation Strategy
 
 ### Client-Side (Frontend)
+
 - `channelId` and `machine` must be provided in all API requests
 - Both fields are required and cannot be empty/null
 
 ### API Layer (NestJS)
+
 - DTOs enforce `@IsNotEmpty()` and `@IsString()` decorators
 - ValidationPipe validates before reaching controllers
 - Controllers reject requests with missing/invalid values (400 Bad Request)
 
 ### Message Queue (RabbitMQ)
+
 - Event payloads typed as required fields
 - Consumer validation checks for presence before persisting
 
 ### Database (MongoDB)
+
 - Prisma schema enforces non-optional fields
 - Database will reject inserts/updates without `channelId` and `machine`
 - Composite indexes optimize channel+user based analytics queries
@@ -218,6 +236,7 @@ StatsEventsConsumer.handleAdClick()
 ## Migration Path
 
 For existing data with null `channelId` or `machine`:
+
 1. No automatic migration - manual data cleanup required
 2. All NEW requests MUST provide both fields
 3. Recommend setting default values for legacy data during migration window
@@ -226,6 +245,7 @@ For existing data with null `channelId` or `machine`:
 ## Testing Recommendations
 
 ### Unit Tests
+
 - ✅ LoginDto validation with required fields
 - ✅ AdClickDto validation with required fields
 - ✅ AdImpressionDto validation with required fields
@@ -233,12 +253,14 @@ For existing data with null `channelId` or `machine`:
 - ✅ StatsEventsConsumer validation of required fields
 
 ### Integration Tests
+
 - ✅ End-to-end login flow with channelId + machine
 - ✅ Ad click tracking with channelId + machine
 - ✅ Ad impression tracking with channelId + machine
 - ✅ Data persists correctly to MongoDB
 
 ### Manual Testing
+
 ```bash
 # Test 1: Login with required fields
 curl -X POST http://localhost:3301/api/v1/auth/login \
@@ -274,31 +296,37 @@ curl -X POST http://localhost:3301/api/v1/auth/login \
 ## Files Modified Summary
 
 ### Prisma Schemas (4 files)
+
 - `prisma/mongo-stats/schema/user.prisma` ✅
 - `prisma/mongo-stats/schema/user-login-history.prisma` ✅
 - `prisma/mongo-stats/schema/ads-click-history.prisma` ✅
 - `prisma/mongo-stats/schema/events.prisma` ✅
 
 ### Event Interfaces (1 file)
+
 - `libs/common/src/events/user-login-event.dto.ts` ✅
 
 ### DTOs (3 files)
+
 - `apps/box-app-api/src/feature/auth/login.dto.ts` ✅
 - `apps/box-app-api/src/feature/ads/dto/ad-click.dto.ts` ✅
 - `apps/box-app-api/src/feature/ads/dto/ad-impression.dto.ts` ✅
 
 ### Services (3 files)
+
 - `apps/box-stats-api/src/feature/user-login/user-login.service.ts` ✅
 - `apps/box-stats-api/src/feature/stats-events/stats-events.consumer.ts` ✅
 - `apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.service.ts` ✅
 
 ### Channel Management (2 files)
+
 - `apps/box-mgnt-api/src/mgnt-backend/feature/channel/channel.dto.ts` ✅
 
 ## Compilation Status
 
 ✅ **Prisma Generation**: All three schemas passed validation
 ✅ **TypeScript**: Phase 4 critical files compile without errors
+
 - Note: Existing errors from Phase 3 (Ads-Channel split) remain in ad-related files but don't impact Phase 4 functionality
 
 ## Next Steps

+ 23 - 3
apps/box-stats-api/src/feature/stats-events/stats-events.consumer.ts

@@ -236,7 +236,13 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
 
     // Validate required fields (use adsId from publisher, adId for backward compatibility)
     const adId = payload?.adId || payload?.adsId;
-    if (!payload || !payload.uid || !adId || !payload.channelId || !payload.machine) {
+    if (
+      !payload ||
+      !payload.uid ||
+      !adId ||
+      !payload.channelId ||
+      !payload.machine
+    ) {
       this.logger.warn(
         `Malformed ad.click message (missing uid, adId, channelId, or machine), dropping: ${msg.content.toString()}`,
       );
@@ -293,7 +299,14 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
   private async handleVideoClick(msg: ConsumeMessage | null): Promise<void> {
     if (!msg) return;
     const payload = this.parseJson<VideoClickMessage>(msg);
-    if (!payload || !payload.messageId || !payload.uid || !payload.videoId || !payload.channelId || !payload.machine) {
+    if (
+      !payload ||
+      !payload.messageId ||
+      !payload.uid ||
+      !payload.videoId ||
+      !payload.channelId ||
+      !payload.machine
+    ) {
       this.logger.warn(
         `Malformed video.click message (missing required fields), dropping: ${msg?.content.toString()}`,
       );
@@ -348,7 +361,14 @@ export class StatsEventsConsumer implements OnModuleInit, OnModuleDestroy {
   private async handleAdImpression(msg: ConsumeMessage | null): Promise<void> {
     if (!msg) return;
     const payload = this.parseJson<AdImpressionMessage>(msg);
-    if (!payload || !payload.messageId || !payload.uid || !payload.adId || !payload.channelId || !payload.machine) {
+    if (
+      !payload ||
+      !payload.messageId ||
+      !payload.uid ||
+      !payload.adId ||
+      !payload.channelId ||
+      !payload.machine
+    ) {
       this.logger.warn(
         `Malformed ad.impression message (missing required fields), dropping: ${msg?.content.toString()}`,
       );