@@ -2,18 +2,22 @@ import {executeBulkOperation} from './execute-bulk-operation.js'
22import { runBulkOperationQuery } from './run-query.js'
33import { runBulkOperationMutation } from './run-mutation.js'
44import { watchBulkOperation } from './watch-bulk-operation.js'
5+ import { downloadBulkOperationResults } from './download-bulk-operation-results.js'
56import { AppLinkedInterface } from '../../models/app/app.js'
67import { BulkOperationRunQueryMutation } from '../../api/graphql/bulk-operations/generated/bulk-operation-run-query.js'
78import { BulkOperationRunMutationMutation } from '../../api/graphql/bulk-operations/generated/bulk-operation-run-mutation.js'
89import { renderSuccess , renderWarning , renderError } from '@shopify/cli-kit/node/ui'
910import { ensureAuthenticatedAdmin } from '@shopify/cli-kit/node/session'
11+ import { writeFile } from '@shopify/cli-kit/node/fs'
1012import { describe , test , expect , vi , beforeEach } from 'vitest'
1113
1214vi . mock ( './run-query.js' )
1315vi . mock ( './run-mutation.js' )
1416vi . mock ( './watch-bulk-operation.js' )
17+ vi . mock ( './download-bulk-operation-results.js' )
1518vi . mock ( '@shopify/cli-kit/node/ui' )
1619vi . mock ( '@shopify/cli-kit/node/session' )
20+ vi . mock ( '@shopify/cli-kit/node/fs' )
1721
1822describe ( 'executeBulkOperation' , ( ) => {
1923 const mockApp = {
@@ -193,6 +197,7 @@ describe('executeBulkOperation', () => {
193197
194198 vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
195199 vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
200+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( '{"id":"gid://shopify/Product/123"}' )
196201
197202 await executeBulkOperation ( {
198203 app : mockApp ,
@@ -205,11 +210,81 @@ describe('executeBulkOperation', () => {
205210 expect ( renderSuccess ) . toHaveBeenCalledWith (
206211 expect . objectContaining ( {
207212 headline : expect . stringContaining ( 'Bulk operation succeeded.' ) ,
208- body : expect . arrayContaining ( [ expect . stringContaining ( 'https://example.com/download' ) ] ) ,
209213 } ) ,
210214 )
211215 } )
212216
217+ test ( 'writes results to file when --output-file flag is provided' , async ( ) => {
218+ const query = '{ products { edges { node { id } } } }'
219+ const outputFile = '/tmp/results.jsonl'
220+ const resultsContent = '{"id":"gid://shopify/Product/123"}\n{"id":"gid://shopify/Product/456"}'
221+
222+ const initialResponse : BulkOperationRunQueryMutation [ 'bulkOperationRunQuery' ] = {
223+ bulkOperation : createdBulkOperation ,
224+ userErrors : [ ] ,
225+ }
226+ const completedOperation = {
227+ ...createdBulkOperation ,
228+ status : 'COMPLETED' as const ,
229+ url : 'https://example.com/download' ,
230+ objectCount : '2' ,
231+ }
232+
233+ vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
234+ vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
235+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( resultsContent )
236+
237+ await executeBulkOperation ( {
238+ app : mockApp ,
239+ storeFqdn,
240+ query,
241+ watch : true ,
242+ outputFile,
243+ } )
244+
245+ expect ( writeFile ) . toHaveBeenCalledWith ( outputFile , resultsContent )
246+ expect ( renderSuccess ) . toHaveBeenCalledWith (
247+ expect . objectContaining ( {
248+ body : expect . arrayContaining ( [ expect . stringContaining ( outputFile ) ] ) ,
249+ } ) ,
250+ )
251+ } )
252+
253+ test ( 'writes results to stdout when --output-file flag is not provided' , async ( ) => {
254+ const query = '{ products { edges { node { id } } } }'
255+ const resultsContent = '{"id":"gid://shopify/Product/123"}\n{"id":"gid://shopify/Product/456"}'
256+
257+ const initialResponse : BulkOperationRunQueryMutation [ 'bulkOperationRunQuery' ] = {
258+ bulkOperation : createdBulkOperation ,
259+ userErrors : [ ] ,
260+ }
261+ const completedOperation = {
262+ ...createdBulkOperation ,
263+ status : 'COMPLETED' as const ,
264+ url : 'https://example.com/download' ,
265+ objectCount : '2' ,
266+ }
267+
268+ const stdoutWriteSpy = vi . spyOn ( process . stdout , 'write' ) . mockImplementation ( ( ) => true )
269+
270+ vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
271+ vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
272+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( resultsContent )
273+
274+ await executeBulkOperation ( {
275+ app : mockApp ,
276+ storeFqdn,
277+ query,
278+ watch : true ,
279+ } )
280+
281+ expect ( stdoutWriteSpy ) . toHaveBeenCalledWith ( resultsContent )
282+ expect ( writeFile ) . not . toHaveBeenCalled ( )
283+ expect ( renderSuccess ) . toHaveBeenCalled ( )
284+
285+ stdoutWriteSpy . mockRestore ( )
286+ } )
287+
213288 test . each ( [ 'FAILED' , 'CANCELED' , 'EXPIRED' ] as const ) (
214289 'waits for operation to finish and renders error when watch is provided and operation finishes with %s status' ,
215290 async ( status ) => {
0 commit comments