Skip to content

Commit 22deb5f

Browse files
Merge pull request #6684 from Shopify/jtv/add-version-flag-to-bulk-ops-cli
introduce `--version` flag to Bulk Operations CLI
2 parents ef15432 + e0bace3 commit 22deb5f

File tree

11 files changed

+144
-17
lines changed

11 files changed

+144
-17
lines changed

packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-query.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,6 @@ export const BulkOperationRunQuery = {
4949
name: {kind: 'Name', value: 'query'},
5050
value: {kind: 'Variable', name: {kind: 'Name', value: 'query'}},
5151
},
52-
{
53-
kind: 'Argument',
54-
name: {kind: 'Name', value: 'groupObjects'},
55-
value: {kind: 'BooleanValue', value: false},
56-
},
5752
],
5853
selectionSet: {
5954
kind: 'SelectionSet',

packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-query.graphql

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
mutation BulkOperationRunQuery($query: String!) {
22
bulkOperationRunQuery(
33
query: $query
4-
# Set to false to optimize for speed over grouped results
5-
groupObjects: false
64
) {
75
bulkOperation {
86
completedAt

packages/app/src/cli/commands/app/bulk/execute.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export default class BulkExecute extends AppLinkedCommand {
5252
variableFile: flags['variable-file'],
5353
watch: flags.watch,
5454
outputFile: flags['output-file'],
55+
...(flags.version && {version: flags.version}),
5556
})
5657

5758
return {app: appContextResult.app}

packages/app/src/cli/flags.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,8 @@ export const bulkOperationFlags = {
7272
description: 'The file path where results should be written. If not specified, results will be written to STDOUT.',
7373
env: 'SHOPIFY_FLAG_OUTPUT_FILE',
7474
}),
75+
version: Flags.string({
76+
description: 'The API version to use for the bulk operation. If not specified, uses the latest stable version.',
77+
env: 'SHOPIFY_FLAG_VERSION',
78+
}),
7579
}

packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ vi.mock('@shopify/cli-kit/node/session', async () => {
2626
ensureAuthenticatedAdminAsApp: vi.fn(),
2727
}
2828
})
29+
vi.mock('@shopify/cli-kit/node/api/admin', async () => {
30+
const actual = await vi.importActual('@shopify/cli-kit/node/api/admin')
31+
return {
32+
...actual,
33+
supportedApiVersions: vi.fn(() => Promise.resolve(['2025-01', '2025-04', '2025-07', '2025-10'])),
34+
}
35+
})
2936

3037
describe('executeBulkOperation', () => {
3138
const mockRemoteApp = {
@@ -512,4 +519,63 @@ describe('executeBulkOperation', () => {
512519

513520
expect(renderSuccess).not.toHaveBeenCalled()
514521
})
522+
523+
test('allows executing bulk operations against unstable', async () => {
524+
const query = '{ products { edges { node { id } } } }'
525+
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
526+
bulkOperation: createdBulkOperation,
527+
userErrors: [],
528+
}
529+
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
530+
531+
await executeBulkOperation({
532+
remoteApp: mockRemoteApp,
533+
storeFqdn,
534+
query,
535+
version: 'unstable',
536+
})
537+
538+
expect(runBulkOperationQuery).toHaveBeenCalledWith({
539+
adminSession: mockAdminSession,
540+
query,
541+
version: 'unstable',
542+
})
543+
})
544+
545+
test('allows executing bulk operations against a specific stable version', async () => {
546+
const query = '{ products { edges { node { id } } } }'
547+
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
548+
bulkOperation: createdBulkOperation,
549+
userErrors: [],
550+
}
551+
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
552+
553+
await executeBulkOperation({
554+
remoteApp: mockRemoteApp,
555+
storeFqdn,
556+
query,
557+
version: '2025-01',
558+
})
559+
560+
expect(runBulkOperationQuery).toHaveBeenCalledWith({
561+
adminSession: mockAdminSession,
562+
query,
563+
version: '2025-01',
564+
})
565+
})
566+
567+
test('throws error when an API version is specified but is not supported', async () => {
568+
const query = '{ products { edges { node { id } } } }'
569+
570+
await expect(
571+
executeBulkOperation({
572+
remoteApp: mockRemoteApp,
573+
storeFqdn,
574+
query,
575+
version: '2099-12',
576+
}),
577+
).rejects.toThrow('Invalid API version')
578+
579+
expect(runBulkOperationQuery).not.toHaveBeenCalled()
580+
})
515581
})

packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {OrganizationApp} from '../../models/organization.js'
77
import {renderSuccess, renderInfo, renderError, renderWarning, TokenItem} from '@shopify/cli-kit/node/ui'
88
import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output'
99
import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session'
10+
import {supportedApiVersions} from '@shopify/cli-kit/node/api/admin'
1011
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
1112
import {AbortController} from '@shopify/cli-kit/node/abort'
1213
import {parse} from 'graphql'
@@ -20,6 +21,7 @@ interface ExecuteBulkOperationInput {
2021
variableFile?: string
2122
watch?: boolean
2223
outputFile?: string
24+
version?: string
2325
}
2426

2527
async function parseVariablesToJsonl(variables?: string[], variableFile?: string): Promise<string | undefined> {
@@ -40,25 +42,37 @@ async function parseVariablesToJsonl(variables?: string[], variableFile?: string
4042
}
4143

4244
export async function executeBulkOperation(input: ExecuteBulkOperationInput): Promise<void> {
43-
const {remoteApp, storeFqdn, query, variables, variableFile, outputFile, watch = false} = input
44-
45-
renderInfo({
46-
headline: 'Starting bulk operation.',
47-
body: `App: ${remoteApp.title}\nStore: ${storeFqdn}`,
48-
})
45+
const {remoteApp, storeFqdn, query, variables, variableFile, outputFile, watch = false, version} = input
4946

5047
const appSecret = remoteApp.apiSecretKeys[0]?.secret
5148
if (!appSecret) throw new BugError('No API secret keys found for app')
5249

5350
const adminSession = await ensureAuthenticatedAdminAsApp(storeFqdn, remoteApp.apiKey, appSecret)
5451

52+
if (version) await validateApiVersion(adminSession, version)
53+
5554
const variablesJsonl = await parseVariablesToJsonl(variables, variableFile)
5655

5756
validateGraphQLDocument(query, variablesJsonl)
5857

58+
renderInfo({
59+
headline: 'Starting bulk operation.',
60+
body: [
61+
{
62+
list: {
63+
items: [
64+
`App: ${remoteApp.title}`,
65+
`Store: ${storeFqdn}`,
66+
`API version: ${version || 'default (latest stable)'}`,
67+
],
68+
},
69+
},
70+
],
71+
})
72+
5973
const bulkOperationResponse = isMutation(query)
60-
? await runBulkOperationMutation({adminSession, query, variablesJsonl})
61-
: await runBulkOperationQuery({adminSession, query})
74+
? await runBulkOperationMutation({adminSession, query, variablesJsonl, version})
75+
: await runBulkOperationQuery({adminSession, query, version})
6276

6377
if (bulkOperationResponse?.userErrors?.length) {
6478
const errorMessages = bulkOperationResponse.userErrors
@@ -172,3 +186,15 @@ function isMutation(graphqlOperation: string): boolean {
172186
const operation = document.definitions.find((def) => def.kind === 'OperationDefinition')
173187
return operation?.kind === 'OperationDefinition' && operation.operation === 'mutation'
174188
}
189+
190+
async function validateApiVersion(adminSession: {token: string; storeFqdn: string}, version: string): Promise<void> {
191+
if (version === 'unstable') return
192+
193+
const supportedVersions = await supportedApiVersions(adminSession)
194+
if (supportedVersions.includes(version)) return
195+
196+
const firstLine = outputContent`Invalid API version: ${version}`.value
197+
const secondLine = outputContent`Supported versions: ${supportedVersions.join(', ')}`.value
198+
199+
throw new AbortError(`${firstLine}\n${secondLine}`)
200+
}

packages/app/src/cli/services/bulk-operations/run-mutation.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,17 @@ describe('runBulkOperationMutation', () => {
4040
expect(bulkOperationResult?.bulkOperation).toEqual(successfulBulkOperation)
4141
expect(bulkOperationResult?.userErrors).toEqual([])
4242
})
43+
44+
test('starts bulk mutation with specific API version when provided', async () => {
45+
vi.mocked(adminRequestDoc).mockResolvedValue(mockSuccessResponse)
46+
47+
await runBulkOperationMutation({
48+
adminSession: mockSession,
49+
query: 'mutation productUpdate($input: ProductInput!) { productUpdate(input: $input) { product { id } } }',
50+
variablesJsonl: '{"input":{"id":"gid://shopify/Product/123","tags":["test"]}}',
51+
version: '2025-01',
52+
})
53+
54+
expect(adminRequestDoc).toHaveBeenCalledWith(expect.objectContaining({version: '2025-01'}))
55+
})
4356
})

packages/app/src/cli/services/bulk-operations/run-mutation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ interface BulkOperationRunMutationOptions {
1111
adminSession: AdminSession
1212
query: string
1313
variablesJsonl?: string
14+
version?: string
1415
}
1516

1617
export async function runBulkOperationMutation(
1718
options: BulkOperationRunMutationOptions,
1819
): Promise<BulkOperationRunMutationMutation['bulkOperationRunMutation']> {
19-
const {adminSession, query: mutation, variablesJsonl} = options
20+
const {adminSession, query: mutation, variablesJsonl, version} = options
2021

2122
const stagedUploadPath = await stageFile({
2223
adminSession,
@@ -30,6 +31,7 @@ export async function runBulkOperationMutation(
3031
mutation,
3132
stagedUploadPath,
3233
},
34+
...(version && {version}),
3335
})
3436

3537
return response.bulkOperationRunMutation

packages/app/src/cli/services/bulk-operations/run-query.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,16 @@ describe('runBulkOperationQuery', () => {
3333
expect(bulkOperationResult?.bulkOperation).toEqual(successfulBulkOperation)
3434
expect(bulkOperationResult?.userErrors).toEqual([])
3535
})
36+
37+
test('starts bulk query with specific API version when provided', async () => {
38+
vi.mocked(adminRequestDoc).mockResolvedValue(mockSuccessResponse)
39+
40+
await runBulkOperationQuery({
41+
adminSession: mockSession,
42+
query: 'query { products { edges { node { id } } } }',
43+
version: '2025-01',
44+
})
45+
46+
expect(adminRequestDoc).toHaveBeenCalledWith(expect.objectContaining({version: '2025-01'}))
47+
})
3648
})

packages/app/src/cli/services/bulk-operations/run-query.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@ import {AdminSession} from '@shopify/cli-kit/node/session'
88
interface BulkOperationRunQueryOptions {
99
adminSession: AdminSession
1010
query: string
11+
version?: string
1112
}
1213

1314
export async function runBulkOperationQuery(
1415
options: BulkOperationRunQueryOptions,
1516
): Promise<BulkOperationRunQueryMutation['bulkOperationRunQuery']> {
16-
const {adminSession, query} = options
17+
const {adminSession, query, version} = options
1718

1819
const response = await adminRequestDoc<BulkOperationRunQueryMutation, {query: string}>({
1920
query: BulkOperationRunQuery,
2021
session: adminSession,
2122
variables: {query},
23+
...(version && {version}),
2224
})
2325

2426
return response.bulkOperationRunQuery

0 commit comments

Comments
 (0)