@@ -27,13 +27,38 @@ export function parseCSV(csv: string): CopilotUsageData[] {
2727 throw new Error ( 'CSV must contain a header row and at least one data row' ) ;
2828 }
2929
30+ // Validate header row
31+ const headerLine = lines [ 0 ] ;
32+ const expectedHeaders = [ 'Timestamp' , 'User' , 'Model' , 'Requests Used' , 'Exceeds Monthly Quota' , 'Total Monthly Quota' ] ;
33+
34+ // Parse header row to check for expected columns
35+ const headerMatches = headerLine . match ( / ( " ( [ ^ " ] * ) " | ( [ ^ , ] * ) ) ( , | $ ) / g) ;
36+ if ( ! headerMatches || headerMatches . length < 6 ) {
37+ throw new Error ( 'CSV header must contain at least 6 columns' ) ;
38+ }
39+
40+ const headers = headerMatches . map ( m =>
41+ m . endsWith ( ',' )
42+ ? m . slice ( 0 , - 1 ) . replace ( / ^ " ( .* ) " $ / , '$1' )
43+ : m . replace ( / ^ " ( .* ) " $ / , '$1' )
44+ ) ;
45+
46+ // Check if all expected headers are present (case-insensitive)
47+ const missingHeaders = expectedHeaders . filter ( expected =>
48+ ! headers . some ( header => header . toLowerCase ( ) . includes ( expected . toLowerCase ( ) ) )
49+ ) ;
50+
51+ if ( missingHeaders . length > 0 ) {
52+ throw new Error ( `CSV is missing required columns: ${ missingHeaders . join ( ', ' ) } . Expected columns: ${ expectedHeaders . join ( ', ' ) } ` ) ;
53+ }
54+
3055 // Skip the header row and process data rows
31- return lines . slice ( 1 ) . map ( line => {
56+ return lines . slice ( 1 ) . map ( ( line , index ) => {
3257 // Handle quoted CSV properly
3358 const matches = line . match ( / ( " ( [ ^ " ] * ) " | ( [ ^ , ] * ) ) ( , | $ ) / g) ;
3459
3560 if ( ! matches || matches . length < 6 ) {
36- throw new Error ( ' Invalid CSV row format' ) ;
61+ throw new Error ( ` Invalid CSV row format at line ${ index + 2 } : expected 6 columns, got ${ matches ? matches . length : 0 } ` ) ;
3762 }
3863
3964 const values = matches . map ( m =>
@@ -42,12 +67,30 @@ export function parseCSV(csv: string): CopilotUsageData[] {
4267 : m . replace ( / ^ " ( .* ) " $ / , '$1' )
4368 ) ;
4469
70+ // Validate timestamp
71+ const timestamp = new Date ( values [ 0 ] ) ;
72+ if ( isNaN ( timestamp . getTime ( ) ) ) {
73+ throw new Error ( `Invalid timestamp format at line ${ index + 2 } : "${ values [ 0 ] } "` ) ;
74+ }
75+
76+ // Validate requests used
77+ const requestsUsed = parseFloat ( values [ 3 ] ) ;
78+ if ( isNaN ( requestsUsed ) ) {
79+ throw new Error ( `Invalid requests used value at line ${ index + 2 } : "${ values [ 3 ] } " must be a number` ) ;
80+ }
81+
82+ // Validate exceeds quota
83+ const exceedsQuotaValue = values [ 4 ] . toLowerCase ( ) ;
84+ if ( exceedsQuotaValue !== 'true' && exceedsQuotaValue !== 'false' ) {
85+ throw new Error ( `Invalid exceeds quota value at line ${ index + 2 } : "${ values [ 4 ] } " must be "true" or "false"` ) ;
86+ }
87+
4588 return {
46- timestamp : new Date ( values [ 0 ] ) ,
89+ timestamp,
4790 user : values [ 1 ] ,
4891 model : values [ 2 ] ,
49- requestsUsed : parseFloat ( values [ 3 ] ) ,
50- exceedsQuota : values [ 4 ] . toLowerCase ( ) === "true" ,
92+ requestsUsed,
93+ exceedsQuota : exceedsQuotaValue === "true" ,
5194 totalMonthlyQuota : values [ 5 ] ,
5295 } ;
5396 } ) ;
0 commit comments