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 @@ -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
Expand Down Expand Up @@ -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'}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'}},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mutation BulkOperationRunMutation(
clientIdentifier: $clientIdentifier
) {
bulkOperation {
type
completedAt
createdAt
errorCode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mutation BulkOperationRunQuery($query: String!) {
query: $query
) {
bulkOperation {
type
completedAt
createdAt
errorCode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
query GetBulkOperationById($id: ID!) {
bulkOperation(id: $id) {
type
completedAt
createdAt
errorCode
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/commands/app/bulk/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/cli/commands/app/bulk/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/commands/app/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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',
Expand All @@ -38,6 +43,7 @@ describe('getBulkOperationStatus', () => {
return {
bulkOperation: {
id: operationId,
type: 'QUERY',
status: 'RUNNING',
errorCode: null,
objectCount: 100,
Expand All @@ -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')
Expand All @@ -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')
})
Expand All @@ -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')
Expand All @@ -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)
Expand All @@ -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.')
})
Expand All @@ -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')
})
Expand All @@ -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')
})
Expand All @@ -160,6 +167,7 @@ describe('listBulkOperations', () => {
bulkOperations: {
nodes: operations.map((op) => ({
id: 'gid://shopify/BulkOperation/123',
type: 'QUERY',
status: 'RUNNING',
errorCode: null,
objectCount: 100,
Expand Down Expand Up @@ -194,7 +202,7 @@ describe('listBulkOperations', () => {
)

const output = mockAndCaptureOutput()
await listBulkOperations({storeFqdn, remoteApp})
await listBulkOperations({organization: mockOrganization, storeFqdn, remoteApp})

const outputLinesWithoutTrailingWhitespace = output
.output()
Expand All @@ -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

──────────────── ────── ─── ──────────── ─────────── ───────────────────────────
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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')
Expand All @@ -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.')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<void> {
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')
Expand All @@ -57,7 +71,18 @@ export async function getBulkOperationStatus(options: GetBulkOperationStatusOpti
}

export async function listBulkOperations(options: ListBulkOperationsOptions): Promise<void> {
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')
Expand Down Expand Up @@ -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,
Expand Down
Loading