Skip to content

Commit 0743817

Browse files
committed
make version handling consistent between app execute and app bulk execute
1 parent 5568d31 commit 0743817

File tree

11 files changed

+123
-62
lines changed

11 files changed

+123
-62
lines changed

docs-shopify.dev/commands/app-execute.doc.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'
33

44
const data: ReferenceEntityTemplateSchema = {
55
name: 'app execute',
6-
description: `Executes a GraphQL query or mutation on the specified store, and writes the result to STDOUT or a file.`,
6+
description: `Executes an Admin API GraphQL query or mutation on the specified dev store.`,
77
overviewPreviewDescription: `Execute GraphQL queries and mutations.`,
88
type: 'command',
99
isVisualComponent: false,

docs-shopify.dev/generated/generated_docs_data.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,7 @@
10001000
},
10011001
{
10021002
"name": "app execute",
1003-
"description": "Executes a GraphQL query or mutation on the specified store, and writes the result to STDOUT or a file.",
1003+
"description": "Executes an Admin API GraphQL query or mutation on the specified dev store.",
10041004
"overviewPreviewDescription": "Execute GraphQL queries and mutations.",
10051005
"type": "command",
10061006
"isVisualComponent": false,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export default class Execute extends AppLinkedCommand {
2525
storeFqdn: store.shopDomain,
2626
query,
2727
variables: flags.variables,
28-
apiVersion: flags.version,
2928
outputFile: flags['output-file'],
29+
...(flags.version && {version: flags.version}),
3030
})
3131

3232
return {app: appContextResult.app}

packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {runBulkOperationQuery} from './run-query.js'
33
import {runBulkOperationMutation} from './run-mutation.js'
44
import {watchBulkOperation} from './watch-bulk-operation.js'
55
import {downloadBulkOperationResults} from './download-bulk-operation-results.js'
6+
import {validateApiVersion} from '../graphql/common.js'
67
import {BulkOperationRunQueryMutation} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-query.js'
78
import {BulkOperationRunMutationMutation} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-mutation.js'
89
import {OrganizationApp} from '../../models/organization.js'
@@ -17,6 +18,13 @@ vi.mock('./run-query.js')
1718
vi.mock('./run-mutation.js')
1819
vi.mock('./watch-bulk-operation.js')
1920
vi.mock('./download-bulk-operation-results.js')
21+
vi.mock('../graphql/common.js', async () => {
22+
const actual = await vi.importActual('../graphql/common.js')
23+
return {
24+
...actual,
25+
validateApiVersion: vi.fn(),
26+
}
27+
})
2028
vi.mock('@shopify/cli-kit/node/ui')
2129
vi.mock('@shopify/cli-kit/node/fs')
2230
vi.mock('@shopify/cli-kit/node/session', async () => {
@@ -26,13 +34,6 @@ vi.mock('@shopify/cli-kit/node/session', async () => {
2634
ensureAuthenticatedAdminAsApp: vi.fn(),
2735
}
2836
})
29-
vi.mock('@shopify/cli-kit/node/api/admin', async () => {
30-
const actual = await vi.importActual('@shopify/cli-kit/node/api/admin')
31-
return {
32-
...actual,
33-
supportedApiVersions: vi.fn(() => Promise.resolve(['2025-01', '2025-04', '2025-07', '2025-10'])),
34-
}
35-
})
3637

3738
describe('executeBulkOperation', () => {
3839
const mockRemoteApp = {
@@ -469,62 +470,46 @@ describe('executeBulkOperation', () => {
469470
expect(renderSuccess).not.toHaveBeenCalled()
470471
})
471472

472-
test('allows executing bulk operations against unstable', async () => {
473+
test('validates API version when provided', async () => {
473474
const query = '{ products { edges { node { id } } } }'
475+
const version = '2025-01'
474476
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
475477
bulkOperation: createdBulkOperation,
476478
userErrors: [],
477479
}
478480
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
481+
vi.mocked(validateApiVersion).mockResolvedValue()
479482

480483
await executeBulkOperation({
481484
remoteApp: mockRemoteApp,
482485
storeFqdn,
483486
query,
484-
version: 'unstable',
487+
version,
485488
})
486489

490+
expect(validateApiVersion).toHaveBeenCalledWith(mockAdminSession, version)
487491
expect(runBulkOperationQuery).toHaveBeenCalledWith({
488492
adminSession: mockAdminSession,
489493
query,
490-
version: 'unstable',
494+
version,
491495
})
492496
})
493497

494-
test('allows executing bulk operations against a specific stable version', async () => {
498+
test('does not validate version when not provided', async () => {
495499
const query = '{ products { edges { node { id } } } }'
496500
const mockResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = {
497501
bulkOperation: createdBulkOperation,
498502
userErrors: [],
499503
}
500504
vi.mocked(runBulkOperationQuery).mockResolvedValue(mockResponse)
505+
vi.mocked(validateApiVersion).mockClear()
501506

502507
await executeBulkOperation({
503508
remoteApp: mockRemoteApp,
504509
storeFqdn,
505510
query,
506-
version: '2025-01',
507-
})
508-
509-
expect(runBulkOperationQuery).toHaveBeenCalledWith({
510-
adminSession: mockAdminSession,
511-
query,
512-
version: '2025-01',
513511
})
514-
})
515-
516-
test('throws error when an API version is specified but is not supported', async () => {
517-
const query = '{ products { edges { node { id } } } }'
518-
519-
await expect(
520-
executeBulkOperation({
521-
remoteApp: mockRemoteApp,
522-
storeFqdn,
523-
query,
524-
version: '2099-12',
525-
}),
526-
).rejects.toThrow('Invalid API version')
527512

528-
expect(runBulkOperationQuery).not.toHaveBeenCalled()
513+
expect(validateApiVersion).not.toHaveBeenCalled()
529514
})
530515
})

packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ import {runBulkOperationMutation} from './run-mutation.js'
33
import {watchBulkOperation, type BulkOperation} from './watch-bulk-operation.js'
44
import {formatBulkOperationStatus} from './format-bulk-operation-status.js'
55
import {downloadBulkOperationResults} from './download-bulk-operation-results.js'
6-
import {createAdminSessionAsApp, validateSingleOperation} from '../graphql/common.js'
6+
import {createAdminSessionAsApp, validateSingleOperation, validateApiVersion} from '../graphql/common.js'
77
import {OrganizationApp} from '../../models/organization.js'
88
import {renderSuccess, renderInfo, renderError, renderWarning, TokenItem} from '@shopify/cli-kit/node/ui'
99
import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output'
10-
import {supportedApiVersions} from '@shopify/cli-kit/node/api/admin'
1110
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
1211
import {AbortController} from '@shopify/cli-kit/node/abort'
1312
import {parse} from 'graphql'
@@ -176,15 +175,3 @@ function isMutation(graphqlOperation: string): boolean {
176175
const operation = document.definitions.find((def) => def.kind === 'OperationDefinition')
177176
return operation?.kind === 'OperationDefinition' && operation.operation === 'mutation'
178177
}
179-
180-
async function validateApiVersion(adminSession: {token: string; storeFqdn: string}, version: string): Promise<void> {
181-
if (version === 'unstable') return
182-
183-
const supportedVersions = await supportedApiVersions(adminSession)
184-
if (supportedVersions.includes(version)) return
185-
186-
const firstLine = outputContent`Invalid API version: ${version}`.value
187-
const secondLine = outputContent`Supported versions: ${supportedVersions.join(', ')}`.value
188-
189-
throw new AbortError(`${firstLine}\n${secondLine}`)
190-
}

packages/app/src/cli/services/execute-operation.test.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {executeOperation} from './execute-operation.js'
2-
import {createAdminSessionAsApp} from './graphql/common.js'
2+
import {createAdminSessionAsApp, validateApiVersion} from './graphql/common.js'
33
import {OrganizationApp} from '../models/organization.js'
44
import {renderSuccess, renderError, renderSingleTask} from '@shopify/cli-kit/node/ui'
55
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
@@ -95,24 +95,41 @@ describe('executeOperation', () => {
9595

9696
test('uses specified API version when provided', async () => {
9797
const query = 'query { shop { name } }'
98-
const apiVersion = '2024-01'
98+
const version = '2024-01'
9999
const mockResult = {data: {shop: {name: 'Test Shop'}}}
100100
vi.mocked(adminRequestDoc).mockResolvedValue(mockResult)
101+
vi.mocked(validateApiVersion).mockResolvedValue()
101102

102103
await executeOperation({
103104
remoteApp: mockRemoteApp,
104105
storeFqdn,
105106
query,
106-
apiVersion,
107+
version,
107108
})
108109

110+
expect(validateApiVersion).toHaveBeenCalledWith(mockAdminSession, version)
109111
expect(adminRequestDoc).toHaveBeenCalledWith(
110112
expect.objectContaining({
111-
version: apiVersion,
113+
version,
112114
}),
113115
)
114116
})
115117

118+
test('does not validate version when not provided', async () => {
119+
const query = 'query { shop { name } }'
120+
const mockResult = {data: {shop: {name: 'Test Shop'}}}
121+
vi.mocked(adminRequestDoc).mockResolvedValue(mockResult)
122+
vi.mocked(validateApiVersion).mockClear()
123+
124+
await executeOperation({
125+
remoteApp: mockRemoteApp,
126+
storeFqdn,
127+
query,
128+
})
129+
130+
expect(validateApiVersion).not.toHaveBeenCalled()
131+
})
132+
116133
test('writes formatted JSON results to stdout by default', async () => {
117134
const query = 'query { shop { name } }'
118135
const mockResult = {data: {shop: {name: 'Test Shop'}}}

packages/app/src/cli/services/execute-operation.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {createAdminSessionAsApp, validateSingleOperation} from './graphql/common.js'
1+
import {createAdminSessionAsApp, validateSingleOperation, validateApiVersion} from './graphql/common.js'
22
import {OrganizationApp} from '../models/organization.js'
33
import {renderSuccess, renderError, renderInfo, renderSingleTask} from '@shopify/cli-kit/node/ui'
44
import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output'
@@ -13,8 +13,8 @@ interface ExecuteOperationInput {
1313
storeFqdn: string
1414
query: string
1515
variables?: string
16-
apiVersion?: string
1716
outputFile?: string
17+
version?: string
1818
}
1919

2020
async function parseVariables(variables?: string): Promise<{[key: string]: unknown} | undefined> {
@@ -32,15 +32,27 @@ async function parseVariables(variables?: string): Promise<{[key: string]: unkno
3232
}
3333

3434
export async function executeOperation(input: ExecuteOperationInput): Promise<void> {
35-
const {remoteApp, storeFqdn, query, variables, apiVersion, outputFile} = input
35+
const {remoteApp, storeFqdn, query, variables, version, outputFile} = input
3636

3737
renderInfo({
3838
headline: 'Executing GraphQL operation.',
39-
body: `App: ${remoteApp.title}\nStore: ${storeFqdn}`,
39+
body: [
40+
{
41+
list: {
42+
items: [
43+
`App: ${remoteApp.title}`,
44+
`Store: ${storeFqdn}`,
45+
`API version: ${version ?? 'default (latest stable)'}`,
46+
],
47+
},
48+
},
49+
],
4050
})
4151

4252
const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn)
4353

54+
if (version) await validateApiVersion(adminSession, version)
55+
4456
const parsedVariables = await parseVariables(variables)
4557

4658
validateSingleOperation(query)
@@ -53,7 +65,7 @@ export async function executeOperation(input: ExecuteOperationInput): Promise<vo
5365
query: parse(query),
5466
session: adminSession,
5567
variables: parsedVariables,
56-
version: apiVersion,
68+
version,
5769
responseOptions: {handleErrors: false},
5870
})
5971
},

packages/app/src/cli/services/graphql/common.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {createAdminSessionAsApp, validateSingleOperation} from './common.js'
1+
import {createAdminSessionAsApp, validateSingleOperation, validateApiVersion} from './common.js'
22
import {OrganizationApp} from '../../models/organization.js'
33
import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session'
4+
import {supportedApiVersions} from '@shopify/cli-kit/node/api/admin'
45
import {describe, test, expect, vi, beforeEach} from 'vitest'
56

67
vi.mock('@shopify/cli-kit/node/session', async () => {
@@ -11,6 +12,14 @@ vi.mock('@shopify/cli-kit/node/session', async () => {
1112
}
1213
})
1314

15+
vi.mock('@shopify/cli-kit/node/api/admin', async () => {
16+
const actual = await vi.importActual('@shopify/cli-kit/node/api/admin')
17+
return {
18+
...actual,
19+
supportedApiVersions: vi.fn(),
20+
}
21+
})
22+
1423
describe('createAdminSessionAsApp', () => {
1524
const mockRemoteApp = {
1625
apiKey: 'test-api-key',
@@ -96,3 +105,29 @@ describe('validateSingleOperation', () => {
96105
expect(() => validateSingleOperation(fragmentOnly)).toThrow('must contain exactly one operation definition')
97106
})
98107
})
108+
109+
describe('validateApiVersion', () => {
110+
const mockAdminSession = {token: 'test-token', storeFqdn: 'test-store.myshopify.com'}
111+
112+
test('allows unstable version without validation', async () => {
113+
await expect(validateApiVersion(mockAdminSession, 'unstable')).resolves.not.toThrow()
114+
115+
expect(supportedApiVersions).not.toHaveBeenCalled()
116+
})
117+
118+
test('allows supported API version', async () => {
119+
vi.mocked(supportedApiVersions).mockResolvedValue(['2024-01', '2024-04', '2024-07'])
120+
121+
await expect(validateApiVersion(mockAdminSession, '2024-04')).resolves.not.toThrow()
122+
123+
expect(supportedApiVersions).toHaveBeenCalledWith(mockAdminSession)
124+
})
125+
126+
test('throws error when API version is not supported', async () => {
127+
vi.mocked(supportedApiVersions).mockResolvedValue(['2024-01', '2024-04', '2024-07'])
128+
129+
await expect(validateApiVersion(mockAdminSession, '2023-01')).rejects.toThrow('Invalid API version: 2023-01')
130+
131+
expect(supportedApiVersions).toHaveBeenCalledWith(mockAdminSession)
132+
})
133+
})

packages/app/src/cli/services/graphql/common.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {OrganizationApp} from '../../models/organization.js'
22
import {ensureAuthenticatedAdminAsApp, AdminSession} from '@shopify/cli-kit/node/session'
33
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
4+
import {outputContent} from '@shopify/cli-kit/node/output'
5+
import {supportedApiVersions} from '@shopify/cli-kit/node/api/admin'
46
import {parse} from 'graphql'
57

68
/**
@@ -42,3 +44,26 @@ export function validateSingleOperation(graphqlOperation: string): void {
4244
)
4345
}
4446
}
47+
48+
/**
49+
* Validates that the specified API version is supported by the store.
50+
* The 'unstable' version is always allowed without validation.
51+
*
52+
* @param adminSession - Admin session containing store credentials.
53+
* @param version - The API version to validate.
54+
* @throws AbortError if the version is not supported by the store.
55+
*/
56+
export async function validateApiVersion(
57+
adminSession: {token: string; storeFqdn: string},
58+
version: string,
59+
): Promise<void> {
60+
if (version === 'unstable') return
61+
62+
const supportedVersions = await supportedApiVersions(adminSession)
63+
if (supportedVersions.includes(version)) return
64+
65+
const firstLine = outputContent`Invalid API version: ${version}`.value
66+
const secondLine = outputContent`Supported versions: ${supportedVersions.join(', ')}`.value
67+
68+
throw new AbortError(`${firstLine}\n${secondLine}`)
69+
}

packages/cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ FLAGS
378378
DESCRIPTION
379379
Execute GraphQL queries and mutations.
380380
381-
Executes a GraphQL query or mutation on the specified store, and writes the result to STDOUT or a file.
381+
Executes an Admin API GraphQL query or mutation on the specified dev store.
382382
```
383383

384384
## `shopify app function build`

0 commit comments

Comments
 (0)