Skip to content

Commit d889f70

Browse files
introduce --version flag to bulk operations CLI
1 parent 3969017 commit d889f70

File tree

9 files changed

+133
-5
lines changed

9 files changed

+133
-5
lines changed

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 = {
@@ -482,4 +489,63 @@ describe('executeBulkOperation', () => {
482489

483490
expect(renderSuccess).not.toHaveBeenCalled()
484491
})
492+
493+
test('allows executing bulk operations against unstable', async () => {
494+
const query = '{ products { edges { node { id } } } }'
495+
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
496+
bulkOperation: createdBulkOperation,
497+
userErrors: [],
498+
}
499+
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
500+
501+
await executeBulkOperation({
502+
remoteApp: mockRemoteApp,
503+
storeFqdn,
504+
query,
505+
version: 'unstable',
506+
})
507+
508+
expect(runBulkOperationQuery).toHaveBeenCalledWith({
509+
adminSession: mockAdminSession,
510+
query,
511+
version: 'unstable',
512+
})
513+
})
514+
515+
test('allows executing bulk operations against a specific stable version', async () => {
516+
const query = '{ products { edges { node { id } } } }'
517+
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
518+
bulkOperation: createdBulkOperation,
519+
userErrors: [],
520+
}
521+
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
522+
523+
await executeBulkOperation({
524+
remoteApp: mockRemoteApp,
525+
storeFqdn,
526+
query,
527+
version: '2025-01',
528+
})
529+
530+
expect(runBulkOperationQuery).toHaveBeenCalledWith({
531+
adminSession: mockAdminSession,
532+
query,
533+
version: '2025-01',
534+
})
535+
})
536+
537+
test('throws error when an API version is specified but is not supported', async () => {
538+
const query = '{ products { edges { node { id } } } }'
539+
540+
await expect(
541+
executeBulkOperation({
542+
remoteApp: mockRemoteApp,
543+
storeFqdn,
544+
query,
545+
version: '2099-12',
546+
}),
547+
).rejects.toThrow('Invalid API version')
548+
549+
expect(runBulkOperationQuery).not.toHaveBeenCalled()
550+
})
485551
})

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

Lines changed: 23 additions & 3 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} 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 {parse} from 'graphql'
1213
import {readFile, writeFile, fileExists} from '@shopify/cli-kit/node/fs'
@@ -19,6 +20,7 @@ interface ExecuteBulkOperationInput {
1920
variableFile?: string
2021
watch?: boolean
2122
outputFile?: string
23+
version?: string
2224
}
2325

2426
async function parseVariablesToJsonl(variables?: string[], variableFile?: string): Promise<string | undefined> {
@@ -39,7 +41,7 @@ async function parseVariablesToJsonl(variables?: string[], variableFile?: string
3941
}
4042

4143
export async function executeBulkOperation(input: ExecuteBulkOperationInput): Promise<void> {
42-
const {remoteApp, storeFqdn, query, variables, variableFile, outputFile, watch = false} = input
44+
const {remoteApp, storeFqdn, query, variables, variableFile, outputFile, watch = false, version} = input
4345

4446
renderInfo({
4547
headline: 'Starting bulk operation.',
@@ -51,13 +53,15 @@ export async function executeBulkOperation(input: ExecuteBulkOperationInput): Pr
5153

5254
const adminSession = await ensureAuthenticatedAdminAsApp(storeFqdn, remoteApp.apiKey, appSecret)
5355

56+
if (version) await validateApiVersion(adminSession, version)
57+
5458
const variablesJsonl = await parseVariablesToJsonl(variables, variableFile)
5559

5660
validateGraphQLDocument(query, variablesJsonl)
5761

5862
const bulkOperationResponse = isMutation(query)
59-
? await runBulkOperationMutation({adminSession, query, variablesJsonl})
60-
: await runBulkOperationQuery({adminSession, query})
63+
? await runBulkOperationMutation({adminSession, query, variablesJsonl, version})
64+
: await runBulkOperationQuery({adminSession, query, version})
6165

6266
if (bulkOperationResponse?.userErrors?.length) {
6367
const errorMessages = bulkOperationResponse.userErrors
@@ -152,3 +156,19 @@ function isMutation(graphqlOperation: string): boolean {
152156
const operation = document.definitions.find((def) => def.kind === 'OperationDefinition')
153157
return operation?.kind === 'OperationDefinition' && operation.operation === 'mutation'
154158
}
159+
160+
async function validateApiVersion(adminSession: {token: string; storeFqdn: string}, version: string): Promise<void> {
161+
if (version === 'unstable') return
162+
163+
const supportedVersions = await supportedApiVersions(adminSession)
164+
if (supportedVersions.includes(version)) return
165+
166+
const formattedVersions = supportedVersions
167+
.map((supportedVersion) => outputContent`${outputToken.green(supportedVersion)}`.value)
168+
.join(', ')
169+
170+
const firstLine = outputContent`Invalid API version: ${outputToken.errorText(version)}`.value
171+
const secondLine = outputContent`Supported versions: ${formattedVersions}`.value
172+
173+
throw new AbortError(`${firstLine}\n${secondLine}`)
174+
}

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

packages/cli/oclif.manifest.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,14 @@
202202
"name": "verbose",
203203
"type": "boolean"
204204
},
205+
"version": {
206+
"description": "The API version to use for the bulk operation. If not specified, uses the latest stable version.",
207+
"env": "SHOPIFY_FLAG_VERSION",
208+
"hasDynamicHelp": false,
209+
"multiple": false,
210+
"name": "version",
211+
"type": "option"
212+
},
205213
"watch": {
206214
"allowNo": false,
207215
"description": "Wait for bulk operation results before exiting.",

0 commit comments

Comments
 (0)