Skip to content

Commit aee9944

Browse files
Merge pull request #6732 from Shopify/app_execute_variable_file
Add `--variable-file` flag to `app execute` command
2 parents f594242 + 39753af commit aee9944

File tree

5 files changed

+134
-14
lines changed

5 files changed

+134
-14
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export default class Execute extends AppLinkedCommand {
2828
storeFqdn: store.shopDomain,
2929
query,
3030
variables: flags.variables,
31+
variableFile: flags['variable-file'],
3132
outputFile: flags['output-file'],
3233
...(flags.version && {version: flags.version}),
3334
})

packages/app/src/cli/flags.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ export const operationFlags = {
9090
char: 'v',
9191
description: 'The values for any GraphQL variables in your query or mutation, in JSON format.',
9292
env: 'SHOPIFY_FLAG_VARIABLES',
93+
exclusive: ['variable-file'],
94+
}),
95+
'variable-file': Flags.string({
96+
description: "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.",
97+
env: 'SHOPIFY_FLAG_VARIABLE_FILE',
98+
parse: async (input) => resolvePath(input),
99+
exclusive: ['variables'],
93100
}),
94101
store: Flags.string({
95102
char: 's',

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,72 @@ describe('executeOperation', () => {
104104
expect(adminRequestDoc).not.toHaveBeenCalled()
105105
})
106106

107+
test('reads and parses variables from a JSON file', async () => {
108+
await inTemporaryDirectory(async (tmpDir) => {
109+
const variableFile = joinPath(tmpDir, 'variables.json')
110+
const variables = {input: {id: 'gid://shopify/Product/123', title: 'Updated'}}
111+
await writeFile(variableFile, JSON.stringify(variables))
112+
113+
const query = 'mutation UpdateProduct($input: ProductInput!) { productUpdate(input: $input) { product { id } } }'
114+
const mockResult = {data: {productUpdate: {product: {id: 'gid://shopify/Product/123'}}}}
115+
vi.mocked(adminRequestDoc).mockResolvedValue(mockResult)
116+
117+
await executeOperation({
118+
organization: mockOrganization,
119+
remoteApp: mockRemoteApp,
120+
storeFqdn,
121+
query,
122+
variableFile,
123+
})
124+
125+
expect(adminRequestDoc).toHaveBeenCalledWith(
126+
expect.objectContaining({
127+
variables,
128+
}),
129+
)
130+
})
131+
})
132+
133+
test('throws AbortError when variable file does not exist', async () => {
134+
await inTemporaryDirectory(async (tmpDir) => {
135+
const nonExistentFile = joinPath(tmpDir, 'nonexistent.json')
136+
const query = 'query { shop { name } }'
137+
138+
await expect(
139+
executeOperation({
140+
organization: mockOrganization,
141+
remoteApp: mockRemoteApp,
142+
storeFqdn,
143+
query,
144+
variableFile: nonExistentFile,
145+
}),
146+
).rejects.toThrow('Variable file not found')
147+
148+
expect(adminRequestDoc).not.toHaveBeenCalled()
149+
})
150+
})
151+
152+
test('throws AbortError when variable file contains invalid JSON', async () => {
153+
await inTemporaryDirectory(async (tmpDir) => {
154+
const variableFile = joinPath(tmpDir, 'invalid.json')
155+
await writeFile(variableFile, '{invalid json}')
156+
157+
const query = 'query { shop { name } }'
158+
159+
await expect(
160+
executeOperation({
161+
organization: mockOrganization,
162+
remoteApp: mockRemoteApp,
163+
storeFqdn,
164+
query,
165+
variableFile,
166+
}),
167+
).rejects.toThrow('Invalid JSON')
168+
169+
expect(adminRequestDoc).not.toHaveBeenCalled()
170+
})
171+
})
172+
107173
test('uses specified API version when provided', async () => {
108174
const query = 'query { shop { name } }'
109175
const version = '2024-01'

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

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,66 @@ import {AbortError} from '@shopify/cli-kit/node/error'
1111
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
1212
import {ClientError} from 'graphql-request'
1313
import {parse} from 'graphql'
14-
import {writeFile} from '@shopify/cli-kit/node/fs'
14+
import {writeFile, readFile, fileExists} from '@shopify/cli-kit/node/fs'
1515

1616
interface ExecuteOperationInput {
1717
organization: Organization
1818
remoteApp: OrganizationApp
1919
storeFqdn: string
2020
query: string
2121
variables?: string
22+
variableFile?: string
2223
outputFile?: string
2324
version?: string
2425
}
2526

26-
async function parseVariables(variables?: string): Promise<{[key: string]: unknown} | undefined> {
27-
if (!variables) return undefined
28-
29-
try {
30-
return JSON.parse(variables)
31-
} catch (error) {
32-
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
33-
throw new AbortError(
34-
outputContent`Invalid JSON in ${outputToken.yellow('--variables')} flag: ${errorMessage}`,
35-
'Please provide valid JSON format.',
36-
)
27+
async function parseVariables(
28+
variables?: string,
29+
variableFile?: string,
30+
): Promise<{[key: string]: unknown} | undefined> {
31+
if (variables) {
32+
try {
33+
return JSON.parse(variables)
34+
} catch (error) {
35+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
36+
throw new AbortError(
37+
outputContent`Invalid JSON in ${outputToken.yellow('--variables')} flag: ${errorMessage}`,
38+
'Please provide valid JSON format.',
39+
)
40+
}
41+
} else if (variableFile) {
42+
if (!(await fileExists(variableFile))) {
43+
throw new AbortError(
44+
outputContent`Variable file not found at ${outputToken.path(
45+
variableFile,
46+
)}. Please check the path and try again.`,
47+
)
48+
}
49+
const fileContent = await readFile(variableFile, {encoding: 'utf8'})
50+
try {
51+
return JSON.parse(fileContent)
52+
} catch (error) {
53+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
54+
throw new AbortError(
55+
outputContent`Invalid JSON in variable file ${outputToken.path(variableFile)}: ${errorMessage}`,
56+
'Please provide valid JSON format.',
57+
)
58+
}
3759
}
60+
return undefined
3861
}
3962

4063
export async function executeOperation(input: ExecuteOperationInput): Promise<void> {
41-
const {organization, remoteApp, storeFqdn, query, variables, version: userSpecifiedVersion, outputFile} = input
64+
const {
65+
organization,
66+
remoteApp,
67+
storeFqdn,
68+
query,
69+
variables,
70+
variableFile,
71+
version: userSpecifiedVersion,
72+
outputFile,
73+
} = input
4274

4375
const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn)
4476

@@ -55,7 +87,7 @@ export async function executeOperation(input: ExecuteOperationInput): Promise<vo
5587
],
5688
})
5789

58-
const parsedVariables = await parseVariables(variables)
90+
const parsedVariables = await parseVariables(variables, variableFile)
5991

6092
validateSingleOperation(query)
6193

packages/cli/oclif.manifest.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,10 +1308,24 @@
13081308
"name": "store",
13091309
"type": "option"
13101310
},
1311+
"variable-file": {
1312+
"description": "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.",
1313+
"env": "SHOPIFY_FLAG_VARIABLE_FILE",
1314+
"exclusive": [
1315+
"variables"
1316+
],
1317+
"hasDynamicHelp": false,
1318+
"multiple": false,
1319+
"name": "variable-file",
1320+
"type": "option"
1321+
},
13111322
"variables": {
13121323
"char": "v",
13131324
"description": "The values for any GraphQL variables in your query or mutation, in JSON format.",
13141325
"env": "SHOPIFY_FLAG_VARIABLES",
1326+
"exclusive": [
1327+
"variable-file"
1328+
],
13151329
"hasDynamicHelp": false,
13161330
"multiple": false,
13171331
"name": "variables",

0 commit comments

Comments
 (0)