Skip to content

Commit a6645ff

Browse files
committed
refactor: extract bulk operation logic into service class; create bulk-operations directory in services directory
1 parent 05a5da9 commit a6645ff

File tree

7 files changed

+170
-54
lines changed

7 files changed

+170
-54
lines changed

bin/get-graphql-schemas.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ const schemas = [
7171
pathToFile: 'areas/core/shopify/db/graphql/functions_cli_api_schema_unstable_public.graphql',
7272
localPath: './packages/app/src/cli/api/graphql/functions/functions_cli_schema.graphql',
7373
},
74+
{
75+
owner: 'shop',
76+
repo: 'world',
77+
pathToFile: 'areas/core/shopify/db/graphql/admin_schema_unstable_public.graphql',
78+
localPath: './packages/app/src/cli/api/graphql/bulk-operations/admin_schema.graphql',
79+
usesLfs: true,
80+
},
7481
]
7582

7683

packages/app/src/cli/api/graphql/bulk-operations/mutations/bulk-operation-run-query.graphql

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
mutation BulkOperationRunQuery($query: String!) {
2-
bulkOperationRunQuery(query: $query) {
2+
bulkOperationRunQuery(
3+
query: $query
4+
# Set to false to optimize for speed over grouped results
5+
groupObjects: false
6+
) {
37
bulkOperation {
48
id
59
status

packages/app/src/cli/commands/app/execute.ts

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import {appFlags, bulkOperationFlags} from '../../flags.js'
22
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../utilities/app-linked-command.js'
33
import {linkedAppContext} from '../../services/app-context.js'
44
import {storeContext} from '../../services/store-context.js'
5-
import {runBulkOperationQuery} from '../../services/bulk-operation-run-query.js'
5+
import {executeBulkOperation} from '../../services/bulk-operations/execute-bulk-operation.js'
66
import {globalFlags} from '@shopify/cli-kit/node/cli'
7-
import {renderSuccess, renderInfo, renderWarning} from '@shopify/cli-kit/node/ui'
8-
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
97

108
export default class Execute extends AppLinkedCommand {
119
static summary = 'Execute bulk operations.'
@@ -36,54 +34,12 @@ export default class Execute extends AppLinkedCommand {
3634
forceReselectStore: flags.reset,
3735
})
3836

39-
renderInfo({
40-
headline: 'Starting bulk operation.',
41-
body: `App: ${appContextResult.app.name}\nStore: ${store.shopDomain}`,
42-
})
43-
44-
const bulkOperationResponse = await runBulkOperationQuery({
37+
await executeBulkOperation({
38+
app: appContextResult.app,
4539
storeFqdn: store.shopDomain,
4640
query: flags.query,
4741
})
4842

49-
if (bulkOperationResponse?.userErrors?.length) {
50-
const errorMessages = bulkOperationResponse.userErrors
51-
.map((error) => `${error.field?.join('.') ?? 'unknown'}: ${error.message}`)
52-
.join('\n')
53-
renderWarning({
54-
headline: 'Bulk operation errors.',
55-
body: errorMessages,
56-
})
57-
return {app: appContextResult.app}
58-
}
59-
60-
const result = bulkOperationResponse?.bulkOperation
61-
if (result) {
62-
const infoSections = [
63-
{
64-
title: 'Bulk Operation Created',
65-
body: [
66-
{
67-
list: {
68-
items: [
69-
outputContent`ID: ${outputToken.cyan(result.id)}`.value,
70-
outputContent`Status: ${outputToken.yellow(result.status)}`.value,
71-
outputContent`Created: ${outputToken.gray(String(result.createdAt))}`.value,
72-
],
73-
},
74-
},
75-
],
76-
},
77-
]
78-
79-
renderInfo({customSections: infoSections})
80-
81-
renderSuccess({
82-
headline: 'Bulk operation started successfully!',
83-
body: 'Congrats!',
84-
})
85-
}
86-
8743
return {app: appContextResult.app}
8844
}
8945
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {executeBulkOperation} from './execute-bulk-operation.js'
2+
import {runBulkOperationQuery} from './run-query.js'
3+
import {AppLinkedInterface} from '../../models/app/app.js'
4+
import {renderSuccess, renderInfo, renderWarning} from '@shopify/cli-kit/node/ui'
5+
import {describe, test, expect, vi} from 'vitest'
6+
7+
vi.mock('./run-query.js')
8+
vi.mock('@shopify/cli-kit/node/ui')
9+
10+
describe('executeBulkOperation', () => {
11+
const mockApp = {
12+
name: 'Test App',
13+
} as AppLinkedInterface
14+
15+
const storeFqdn = 'test-store.myshopify.com'
16+
const query = 'query { products { edges { node { id } } } }'
17+
18+
const successfulBulkOperation = {
19+
id: 'gid://shopify/BulkOperation/123',
20+
status: 'CREATED',
21+
errorCode: null,
22+
createdAt: '2024-01-01T00:00:00Z',
23+
objectCount: '0',
24+
fileSize: '0',
25+
url: null,
26+
}
27+
28+
test('executeBulkOperation successfully runs', async () => {
29+
const mockResponse = {
30+
bulkOperation: successfulBulkOperation,
31+
userErrors: [],
32+
}
33+
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse as any)
34+
35+
await executeBulkOperation({
36+
app: mockApp,
37+
storeFqdn,
38+
query,
39+
})
40+
41+
expect(runBulkOperationQuery).toHaveBeenCalledWith({
42+
storeFqdn,
43+
query,
44+
})
45+
46+
expect(renderInfo).toHaveBeenCalledWith({
47+
headline: 'Starting bulk operation.',
48+
body: `App: ${mockApp.name}\nStore: ${storeFqdn}`,
49+
})
50+
51+
expect(renderInfo).toHaveBeenCalledWith({
52+
customSections: expect.arrayContaining([
53+
expect.objectContaining({
54+
title: 'Bulk Operation Created',
55+
}),
56+
]),
57+
})
58+
59+
expect(renderSuccess).toHaveBeenCalledWith({
60+
headline: 'Bulk operation started successfully!',
61+
body: 'Congrats!',
62+
})
63+
})
64+
65+
test('executeBulkOperation renders warning when user errors are present', async () => {
66+
const mockResponse = {
67+
bulkOperation: null,
68+
userErrors: [
69+
{field: ['query'], message: 'Invalid query syntax'},
70+
{field: null, message: 'Another error'},
71+
],
72+
}
73+
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse as any)
74+
75+
await executeBulkOperation({
76+
app: mockApp,
77+
storeFqdn,
78+
query,
79+
})
80+
81+
expect(renderWarning).toHaveBeenCalledWith({
82+
headline: 'Bulk operation errors.',
83+
body: 'query: Invalid query syntax\nunknown: Another error',
84+
})
85+
86+
expect(renderSuccess).not.toHaveBeenCalled()
87+
})
88+
})
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {runBulkOperationQuery} from './run-query.js'
2+
import {AppLinkedInterface} from '../../models/app/app.js'
3+
import {renderSuccess, renderInfo, renderWarning} from '@shopify/cli-kit/node/ui'
4+
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
5+
6+
interface ExecuteBulkOperationInput {
7+
app: AppLinkedInterface
8+
storeFqdn: string
9+
query: string
10+
}
11+
12+
export async function executeBulkOperation(input: ExecuteBulkOperationInput): Promise<void> {
13+
const {app, storeFqdn, query} = input
14+
15+
renderInfo({
16+
headline: 'Starting bulk operation.',
17+
body: `App: ${app.name}\nStore: ${storeFqdn}`,
18+
})
19+
20+
const bulkOperationResponse = await runBulkOperationQuery({
21+
storeFqdn,
22+
query,
23+
})
24+
25+
if (bulkOperationResponse?.userErrors?.length) {
26+
const errorMessages = bulkOperationResponse.userErrors
27+
.map(
28+
(error: {field?: string[] | null; message: string}) =>
29+
`${error.field?.join('.') ?? 'unknown'}: ${error.message}`,
30+
)
31+
.join('\n')
32+
renderWarning({
33+
headline: 'Bulk operation errors.',
34+
body: errorMessages,
35+
})
36+
return
37+
}
38+
39+
const result = bulkOperationResponse?.bulkOperation
40+
if (result) {
41+
const infoSections = [
42+
{
43+
title: 'Bulk Operation Created',
44+
body: [
45+
{
46+
list: {
47+
items: [
48+
outputContent`ID: ${outputToken.cyan(result.id)}`.value,
49+
outputContent`Status: ${outputToken.yellow(result.status)}`.value,
50+
outputContent`Created: ${outputToken.gray(String(result.createdAt))}`.value,
51+
],
52+
},
53+
},
54+
],
55+
},
56+
]
57+
58+
renderInfo({customSections: infoSections})
59+
60+
renderSuccess({
61+
headline: 'Bulk operation started successfully!',
62+
body: 'Congrats!',
63+
})
64+
}
65+
}

packages/app/src/cli/services/bulk-operations-run-query.test.ts renamed to packages/app/src/cli/services/bulk-operations/run-query.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {runBulkOperationQuery} from './bulk-operation-run-query.js'
1+
import {runBulkOperationQuery} from './run-query.js'
22
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
33
import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session'
44
import {describe, test, expect, vi, beforeEach} from 'vitest'

packages/app/src/cli/services/bulk-operation-run-query.ts renamed to packages/app/src/cli/services/bulk-operations/run-query.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
BulkOperationRunQuery,
33
BulkOperationRunQueryMutation,
4-
} from '../api/graphql/bulk-operations/generated/bulk-operation-run-query.js'
4+
} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-query.js'
55
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
66
import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session'
77

@@ -10,10 +10,6 @@ interface BulkOperationRunQueryOptions {
1010
query: string
1111
}
1212

13-
/**
14-
* Executes a bulk operation query against the Shopify Admin API.
15-
* The operation runs asynchronously in the background.
16-
*/
1713
export async function runBulkOperationQuery(
1814
options: BulkOperationRunQueryOptions,
1915
): Promise<BulkOperationRunQueryMutation['bulkOperationRunQuery']> {

0 commit comments

Comments
 (0)