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
1 change: 1 addition & 0 deletions packages/app/src/cli/commands/app/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default class Execute extends AppLinkedCommand {
storeFqdn: store.shopDomain,
query,
variables: flags.variables,
variableFile: flags['variable-file'],
outputFile: flags['output-file'],
...(flags.version && {version: flags.version}),
})
Expand Down
7 changes: 7 additions & 0 deletions packages/app/src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ export const operationFlags = {
char: 'v',
description: 'The values for any GraphQL variables in your query or mutation, in JSON format.',
env: 'SHOPIFY_FLAG_VARIABLES',
exclusive: ['variable-file'],
}),
'variable-file': Flags.string({
description: "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.",
env: 'SHOPIFY_FLAG_VARIABLE_FILE',
parse: async (input) => resolvePath(input),
exclusive: ['variables'],
}),
store: Flags.string({
char: 's',
Expand Down
66 changes: 66 additions & 0 deletions packages/app/src/cli/services/execute-operation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,72 @@ describe('executeOperation', () => {
expect(adminRequestDoc).not.toHaveBeenCalled()
})

test('reads and parses variables from a JSON file', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const variableFile = joinPath(tmpDir, 'variables.json')
const variables = {input: {id: 'gid://shopify/Product/123', title: 'Updated'}}
await writeFile(variableFile, JSON.stringify(variables))

const query = 'mutation UpdateProduct($input: ProductInput!) { productUpdate(input: $input) { product { id } } }'
const mockResult = {data: {productUpdate: {product: {id: 'gid://shopify/Product/123'}}}}
vi.mocked(adminRequestDoc).mockResolvedValue(mockResult)

await executeOperation({
organization: mockOrganization,
remoteApp: mockRemoteApp,
storeFqdn,
query,
variableFile,
})

expect(adminRequestDoc).toHaveBeenCalledWith(
expect.objectContaining({
variables,
}),
)
})
})

test('throws AbortError when variable file does not exist', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const nonExistentFile = joinPath(tmpDir, 'nonexistent.json')
const query = 'query { shop { name } }'

await expect(
executeOperation({
organization: mockOrganization,
remoteApp: mockRemoteApp,
storeFqdn,
query,
variableFile: nonExistentFile,
}),
).rejects.toThrow('Variable file not found')

expect(adminRequestDoc).not.toHaveBeenCalled()
})
})

test('throws AbortError when variable file contains invalid JSON', async () => {
await inTemporaryDirectory(async (tmpDir) => {
const variableFile = joinPath(tmpDir, 'invalid.json')
await writeFile(variableFile, '{invalid json}')

const query = 'query { shop { name } }'

await expect(
executeOperation({
organization: mockOrganization,
remoteApp: mockRemoteApp,
storeFqdn,
query,
variableFile,
}),
).rejects.toThrow('Invalid JSON')

expect(adminRequestDoc).not.toHaveBeenCalled()
})
})

test('uses specified API version when provided', async () => {
const query = 'query { shop { name } }'
const version = '2024-01'
Expand Down
60 changes: 46 additions & 14 deletions packages/app/src/cli/services/execute-operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,66 @@ import {AbortError} from '@shopify/cli-kit/node/error'
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
import {ClientError} from 'graphql-request'
import {parse} from 'graphql'
import {writeFile} from '@shopify/cli-kit/node/fs'
import {writeFile, readFile, fileExists} from '@shopify/cli-kit/node/fs'

interface ExecuteOperationInput {
organization: Organization
remoteApp: OrganizationApp
storeFqdn: string
query: string
variables?: string
variableFile?: string
outputFile?: string
version?: string
}

async function parseVariables(variables?: string): Promise<{[key: string]: unknown} | undefined> {
if (!variables) return undefined

try {
return JSON.parse(variables)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
throw new AbortError(
outputContent`Invalid JSON in ${outputToken.yellow('--variables')} flag: ${errorMessage}`,
'Please provide valid JSON format.',
)
async function parseVariables(
variables?: string,
variableFile?: string,
): Promise<{[key: string]: unknown} | undefined> {
if (variables) {
try {
return JSON.parse(variables)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
throw new AbortError(
outputContent`Invalid JSON in ${outputToken.yellow('--variables')} flag: ${errorMessage}`,
'Please provide valid JSON format.',
)
}
} else if (variableFile) {
if (!(await fileExists(variableFile))) {
throw new AbortError(
outputContent`Variable file not found at ${outputToken.path(
variableFile,
)}. Please check the path and try again.`,
)
}
const fileContent = await readFile(variableFile, {encoding: 'utf8'})
try {
return JSON.parse(fileContent)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
throw new AbortError(
outputContent`Invalid JSON in variable file ${outputToken.path(variableFile)}: ${errorMessage}`,
'Please provide valid JSON format.',
)
}
}
return undefined
}

export async function executeOperation(input: ExecuteOperationInput): Promise<void> {
const {organization, remoteApp, storeFqdn, query, variables, version: userSpecifiedVersion, outputFile} = input
const {
organization,
remoteApp,
storeFqdn,
query,
variables,
variableFile,
version: userSpecifiedVersion,
outputFile,
} = input

const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn)

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

const parsedVariables = await parseVariables(variables)
const parsedVariables = await parseVariables(variables, variableFile)

validateSingleOperation(query)

Expand Down
14 changes: 14 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1212,10 +1212,24 @@
"name": "store",
"type": "option"
},
"variable-file": {
"description": "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.",
"env": "SHOPIFY_FLAG_VARIABLE_FILE",
"exclusive": [
"variables"
],
"hasDynamicHelp": false,
"multiple": false,
"name": "variable-file",
"type": "option"
},
"variables": {
"char": "v",
"description": "The values for any GraphQL variables in your query or mutation, in JSON format.",
"env": "SHOPIFY_FLAG_VARIABLES",
"exclusive": [
"variable-file"
],
"hasDynamicHelp": false,
"multiple": false,
"name": "variables",
Expand Down
Loading