Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
14 changes: 14 additions & 0 deletions packages/app/src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ export const bulkOperationFlags = {
description: 'The GraphQL query or mutation to run as a bulk operation. If omitted, reads from standard input.',
env: 'SHOPIFY_FLAG_QUERY',
required: false,
exclusive: ['query-file'],
}),
'query-file': Flags.string({
description: "Path to a file containing the GraphQL query or mutation. Can't be used with --query.",
env: 'SHOPIFY_FLAG_QUERY_FILE',
parse: async (input) => resolvePath(input),
exclusive: ['query'],
}),
variables: Flags.string({
char: 'v',
Expand Down Expand Up @@ -85,6 +92,13 @@ export const operationFlags = {
description: 'The GraphQL query or mutation, as a string.',
env: 'SHOPIFY_FLAG_QUERY',
required: false,
exclusive: ['query-file'],
}),
'query-file': Flags.string({
description: "Path to a file containing the GraphQL query or mutation. Can't be used with --query.",
env: 'SHOPIFY_FLAG_QUERY_FILE',
parse: async (input) => resolvePath(input),
exclusive: ['query'],
}),
variables: Flags.string({
char: 'v',
Expand Down
36 changes: 36 additions & 0 deletions packages/app/src/cli/utilities/execute-command-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {prepareAppStoreContext, prepareExecuteContext} from './execute-command-h
import {linkedAppContext} from '../services/app-context.js'
import {storeContext} from '../services/store-context.js'
import {readStdinString} from '@shopify/cli-kit/node/system'
import {readFile, fileExists} from '@shopify/cli-kit/node/fs'
import {describe, test, expect, vi, beforeEach} from 'vitest'

vi.mock('../services/app-context.js')
vi.mock('../services/store-context.js')
vi.mock('@shopify/cli-kit/node/system')
vi.mock('@shopify/cli-kit/node/fs')

describe('prepareAppStoreContext', () => {
const mockFlags = {
Expand Down Expand Up @@ -170,4 +172,38 @@ describe('prepareExecuteContext', () => {
expect(linkedAppContext).toHaveBeenCalled()
expect(storeContext).toHaveBeenCalled()
})

test('reads query from file when query-file flag is provided', async () => {
const queryFileContent = 'query { shop { name } }'
vi.mocked(fileExists).mockResolvedValue(true)
vi.mocked(readFile).mockResolvedValue(queryFileContent as any)

const flagsWithQueryFile = {...mockFlags, query: undefined, 'query-file': '/path/to/query.graphql'}
const result = await prepareExecuteContext(flagsWithQueryFile)

expect(fileExists).toHaveBeenCalledWith('/path/to/query.graphql')
expect(readFile).toHaveBeenCalledWith('/path/to/query.graphql', {encoding: 'utf8'})
expect(result.query).toBe(queryFileContent)
expect(readStdinString).not.toHaveBeenCalled()
})

test('throws AbortError when query file does not exist', async () => {
vi.mocked(fileExists).mockResolvedValue(false)

const flagsWithQueryFile = {...mockFlags, query: undefined, 'query-file': '/path/to/nonexistent.graphql'}

await expect(prepareExecuteContext(flagsWithQueryFile)).rejects.toThrow('Query file not found')
expect(readFile).not.toHaveBeenCalled()
})

test('falls back to stdin when neither query nor query-file provided', async () => {
const stdinQuery = 'query { shop { name } }'
vi.mocked(readStdinString).mockResolvedValue(stdinQuery)

const flagsWithoutQueryOrFile = {...mockFlags, query: undefined}
const result = await prepareExecuteContext(flagsWithoutQueryOrFile)

expect(readStdinString).toHaveBeenCalled()
expect(result.query).toBe(stdinQuery)
})
})
26 changes: 22 additions & 4 deletions packages/app/src/cli/utilities/execute-command-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {storeContext} from '../services/store-context.js'
import {OrganizationStore} from '../models/organization.js'
import {readStdinString} from '@shopify/cli-kit/node/system'
import {AbortError} from '@shopify/cli-kit/node/error'
import {readFile, fileExists} from '@shopify/cli-kit/node/fs'
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'

interface AppStoreContextFlags {
path: string
Expand All @@ -19,6 +21,7 @@ interface AppStoreContext {

interface ExecuteCommandFlags extends AppStoreContextFlags {
query?: string
'query-file'?: string
}

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

/**
* Prepares the execution context for GraphQL operations.
* Handles query input from flag or stdin, and sets up app and store contexts.
* Handles query input from flag, file, or stdin, and sets up app and store contexts.
*
* @param flags - Command flags containing configuration options.
* @param commandName - Name of the command for error messages (e.g., 'execute', 'bulk execute').
Expand All @@ -61,11 +64,26 @@ export async function prepareExecuteContext(
flags: ExecuteCommandFlags,
commandName = 'execute',
): Promise<ExecuteContext> {
const query = flags.query ?? (await readStdinString())
let query: string | undefined

if (flags.query) {
query = flags.query
} else if (flags['query-file']) {
Comment on lines +70 to +72

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if a user submits both?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OCLIF should prevent that based on the flag config

const queryFile = flags['query-file']
if (!(await fileExists(queryFile))) {
throw new AbortError(
outputContent`Query file not found at ${outputToken.path(queryFile)}. Please check the path and try again.`,
)
}
query = await readFile(queryFile, {encoding: 'utf8'})
} else {
query = await readStdinString()
}

if (!query) {
throw new AbortError(
'No query provided. Use the --query flag or pipe input via stdin.',
`Example: echo "query { ... }" | shopify app ${commandName}`,
'No query provided. Use the --query flag, --query-file flag, or pipe input via stdin.',
`Example: shopify app ${commandName} --query-file query.graphql`,
)
}

Expand Down
28 changes: 28 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,12 +148,26 @@
"char": "q",
"description": "The GraphQL query or mutation to run as a bulk operation. If omitted, reads from standard input.",
"env": "SHOPIFY_FLAG_QUERY",
"exclusive": [
"query-file"
],
"hasDynamicHelp": false,
"multiple": false,
"name": "query",
"required": false,
"type": "option"
},
"query-file": {
"description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.",
"env": "SHOPIFY_FLAG_QUERY_FILE",
"exclusive": [
"query"
],
"hasDynamicHelp": false,
"multiple": false,
"name": "query-file",
"type": "option"
},
"reset": {
"allowNo": false,
"description": "Reset all your settings.",
Expand Down Expand Up @@ -1186,12 +1200,26 @@
"char": "q",
"description": "The GraphQL query or mutation, as a string.",
"env": "SHOPIFY_FLAG_QUERY",
"exclusive": [
"query-file"
],
"hasDynamicHelp": false,
"multiple": false,
"name": "query",
"required": false,
"type": "option"
},
"query-file": {
"description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.",
"env": "SHOPIFY_FLAG_QUERY_FILE",
"exclusive": [
"query"
],
"hasDynamicHelp": false,
"multiple": false,
"name": "query-file",
"type": "option"
},
"reset": {
"allowNo": false,
"description": "Reset all your settings.",
Expand Down