Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@ import {
normalizeBulkOperationId,
extractBulkOperationId,
} from './bulk-operation-status.js'
import {BULK_OPERATIONS_MIN_API_VERSION} from './constants.js'
import {GetBulkOperationByIdQuery} from '../../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js'
import {OrganizationApp, Organization, OrganizationSource} from '../../models/organization.js'
import {ListBulkOperationsQuery} from '../../api/graphql/bulk-operations/generated/list-bulk-operations.js'
import {resolveApiVersion} from '../graphql/common.js'
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest'
import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session'
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'

vi.mock('@shopify/cli-kit/node/session')
vi.mock('@shopify/cli-kit/node/api/admin')
vi.mock('../graphql/common.js', async () => {
const actual = await vi.importActual('../graphql/common.js')
return {
...actual,
resolveApiVersion: vi.fn(),
}
})

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

beforeEach(() => {
vi.mocked(ensureAuthenticatedAdminAsApp).mockResolvedValue({token: 'test-token', storeFqdn})
vi.mocked(resolveApiVersion).mockResolvedValue(BULK_OPERATIONS_MIN_API_VERSION)
})

afterEach(() => {
Expand Down Expand Up @@ -169,6 +179,30 @@ describe('getBulkOperationStatus', () => {
expect(output.info()).toContain('Bulk operation canceled.')
})

test('calls resolveApiVersion with minimum API version constant', async () => {
vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'RUNNING'}))

await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp})

expect(resolveApiVersion).toHaveBeenCalledWith({
adminSession: {token: 'test-token', storeFqdn},
minimumDefaultVersion: BULK_OPERATIONS_MIN_API_VERSION,
})
})

test('uses resolved API version in admin request', async () => {
vi.mocked(resolveApiVersion).mockResolvedValue('test-api-version')
vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'RUNNING'}))

await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp})

expect(adminRequestDoc).toHaveBeenCalledWith(
expect.objectContaining({
version: 'test-api-version',
}),
)
})

describe('time formatting', () => {
test('uses "Started" for running operations', async () => {
vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'RUNNING'}))
Expand Down Expand Up @@ -328,4 +362,28 @@ describe('listBulkOperations', () => {
expect(output.info()).toContain('Listing bulk operations.')
expect(output.info()).toContain('No bulk operations found in the last 7 days.')
})

test('calls resolveApiVersion with minimum API version constant', async () => {
vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperationsList([]))

await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp})

expect(resolveApiVersion).toHaveBeenCalledWith({
adminSession: {token: 'test-token', storeFqdn},
minimumDefaultVersion: BULK_OPERATIONS_MIN_API_VERSION,
})
})

test('uses resolved API version in admin request', async () => {
vi.mocked(resolveApiVersion).mockResolvedValue('test-api-version')
vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperationsList([]))

await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp})

expect(adminRequestDoc).toHaveBeenCalledWith(
expect.objectContaining({
version: 'test-api-version',
}),
)
})
})
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {BulkOperation} from './watch-bulk-operation.js'
import {formatBulkOperationStatus} from './format-bulk-operation-status.js'
import {BULK_OPERATIONS_MIN_API_VERSION} from './constants.js'
import {
GetBulkOperationById,
GetBulkOperationByIdQuery,
} from '../../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js'
import {formatOperationInfo} from '../graphql/common.js'
import {formatOperationInfo, resolveApiVersion} from '../graphql/common.js'
import {OrganizationApp, Organization} from '../../models/organization.js'
import {
ListBulkOperations,
Expand All @@ -19,8 +20,6 @@ import {timeAgo, formatDate} from '@shopify/cli-kit/common/string'
import {BugError} from '@shopify/cli-kit/node/error'
import colors from '@shopify/cli-kit/node/colors'

const API_VERSION = '2026-01'

export function normalizeBulkOperationId(id: string): string {
// If already a GID, return as-is
if (id.startsWith('gid://')) {
Expand Down Expand Up @@ -63,7 +62,7 @@ export async function getBulkOperationStatus(options: GetBulkOperationStatusOpti
body: [
{
list: {
items: formatOperationInfo({organization, remoteApp, storeFqdn, showVersion: false}),
items: formatOperationInfo({organization, remoteApp, storeFqdn}),
},
},
],
Expand All @@ -78,7 +77,10 @@ export async function getBulkOperationStatus(options: GetBulkOperationStatusOpti
query: GetBulkOperationById,
session: adminSession,
variables: {id: operationId},
version: API_VERSION,
version: await resolveApiVersion({
adminSession,
minimumDefaultVersion: BULK_OPERATIONS_MIN_API_VERSION,
}),
})

if (response.bulkOperation) {
Expand All @@ -99,7 +101,7 @@ export async function listBulkOperations(options: ListBulkOperationsOptions): Pr
body: [
{
list: {
items: formatOperationInfo({organization, remoteApp, storeFqdn, showVersion: false}),
items: formatOperationInfo({organization, remoteApp, storeFqdn}),
},
},
],
Expand All @@ -121,7 +123,10 @@ export async function listBulkOperations(options: ListBulkOperationsOptions): Pr
sortKey: 'CREATED_AT',
reverse: true,
},
version: API_VERSION,
version: await resolveApiVersion({
adminSession,
minimumDefaultVersion: BULK_OPERATIONS_MIN_API_VERSION,
}),
})

const operations = response.bulkOperations.nodes.map((operation) => ({
Expand Down
5 changes: 5 additions & 0 deletions packages/app/src/cli/services/bulk-operations/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Minimum API version for bulk operations.
* This ensures bulk operation features work correctly across all operations.
*/
export const BULK_OPERATIONS_MIN_API_VERSION = '2026-01'
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import {runBulkOperationQuery} from './run-query.js'
import {runBulkOperationMutation} from './run-mutation.js'
import {watchBulkOperation, shortBulkOperationPoll} from './watch-bulk-operation.js'
import {downloadBulkOperationResults} from './download-bulk-operation-results.js'
import {validateApiVersion} from '../graphql/common.js'
import {BULK_OPERATIONS_MIN_API_VERSION} from './constants.js'
import {resolveApiVersion} from '../graphql/common.js'
import {BulkOperationRunQueryMutation} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-query.js'
import {BulkOperationRunMutationMutation} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-mutation.js'
import {OrganizationApp, OrganizationSource} from '../../models/organization.js'
Expand All @@ -22,7 +23,7 @@ vi.mock('../graphql/common.js', async () => {
const actual = await vi.importActual('../graphql/common.js')
return {
...actual,
validateApiVersion: vi.fn(),
resolveApiVersion: vi.fn(),
}
})
vi.mock('@shopify/cli-kit/node/ui')
Expand Down Expand Up @@ -68,6 +69,7 @@ describe('executeBulkOperation', () => {
beforeEach(() => {
vi.mocked(ensureAuthenticatedAdminAsApp).mockResolvedValue(mockAdminSession)
vi.mocked(shortBulkOperationPoll).mockResolvedValue(createdBulkOperation)
vi.mocked(resolveApiVersion).mockResolvedValue(BULK_OPERATIONS_MIN_API_VERSION)
})

afterEach(() => {
Expand All @@ -92,6 +94,7 @@ describe('executeBulkOperation', () => {
expect(runBulkOperationQuery).toHaveBeenCalledWith({
adminSession: mockAdminSession,
query,
version: BULK_OPERATIONS_MIN_API_VERSION,
})
expect(runBulkOperationMutation).not.toHaveBeenCalled()
})
Expand All @@ -114,6 +117,7 @@ describe('executeBulkOperation', () => {
expect(runBulkOperationQuery).toHaveBeenCalledWith({
adminSession: mockAdminSession,
query,
version: BULK_OPERATIONS_MIN_API_VERSION,
})
expect(runBulkOperationMutation).not.toHaveBeenCalled()
})
Expand All @@ -137,6 +141,7 @@ describe('executeBulkOperation', () => {
adminSession: mockAdminSession,
query: mutation,
variablesJsonl: undefined,
version: BULK_OPERATIONS_MIN_API_VERSION,
})
expect(runBulkOperationQuery).not.toHaveBeenCalled()
})
Expand All @@ -162,6 +167,7 @@ describe('executeBulkOperation', () => {
adminSession: mockAdminSession,
query: mutation,
variablesJsonl: '{"input":{"id":"gid://shopify/Product/123","tags":["test"]}}',
version: BULK_OPERATIONS_MIN_API_VERSION,
})
})

Expand Down Expand Up @@ -204,9 +210,13 @@ describe('executeBulkOperation', () => {
query,
})

expect(renderWarning).toHaveBeenCalledWith({
headline: 'Bulk operation errors.',
body: 'query: Invalid query syntax\nunknown: Another error',
expect(renderError).toHaveBeenCalledWith({
headline: 'Error creating bulk operation.',
body: {
list: {
items: ['query: Invalid query syntax', 'Another error'],
},
},
})

expect(renderSuccess).not.toHaveBeenCalled()
Expand Down Expand Up @@ -558,51 +568,6 @@ describe('executeBulkOperation', () => {
expect(renderSuccess).not.toHaveBeenCalled()
})

test('validates API version when provided', async () => {
const query = '{ products { edges { node { id } } } }'
const version = '2025-01'
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
bulkOperation: createdBulkOperation,
userErrors: [],
}
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
vi.mocked(validateApiVersion).mockResolvedValue()

await executeBulkOperation({
organization: mockOrganization,
remoteApp: mockRemoteApp,
storeFqdn,
query,
version,
})

expect(validateApiVersion).toHaveBeenCalledWith(mockAdminSession, version)
expect(runBulkOperationQuery).toHaveBeenCalledWith({
adminSession: mockAdminSession,
query,
version,
})
})

test('does not validate version when not provided', async () => {
const query = '{ products { edges { node { id } } } }'
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
bulkOperation: createdBulkOperation,
userErrors: [],
}
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
vi.mocked(validateApiVersion).mockClear()

await executeBulkOperation({
organization: mockOrganization,
remoteApp: mockRemoteApp,
storeFqdn,
query,
})

expect(validateApiVersion).not.toHaveBeenCalled()
})

test('renders warning when completed operation results contain userErrors', async () => {
const query = '{ products { edges { node { id } } } }'
const resultsWithErrors = '{"data":{"productUpdate":{"userErrors":[{"message":"invalid input"}]}},"__lineNumber":0}'
Expand Down Expand Up @@ -711,4 +676,49 @@ describe('executeBulkOperation', () => {
}),
)
})

test('calls resolveApiVersion with minimum API version constant', async () => {
const query = '{ products { edges { node { id } } } }'
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
bulkOperation: createdBulkOperation,
userErrors: [],
}
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)

await executeBulkOperation({
organization: mockOrganization,
remoteApp: mockRemoteApp,
storeFqdn,
query,
})

expect(resolveApiVersion).toHaveBeenCalledWith({
adminSession: mockAdminSession,
userSpecifiedVersion: undefined,
minimumDefaultVersion: BULK_OPERATIONS_MIN_API_VERSION,
})
})

test('uses resolved API version when running bulk operation', async () => {
vi.mocked(resolveApiVersion).mockResolvedValue('test-api-version')
const query = '{ products { edges { node { id } } } }'
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
bulkOperation: createdBulkOperation,
userErrors: [],
}
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)

await executeBulkOperation({
organization: mockOrganization,
remoteApp: mockRemoteApp,
storeFqdn,
query,
})

expect(runBulkOperationQuery).toHaveBeenCalledWith({
adminSession: mockAdminSession,
query,
version: 'test-api-version',
})
})
})
Loading
Loading