@@ -8,13 +8,57 @@ import { generateRequestId } from '@/lib/core/utils/request'
88import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
99import { getCredential , refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
1010import { GRAPH_ID_PATTERN } from '@/tools/microsoft_excel/utils'
11+ import { assertGraphNextPageUrl , getGraphNextPageUrl } from '@/tools/sharepoint/utils'
1112
1213export const dynamic = 'force-dynamic'
1314
1415const logger = createLogger ( 'MicrosoftFilesAPI' )
1516
1617/**
17- * Get Excel files from Microsoft OneDrive
18+ * Microsoft Graph paginates `search()` results via the `@odata.nextLink`
19+ * absolute URL in the response body. Request the largest page (`$top` caps at
20+ * 999) and drain following nextLink, bounded by a page cap.
21+ * See https://learn.microsoft.com/en-us/graph/paging
22+ */
23+ const MICROSOFT_FILES_PAGE_SIZE = 999
24+ const MAX_MICROSOFT_FILES_PAGES = 20
25+
26+ interface MicrosoftGraphFile {
27+ id : string
28+ name ?: string
29+ mimeType ?: string
30+ webUrl ?: string
31+ size ?: number
32+ createdDateTime ?: string
33+ lastModifiedDateTime ?: string
34+ thumbnails ?: Array < { small ?: { url ?: string } ; medium ?: { url ?: string } } >
35+ createdBy ?: { user ?: { displayName ?: string ; email ?: string } }
36+ }
37+
38+ /**
39+ * The shared `/api/auth/oauth/microsoft/files` route serves both the
40+ * `microsoft.excel` and `microsoft.word` selectors. The two are distinguished
41+ * by the `fileType` query parameter the selector forwards (defaulting to
42+ * `excel` for backward compatibility), which drives both the search-query
43+ * extension hint and the server-side result filter.
44+ */
45+ const FILE_TYPE_CONFIG = {
46+ excel : {
47+ extension : '.xlsx' ,
48+ mimeType : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ,
49+ } ,
50+ word : {
51+ extension : '.docx' ,
52+ mimeType : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ,
53+ } ,
54+ } as const
55+
56+ type MicrosoftFileType = keyof typeof FILE_TYPE_CONFIG
57+
58+ /**
59+ * Get Excel or Word files from Microsoft OneDrive / SharePoint. The
60+ * `fileType` query parameter selects which Office document type to return
61+ * (defaults to `excel`).
1862 */
1963export const GET = withRouteHandler ( async ( request : NextRequest ) => {
2064 const requestId = generateRequestId ( )
@@ -27,6 +71,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
2771 query : searchParams . get ( 'query' ) ?? undefined ,
2872 driveId : searchParams . get ( 'driveId' ) ?? undefined ,
2973 workflowId : searchParams . get ( 'workflowId' ) ?? undefined ,
74+ fileType : searchParams . get ( 'fileType' ) ?? undefined ,
3075 } )
3176
3277 if ( ! parsedQuery . success ) {
@@ -40,6 +85,9 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
4085 const { credentialId, driveId, workflowId } = parsedQuery . data
4186 const query = parsedQuery . data . query ?? ''
4287
88+ const fileType : MicrosoftFileType = parsedQuery . data . fileType ?? 'excel'
89+ const { extension, mimeType : targetMimeType } = FILE_TYPE_CONFIG [ fileType ]
90+
4391 const authz = await authorizeCredentialUse ( request , {
4492 credentialId,
4593 workflowId,
@@ -72,19 +120,16 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
72120 return NextResponse . json ( { error : 'Failed to obtain valid access token' } , { status : 401 } )
73121 }
74122
75- // Build search query for Excel files
76- let searchQuery = '.xlsx'
77- if ( query ) {
78- searchQuery = `${ query } .xlsx`
79- }
123+ // Build search query for the requested Office document type
124+ const searchQuery = query ? `${ query } ${ extension } ` : extension
80125
81126 // Build the query parameters for Microsoft Graph API
82127 const searchParams_new = new URLSearchParams ( )
83128 searchParams_new . append (
84129 '$select' ,
85130 'id,name,mimeType,webUrl,thumbnails,createdDateTime,lastModifiedDateTime,size,createdBy'
86131 )
87- searchParams_new . append ( '$top' , '50' )
132+ searchParams_new . append ( '$top' , String ( MICROSOFT_FILES_PAGE_SIZE ) )
88133
89134 // When driveId is provided (SharePoint), search within that specific drive.
90135 // Otherwise, search the user's personal OneDrive.
@@ -99,44 +144,57 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
99144 }
100145 const drivePath = driveId ? `drives/${ driveId } ` : 'me/drive'
101146
102- const response = await fetch (
103- `https://graph.microsoft.com/v1.0/${ drivePath } /root/search(q='${ encodeURIComponent ( searchQuery ) } ')?${ searchParams_new . toString ( ) } ` ,
104- {
147+ const rawFiles : MicrosoftGraphFile [ ] = [ ]
148+ let nextUrl : string | undefined =
149+ `https://graph.microsoft.com/v1.0/${ drivePath } /root/search(q='${ encodeURIComponent ( searchQuery ) } ')?${ searchParams_new . toString ( ) } `
150+
151+ for ( let page = 0 ; page < MAX_MICROSOFT_FILES_PAGES && nextUrl ; page ++ ) {
152+ const response = await fetch ( nextUrl , {
105153 headers : {
106154 Authorization : `Bearer ${ accessToken } ` ,
107155 } ,
156+ } )
157+
158+ if ( ! response . ok ) {
159+ const errorData = await response
160+ . json ( )
161+ . catch ( ( ) => ( { error : { message : 'Unknown error' } } ) )
162+ logger . error ( `[${ requestId } ] Microsoft Graph API error` , {
163+ status : response . status ,
164+ error : errorData . error ?. message || 'Failed to fetch files from Microsoft OneDrive' ,
165+ } )
166+ return NextResponse . json (
167+ {
168+ error : errorData . error ?. message || 'Failed to fetch files from Microsoft OneDrive' ,
169+ } ,
170+ { status : response . status }
171+ )
108172 }
109- )
110173
111- if ( ! response . ok ) {
112- const errorData = await response . json ( ) . catch ( ( ) => ( { error : { message : 'Unknown error' } } ) )
113- logger . error ( `[${ requestId } ] Microsoft Graph API error` , {
114- status : response . status ,
115- error : errorData . error ?. message || 'Failed to fetch Excel files from Microsoft OneDrive' ,
116- } )
117- return NextResponse . json (
118- {
119- error : errorData . error ?. message || 'Failed to fetch Excel files from Microsoft OneDrive' ,
120- } ,
121- { status : response . status }
122- )
123- }
174+ const data = await response . json ( )
175+ rawFiles . push ( ...( ( data . value as MicrosoftGraphFile [ ] ) || [ ] ) )
176+
177+ const nextLink = getGraphNextPageUrl ( data )
178+ nextUrl = nextLink ? assertGraphNextPageUrl ( nextLink ) : undefined
124179
125- const data = await response . json ( )
126- let files = data . value || [ ]
180+ if ( nextUrl && page === MAX_MICROSOFT_FILES_PAGES - 1 ) {
181+ logger . warn (
182+ `[${ requestId } ] Microsoft files search hit pagination cap; list may be incomplete` ,
183+ { fileType, pages : MAX_MICROSOFT_FILES_PAGES , collected : rawFiles . length }
184+ )
185+ }
186+ }
127187
128- // Transform Microsoft Graph response to match expected format and filter for Excel files
129- files = files
188+ // Transform Microsoft Graph response and filter to the requested file type
189+ const files = rawFiles
130190 . filter (
131- ( file : any ) =>
132- file . name ?. toLowerCase ( ) . endsWith ( '.xlsx' ) ||
133- file . mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
191+ ( file : MicrosoftGraphFile ) =>
192+ file . name ?. toLowerCase ( ) . endsWith ( extension ) || file . mimeType === targetMimeType
134193 )
135- . map ( ( file : any ) => ( {
194+ . map ( ( file : MicrosoftGraphFile ) => ( {
136195 id : file . id ,
137196 name : file . name ,
138- mimeType :
139- file . mimeType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ,
197+ mimeType : file . mimeType || targetMimeType ,
140198 iconLink : file . thumbnails ?. [ 0 ] ?. small ?. url ,
141199 webViewLink : file . webUrl ,
142200 thumbnailLink : file . thumbnails ?. [ 0 ] ?. medium ?. url ,
@@ -155,7 +213,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
155213
156214 return NextResponse . json ( { files } , { status : 200 } )
157215 } catch ( error ) {
158- logger . error ( `[${ requestId } ] Error fetching Excel files from Microsoft OneDrive` , error )
216+ logger . error ( `[${ requestId } ] Error fetching files from Microsoft OneDrive` , error )
159217 return NextResponse . json ( { error : 'Internal server error' } , { status : 500 } )
160218 }
161219} )
0 commit comments