@@ -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 = {
@@ -42,6 +46,10 @@ describe('executeBulkOperation', () => {
4246 vi . mocked ( ensureAuthenticatedAdmin ) . mockResolvedValue ( mockAdminSession )
4347 } )
4448
49+ afterEach ( ( ) => {
50+ mockAndCaptureOutput ( ) . clear ( )
51+ } )
52+
4553 test ( 'runs query operation when GraphQL document starts with query' , async ( ) => {
4654 const query = 'query { products { edges { node { id } } } }'
4755 const mockResponse : BulkOperationRunQueryMutation [ 'bulkOperationRunQuery' ] = {
@@ -330,6 +338,7 @@ describe('executeBulkOperation', () => {
330338
331339 vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
332340 vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
341+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( '{"id":"gid://shopify/Product/123"}' )
333342
334343 await executeBulkOperation ( {
335344 app : mockApp ,
@@ -342,11 +351,73 @@ describe('executeBulkOperation', () => {
342351 expect ( renderSuccess ) . toHaveBeenCalledWith (
343352 expect . objectContaining ( {
344353 headline : expect . stringContaining ( 'Bulk operation succeeded:' ) ,
345- body : expect . arrayContaining ( [ expect . stringContaining ( 'https://example.com/download' ) ] ) ,
346354 } ) ,
347355 )
348356 } )
349357
358+ test ( 'writes results to file when --output-file flag is provided' , async ( ) => {
359+ const query = '{ products { edges { node { id } } } }'
360+ const outputFile = '/tmp/results.jsonl'
361+ const resultsContent = '{"id":"gid://shopify/Product/123"}\n{"id":"gid://shopify/Product/456"}'
362+
363+ const initialResponse : BulkOperationRunQueryMutation [ 'bulkOperationRunQuery' ] = {
364+ bulkOperation : createdBulkOperation ,
365+ userErrors : [ ] ,
366+ }
367+ const completedOperation = {
368+ ...createdBulkOperation ,
369+ status : 'COMPLETED' as const ,
370+ url : 'https://example.com/download' ,
371+ objectCount : '2' ,
372+ }
373+
374+ vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
375+ vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
376+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( resultsContent )
377+
378+ await executeBulkOperation ( {
379+ app : mockApp ,
380+ storeFqdn,
381+ query,
382+ watch : true ,
383+ outputFile,
384+ } )
385+
386+ expect ( writeFile ) . toHaveBeenCalledWith ( outputFile , resultsContent )
387+ } )
388+
389+ test ( 'writes results to stdout when --output-file flag is not provided' , async ( ) => {
390+ const query = '{ products { edges { node { id } } } }'
391+ const resultsContent = '{"id":"gid://shopify/Product/123"}\n{"id":"gid://shopify/Product/456"}'
392+
393+ const initialResponse : BulkOperationRunQueryMutation [ 'bulkOperationRunQuery' ] = {
394+ bulkOperation : createdBulkOperation ,
395+ userErrors : [ ] ,
396+ }
397+ const completedOperation = {
398+ ...createdBulkOperation ,
399+ status : 'COMPLETED' as const ,
400+ url : 'https://example.com/download' ,
401+ objectCount : '2' ,
402+ }
403+
404+ const mockOutput = mockAndCaptureOutput ( )
405+
406+ vi . mocked ( runBulkOperationQuery ) . mockResolvedValue ( initialResponse )
407+ vi . mocked ( watchBulkOperation ) . mockResolvedValue ( completedOperation )
408+ vi . mocked ( downloadBulkOperationResults ) . mockResolvedValue ( resultsContent )
409+
410+ await executeBulkOperation ( {
411+ app : mockApp ,
412+ storeFqdn,
413+ query,
414+ watch : true ,
415+ } )
416+
417+ expect ( mockOutput . info ( ) ) . toContain ( resultsContent )
418+ expect ( writeFile ) . not . toHaveBeenCalled ( )
419+ } )
420+
350421 test . each ( [ 'FAILED' , 'CANCELED' , 'EXPIRED' ] as const ) (
351422 'waits for operation to finish and renders error when watch is provided and operation finishes with %s status' ,
352423 async ( status ) => {
0 commit comments