Skip to content

Commit 8d02b7a

Browse files
committed
refactor version validation to allow RCs and default to 2026-01 for bulk operations
1 parent 655f46b commit 8d02b7a

File tree

10 files changed

+223
-117
lines changed

10 files changed

+223
-117
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,25 @@ import {
44
normalizeBulkOperationId,
55
extractBulkOperationId,
66
} from './bulk-operation-status.js'
7+
import {BULK_OPERATIONS_MIN_API_VERSION} from './constants.js'
78
import {GetBulkOperationByIdQuery} from '../../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js'
89
import {OrganizationApp, Organization, OrganizationSource} from '../../models/organization.js'
910
import {ListBulkOperationsQuery} from '../../api/graphql/bulk-operations/generated/list-bulk-operations.js'
11+
import {resolveApiVersion} from '../graphql/common.js'
1012
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'
1113
import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session'
1214
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
1315
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'
1416

1517
vi.mock('@shopify/cli-kit/node/session')
1618
vi.mock('@shopify/cli-kit/node/api/admin')
19+
vi.mock('../graphql/common.js', async () => {
20+
const actual = await vi.importActual('../graphql/common.js')
21+
return {
22+
...actual,
23+
resolveApiVersion: vi.fn(),
24+
}
25+
})
1726

1827
const storeFqdn = 'test-store.myshopify.com'
1928
const operationId = 'gid://shopify/BulkOperation/123'
@@ -35,6 +44,7 @@ const remoteApp = {
3544

3645
beforeEach(() => {
3746
vi.mocked(ensureAuthenticatedAdminAsApp).mockResolvedValue({token: 'test-token', storeFqdn})
47+
vi.mocked(resolveApiVersion).mockResolvedValue('2026-01')
3848
})
3949

4050
afterEach(() => {
@@ -169,6 +179,18 @@ describe('getBulkOperationStatus', () => {
169179
expect(output.info()).toContain('Bulk operation canceled.')
170180
})
171181

182+
test('calls resolveApiVersion with minimum API version', async () => {
183+
vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'RUNNING'}))
184+
185+
await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp})
186+
187+
expect(resolveApiVersion).toHaveBeenCalledWith(
188+
{token: 'test-token', storeFqdn},
189+
undefined,
190+
BULK_OPERATIONS_MIN_API_VERSION,
191+
)
192+
})
193+
172194
describe('time formatting', () => {
173195
test('uses "Started" for running operations', async () => {
174196
vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'RUNNING'}))
@@ -328,4 +350,16 @@ describe('listBulkOperations', () => {
328350
expect(output.info()).toContain('Listing bulk operations.')
329351
expect(output.info()).toContain('No bulk operations found in the last 7 days.')
330352
})
353+
354+
test('calls resolveApiVersion with minimum API version', async () => {
355+
vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperationsList([]))
356+
357+
await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp})
358+
359+
expect(resolveApiVersion).toHaveBeenCalledWith(
360+
{token: 'test-token', storeFqdn},
361+
undefined,
362+
BULK_OPERATIONS_MIN_API_VERSION,
363+
)
364+
})
331365
})

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {BulkOperation} from './watch-bulk-operation.js'
22
import {formatBulkOperationStatus} from './format-bulk-operation-status.js'
3+
import {BULK_OPERATIONS_MIN_API_VERSION} from './constants.js'
34
import {
45
GetBulkOperationById,
56
GetBulkOperationByIdQuery,
67
} from '../../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js'
7-
import {formatOperationInfo} from '../graphql/common.js'
8+
import {formatOperationInfo, resolveApiVersion} from '../graphql/common.js'
89
import {OrganizationApp, Organization} from '../../models/organization.js'
910
import {
1011
ListBulkOperations,
@@ -19,8 +20,6 @@ import {timeAgo, formatDate} from '@shopify/cli-kit/common/string'
1920
import {BugError} from '@shopify/cli-kit/node/error'
2021
import colors from '@shopify/cli-kit/node/colors'
2122

22-
const API_VERSION = '2026-01'
23-
2423
export function normalizeBulkOperationId(id: string): string {
2524
// If already a GID, return as-is
2625
if (id.startsWith('gid://')) {
@@ -63,7 +62,7 @@ export async function getBulkOperationStatus(options: GetBulkOperationStatusOpti
6362
body: [
6463
{
6564
list: {
66-
items: formatOperationInfo({organization, remoteApp, storeFqdn, showVersion: false}),
65+
items: formatOperationInfo({organization, remoteApp, storeFqdn}),
6766
},
6867
},
6968
],
@@ -78,7 +77,7 @@ export async function getBulkOperationStatus(options: GetBulkOperationStatusOpti
7877
query: GetBulkOperationById,
7978
session: adminSession,
8079
variables: {id: operationId},
81-
version: API_VERSION,
80+
version: await resolveApiVersion(adminSession, undefined, BULK_OPERATIONS_MIN_API_VERSION),
8281
})
8382

8483
if (response.bulkOperation) {
@@ -99,7 +98,7 @@ export async function listBulkOperations(options: ListBulkOperationsOptions): Pr
9998
body: [
10099
{
101100
list: {
102-
items: formatOperationInfo({organization, remoteApp, storeFqdn, showVersion: false}),
101+
items: formatOperationInfo({organization, remoteApp, storeFqdn}),
103102
},
104103
},
105104
],
@@ -121,7 +120,7 @@ export async function listBulkOperations(options: ListBulkOperationsOptions): Pr
121120
sortKey: 'CREATED_AT',
122121
reverse: true,
123122
},
124-
version: API_VERSION,
123+
version: await resolveApiVersion(adminSession, undefined, BULK_OPERATIONS_MIN_API_VERSION),
125124
})
126125

127126
const operations = response.bulkOperations.nodes.map((operation) => ({
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* Minimum API version for bulk operations.
3+
* This ensures bulk operation features work correctly across all operations.
4+
*/
5+
export const BULK_OPERATIONS_MIN_API_VERSION = '2026-01'

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

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {runBulkOperationQuery} from './run-query.js'
33
import {runBulkOperationMutation} from './run-mutation.js'
44
import {watchBulkOperation, shortBulkOperationPoll} from './watch-bulk-operation.js'
55
import {downloadBulkOperationResults} from './download-bulk-operation-results.js'
6-
import {validateApiVersion} from '../graphql/common.js'
6+
import {resolveApiVersion} from '../graphql/common.js'
77
import {BulkOperationRunQueryMutation} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-query.js'
88
import {BulkOperationRunMutationMutation} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-mutation.js'
99
import {OrganizationApp, OrganizationSource} from '../../models/organization.js'
@@ -22,7 +22,7 @@ vi.mock('../graphql/common.js', async () => {
2222
const actual = await vi.importActual('../graphql/common.js')
2323
return {
2424
...actual,
25-
validateApiVersion: vi.fn(),
25+
resolveApiVersion: vi.fn(),
2626
}
2727
})
2828
vi.mock('@shopify/cli-kit/node/ui')
@@ -68,6 +68,7 @@ describe('executeBulkOperation', () => {
6868
beforeEach(() => {
6969
vi.mocked(ensureAuthenticatedAdminAsApp).mockResolvedValue(mockAdminSession)
7070
vi.mocked(shortBulkOperationPoll).mockResolvedValue(createdBulkOperation)
71+
vi.mocked(resolveApiVersion).mockResolvedValue('2026-01')
7172
})
7273

7374
afterEach(() => {
@@ -92,6 +93,7 @@ describe('executeBulkOperation', () => {
9293
expect(runBulkOperationQuery).toHaveBeenCalledWith({
9394
adminSession: mockAdminSession,
9495
query,
96+
version: '2026-01',
9597
})
9698
expect(runBulkOperationMutation).not.toHaveBeenCalled()
9799
})
@@ -114,6 +116,7 @@ describe('executeBulkOperation', () => {
114116
expect(runBulkOperationQuery).toHaveBeenCalledWith({
115117
adminSession: mockAdminSession,
116118
query,
119+
version: '2026-01',
117120
})
118121
expect(runBulkOperationMutation).not.toHaveBeenCalled()
119122
})
@@ -137,6 +140,7 @@ describe('executeBulkOperation', () => {
137140
adminSession: mockAdminSession,
138141
query: mutation,
139142
variablesJsonl: undefined,
143+
version: '2026-01',
140144
})
141145
expect(runBulkOperationQuery).not.toHaveBeenCalled()
142146
})
@@ -162,6 +166,7 @@ describe('executeBulkOperation', () => {
162166
adminSession: mockAdminSession,
163167
query: mutation,
164168
variablesJsonl: '{"input":{"id":"gid://shopify/Product/123","tags":["test"]}}',
169+
version: '2026-01',
165170
})
166171
})
167172

@@ -562,51 +567,6 @@ describe('executeBulkOperation', () => {
562567
expect(renderSuccess).not.toHaveBeenCalled()
563568
})
564569

565-
test('validates API version when provided', async () => {
566-
const query = '{ products { edges { node { id } } } }'
567-
const version = '2025-01'
568-
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
569-
bulkOperation: createdBulkOperation,
570-
userErrors: [],
571-
}
572-
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
573-
vi.mocked(validateApiVersion).mockResolvedValue()
574-
575-
await executeBulkOperation({
576-
organization: mockOrganization,
577-
remoteApp: mockRemoteApp,
578-
storeFqdn,
579-
query,
580-
version,
581-
})
582-
583-
expect(validateApiVersion).toHaveBeenCalledWith(mockAdminSession, version)
584-
expect(runBulkOperationQuery).toHaveBeenCalledWith({
585-
adminSession: mockAdminSession,
586-
query,
587-
version,
588-
})
589-
})
590-
591-
test('does not validate version when not provided', async () => {
592-
const query = '{ products { edges { node { id } } } }'
593-
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
594-
bulkOperation: createdBulkOperation,
595-
userErrors: [],
596-
}
597-
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
598-
vi.mocked(validateApiVersion).mockClear()
599-
600-
await executeBulkOperation({
601-
organization: mockOrganization,
602-
remoteApp: mockRemoteApp,
603-
storeFqdn,
604-
query,
605-
})
606-
607-
expect(validateApiVersion).not.toHaveBeenCalled()
608-
})
609-
610570
test('renders warning when completed operation results contain userErrors', async () => {
611571
const query = '{ products { edges { node { id } } } }'
612572
const resultsWithErrors = '{"data":{"productUpdate":{"userErrors":[{"message":"invalid input"}]}},"__lineNumber":0}'

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import {watchBulkOperation, shortBulkOperationPoll, type BulkOperation} from './
44
import {formatBulkOperationStatus} from './format-bulk-operation-status.js'
55
import {downloadBulkOperationResults} from './download-bulk-operation-results.js'
66
import {extractBulkOperationId} from './bulk-operation-status.js'
7+
import {BULK_OPERATIONS_MIN_API_VERSION} from './constants.js'
78
import {
89
createAdminSessionAsApp,
910
validateSingleOperation,
10-
validateApiVersion,
1111
formatOperationInfo,
12+
resolveApiVersion,
1213
} from '../graphql/common.js'
1314
import {OrganizationApp, Organization} from '../../models/organization.js'
1415
import {renderSuccess, renderInfo, renderError, renderWarning, TokenItem} from '@shopify/cli-kit/node/ui'
@@ -48,11 +49,21 @@ async function parseVariablesToJsonl(variables?: string[], variableFile?: string
4849
}
4950

5051
export async function executeBulkOperation(input: ExecuteBulkOperationInput): Promise<void> {
51-
const {organization, remoteApp, storeFqdn, query, variables, variableFile, outputFile, watch = false, version} = input
52+
const {
53+
organization,
54+
remoteApp,
55+
storeFqdn,
56+
query,
57+
variables,
58+
variableFile,
59+
outputFile,
60+
watch = false,
61+
version: versionFlag,
62+
} = input
5263

5364
const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn)
5465

55-
if (version) await validateApiVersion(adminSession, version)
66+
const version = await resolveApiVersion(adminSession, versionFlag, BULK_OPERATIONS_MIN_API_VERSION)
5667

5768
const variablesJsonl = await parseVariablesToJsonl(variables, variableFile)
5869

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

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {executeOperation} from './execute-operation.js'
2-
import {createAdminSessionAsApp, validateApiVersion} from './graphql/common.js'
2+
import {createAdminSessionAsApp, resolveApiVersion} from './graphql/common.js'
33
import {OrganizationApp, OrganizationSource} from '../models/organization.js'
44
import {renderSuccess, renderError, renderSingleTask} from '@shopify/cli-kit/node/ui'
55
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
@@ -32,6 +32,7 @@ describe('executeOperation', () => {
3232

3333
beforeEach(() => {
3434
vi.mocked(createAdminSessionAsApp).mockResolvedValue(mockAdminSession)
35+
vi.mocked(resolveApiVersion).mockResolvedValue('2024-07')
3536
vi.mocked(renderSingleTask).mockImplementation(async ({task}) => {
3637
return task(() => {})
3738
})
@@ -54,12 +55,13 @@ describe('executeOperation', () => {
5455
})
5556

5657
expect(createAdminSessionAsApp).toHaveBeenCalledWith(mockRemoteApp, storeFqdn)
58+
expect(resolveApiVersion).toHaveBeenCalledWith(mockAdminSession, undefined)
5759
expect(adminRequestDoc).toHaveBeenCalledWith({
5860
// parsed GraphQL document
5961
query: expect.any(Object),
6062
session: mockAdminSession,
6163
variables: undefined,
62-
version: undefined,
64+
version: '2024-07',
6365
responseOptions: {handleErrors: false},
6466
})
6567
})
@@ -107,7 +109,7 @@ describe('executeOperation', () => {
107109
const version = '2024-01'
108110
const mockResult = {data: {shop: {name: 'Test Shop'}}}
109111
vi.mocked(adminRequestDoc).mockResolvedValue(mockResult)
110-
vi.mocked(validateApiVersion).mockResolvedValue()
112+
vi.mocked(resolveApiVersion).mockResolvedValue(version)
111113

112114
await executeOperation({
113115
organization: mockOrganization,
@@ -117,30 +119,14 @@ describe('executeOperation', () => {
117119
version,
118120
})
119121

120-
expect(validateApiVersion).toHaveBeenCalledWith(mockAdminSession, version)
122+
expect(resolveApiVersion).toHaveBeenCalledWith(mockAdminSession, version)
121123
expect(adminRequestDoc).toHaveBeenCalledWith(
122124
expect.objectContaining({
123125
version,
124126
}),
125127
)
126128
})
127129

128-
test('does not validate version when not provided', async () => {
129-
const query = 'query { shop { name } }'
130-
const mockResult = {data: {shop: {name: 'Test Shop'}}}
131-
vi.mocked(adminRequestDoc).mockResolvedValue(mockResult)
132-
vi.mocked(validateApiVersion).mockClear()
133-
134-
await executeOperation({
135-
organization: mockOrganization,
136-
remoteApp: mockRemoteApp,
137-
storeFqdn,
138-
query,
139-
})
140-
141-
expect(validateApiVersion).not.toHaveBeenCalled()
142-
})
143-
144130
test('writes formatted JSON results to stdout by default', async () => {
145131
const query = 'query { shop { name } }'
146132
const mockResult = {data: {shop: {name: 'Test Shop'}}}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
createAdminSessionAsApp,
33
validateSingleOperation,
4-
validateApiVersion,
4+
resolveApiVersion,
55
formatOperationInfo,
66
} from './graphql/common.js'
77
import {OrganizationApp, Organization} from '../models/organization.js'
@@ -38,7 +38,11 @@ async function parseVariables(variables?: string): Promise<{[key: string]: unkno
3838
}
3939

4040
export async function executeOperation(input: ExecuteOperationInput): Promise<void> {
41-
const {organization, remoteApp, storeFqdn, query, variables, version, outputFile} = input
41+
const {organization, remoteApp, storeFqdn, query, variables, version: versionFlag, outputFile} = input
42+
43+
const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn)
44+
45+
const version = await resolveApiVersion(adminSession, versionFlag)
4246

4347
renderInfo({
4448
headline: 'Executing GraphQL operation.',
@@ -51,10 +55,6 @@ export async function executeOperation(input: ExecuteOperationInput): Promise<vo
5155
],
5256
})
5357

54-
const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn)
55-
56-
if (version) await validateApiVersion(adminSession, version)
57-
5858
const parsedVariables = await parseVariables(variables)
5959

6060
validateSingleOperation(query)

0 commit comments

Comments
 (0)