@@ -2,20 +2,24 @@ 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'
1011import { inTemporaryDirectory , writeFile } from '@shopify/cli-kit/node/fs'
1112import { joinPath } from '@shopify/cli-kit/node/path'
12- import { describe , test , expect , vi , beforeEach } from 'vitest'
13+ import { mockAndCaptureOutput } from '@shopify/cli-kit/node/testing/output'
14+ import { describe , test , expect , vi , beforeEach , afterEach } from 'vitest'
1315
1416vi . mock ( './run-query.js' )
1517vi . mock ( './run-mutation.js' )
1618vi . mock ( './watch-bulk-operation.js' )
19+ vi . mock ( './download-bulk-operation-results.js' )
1720vi . mock ( '@shopify/cli-kit/node/ui' )
1821vi . mock ( '@shopify/cli-kit/node/session' )
22+ vi . mock ( '@shopify/cli-kit/node/fs' )
1923
2024describe ( 'executeBulkOperation' , ( ) => {
2125 const mockApp = {
@@ -46,6 +50,10 @@ describe('executeBulkOperation', () => {
4650 vi . mocked ( ensureAuthenticatedAdmin ) . mockResolvedValue ( mockAdminSession )
4751 } )
4852
53+ afterEach ( ( ) => {
54+ mockAndCaptureOutput ( ) . clear ( )
55+ } )
56+
4957 test ( 'runs query operation when GraphQL document starts with query' , async ( ) => {
5058 const query = 'query { products { edges { node { id } } } }'
5159 const mockResponse : BulkOperationRunQueryMutation [ 'bulkOperationRunQuery' ] = {
@@ -334,6 +342,7 @@ describe('executeBulkOperation', () => {
334342
335343 vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
336344 vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
345+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( '{"id":"gid://shopify/Product/123"}' )
337346
338347 await executeBulkOperation ( {
339348 app : mockApp ,
@@ -346,11 +355,73 @@ describe('executeBulkOperation', () => {
346355 expect ( renderSuccess ) . toHaveBeenCalledWith (
347356 expect . objectContaining ( {
348357 headline : expect . stringContaining ( 'Bulk operation succeeded:' ) ,
349- body : expect . arrayContaining ( [ expect . stringContaining ( 'https://example.com/download' ) ] ) ,
350358 } ) ,
351359 )
352360 } )
353361
362+ test ( 'writes results to file when --output-file flag is provided' , async ( ) => {
363+ const query = '{ products { edges { node { id } } } }'
364+ const outputFile = '/tmp/results.jsonl'
365+ const resultsContent = '{"id":"gid://shopify/Product/123"}\n{"id":"gid://shopify/Product/456"}'
366+
367+ const initialResponse : BulkOperationRunQueryMutation [ 'bulkOperationRunQuery' ] = {
368+ bulkOperation : createdBulkOperation ,
369+ userErrors : [ ] ,
370+ }
371+ const completedOperation = {
372+ ...createdBulkOperation ,
373+ status : 'COMPLETED' as const ,
374+ url : 'https://example.com/download' ,
375+ objectCount : '2' ,
376+ }
377+
378+ vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
379+ vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
380+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( resultsContent )
381+
382+ await executeBulkOperation ( {
383+ app : mockApp ,
384+ storeFqdn,
385+ query,
386+ watch : true ,
387+ outputFile,
388+ } )
389+
390+ expect ( writeFile ) . toHaveBeenCalledWith ( outputFile , resultsContent )
391+ } )
392+
393+ test ( 'writes results to stdout when --output-file flag is not provided' , async ( ) => {
394+ const query = '{ products { edges { node { id } } } }'
395+ const resultsContent = '{"id":"gid://shopify/Product/123"}\n{"id":"gid://shopify/Product/456"}'
396+
397+ const initialResponse : BulkOperationRunQueryMutation [ 'bulkOperationRunQuery' ] = {
398+ bulkOperation : createdBulkOperation ,
399+ userErrors : [ ] ,
400+ }
401+ const completedOperation = {
402+ ...createdBulkOperation ,
403+ status : 'COMPLETED' as const ,
404+ url : 'https://example.com/download' ,
405+ objectCount : '2' ,
406+ }
407+
408+ const mockOutput = mockAndCaptureOutput ( )
409+
410+ vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
411+ vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
412+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( resultsContent )
413+
414+ await executeBulkOperation ( {
415+ app : mockApp ,
416+ storeFqdn,
417+ query,
418+ watch : true ,
419+ } )
420+
421+ expect ( mockOutput . info ( ) ) . toContain ( resultsContent )
422+ expect ( writeFile ) . not . toHaveBeenCalled ( )
423+ } )
424+
354425 test . each ( [ 'FAILED' , 'CANCELED' , 'EXPIRED' ] as const ) (
355426 'waits for operation to finish and renders error when watch is provided and operation finishes with %s status' ,
356427 async ( status ) => {
0 commit comments