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
11 changes: 8 additions & 3 deletions packages/app/src/cli/commands/app/bulk/status.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import {appFlags} from '../../../flags.js'
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
import {prepareAppStoreContext} from '../../../utilities/execute-command-helpers.js'
import {getBulkOperationStatus, listBulkOperations} from '../../../services/bulk-operations/bulk-operation-status.js'
import {
getBulkOperationStatus,
listBulkOperations,
normalizeBulkOperationId,
} from '../../../services/bulk-operations/bulk-operation-status.js'
import {Flags} from '@oclif/core'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn'
Expand All @@ -18,7 +22,8 @@ export default class BulkStatus extends AppLinkedCommand {
...globalFlags,
...appFlags,
id: Flags.string({
description: 'The bulk operation ID. If not provided, lists all bulk operations in the last 7 days.',
description:
'The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations in the last 7 days.',
env: 'SHOPIFY_FLAG_ID',
}),
store: Flags.string({
Expand All @@ -38,7 +43,7 @@ export default class BulkStatus extends AppLinkedCommand {
await getBulkOperationStatus({
organization: appContextResult.organization,
storeFqdn: store.shopDomain,
operationId: flags.id,
operationId: normalizeBulkOperationId(flags.id),
remoteApp: appContextResult.remoteApp,
})
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {getBulkOperationStatus, listBulkOperations} from './bulk-operation-status.js'
import {
getBulkOperationStatus,
listBulkOperations,
normalizeBulkOperationId,
extractBulkOperationId,
} from './bulk-operation-status.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'
Expand Down Expand Up @@ -36,6 +41,37 @@ afterEach(() => {
mockAndCaptureOutput().clear()
})

describe('normalizeBulkOperationId', () => {
test('returns GID as-is when already in GID format', () => {
const gid = 'gid://shopify/BulkOperation/123'
expect(normalizeBulkOperationId(gid)).toBe(gid)
})

test('converts numeric ID to GID format', () => {
expect(normalizeBulkOperationId('123')).toBe('gid://shopify/BulkOperation/123')
expect(normalizeBulkOperationId('456789')).toBe('gid://shopify/BulkOperation/456789')
})

test('returns non-numeric, non-GID string as-is', () => {
const invalidId = 'invalid-id'
expect(normalizeBulkOperationId(invalidId)).toBe(invalidId)
})
})

describe('extractBulkOperationId', () => {
test('extracts numeric ID from GID', () => {
expect(extractBulkOperationId('gid://shopify/BulkOperation/123')).toBe('123')
expect(extractBulkOperationId('gid://shopify/BulkOperation/456789')).toBe('456789')
})

test('returns input as-is if not a valid GID format', () => {
expect(extractBulkOperationId('gid://shopify/BulkOperation/ABC')).toBe('gid://shopify/BulkOperation/ABC')
expect(extractBulkOperationId('BulkOperation/123')).toBe('BulkOperation/123')
expect(extractBulkOperationId('invalid-id')).toBe('invalid-id')
expect(extractBulkOperationId('123')).toBe('123')
})
})

describe('getBulkOperationStatus', () => {
function mockBulkOperation(
overrides?: Partial<NonNullable<GetBulkOperationByIdQuery['bulkOperation']>>,
Expand Down Expand Up @@ -222,15 +258,15 @@ describe('listBulkOperations', () => {
│ │
╰──────────────────────────────────────────────────────────────────────────────╯

ID STATUS COU DATE CREATED DATE RESULTS
T FINISHED
I STATUS COUNT DATE CREATED DATE FINISHED RESULTS

──────────────── ────── ─── ──────────── ─────────── ───────────────────────────
──────────── ── ── ─────── ─────── ───────────────────
gid://shopify/Bu COMPLE 123 2025-11-10 2025-11-10 download ( https://example.
kOperation/1 ED 5K 12:37:52 16:37:12 com/results.jsonl )
gid://shopify/Bu RUNNIN 100 2025-11-11
kOperation/2 15:37:52"
─ ─────── ───── ────────────── ────────────── ──────────────────────────────────
─ ──── ──── ────────────
1 COMPLET 123.5 2025-11-10 2025-11-10 download (
D 12:37:52 16:37:12 https://example.com/results.jsonl
)
2 RUNNING 100 2025-11-11
15:37:52"
`)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ 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://')) {
return id
}

// If numeric, convert to GID
if (/^\d+$/.test(id)) {
return `gid://shopify/BulkOperation/${id}`
}

// Otherwise return as-is (let API handle any errors)
return id
}

export function extractBulkOperationId(gid: string): string {
// Extract the numeric ID from a GID like "gid://shopify/BulkOperation/123"
const match = gid.match(/^gid:\/\/shopify\/BulkOperation\/(\d+)$/)
return match?.[1] ?? gid
}

interface GetBulkOperationStatusOptions {
organization: Organization
storeFqdn: string
Expand Down Expand Up @@ -104,7 +125,7 @@ export async function listBulkOperations(options: ListBulkOperationsOptions): Pr
})

const operations = response.bulkOperations.nodes.map((operation) => ({
id: operation.id,
id: extractBulkOperationId(operation.id),
status: formatStatus(operation.status),
count: formatCount(operation.objectCount as number),
dateCreated: formatDate(new Date(String(operation.createdAt))),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ describe('executeBulkOperation', () => {

expect(renderInfo).toHaveBeenCalledWith({
headline: `Bulk operation ${createdBulkOperation.id} is still running in the background.`,
body: ['Monitor its progress with:', {command: expect.stringContaining('shopify app bulk status')}],
body: ['Monitor its progress with:\n', {command: expect.stringContaining('shopify app bulk status')}],
})
expect(downloadBulkOperationResults).not.toHaveBeenCalled()
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ 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 {extractBulkOperationId} from './bulk-operation-status.js'
import {
createAdminSessionAsApp,
validateSingleOperation,
Expand Down Expand Up @@ -169,7 +170,10 @@ function validateGraphQLDocument(graphqlOperation: string, variablesJsonl?: stri
}

function statusCommandHelpMessage(operationId: string): TokenItem {
return ['Monitor its progress with:', {command: `shopify app bulk status --id="${operationId}}"`}]
return [
'Monitor its progress with:\n',
{command: `shopify app bulk status --id=${extractBulkOperationId(operationId)}`},
]
}

function isMutation(graphqlOperation: string): boolean {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@
"type": "option"
},
"id": {
"description": "The bulk operation ID. If not provided, lists all bulk operations in the last 7 days.",
"description": "The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations in the last 7 days.",
"env": "SHOPIFY_FLAG_ID",
"hasDynamicHelp": false,
"multiple": false,
Expand Down