This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

daily activity stats

+177 -13
+38 -6
src/modules/user/application/useCases/queries/GetUserStatsUseCase.ts
··· 4 4 IUserStatsRepository, 5 5 UserGrowthStatsDTO, 6 6 UserEngagementStatsDTO, 7 + DailyActivityStatsDTO, 7 8 UserStatType, 8 9 TimeInterval, 9 10 } from '../../../domain/IUserStatsRepository'; ··· 16 17 // Future parameters can be added here for other stat types 17 18 } 18 19 19 - export type GetUserStatsResult = UserGrowthStatsDTO | UserEngagementStatsDTO; // Union type for different stat types 20 + export type GetUserStatsResult = 21 + | UserGrowthStatsDTO 22 + | UserEngagementStatsDTO 23 + | DailyActivityStatsDTO; // Union type for different stat types 20 24 21 25 export class ValidationError extends Error { 22 26 constructor(message: string) { ··· 45 49 case 'engagement': 46 50 return await this.handleEngagementStats(query); 47 51 48 - // Future stat types can be added here 49 - // case 'activity': 50 - // return await this.handleActivityStats(query); 52 + case 'activity': 53 + return await this.handleActivityStats(query); 51 54 52 55 default: 53 56 return err( ··· 129 132 } 130 133 } 131 134 132 - // Future handler methods can be added here 133 - // private async handleActivityStats(query: GetUserStatsQuery): Promise<Result<UserActivityStatsDTO>> { ... } 135 + private async handleActivityStats( 136 + query: GetUserStatsQuery, 137 + ): Promise<Result<DailyActivityStatsDTO>> { 138 + // Apply defaults 139 + const interval = query.interval || 'day'; 140 + const limit = query.limit || 30; 141 + 142 + // Validate parameters 143 + if (!['day', 'week', 'month'].includes(interval)) { 144 + return err(new ValidationError(`Invalid interval: ${interval}`)); 145 + } 146 + 147 + if (limit < 1 || limit > 365) { 148 + return err(new ValidationError('Limit must be between 1 and 365')); 149 + } 150 + 151 + try { 152 + const stats = await this.userStatsRepository.getDailyActivityStats({ 153 + interval, 154 + limit, 155 + }); 156 + 157 + return ok(stats); 158 + } catch (error) { 159 + return err( 160 + new Error( 161 + `Failed to retrieve activity stats: ${error instanceof Error ? error.message : 'Unknown error'}`, 162 + ), 163 + ); 164 + } 165 + } 134 166 }
+36 -2
src/modules/user/domain/IUserStatsRepository.ts
··· 56 56 includeTimeSeries?: boolean; 57 57 } 58 58 59 + // Daily Activity Statistics DTOs 60 + 61 + export interface DailyActivityDataPoint { 62 + date: string; // ISO date string 63 + cardsCreated: number; 64 + collectionsCreated: number; 65 + connectionsCreated: number; 66 + followsCreated: number; 67 + totalActions: number; // Sum of all above 68 + } 69 + 70 + export interface DailyActivityStatsDTO { 71 + dataPoints: DailyActivityDataPoint[]; 72 + totals: { 73 + cardsCreated: number; 74 + collectionsCreated: number; 75 + connectionsCreated: number; 76 + followsCreated: number; 77 + totalActions: number; 78 + }; 79 + periodStart: string; 80 + periodEnd: string; 81 + } 82 + 83 + export interface DailyActivityStatsOptions { 84 + interval: TimeInterval; // day, week, month 85 + limit: number; // Number of intervals to return 86 + } 87 + 59 88 // Future stat types can be added here 60 89 export type UserStatType = 'growth' | 'activity' | 'engagement'; 61 90 ··· 80 109 options: UserEngagementStatsOptions, 81 110 ): Promise<UserEngagementStatsDTO>; 82 111 83 - // Future methods can be added here: 84 - // getUserActivityStats(options: UserActivityStatsOptions): Promise<UserActivityStatsDTO>; 112 + /** 113 + * Get daily activity statistics 114 + * Returns content creation volume over time periods 115 + */ 116 + getDailyActivityStats( 117 + options: DailyActivityStatsOptions, 118 + ): Promise<DailyActivityStatsDTO>; 85 119 }
+103 -5
src/modules/user/infrastructure/repositories/DrizzleUserStatsRepository.ts
··· 8 8 UserEngagementStatsDTO, 9 9 UserEngagementStatsOptions, 10 10 UserEngagementDataPoint, 11 + DailyActivityStatsDTO, 12 + DailyActivityStatsOptions, 13 + DailyActivityDataPoint, 11 14 } from '../../domain/IUserStatsRepository'; 12 15 import { users } from './schema/user.sql'; 13 16 import { cards } from '../../../cards/infrastructure/repositories/schema/card.sql'; ··· 93 96 WITH user_activity AS ( 94 97 SELECT 95 98 u.id AS user_id, 96 - EXISTS(SELECT 1 FROM ${cards} WHERE author_id = u.id) AS has_cards, 99 + EXISTS(SELECT 1 FROM ${cards} WHERE author_id = u.id AND type = 'URL') AS has_cards, 97 100 EXISTS(SELECT 1 FROM ${collections} WHERE author_id = u.id) AS has_collections, 98 101 EXISTS(SELECT 1 FROM ${connections} WHERE curator_id = u.id) AS has_connections, 99 102 EXISTS(SELECT 1 FROM ${follows} WHERE follower_id = u.id) AS has_follows, 100 103 EXISTS(SELECT 1 FROM ${collectionCards} WHERE added_by = u.id) AS has_contributions, 101 104 ( 102 - (SELECT COUNT(*) FROM ${cards} WHERE author_id = u.id) + 105 + (SELECT COUNT(*) FROM ${cards} WHERE author_id = u.id AND type = 'URL') + 103 106 (SELECT COUNT(*) FROM ${collections} WHERE author_id = u.id) + 104 107 (SELECT COUNT(*) FROM ${connections} WHERE curator_id = u.id) + 105 108 (SELECT COUNT(*) FROM ${follows} WHERE follower_id = u.id) ··· 175 178 u.id AS user_id, 176 179 u.linked_at, 177 180 LEAST( 178 - COALESCE((SELECT MIN(created_at) FROM ${cards} WHERE author_id = u.id), '9999-12-31'::timestamp), 181 + COALESCE((SELECT MIN(created_at) FROM ${cards} WHERE author_id = u.id AND type = 'URL'), '9999-12-31'::timestamp), 179 182 COALESCE((SELECT MIN(created_at) FROM ${collections} WHERE author_id = u.id), '9999-12-31'::timestamp), 180 183 COALESCE((SELECT MIN(created_at) FROM ${connections} WHERE curator_id = u.id), '9999-12-31'::timestamp), 181 184 COALESCE((SELECT MIN(created_at) FROM ${follows} WHERE follower_id = u.id), '9999-12-31'::timestamp), ··· 218 221 .reverse(); // Reverse to get chronological order 219 222 } 220 223 221 - // Future stat methods can be added here 222 - // async getUserActivityStats(options: UserActivityStatsOptions): Promise<UserActivityStatsDTO> { ... } 224 + async getDailyActivityStats( 225 + options: DailyActivityStatsOptions, 226 + ): Promise<DailyActivityStatsDTO> { 227 + const { interval, limit } = options; 228 + 229 + // Query for content creation over time 230 + const activityQuery = sql` 231 + WITH date_periods AS ( 232 + SELECT DISTINCT date_trunc(${interval}, created_at) AS period 233 + FROM ( 234 + SELECT created_at FROM ${cards} WHERE type = 'URL' 235 + UNION ALL 236 + SELECT created_at FROM ${collections} 237 + UNION ALL 238 + SELECT created_at FROM ${connections} 239 + UNION ALL 240 + SELECT created_at FROM ${follows} 241 + ) all_dates 242 + WHERE created_at IS NOT NULL 243 + ), 244 + activity_counts AS ( 245 + SELECT 246 + dp.period, 247 + COALESCE(COUNT(DISTINCT c.id), 0)::int AS cards_created, 248 + COALESCE(COUNT(DISTINCT col.id), 0)::int AS collections_created, 249 + COALESCE(COUNT(DISTINCT con.id), 0)::int AS connections_created, 250 + COALESCE(COUNT(DISTINCT f.follower_id || '-' || f.target_id || '-' || f.target_type), 0)::int AS follows_created 251 + FROM date_periods dp 252 + LEFT JOIN ${cards} c ON date_trunc(${interval}, c.created_at) = dp.period AND c.type = 'URL' 253 + LEFT JOIN ${collections} col ON date_trunc(${interval}, col.created_at) = dp.period 254 + LEFT JOIN ${connections} con ON date_trunc(${interval}, con.created_at) = dp.period 255 + LEFT JOIN ${follows} f ON date_trunc(${interval}, f.created_at) = dp.period 256 + GROUP BY dp.period 257 + ) 258 + SELECT 259 + period::text AS date, 260 + cards_created, 261 + collections_created, 262 + connections_created, 263 + follows_created, 264 + (cards_created + collections_created + connections_created + follows_created) AS total_actions 265 + FROM activity_counts 266 + ORDER BY period DESC 267 + LIMIT ${limit} 268 + `; 269 + 270 + const result = await this.db.execute(activityQuery); 271 + 272 + // Calculate totals 273 + let totals = { 274 + cardsCreated: 0, 275 + collectionsCreated: 0, 276 + connectionsCreated: 0, 277 + followsCreated: 0, 278 + totalActions: 0, 279 + }; 280 + 281 + const dataPoints: DailyActivityDataPoint[] = result 282 + .map((row: any) => { 283 + const point = { 284 + date: row.date, 285 + cardsCreated: row.cards_created || 0, 286 + collectionsCreated: row.collections_created || 0, 287 + connectionsCreated: row.connections_created || 0, 288 + followsCreated: row.follows_created || 0, 289 + totalActions: row.total_actions || 0, 290 + }; 291 + 292 + // Accumulate totals 293 + totals.cardsCreated += point.cardsCreated; 294 + totals.collectionsCreated += point.collectionsCreated; 295 + totals.connectionsCreated += point.connectionsCreated; 296 + totals.followsCreated += point.followsCreated; 297 + totals.totalActions += point.totalActions; 298 + 299 + return point; 300 + }) 301 + .reverse(); // Reverse to get chronological order 302 + 303 + // Determine the period range 304 + const periodStart = 305 + dataPoints.length > 0 && dataPoints[0] 306 + ? dataPoints[0].date 307 + : new Date().toISOString(); 308 + const lastDataPoint = 309 + dataPoints.length > 0 ? dataPoints[dataPoints.length - 1] : undefined; 310 + const periodEnd = lastDataPoint 311 + ? lastDataPoint.date 312 + : new Date().toISOString(); 313 + 314 + return { 315 + dataPoints, 316 + totals, 317 + periodStart, 318 + periodEnd, 319 + }; 320 + } 223 321 }