diff --git a/packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-mutation.ts b/packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-mutation.ts index b282c6e3f7..7252cc29a5 100644 --- a/packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-mutation.ts +++ b/packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-mutation.ts @@ -12,6 +12,7 @@ export type BulkOperationRunMutationMutationVariables = Types.Exact<{ export type BulkOperationRunMutationMutation = { bulkOperationRunMutation?: { bulkOperation?: { + type: Types.BulkOperationType completedAt?: unknown | null createdAt: unknown errorCode?: Types.BulkOperationErrorCode | null @@ -81,6 +82,7 @@ export const BulkOperationRunMutation = { selectionSet: { kind: 'SelectionSet', selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'type'}}, {kind: 'Field', name: {kind: 'Name', value: 'completedAt'}}, {kind: 'Field', name: {kind: 'Name', value: 'createdAt'}}, {kind: 'Field', name: {kind: 'Name', value: 'errorCode'}}, diff --git a/packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-query.ts b/packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-query.ts index 480d02c9f1..c514367800 100644 --- a/packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-query.ts +++ b/packages/app/src/cli/api/graphql/bulk-operations/generated/bulk-operation-run-query.ts @@ -10,6 +10,7 @@ export type BulkOperationRunQueryMutationVariables = Types.Exact<{ export type BulkOperationRunQueryMutation = { bulkOperationRunQuery?: { bulkOperation?: { + type: Types.BulkOperationType completedAt?: unknown | null createdAt: unknown errorCode?: Types.BulkOperationErrorCode | null @@ -59,6 +60,7 @@ export const BulkOperationRunQuery = { selectionSet: { kind: 'SelectionSet', selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'type'}}, {kind: 'Field', name: {kind: 'Name', value: 'completedAt'}}, {kind: 'Field', name: {kind: 'Name', value: 'createdAt'}}, {kind: 'Field', name: {kind: 'Name', value: 'errorCode'}}, diff --git a/packages/app/src/cli/api/graphql/bulk-operations/generated/get-bulk-operation-by-id.ts b/packages/app/src/cli/api/graphql/bulk-operations/generated/get-bulk-operation-by-id.ts index 6dea755636..eaa3c34352 100644 --- a/packages/app/src/cli/api/graphql/bulk-operations/generated/get-bulk-operation-by-id.ts +++ b/packages/app/src/cli/api/graphql/bulk-operations/generated/get-bulk-operation-by-id.ts @@ -9,6 +9,7 @@ export type GetBulkOperationByIdQueryVariables = Types.Exact<{ export type GetBulkOperationByIdQuery = { bulkOperation?: { + type: Types.BulkOperationType completedAt?: unknown | null createdAt: unknown errorCode?: Types.BulkOperationErrorCode | null @@ -50,6 +51,7 @@ export const GetBulkOperationById = { selectionSet: { kind: 'SelectionSet', selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'type'}}, {kind: 'Field', name: {kind: 'Name', value: 'completedAt'}}, {kind: 'Field', name: {kind: 'Name', value: 'createdAt'}}, {kind: 'Field', name: {kind: 'Name', value: 'errorCode'}}, diff --git a/packages/app/src/cli/api/graphql/bulk-operations/generated/types.d.ts b/packages/app/src/cli/api/graphql/bulk-operations/generated/types.d.ts index bdb0cdb668..f52c0a5a9a 100644 --- a/packages/app/src/cli/api/graphql/bulk-operations/generated/types.d.ts +++ b/packages/app/src/cli/api/graphql/bulk-operations/generated/types.d.ts @@ -188,6 +188,13 @@ export type BulkOperationStatus = /** The bulk operation is runnning. */ | 'RUNNING' +/** The valid values for the bulk operation's type. */ +export type BulkOperationType = + /** The bulk operation is a mutation. */ + | 'MUTATION' + /** The bulk operation is a query. */ + | 'QUERY' + /** Possible error codes that can be returned by `BulkOperationUserError`. */ export type BulkOperationUserErrorCode = /** The input value is invalid. */ diff --git a/packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-mutation.graphql b/packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-mutation.graphql index a0d58086e0..9e03970022 100644 --- a/packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-mutation.graphql +++ b/packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-mutation.graphql @@ -9,6 +9,7 @@ mutation BulkOperationRunMutation( clientIdentifier: $clientIdentifier ) { bulkOperation { + type completedAt createdAt errorCode diff --git a/packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-query.graphql b/packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-query.graphql index 9922c8acc8..8a2101ade8 100644 --- a/packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-query.graphql +++ b/packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-query.graphql @@ -3,6 +3,7 @@ mutation BulkOperationRunQuery($query: String!) { query: $query ) { bulkOperation { + type completedAt createdAt errorCode diff --git a/packages/app/src/cli/api/graphql/bulk-operations/queries/get-bulk-operation-by-id.graphql b/packages/app/src/cli/api/graphql/bulk-operations/queries/get-bulk-operation-by-id.graphql index f925b656df..3567e5b487 100644 --- a/packages/app/src/cli/api/graphql/bulk-operations/queries/get-bulk-operation-by-id.graphql +++ b/packages/app/src/cli/api/graphql/bulk-operations/queries/get-bulk-operation-by-id.graphql @@ -1,5 +1,6 @@ query GetBulkOperationById($id: ID!) { bulkOperation(id: $id) { + type completedAt createdAt errorCode diff --git a/packages/app/src/cli/commands/app/bulk/execute.ts b/packages/app/src/cli/commands/app/bulk/execute.ts index d54f2d287d..92857c6916 100644 --- a/packages/app/src/cli/commands/app/bulk/execute.ts +++ b/packages/app/src/cli/commands/app/bulk/execute.ts @@ -24,6 +24,7 @@ export default class BulkExecute extends AppLinkedCommand { const {query, appContextResult, store} = await prepareExecuteContext(flags, 'bulk execute') await executeBulkOperation({ + organization: appContextResult.organization, remoteApp: appContextResult.remoteApp, storeFqdn: store.shopDomain, query, diff --git a/packages/app/src/cli/commands/app/bulk/status.ts b/packages/app/src/cli/commands/app/bulk/status.ts index fd3ee4fb72..5923e1e3b1 100644 --- a/packages/app/src/cli/commands/app/bulk/status.ts +++ b/packages/app/src/cli/commands/app/bulk/status.ts @@ -36,12 +36,14 @@ export default class BulkStatus extends AppLinkedCommand { if (flags.id) { await getBulkOperationStatus({ + organization: appContextResult.organization, storeFqdn: store.shopDomain, operationId: flags.id, remoteApp: appContextResult.remoteApp, }) } else { await listBulkOperations({ + organization: appContextResult.organization, storeFqdn: store.shopDomain, remoteApp: appContextResult.remoteApp, }) diff --git a/packages/app/src/cli/commands/app/execute.ts b/packages/app/src/cli/commands/app/execute.ts index 37fb1ffcd8..9dfa85c342 100644 --- a/packages/app/src/cli/commands/app/execute.ts +++ b/packages/app/src/cli/commands/app/execute.ts @@ -21,6 +21,7 @@ export default class Execute extends AppLinkedCommand { const {query, appContextResult, store} = await prepareExecuteContext(flags, 'execute') await executeOperation({ + organization: appContextResult.organization, remoteApp: appContextResult.remoteApp, storeFqdn: store.shopDomain, query, diff --git a/packages/app/src/cli/services/bulk-operations/bulk-operation-status.test.ts b/packages/app/src/cli/services/bulk-operations/bulk-operation-status.test.ts index 5930a087c9..2daf2a8196 100644 --- a/packages/app/src/cli/services/bulk-operations/bulk-operation-status.test.ts +++ b/packages/app/src/cli/services/bulk-operations/bulk-operation-status.test.ts @@ -1,6 +1,6 @@ import {getBulkOperationStatus, listBulkOperations} from './bulk-operation-status.js' import {GetBulkOperationByIdQuery} from '../../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js' -import {OrganizationApp} from '../../models/organization.js' +import {OrganizationApp, Organization, OrganizationSource} from '../../models/organization.js' import {ListBulkOperationsQuery} from '../../api/graphql/bulk-operations/generated/list-bulk-operations.js' import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest' import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session' @@ -12,6 +12,11 @@ vi.mock('@shopify/cli-kit/node/api/admin') const storeFqdn = 'test-store.myshopify.com' const operationId = 'gid://shopify/BulkOperation/123' +const mockOrganization: Organization = { + id: 'test-org-id', + businessName: 'Test Organization', + source: OrganizationSource.BusinessPlatform, +} const remoteApp = { id: '123', title: 'Test App', @@ -38,6 +43,7 @@ describe('getBulkOperationStatus', () => { return { bulkOperation: { id: operationId, + type: 'QUERY', status: 'RUNNING', errorCode: null, objectCount: 100, @@ -60,7 +66,7 @@ describe('getBulkOperationStatus', () => { ) const output = mockAndCaptureOutput() - await getBulkOperationStatus({storeFqdn, operationId, remoteApp}) + await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp}) expect(output.output()).toContain('Bulk operation succeeded:') expect(output.output()).toContain('100 objects') @@ -73,9 +79,10 @@ describe('getBulkOperationStatus', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'RUNNING', objectCount: 500})) const output = mockAndCaptureOutput() - await getBulkOperationStatus({storeFqdn, operationId, remoteApp}) + await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp}) - expect(output.info()).toContain('Bulk operation in progress...') + expect(output.info()).toContain('Checking bulk operation status.') + expect(output.info()).toContain('Bulk operation in progress') expect(output.info()).toContain('500 objects') expect(output.info()).toContain('Started') }) @@ -91,7 +98,7 @@ describe('getBulkOperationStatus', () => { ) const output = mockAndCaptureOutput() - await getBulkOperationStatus({storeFqdn, operationId, remoteApp}) + await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp}) expect(output.error()).toContain('Error: ACCESS_DENIED') expect(output.error()).toContain('Finished') @@ -102,7 +109,7 @@ describe('getBulkOperationStatus', () => { vi.mocked(adminRequestDoc).mockResolvedValue({bulkOperation: null}) const output = mockAndCaptureOutput() - await getBulkOperationStatus({storeFqdn, operationId, remoteApp}) + await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp}) expect(output.error()).toContain('Bulk operation not found.') expect(output.error()).toContain(operationId) @@ -112,16 +119,16 @@ describe('getBulkOperationStatus', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'CREATED', objectCount: 0})) const output = mockAndCaptureOutput() - await getBulkOperationStatus({storeFqdn, operationId, remoteApp}) + await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp}) - expect(output.info()).toContain('Starting...') + expect(output.info()).toContain('Starting') }) test('renders info banner for canceled operation', async () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'CANCELED'})) const output = mockAndCaptureOutput() - await getBulkOperationStatus({storeFqdn, operationId, remoteApp}) + await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp}) expect(output.info()).toContain('Bulk operation canceled.') }) @@ -131,7 +138,7 @@ describe('getBulkOperationStatus', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperation({status: 'RUNNING'})) const output = mockAndCaptureOutput() - await getBulkOperationStatus({storeFqdn, operationId, remoteApp}) + await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp}) expect(output.output()).toContain('Started') }) @@ -145,7 +152,7 @@ describe('getBulkOperationStatus', () => { ) const output = mockAndCaptureOutput() - await getBulkOperationStatus({storeFqdn, operationId, remoteApp}) + await getBulkOperationStatus({organization: mockOrganization, storeFqdn, operationId, remoteApp}) expect(output.output()).toContain('Finished') }) @@ -160,6 +167,7 @@ describe('listBulkOperations', () => { bulkOperations: { nodes: operations.map((op) => ({ id: 'gid://shopify/BulkOperation/123', + type: 'QUERY', status: 'RUNNING', errorCode: null, objectCount: 100, @@ -194,7 +202,7 @@ describe('listBulkOperations', () => { ) const output = mockAndCaptureOutput() - await listBulkOperations({storeFqdn, remoteApp}) + await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp}) const outputLinesWithoutTrailingWhitespace = output .output() @@ -204,7 +212,17 @@ describe('listBulkOperations', () => { // terminal width in test environment is quite narrow, so values in the snapshot get wrapped expect(outputLinesWithoutTrailingWhitespace).toMatchInlineSnapshot(` - "ID STATUS COU DATE CREATED DATE RESULTS + "╭─ info ───────────────────────────────────────────────────────────────────────╮ + │ │ + │ Listing bulk operations. │ + │ │ + │ • Organization: Test Organization │ + │ • App: Test App │ + │ • Store: test-store.myshopify.com │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────────╯ + + ID STATUS COU DATE CREATED DATE RESULTS T FINISHED ──────────────── ────── ─── ──────────── ─────────── ─────────────────────────── @@ -222,7 +240,7 @@ describe('listBulkOperations', () => { ) const output = mockAndCaptureOutput() - await listBulkOperations({storeFqdn, remoteApp}) + await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp}) expect(output.output()).toContain('1.2M') expect(output.output()).toContain('5.5K') @@ -242,7 +260,7 @@ describe('listBulkOperations', () => { ) const output = mockAndCaptureOutput() - await listBulkOperations({storeFqdn, remoteApp}) + await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp}) expect(output.output()).toContain('download') expect(output.output()).toContain('partial.jsonl') @@ -259,7 +277,7 @@ describe('listBulkOperations', () => { ) const output = mockAndCaptureOutput() - await listBulkOperations({storeFqdn, remoteApp}) + await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp}) expect(output.output()).toContain('download') expect(output.output()).toContain('results.jsonl') @@ -269,8 +287,9 @@ describe('listBulkOperations', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockBulkOperationsList([])) const output = mockAndCaptureOutput() - await listBulkOperations({storeFqdn, remoteApp}) + await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp}) - expect(output.info()).toContain('no bulk operations found in the last 7 days') + expect(output.info()).toContain('Listing bulk operations.') + expect(output.info()).toContain('No bulk operations found in the last 7 days.') }) }) diff --git a/packages/app/src/cli/services/bulk-operations/bulk-operation-status.ts b/packages/app/src/cli/services/bulk-operations/bulk-operation-status.ts index 555f1a3dbc..d404d95092 100644 --- a/packages/app/src/cli/services/bulk-operations/bulk-operation-status.ts +++ b/packages/app/src/cli/services/bulk-operations/bulk-operation-status.ts @@ -4,7 +4,8 @@ import { GetBulkOperationById, GetBulkOperationByIdQuery, } from '../../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js' -import {OrganizationApp} from '../../models/organization.js' +import {formatOperationInfo} from '../graphql/common.js' +import {OrganizationApp, Organization} from '../../models/organization.js' import { ListBulkOperations, ListBulkOperationsQuery, @@ -21,18 +22,31 @@ import colors from '@shopify/cli-kit/node/colors' const API_VERSION = '2026-01' interface GetBulkOperationStatusOptions { + organization: Organization storeFqdn: string operationId: string remoteApp: OrganizationApp } interface ListBulkOperationsOptions { + organization: Organization storeFqdn: string remoteApp: OrganizationApp } export async function getBulkOperationStatus(options: GetBulkOperationStatusOptions): Promise { - const {storeFqdn, operationId, remoteApp} = options + const {organization, storeFqdn, operationId, remoteApp} = options + + renderInfo({ + headline: 'Checking bulk operation status.', + body: [ + { + list: { + items: formatOperationInfo({organization, remoteApp, storeFqdn, showVersion: false}), + }, + }, + ], + }) const appSecret = remoteApp.apiSecretKeys[0]?.secret if (!appSecret) throw new BugError('No API secret keys found for app') @@ -57,7 +71,18 @@ export async function getBulkOperationStatus(options: GetBulkOperationStatusOpti } export async function listBulkOperations(options: ListBulkOperationsOptions): Promise { - const {storeFqdn, remoteApp} = options + const {organization, storeFqdn, remoteApp} = options + + renderInfo({ + headline: 'Listing bulk operations.', + body: [ + { + list: { + items: formatOperationInfo({organization, remoteApp, storeFqdn, showVersion: false}), + }, + }, + ], + }) const appSecret = remoteApp.apiSecretKeys[0]?.secret if (!appSecret) throw new BugError('No API secret keys found for app') @@ -90,7 +115,7 @@ export async function listBulkOperations(options: ListBulkOperationsOptions): Pr outputNewline() if (operations.length === 0) { - renderInfo({body: 'no bulk operations found in the last 7 days'}) + renderInfo({body: 'No bulk operations found in the last 7 days.'}) } else { renderTable({ rows: operations, diff --git a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts index c0dd52143b..7769b4023c 100644 --- a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts +++ b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts @@ -6,7 +6,7 @@ import {downloadBulkOperationResults} from './download-bulk-operation-results.js import {validateApiVersion} 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} from '../../models/organization.js' +import {OrganizationApp, OrganizationSource} from '../../models/organization.js' import {renderSuccess, renderWarning, renderError, renderInfo} from '@shopify/cli-kit/node/ui' import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session' import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs' @@ -36,6 +36,12 @@ vi.mock('@shopify/cli-kit/node/session', async () => { }) describe('executeBulkOperation', () => { + const mockOrganization = { + id: 'test-org-id', + businessName: 'Test Organization', + source: OrganizationSource.BusinessPlatform, + } + const mockRemoteApp = { apiKey: 'test-app-client-id', apiSecretKeys: [{secret: 'test-api-secret'}], @@ -49,6 +55,7 @@ describe('executeBulkOperation', () => { NonNullable['bulkOperation'] > = { id: 'gid://shopify/BulkOperation/123', + type: 'QUERY', status: 'CREATED', errorCode: null, createdAt: '2024-01-01T00:00:00Z', @@ -75,6 +82,7 @@ describe('executeBulkOperation', () => { vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -96,6 +104,7 @@ describe('executeBulkOperation', () => { vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -117,6 +126,7 @@ describe('executeBulkOperation', () => { vi.mocked(runBulkOperationMutation).mockResolvedValue(mockResponse) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query: mutation, @@ -140,6 +150,7 @@ describe('executeBulkOperation', () => { vi.mocked(runBulkOperationMutation).mockResolvedValue(mockResponse) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query: mutation, @@ -161,6 +172,7 @@ describe('executeBulkOperation', () => { } vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -185,6 +197,7 @@ describe('executeBulkOperation', () => { vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -216,6 +229,7 @@ describe('executeBulkOperation', () => { vi.mocked(runBulkOperationMutation).mockResolvedValue(mockResponse as any) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query: mutation, @@ -238,6 +252,7 @@ describe('executeBulkOperation', () => { await expect( executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query: mutation, @@ -256,6 +271,7 @@ describe('executeBulkOperation', () => { await expect( executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -276,6 +292,7 @@ describe('executeBulkOperation', () => { await expect( executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -306,6 +323,7 @@ describe('executeBulkOperation', () => { vi.mocked(downloadBulkOperationResults).mockResolvedValue('{"id":"gid://shopify/Product/123"}') await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -338,6 +356,7 @@ describe('executeBulkOperation', () => { }) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -372,6 +391,7 @@ describe('executeBulkOperation', () => { vi.mocked(downloadBulkOperationResults).mockResolvedValue(resultsContent) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -404,6 +424,7 @@ describe('executeBulkOperation', () => { vi.mocked(downloadBulkOperationResults).mockResolvedValue(resultsContent) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -432,6 +453,7 @@ describe('executeBulkOperation', () => { vi.mocked(watchBulkOperation).mockResolvedValue(finishedOperation) await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -456,6 +478,7 @@ describe('executeBulkOperation', () => { await expect( executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -481,6 +504,7 @@ describe('executeBulkOperation', () => { vi.mocked(validateApiVersion).mockResolvedValue() await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -505,6 +529,7 @@ describe('executeBulkOperation', () => { vi.mocked(validateApiVersion).mockClear() await executeBulkOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, diff --git a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts index 75a5dc432f..8895d030ed 100644 --- a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts +++ b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts @@ -3,8 +3,13 @@ import {runBulkOperationMutation} from './run-mutation.js' import {watchBulkOperation, type BulkOperation} from './watch-bulk-operation.js' import {formatBulkOperationStatus} from './format-bulk-operation-status.js' import {downloadBulkOperationResults} from './download-bulk-operation-results.js' -import {createAdminSessionAsApp, validateSingleOperation, validateApiVersion} from '../graphql/common.js' -import {OrganizationApp} from '../../models/organization.js' +import { + createAdminSessionAsApp, + validateSingleOperation, + validateApiVersion, + formatOperationInfo, +} from '../graphql/common.js' +import {OrganizationApp, Organization} from '../../models/organization.js' import {renderSuccess, renderInfo, renderError, renderWarning, TokenItem} from '@shopify/cli-kit/node/ui' import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output' import {AbortError, BugError} from '@shopify/cli-kit/node/error' @@ -13,6 +18,7 @@ import {parse} from 'graphql' import {readFile, writeFile, fileExists} from '@shopify/cli-kit/node/fs' interface ExecuteBulkOperationInput { + organization: Organization remoteApp: OrganizationApp storeFqdn: string query: string @@ -41,7 +47,7 @@ async function parseVariablesToJsonl(variables?: string[], variableFile?: string } export async function executeBulkOperation(input: ExecuteBulkOperationInput): Promise { - const {remoteApp, storeFqdn, query, variables, variableFile, outputFile, watch = false, version} = input + const {organization, remoteApp, storeFqdn, query, variables, variableFile, outputFile, watch = false, version} = input const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn) @@ -56,11 +62,7 @@ export async function executeBulkOperation(input: ExecuteBulkOperationInput): Pr body: [ { list: { - items: [ - `App: ${remoteApp.title}`, - `Store: ${storeFqdn}`, - `API version: ${version || 'default (latest stable)'}`, - ], + items: formatOperationInfo({organization, remoteApp, storeFqdn, version}), }, }, ], diff --git a/packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.test.ts b/packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.test.ts index a97fbbdadb..f20ce88d58 100644 --- a/packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.test.ts +++ b/packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.test.ts @@ -8,54 +8,66 @@ function createMockOperation(overrides: Partial = {}): BulkOperat return { id: 'gid://shopify/BulkOperation/123', status: 'CREATED', + type: 'QUERY', errorCode: null, createdAt: '2024-01-01T00:00:00Z', completedAt: null, objectCount: '0', url: null, + partialDataUrl: null, ...overrides, } } describe('formatBulkOperationStatus', () => { - test('formats RUNNING status with object count', () => { - const result = formatBulkOperationStatus(createMockOperation({status: 'RUNNING', objectCount: 42})) - expect(result.value).toContain('Bulk operation in progress...') - expect(result.value).toContain('(42 objects)') + test('formats RUNNING status for query with object count', () => { + const result = formatBulkOperationStatus(createMockOperation({status: 'RUNNING', type: 'QUERY', objectCount: '42'})) + expect(result.value).toContain('Bulk operation in progress') + expect(result.value).toContain('(42 objects read)') + }) + + test('formats RUNNING status for mutation with object count', () => { + const result = formatBulkOperationStatus( + createMockOperation({status: 'RUNNING', type: 'MUTATION', objectCount: '42'}), + ) + expect(result.value).toContain('Bulk operation in progress') + expect(result.value).toContain('(42 objects written)') }) test('formats CREATED status', () => { const result = formatBulkOperationStatus(createMockOperation({status: 'CREATED'})) - expect(result.value).toBe('Starting...') + expect(result.value).toBe('Starting') }) test('formats COMPLETED status', () => { - const result = formatBulkOperationStatus(createMockOperation({status: 'COMPLETED', objectCount: 100})) + const result = formatBulkOperationStatus(createMockOperation({status: 'COMPLETED', objectCount: '100'})) expect(result.value).toContain('Bulk operation succeeded:') expect(result.value).toContain('100 objects') }) test('formats FAILED status with error code', () => { const result = formatBulkOperationStatus( - createMockOperation({status: 'FAILED', objectCount: 10, errorCode: 'ACCESS_DENIED'}), + createMockOperation({status: 'FAILED', objectCount: '10', errorCode: 'ACCESS_DENIED'}), ) expect(result.value).toContain('Bulk operation failed.') expect(result.value).toContain('Error: ACCESS_DENIED') }) test('formats FAILED status without error code', () => { - const result = formatBulkOperationStatus(createMockOperation({status: 'FAILED', objectCount: 10, errorCode: null})) + const result = formatBulkOperationStatus( + createMockOperation({status: 'FAILED', objectCount: '10', errorCode: null}), + ) expect(result.value).toContain('Bulk operation failed.') expect(result.value).toContain('Error: unknown') }) test('formats CANCELING status', () => { - const result = formatBulkOperationStatus(createMockOperation({status: 'CANCELING', objectCount: 5})) + const result = formatBulkOperationStatus(createMockOperation({status: 'CANCELING', objectCount: '5'})) expect(result.value).toBe('Bulk operation canceling...') }) test('formats CANCELED status', () => { - const result = formatBulkOperationStatus(createMockOperation({status: 'CANCELED', objectCount: 5})) + const result = formatBulkOperationStatus(createMockOperation({status: 'CANCELED', objectCount: '5'})) expect(result.value).toBe('Bulk operation canceled.') }) diff --git a/packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.ts b/packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.ts index 08af5687e5..90e5e9871f 100644 --- a/packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.ts +++ b/packages/app/src/cli/services/bulk-operations/format-bulk-operation-status.ts @@ -6,11 +6,11 @@ export function formatBulkOperationStatus( ): TokenizedString { switch (operation.status) { case 'RUNNING': - return outputContent`Bulk operation in progress... ${outputToken.gray( - `(${String(operation.objectCount)} objects)`, + return outputContent`Bulk operation in progress ${outputToken.gray( + `(${String(operation.objectCount)} objects ${operation.type === 'MUTATION' ? 'written' : 'read'})`, )}` case 'CREATED': - return outputContent`Starting...` + return outputContent`Starting` case 'COMPLETED': return outputContent`Bulk operation succeeded: ${outputToken.gray(`${String(operation.objectCount)} objects`)}` case 'FAILED': diff --git a/packages/app/src/cli/services/execute-operation.test.ts b/packages/app/src/cli/services/execute-operation.test.ts index fdbb538a4a..de5571b26d 100644 --- a/packages/app/src/cli/services/execute-operation.test.ts +++ b/packages/app/src/cli/services/execute-operation.test.ts @@ -1,6 +1,6 @@ import {executeOperation} from './execute-operation.js' import {createAdminSessionAsApp, validateApiVersion} from './graphql/common.js' -import {OrganizationApp} from '../models/organization.js' +import {OrganizationApp, OrganizationSource} from '../models/organization.js' import {renderSuccess, renderError, renderSingleTask} from '@shopify/cli-kit/node/ui' import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin' import {ClientError} from 'graphql-request' @@ -15,6 +15,12 @@ vi.mock('@shopify/cli-kit/node/api/admin') vi.mock('@shopify/cli-kit/node/fs') describe('executeOperation', () => { + const mockOrganization = { + id: 'test-org-id', + businessName: 'Test Organization', + source: OrganizationSource.BusinessPlatform, + } + const mockRemoteApp = { apiKey: 'test-app-client-id', apiSecretKeys: [{secret: 'test-api-secret'}], @@ -41,6 +47,7 @@ describe('executeOperation', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockResult) await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -64,6 +71,7 @@ describe('executeOperation', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockResult) await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -83,6 +91,7 @@ describe('executeOperation', () => { await expect( executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -101,6 +110,7 @@ describe('executeOperation', () => { vi.mocked(validateApiVersion).mockResolvedValue() await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -122,6 +132,7 @@ describe('executeOperation', () => { vi.mocked(validateApiVersion).mockClear() await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -138,6 +149,7 @@ describe('executeOperation', () => { const mockOutput = mockAndCaptureOutput() await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -156,6 +168,7 @@ describe('executeOperation', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockResult) await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -178,6 +191,7 @@ describe('executeOperation', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockResult) await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -197,6 +211,7 @@ describe('executeOperation', () => { await expect( executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -213,6 +228,7 @@ describe('executeOperation', () => { vi.mocked(adminRequestDoc).mockResolvedValue(mockResult) await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, @@ -236,6 +252,7 @@ describe('executeOperation', () => { vi.mocked(adminRequestDoc).mockRejectedValue(clientError) await executeOperation({ + organization: mockOrganization, remoteApp: mockRemoteApp, storeFqdn, query, diff --git a/packages/app/src/cli/services/execute-operation.ts b/packages/app/src/cli/services/execute-operation.ts index 6c8c04da1c..a84112bb54 100644 --- a/packages/app/src/cli/services/execute-operation.ts +++ b/packages/app/src/cli/services/execute-operation.ts @@ -1,5 +1,10 @@ -import {createAdminSessionAsApp, validateSingleOperation, validateApiVersion} from './graphql/common.js' -import {OrganizationApp} from '../models/organization.js' +import { + createAdminSessionAsApp, + validateSingleOperation, + validateApiVersion, + formatOperationInfo, +} from './graphql/common.js' +import {OrganizationApp, Organization} from '../models/organization.js' import {renderSuccess, renderError, renderInfo, renderSingleTask} from '@shopify/cli-kit/node/ui' import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output' import {AbortError} from '@shopify/cli-kit/node/error' @@ -9,6 +14,7 @@ import {parse} from 'graphql' import {writeFile} from '@shopify/cli-kit/node/fs' interface ExecuteOperationInput { + organization: Organization remoteApp: OrganizationApp storeFqdn: string query: string @@ -32,18 +38,14 @@ async function parseVariables(variables?: string): Promise<{[key: string]: unkno } export async function executeOperation(input: ExecuteOperationInput): Promise { - const {remoteApp, storeFqdn, query, variables, version, outputFile} = input + const {organization, remoteApp, storeFqdn, query, variables, version, outputFile} = input renderInfo({ headline: 'Executing GraphQL operation.', body: [ { list: { - items: [ - `App: ${remoteApp.title}`, - `Store: ${storeFqdn}`, - `API version: ${version ?? 'default (latest stable)'}`, - ], + items: formatOperationInfo({organization, remoteApp, storeFqdn, version}), }, }, ], diff --git a/packages/app/src/cli/services/graphql/common.ts b/packages/app/src/cli/services/graphql/common.ts index 24474cf3f0..503339b0a0 100644 --- a/packages/app/src/cli/services/graphql/common.ts +++ b/packages/app/src/cli/services/graphql/common.ts @@ -67,3 +67,28 @@ export async function validateApiVersion( throw new AbortError(`${firstLine}\n${secondLine}`) } + +/** + * Creates formatted info list items for GraphQL operations. + * Includes organization, app, store, and API version information. + * + * @param options - The operation context information + * @returns Array of formatted strings for display + */ +export function formatOperationInfo(options: { + organization: {businessName: string} + remoteApp: {title: string} + storeFqdn: string + version?: string + showVersion?: boolean +}): string[] { + const {organization, remoteApp, storeFqdn, version, showVersion = true} = options + + const items = [`Organization: ${organization.businessName}`, `App: ${remoteApp.title}`, `Store: ${storeFqdn}`] + + if (showVersion) { + items.push(`API version: ${version ?? 'default (latest stable)'}`) + } + + return items +}