Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion packages/app/src/cli/commands/app/bulk/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default class BulkExecute extends AppLinkedCommand {
async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(BulkExecute)

const {query, appContextResult, store} = await prepareExecuteContext(flags, 'bulk execute')
const {query, appContextResult, store} = await prepareExecuteContext(flags)

await executeBulkOperation({
organization: appContextResult.organization,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/cli/commands/app/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default class Execute extends AppLinkedCommand {
async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(Execute)

const {query, appContextResult, store} = await prepareExecuteContext(flags, 'execute')
const {query, appContextResult, store} = await prepareExecuteContext(flags)

await executeOperation({
organization: appContextResult.organization,
Expand Down
10 changes: 5 additions & 5 deletions packages/app/src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@ export const appFlags = {
export const bulkOperationFlags = {
query: Flags.string({
char: 'q',
description: 'The GraphQL query or mutation to run as a bulk operation. If omitted, reads from standard input.',
description: 'The GraphQL query or mutation to run as a bulk operation.',
env: 'SHOPIFY_FLAG_QUERY',
required: false,
exclusive: ['query-file'],
exactlyOne: ['query', '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'],
exactlyOne: ['query', 'query-file'],
}),
variables: Flags.string({
char: 'v',
Expand Down Expand Up @@ -92,13 +92,13 @@ export const operationFlags = {
description: 'The GraphQL query or mutation, as a string.',
env: 'SHOPIFY_FLAG_QUERY',
required: false,
exclusive: ['query-file'],
exactlyOne: ['query', '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'],
exactlyOne: ['query', 'query-file'],
}),
variables: Flags.string({
char: 'v',
Expand Down
47 changes: 2 additions & 45 deletions packages/app/src/cli/utilities/execute-command-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {linkedAppContext} from '../services/app-context.js'
import {storeContext} from '../services/store-context.js'
import {validateSingleOperation} from '../services/graphql/common.js'
import {readFile, fileExists} from '@shopify/cli-kit/node/fs'
import {readStdinString} from '@shopify/cli-kit/node/system'
import {describe, test, expect, vi, beforeEach} from 'vitest'

vi.mock('../services/app-context.js')
Expand Down Expand Up @@ -116,48 +115,18 @@ describe('prepareExecuteContext', () => {
beforeEach(() => {
vi.mocked(linkedAppContext).mockResolvedValue(mockAppContextResult as any)
vi.mocked(storeContext).mockResolvedValue(mockStore as any)
vi.mocked(readStdinString).mockResolvedValue('')
})

test('uses query from flags when provided', async () => {
const result = await prepareExecuteContext(mockFlags)

expect(result.query).toBe(mockFlags.query)
expect(readStdinString).not.toHaveBeenCalled()
})

test('reads query from stdin when flag not provided', async () => {
const stdinQuery = 'query { products { edges { node { id } } } }'
vi.mocked(readStdinString).mockResolvedValue(stdinQuery)

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

expect(readStdinString).toHaveBeenCalled()
expect(result.query).toBe(stdinQuery)
})

test('throws AbortError when no query provided via flag or stdin', async () => {
vi.mocked(readStdinString).mockResolvedValue('')

const flagsWithoutQuery = {...mockFlags, query: undefined}

await expect(prepareExecuteContext(flagsWithoutQuery)).rejects.toThrow('No query provided')
})

test('includes command name in error message', async () => {
vi.mocked(readStdinString).mockResolvedValue('')

test('throws BugError when no query provided', async () => {
const flagsWithoutQuery = {...mockFlags, query: undefined}

try {
await prepareExecuteContext(flagsWithoutQuery, 'bulk execute')
expect.fail('Should have thrown an error')
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error: any) {
expect(error.message).toContain('No query provided')
expect(error.tryMessage).toMatch(/shopify app bulk execute/)
}
await expect(prepareExecuteContext(flagsWithoutQuery)).rejects.toThrow('exactlyOne constraint')
})

test('returns query, app context, and store', async () => {
Expand Down Expand Up @@ -188,7 +157,6 @@ describe('prepareExecuteContext', () => {
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 () => {
Expand All @@ -200,17 +168,6 @@ describe('prepareExecuteContext', () => {
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)
})

test('validates GraphQL query using validateSingleOperation', async () => {
await prepareExecuteContext(mockFlags)

Expand Down
18 changes: 5 additions & 13 deletions packages/app/src/cli/utilities/execute-command-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import {linkedAppContext, LoadedAppContextOutput} from '../services/app-context.
import {storeContext} from '../services/store-context.js'
import {validateSingleOperation} from '../services/graphql/common.js'
import {OrganizationStore} from '../models/organization.js'
import {readStdinString} from '@shopify/cli-kit/node/system'
import {AbortError} from '@shopify/cli-kit/node/error'
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
import {readFile, fileExists} from '@shopify/cli-kit/node/fs'
import {outputContent, outputToken} from '@shopify/cli-kit/node/output'

Expand Down Expand Up @@ -55,16 +54,12 @@ export async function prepareAppStoreContext(flags: AppStoreContextFlags): Promi

/**
* Prepares the execution context for GraphQL operations.
* Handles query input from flag, file, or stdin, validates GraphQL syntax, and sets up app and store contexts.
* Handles query input from flag or file, validates GraphQL syntax, 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').
* @returns Context object containing query, app context, and store information.
*/
export async function prepareExecuteContext(
flags: ExecuteCommandFlags,
commandName = 'execute',
): Promise<ExecuteContext> {
export async function prepareExecuteContext(flags: ExecuteCommandFlags): Promise<ExecuteContext> {
let query: string | undefined

if (flags.query) {
Expand All @@ -77,14 +72,11 @@ export async function prepareExecuteContext(
)
}
query = await readFile(queryFile, {encoding: 'utf8'})
} else {
query = await readStdinString()
}

if (!query) {
throw new AbortError(
'No query provided. Use the --query flag, --query-file flag, or pipe input via stdin.',
`Example: shopify app ${commandName} --query-file query.graphql`,
throw new BugError(
'Query should have been provided via --query or --query-file flags due to exactlyOne constraint. This indicates the oclif flag validation failed.',
)
}

Expand Down
14 changes: 1 addition & 13 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,8 @@
},
"query": {
"char": "q",
"description": "The GraphQL query or mutation to run as a bulk operation. If omitted, reads from standard input.",
"description": "The GraphQL query or mutation to run as a bulk operation.",
"env": "SHOPIFY_FLAG_QUERY",
"exclusive": [
"query-file"
],
"hasDynamicHelp": false,
"multiple": false,
"name": "query",
Expand All @@ -160,9 +157,6 @@
"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",
Expand Down Expand Up @@ -1200,9 +1194,6 @@
"char": "q",
"description": "The GraphQL query or mutation, as a string.",
"env": "SHOPIFY_FLAG_QUERY",
"exclusive": [
"query-file"
],
"hasDynamicHelp": false,
"multiple": false,
"name": "query",
Expand All @@ -1212,9 +1203,6 @@
"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",
Expand Down
Loading