diff --git a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts index c727734aa0..5e6ac79048 100644 --- a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts +++ b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.test.ts @@ -99,7 +99,7 @@ describe('executeBulkOperation', () => { expect(runBulkOperationQuery).not.toHaveBeenCalled() }) - test('passes variables to mutation when provided with `--variables` flag', async () => { + test('passes variables parameter to runBulkOperationMutation when variables are provided', async () => { const mutation = 'mutation productUpdate($input: ProductInput!) { productUpdate(input: $input) { product { id } } }' const variables = ['{"input":{"id":"gid://shopify/Product/123","tags":["test"]}}'] const mockResponse = { @@ -122,7 +122,7 @@ describe('executeBulkOperation', () => { }) }) - test('renders success message when bulk operation is created', async () => { + test('renders success message when bulk operation returns without user errors', async () => { const query = '{ products { edges { node { id } } } }' const mockResponse = { bulkOperation: successfulBulkOperation, @@ -141,7 +141,7 @@ describe('executeBulkOperation', () => { }) }) - test('renders warning when user errors are present', async () => { + test('renders warning with formatted field errors when bulk operation returns user errors', async () => { const query = '{ products { edges { node { id } } } }' const mockResponse = { bulkOperation: null, @@ -165,4 +165,55 @@ describe('executeBulkOperation', () => { expect(renderSuccess).not.toHaveBeenCalled() }) + + test('throws GraphQL syntax error when given malformed GraphQL document', async () => { + const malformedQuery = '{ products { edges { node { id } }' + + await expect( + executeBulkOperation({ + app: mockApp, + storeFqdn, + query: malformedQuery, + }), + ).rejects.toThrow('Syntax Error') + + expect(runBulkOperationQuery).not.toHaveBeenCalled() + expect(runBulkOperationMutation).not.toHaveBeenCalled() + }) + + test('throws error when GraphQL document contains multiple operation definitions', async () => { + const multipleOperations = + 'mutation { productUpdate(input: {}) { product { id } } } mutation { productDelete(input: {}) { deletedProductId } }' + + await expect( + executeBulkOperation({ + app: mockApp, + storeFqdn, + query: multipleOperations, + }), + ).rejects.toThrow('Multiple operations are not supported') + + expect(runBulkOperationQuery).not.toHaveBeenCalled() + expect(runBulkOperationMutation).not.toHaveBeenCalled() + }) + + test('throws error when GraphQL document contains no operation definitions', async () => { + const noOperations = ` + fragment ProductFields on Product { + id + title + } + ` + + await expect( + executeBulkOperation({ + app: mockApp, + storeFqdn, + query: noOperations, + }), + ).rejects.toThrow('must contain exactly one operation definition') + + expect(runBulkOperationQuery).not.toHaveBeenCalled() + expect(runBulkOperationMutation).not.toHaveBeenCalled() + }) }) diff --git a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts index 6617533249..9399957a73 100644 --- a/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts +++ b/packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts @@ -24,14 +24,9 @@ export async function executeBulkOperation(input: ExecuteBulkOperationInput): Pr const adminSession = await ensureAuthenticatedAdmin(storeFqdn) - const operationIsMutation = isMutation(query) - if (!operationIsMutation && variables) { - throw new AbortError( - outputContent`The ${outputToken.yellow('--variables')} flag can only be used with mutations, not queries.`, - ) - } + validateGraphQLDocument(query, variables) - const bulkOperationResponse = operationIsMutation + const bulkOperationResponse = isMutation(query) ? await runBulkOperationMutation({adminSession, query, variables}) : await runBulkOperationQuery({adminSession, query}) @@ -77,9 +72,25 @@ export async function executeBulkOperation(input: ExecuteBulkOperationInput): Pr } } -function isMutation(graphqlOperation: string): boolean { +function validateGraphQLDocument(graphqlOperation: string, variables?: string[]): void { const document = parse(graphqlOperation) - const firstOperation = document.definitions.find((def) => def.kind === 'OperationDefinition') + const operationDefinitions = document.definitions.filter((def) => def.kind === 'OperationDefinition') + + if (operationDefinitions.length !== 1) { + throw new AbortError( + 'GraphQL document must contain exactly one operation definition. Multiple operations are not supported.', + ) + } - return firstOperation?.kind === 'OperationDefinition' && firstOperation.operation === 'mutation' + if (!isMutation(graphqlOperation) && variables) { + throw new AbortError( + outputContent`The ${outputToken.yellow('--variables')} flag can only be used with mutations, not queries.`, + ) + } +} + +function isMutation(graphqlOperation: string): boolean { + const document = parse(graphqlOperation) + const operation = document.definitions.find((def) => def.kind === 'OperationDefinition') + return operation?.kind === 'OperationDefinition' && operation.operation === 'mutation' }