Skip to content

Commit 9f6e6b2

Browse files
committed
add support for --query-file to both app execute and app bulk execute
1 parent 39753af commit 9f6e6b2

File tree

3 files changed

+72
-4
lines changed

3 files changed

+72
-4
lines changed

packages/app/src/cli/flags.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ export const bulkOperationFlags = {
4141
description: 'The GraphQL query or mutation to run as a bulk operation. If omitted, reads from standard input.',
4242
env: 'SHOPIFY_FLAG_QUERY',
4343
required: false,
44+
exclusive: ['query-file'],
45+
}),
46+
'query-file': Flags.string({
47+
description: "Path to a file containing the GraphQL query or mutation. Can't be used with --query.",
48+
env: 'SHOPIFY_FLAG_QUERY_FILE',
49+
parse: async (input) => resolvePath(input),
50+
exclusive: ['query'],
4451
}),
4552
variables: Flags.string({
4653
char: 'v',
@@ -85,6 +92,13 @@ export const operationFlags = {
8592
description: 'The GraphQL query or mutation, as a string.',
8693
env: 'SHOPIFY_FLAG_QUERY',
8794
required: false,
95+
exclusive: ['query-file'],
96+
}),
97+
'query-file': Flags.string({
98+
description: "Path to a file containing the GraphQL query or mutation. Can't be used with --query.",
99+
env: 'SHOPIFY_FLAG_QUERY_FILE',
100+
parse: async (input) => resolvePath(input),
101+
exclusive: ['query'],
88102
}),
89103
variables: Flags.string({
90104
char: 'v',

packages/app/src/cli/utilities/execute-command-helpers.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {prepareAppStoreContext, prepareExecuteContext} from './execute-command-h
22
import {linkedAppContext} from '../services/app-context.js'
33
import {storeContext} from '../services/store-context.js'
44
import {readStdinString} from '@shopify/cli-kit/node/system'
5+
import {readFile, fileExists} from '@shopify/cli-kit/node/fs'
56
import {describe, test, expect, vi, beforeEach} from 'vitest'
67

78
vi.mock('../services/app-context.js')
89
vi.mock('../services/store-context.js')
910
vi.mock('@shopify/cli-kit/node/system')
11+
vi.mock('@shopify/cli-kit/node/fs')
1012

1113
describe('prepareAppStoreContext', () => {
1214
const mockFlags = {
@@ -170,4 +172,38 @@ describe('prepareExecuteContext', () => {
170172
expect(linkedAppContext).toHaveBeenCalled()
171173
expect(storeContext).toHaveBeenCalled()
172174
})
175+
176+
test('reads query from file when query-file flag is provided', async () => {
177+
const queryFileContent = 'query { shop { name } }'
178+
vi.mocked(fileExists).mockResolvedValue(true)
179+
vi.mocked(readFile).mockResolvedValue(queryFileContent as any)
180+
181+
const flagsWithQueryFile = {...mockFlags, query: undefined, 'query-file': '/path/to/query.graphql'}
182+
const result = await prepareExecuteContext(flagsWithQueryFile)
183+
184+
expect(fileExists).toHaveBeenCalledWith('/path/to/query.graphql')
185+
expect(readFile).toHaveBeenCalledWith('/path/to/query.graphql', {encoding: 'utf8'})
186+
expect(result.query).toBe(queryFileContent)
187+
expect(readStdinString).not.toHaveBeenCalled()
188+
})
189+
190+
test('throws AbortError when query file does not exist', async () => {
191+
vi.mocked(fileExists).mockResolvedValue(false)
192+
193+
const flagsWithQueryFile = {...mockFlags, query: undefined, 'query-file': '/path/to/nonexistent.graphql'}
194+
195+
await expect(prepareExecuteContext(flagsWithQueryFile)).rejects.toThrow('Query file not found')
196+
expect(readFile).not.toHaveBeenCalled()
197+
})
198+
199+
test('falls back to stdin when neither query nor query-file provided', async () => {
200+
const stdinQuery = 'query { shop { name } }'
201+
vi.mocked(readStdinString).mockResolvedValue(stdinQuery)
202+
203+
const flagsWithoutQueryOrFile = {...mockFlags, query: undefined}
204+
const result = await prepareExecuteContext(flagsWithoutQueryOrFile)
205+
206+
expect(readStdinString).toHaveBeenCalled()
207+
expect(result.query).toBe(stdinQuery)
208+
})
173209
})

packages/app/src/cli/utilities/execute-command-helpers.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {storeContext} from '../services/store-context.js'
33
import {OrganizationStore} from '../models/organization.js'
44
import {readStdinString} from '@shopify/cli-kit/node/system'
55
import {AbortError} from '@shopify/cli-kit/node/error'
6+
import {readFile, fileExists} from '@shopify/cli-kit/node/fs'
7+
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'
68

79
interface AppStoreContextFlags {
810
path: string
@@ -19,6 +21,7 @@ interface AppStoreContext {
1921

2022
interface ExecuteCommandFlags extends AppStoreContextFlags {
2123
query?: string
24+
'query-file'?: string
2225
}
2326

2427
interface ExecuteContext extends AppStoreContext {
@@ -51,7 +54,7 @@ export async function prepareAppStoreContext(flags: AppStoreContextFlags): Promi
5154

5255
/**
5356
* Prepares the execution context for GraphQL operations.
54-
* Handles query input from flag or stdin, and sets up app and store contexts.
57+
* Handles query input from flag, file, or stdin, and sets up app and store contexts.
5558
*
5659
* @param flags - Command flags containing configuration options.
5760
* @param commandName - Name of the command for error messages (e.g., 'execute', 'bulk execute').
@@ -61,11 +64,26 @@ export async function prepareExecuteContext(
6164
flags: ExecuteCommandFlags,
6265
commandName = 'execute',
6366
): Promise<ExecuteContext> {
64-
const query = flags.query ?? (await readStdinString())
67+
let query: string | undefined
68+
69+
if (flags.query) {
70+
query = flags.query
71+
} else if (flags['query-file']) {
72+
const queryFile = flags['query-file']
73+
if (!(await fileExists(queryFile))) {
74+
throw new AbortError(
75+
outputContent`Query file not found at ${outputToken.path(queryFile)}. Please check the path and try again.`,
76+
)
77+
}
78+
query = await readFile(queryFile, {encoding: 'utf8'})
79+
} else {
80+
query = await readStdinString()
81+
}
82+
6583
if (!query) {
6684
throw new AbortError(
67-
'No query provided. Use the --query flag or pipe input via stdin.',
68-
`Example: echo "query { ... }" | shopify app ${commandName}`,
85+
'No query provided. Use the --query flag, --query-file flag, or pipe input via stdin.',
86+
`Example: shopify app ${commandName} --query-file query.graphql`,
6987
)
7088
}
7189

0 commit comments

Comments
 (0)