From 6af70ad35f7c311f0381ea13c69af7faaea9fc6b Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:56:08 -0400 Subject: [PATCH 1/2] Add `shopify store bulk` commands (execute, status, cancel) Mirrors `app bulk` commands using user auth instead of app credentials. These commands don't require an app to be linked or installed on the target store. Co-Authored-By: Claude Opus 4.6 --- .../cli/commands/store/bulk/cancel.test.ts | 51 ++++ .../app/src/cli/commands/store/bulk/cancel.ts | 37 +++ .../cli/commands/store/bulk/execute.test.ts | 99 ++++++++ .../src/cli/commands/store/bulk/execute.ts | 38 +++ .../cli/commands/store/bulk/status.test.ts | 55 ++++ .../app/src/cli/commands/store/bulk/status.ts | 52 ++++ packages/app/src/cli/flags.ts | 52 ++++ packages/app/src/cli/index.ts | 6 + .../app/src/cli/services/graphql/common.ts | 18 ++ .../services/store-bulk-cancel-operation.ts | 74 ++++++ .../services/store-bulk-execute-operation.ts | 236 ++++++++++++++++++ .../services/store-bulk-operation-status.ts | 170 +++++++++++++ packages/cli/README.md | 95 +++++++ packages/cli/oclif.manifest.json | 224 +++++++++++++++++ 14 files changed, 1207 insertions(+) create mode 100644 packages/app/src/cli/commands/store/bulk/cancel.test.ts create mode 100644 packages/app/src/cli/commands/store/bulk/cancel.ts create mode 100644 packages/app/src/cli/commands/store/bulk/execute.test.ts create mode 100644 packages/app/src/cli/commands/store/bulk/execute.ts create mode 100644 packages/app/src/cli/commands/store/bulk/status.test.ts create mode 100644 packages/app/src/cli/commands/store/bulk/status.ts create mode 100644 packages/app/src/cli/services/store-bulk-cancel-operation.ts create mode 100644 packages/app/src/cli/services/store-bulk-execute-operation.ts create mode 100644 packages/app/src/cli/services/store-bulk-operation-status.ts diff --git a/packages/app/src/cli/commands/store/bulk/cancel.test.ts b/packages/app/src/cli/commands/store/bulk/cancel.test.ts new file mode 100644 index 00000000000..0377bf4ea5f --- /dev/null +++ b/packages/app/src/cli/commands/store/bulk/cancel.test.ts @@ -0,0 +1,51 @@ +import StoreBulkCancel from './cancel.js' +import {storeCancelBulkOperation} from '../../../services/store-bulk-cancel-operation.js' +import {describe, expect, test, vi} from 'vitest' + +vi.mock('../../../services/store-bulk-cancel-operation.js') + +describe('store bulk cancel command', () => { + test('requires --store flag', async () => { + await expect( + StoreBulkCancel.run(['--id', '123'], import.meta.url), + ).rejects.toThrow() + + expect(storeCancelBulkOperation).not.toHaveBeenCalled() + }) + + test('requires --id flag', async () => { + await expect( + StoreBulkCancel.run(['--store', 'test-store.myshopify.com'], import.meta.url), + ).rejects.toThrow() + + expect(storeCancelBulkOperation).not.toHaveBeenCalled() + }) + + test('calls storeCancelBulkOperation with correct arguments', async () => { + vi.mocked(storeCancelBulkOperation).mockResolvedValue() + + await StoreBulkCancel.run( + ['--store', 'test-store.myshopify.com', '--id', '123'], + import.meta.url, + ) + + expect(storeCancelBulkOperation).toHaveBeenCalledWith({ + storeFqdn: 'test-store.myshopify.com', + operationId: 'gid://shopify/BulkOperation/123', + }) + }) + + test('accepts full GID format for --id', async () => { + vi.mocked(storeCancelBulkOperation).mockResolvedValue() + + await StoreBulkCancel.run( + ['--store', 'test-store.myshopify.com', '--id', 'gid://shopify/BulkOperation/456'], + import.meta.url, + ) + + expect(storeCancelBulkOperation).toHaveBeenCalledWith({ + storeFqdn: 'test-store.myshopify.com', + operationId: 'gid://shopify/BulkOperation/456', + }) + }) +}) diff --git a/packages/app/src/cli/commands/store/bulk/cancel.ts b/packages/app/src/cli/commands/store/bulk/cancel.ts new file mode 100644 index 00000000000..6671cab5494 --- /dev/null +++ b/packages/app/src/cli/commands/store/bulk/cancel.ts @@ -0,0 +1,37 @@ +import {storeCancelBulkOperation} from '../../../services/store-bulk-cancel-operation.js' +import {normalizeBulkOperationId} from '../../../services/bulk-operations/bulk-operation-status.js' +import {Flags} from '@oclif/core' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import BaseCommand from '@shopify/cli-kit/node/base-command' + +export default class StoreBulkCancel extends BaseCommand { + static summary = 'Cancel a bulk operation on a store.' + + static description = 'Cancels a running bulk operation by ID, authenticated as the current user.' + + static flags = { + ...globalFlags, + id: Flags.string({ + description: 'The bulk operation ID to cancel (numeric ID or full GID).', + env: 'SHOPIFY_FLAG_ID', + required: true, + }), + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain of the store.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + required: true, + }), + } + + async run(): Promise { + const {flags} = await this.parse(StoreBulkCancel) + + await storeCancelBulkOperation({ + storeFqdn: flags.store, + operationId: normalizeBulkOperationId(flags.id), + }) + } +} diff --git a/packages/app/src/cli/commands/store/bulk/execute.test.ts b/packages/app/src/cli/commands/store/bulk/execute.test.ts new file mode 100644 index 00000000000..d454759e419 --- /dev/null +++ b/packages/app/src/cli/commands/store/bulk/execute.test.ts @@ -0,0 +1,99 @@ +import StoreBulkExecute from './execute.js' +import {storeExecuteBulkOperation} from '../../../services/store-bulk-execute-operation.js' +import {loadQuery} from '../../../utilities/execute-command-helpers.js' +import {describe, expect, test, vi} from 'vitest' + +vi.mock('../../../services/store-bulk-execute-operation.js') +vi.mock('../../../utilities/execute-command-helpers.js') + +describe('store bulk execute command', () => { + test('requires --store flag', async () => { + vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }') + vi.mocked(storeExecuteBulkOperation).mockResolvedValue() + + await expect( + StoreBulkExecute.run(['--query', 'query { shop { name } }'], import.meta.url), + ).rejects.toThrow() + + expect(storeExecuteBulkOperation).not.toHaveBeenCalled() + }) + + test('calls storeExecuteBulkOperation with correct arguments', async () => { + vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }') + vi.mocked(storeExecuteBulkOperation).mockResolvedValue() + + await StoreBulkExecute.run( + ['--store', 'test-store.myshopify.com', '--query', 'query { shop { name } }'], + import.meta.url, + ) + + expect(loadQuery).toHaveBeenCalledWith( + expect.objectContaining({query: 'query { shop { name } }'}), + ) + expect(storeExecuteBulkOperation).toHaveBeenCalledWith( + expect.objectContaining({ + storeFqdn: 'test-store.myshopify.com', + query: 'query { shop { name } }', + watch: false, + }), + ) + }) + + test('passes version flag when provided', async () => { + vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }') + vi.mocked(storeExecuteBulkOperation).mockResolvedValue() + + await StoreBulkExecute.run( + ['--store', 'test-store.myshopify.com', '--query', 'query { shop { name } }', '--version', '2024-01'], + import.meta.url, + ) + + expect(storeExecuteBulkOperation).toHaveBeenCalledWith( + expect.objectContaining({ + version: '2024-01', + }), + ) + }) + + test('passes watch and output-file flags when provided', async () => { + vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }') + vi.mocked(storeExecuteBulkOperation).mockResolvedValue() + + await StoreBulkExecute.run( + [ + '--store', 'test-store.myshopify.com', + '--query', 'query { shop { name } }', + '--watch', + '--output-file', '/tmp/out.jsonl', + ], + import.meta.url, + ) + + expect(storeExecuteBulkOperation).toHaveBeenCalledWith( + expect.objectContaining({ + watch: true, + outputFile: '/tmp/out.jsonl', + }), + ) + }) + + test('passes variables flag when provided', async () => { + vi.mocked(loadQuery).mockResolvedValue('mutation { productCreate { product { id } } }') + vi.mocked(storeExecuteBulkOperation).mockResolvedValue() + + await StoreBulkExecute.run( + [ + '--store', 'test-store.myshopify.com', + '--query', 'mutation { productCreate { product { id } } }', + '--variables', '{"input": {"title": "test"}}', + ], + import.meta.url, + ) + + expect(storeExecuteBulkOperation).toHaveBeenCalledWith( + expect.objectContaining({ + variables: ['{"input": {"title": "test"}}'], + }), + ) + }) +}) diff --git a/packages/app/src/cli/commands/store/bulk/execute.ts b/packages/app/src/cli/commands/store/bulk/execute.ts new file mode 100644 index 00000000000..d83da17d89f --- /dev/null +++ b/packages/app/src/cli/commands/store/bulk/execute.ts @@ -0,0 +1,38 @@ +import {storeBulkOperationFlags} from '../../../flags.js' +import {storeExecuteBulkOperation} from '../../../services/store-bulk-execute-operation.js' +import {loadQuery} from '../../../utilities/execute-command-helpers.js' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import BaseCommand from '@shopify/cli-kit/node/base-command' + +export default class StoreBulkExecute extends BaseCommand { + static summary = 'Execute bulk operations against a store.' + + static descriptionWithMarkdown = `Executes an Admin API GraphQL query or mutation on the specified store as a bulk operation, authenticated as the current user. + + Unlike [\`app bulk execute\`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute), this command does not require an app to be linked or installed on the target store. + + Bulk operations allow you to process large amounts of data asynchronously. Learn more about [bulk query operations](https://shopify.dev/docs/api/usage/bulk-operations/queries) and [bulk mutation operations](https://shopify.dev/docs/api/usage/bulk-operations/imports). + + Use [\`store bulk status\`](https://shopify.dev/docs/api/shopify-cli/store/store-bulk-status) to check the status of your bulk operations.` + + static description = this.descriptionWithoutMarkdown() + + static flags = { + ...globalFlags, + ...storeBulkOperationFlags, + } + + async run(): Promise { + const {flags} = await this.parse(StoreBulkExecute) + const query = await loadQuery(flags) + await storeExecuteBulkOperation({ + storeFqdn: flags.store, + query, + variables: flags.variables, + variableFile: flags['variable-file'], + watch: flags.watch ?? false, + outputFile: flags['output-file'], + ...(flags.version && {version: flags.version}), + }) + } +} diff --git a/packages/app/src/cli/commands/store/bulk/status.test.ts b/packages/app/src/cli/commands/store/bulk/status.test.ts new file mode 100644 index 00000000000..06b3abdadb8 --- /dev/null +++ b/packages/app/src/cli/commands/store/bulk/status.test.ts @@ -0,0 +1,55 @@ +import StoreBulkStatus from './status.js' +import {storeGetBulkOperationStatus, storeListBulkOperations} from '../../../services/store-bulk-operation-status.js' +import {describe, expect, test, vi} from 'vitest' + +vi.mock('../../../services/store-bulk-operation-status.js') + +describe('store bulk status command', () => { + test('requires --store flag', async () => { + await expect(StoreBulkStatus.run([], import.meta.url)).rejects.toThrow() + + expect(storeGetBulkOperationStatus).not.toHaveBeenCalled() + expect(storeListBulkOperations).not.toHaveBeenCalled() + }) + + test('calls storeGetBulkOperationStatus when --id is provided', async () => { + vi.mocked(storeGetBulkOperationStatus).mockResolvedValue() + + await StoreBulkStatus.run( + ['--store', 'test-store.myshopify.com', '--id', '123'], + import.meta.url, + ) + + expect(storeGetBulkOperationStatus).toHaveBeenCalledWith({ + storeFqdn: 'test-store.myshopify.com', + operationId: 'gid://shopify/BulkOperation/123', + }) + }) + + test('calls storeListBulkOperations when --id is not provided', async () => { + vi.mocked(storeListBulkOperations).mockResolvedValue() + + await StoreBulkStatus.run( + ['--store', 'test-store.myshopify.com'], + import.meta.url, + ) + + expect(storeListBulkOperations).toHaveBeenCalledWith({ + storeFqdn: 'test-store.myshopify.com', + }) + }) + + test('accepts full GID format for --id', async () => { + vi.mocked(storeGetBulkOperationStatus).mockResolvedValue() + + await StoreBulkStatus.run( + ['--store', 'test-store.myshopify.com', '--id', 'gid://shopify/BulkOperation/456'], + import.meta.url, + ) + + expect(storeGetBulkOperationStatus).toHaveBeenCalledWith({ + storeFqdn: 'test-store.myshopify.com', + operationId: 'gid://shopify/BulkOperation/456', + }) + }) +}) diff --git a/packages/app/src/cli/commands/store/bulk/status.ts b/packages/app/src/cli/commands/store/bulk/status.ts new file mode 100644 index 00000000000..44f50a5cb13 --- /dev/null +++ b/packages/app/src/cli/commands/store/bulk/status.ts @@ -0,0 +1,52 @@ +import { + storeGetBulkOperationStatus, + storeListBulkOperations, +} from '../../../services/store-bulk-operation-status.js' +import {normalizeBulkOperationId} from '../../../services/bulk-operations/bulk-operation-status.js' +import {Flags} from '@oclif/core' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' +import BaseCommand from '@shopify/cli-kit/node/base-command' + +export default class StoreBulkStatus extends BaseCommand { + static summary = 'Check the status of bulk operations on a store.' + + static descriptionWithMarkdown = `Check the status of a specific bulk operation by ID, or list all bulk operations on this store in the last 7 days. + + Unlike [\`app bulk status\`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status), this command does not require an app to be linked or installed on the target store. + + Use [\`store bulk execute\`](https://shopify.dev/docs/api/shopify-cli/store/store-bulk-execute) to start a new bulk operation.` + + static description = this.descriptionWithoutMarkdown() + + static flags = { + ...globalFlags, + id: Flags.string({ + description: + 'The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations on this store in the last 7 days.', + env: 'SHOPIFY_FLAG_ID', + }), + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain of the store.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + required: true, + }), + } + + async run(): Promise { + const {flags} = await this.parse(StoreBulkStatus) + + if (flags.id) { + await storeGetBulkOperationStatus({ + storeFqdn: flags.store, + operationId: normalizeBulkOperationId(flags.id), + }) + } else { + await storeListBulkOperations({ + storeFqdn: flags.store, + }) + } + } +} diff --git a/packages/app/src/cli/flags.ts b/packages/app/src/cli/flags.ts index 55d8525c5dc..4955fe2039f 100644 --- a/packages/app/src/cli/flags.ts +++ b/packages/app/src/cli/flags.ts @@ -129,6 +129,58 @@ export const storeOperationFlags = { }), } +export const storeBulkOperationFlags = { + query: Flags.string({ + char: 'q', + description: 'The GraphQL query or mutation to run as a bulk operation.', + env: 'SHOPIFY_FLAG_QUERY', + required: false, + 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), + exactlyOne: ['query', 'query-file'], + }), + variables: Flags.string({ + char: 'v', + description: + 'The values for any GraphQL variables in your mutation, in JSON format. Can be specified multiple times.', + env: 'SHOPIFY_FLAG_VARIABLES', + multiple: true, + exclusive: ['variable-file'], + }), + 'variable-file': Flags.string({ + description: + "Path to a file containing GraphQL variables in JSONL format (one JSON object per line). Can't be used with --variables.", + env: 'SHOPIFY_FLAG_VARIABLE_FILE', + parse: async (input) => resolvePath(input), + exclusive: ['variables'], + }), + store: Flags.string({ + char: 's', + description: 'The myshopify.com domain of the store to execute against.', + env: 'SHOPIFY_FLAG_STORE', + parse: async (input) => normalizeStoreFqdn(input), + required: true, + }), + watch: Flags.boolean({ + description: 'Wait for bulk operation results before exiting. Defaults to false.', + env: 'SHOPIFY_FLAG_WATCH', + }), + 'output-file': Flags.string({ + description: + 'The file path where results should be written if --watch is specified. If not specified, results will be written to STDOUT.', + env: 'SHOPIFY_FLAG_OUTPUT_FILE', + dependsOn: ['watch'], + }), + version: Flags.string({ + description: 'The API version to use for the bulk operation. If not specified, uses the latest stable version.', + env: 'SHOPIFY_FLAG_VERSION', + }), +} + export const operationFlags = { query: Flags.string({ char: 'q', diff --git a/packages/app/src/cli/index.ts b/packages/app/src/cli/index.ts index ba206c0b4df..823b90eca3e 100644 --- a/packages/app/src/cli/index.ts +++ b/packages/app/src/cli/index.ts @@ -38,6 +38,9 @@ import FunctionInfo from './commands/app/function/info.js' import ImportCustomDataDefinitions from './commands/app/import-custom-data-definitions.js' import OrganizationList from './commands/organization/list.js' import StoreExecute from './commands/store/execute.js' +import StoreBulkExecute from './commands/store/bulk/execute.js' +import StoreBulkStatus from './commands/store/bulk/status.js' +import StoreBulkCancel from './commands/store/bulk/cancel.js' import BaseCommand from '@shopify/cli-kit/node/base-command' /** @@ -80,6 +83,9 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin 'demo:watcher': DemoWatcher, 'organization:list': OrganizationList, 'store:execute': StoreExecute, + 'store:bulk:execute': StoreBulkExecute, + 'store:bulk:status': StoreBulkStatus, + 'store:bulk:cancel': StoreBulkCancel, } export const AppSensitiveMetadataHook = gatherSensitiveMetadata diff --git a/packages/app/src/cli/services/graphql/common.ts b/packages/app/src/cli/services/graphql/common.ts index 12d237f9e1d..763df163cc0 100644 --- a/packages/app/src/cli/services/graphql/common.ts +++ b/packages/app/src/cli/services/graphql/common.ts @@ -115,6 +115,24 @@ export function formatOperationInfo(options: { return items } +/** + * Creates formatted info list items for store GraphQL operations (no org/app context). + * + * @param options - The operation context information + * @returns Array of formatted strings for display + */ +export function formatStoreOperationInfo(options: {storeFqdn: string; version?: string}): string[] { + const {storeFqdn, version} = options + + const items = [`Store: ${storeFqdn}`] + + if (version) { + items.push(`API version: ${version}`) + } + + return items +} + /** * Checks if a GraphQL operation is a mutation. * diff --git a/packages/app/src/cli/services/store-bulk-cancel-operation.ts b/packages/app/src/cli/services/store-bulk-cancel-operation.ts new file mode 100644 index 00000000000..50ae49deedb --- /dev/null +++ b/packages/app/src/cli/services/store-bulk-cancel-operation.ts @@ -0,0 +1,74 @@ +import {renderBulkOperationUserErrors, formatBulkOperationCancellationResult} from './bulk-operations/format-bulk-operation-status.js' +import { + BulkOperationCancel, + BulkOperationCancelMutation, + BulkOperationCancelMutationVariables, +} from '../api/graphql/bulk-operations/generated/bulk-operation-cancel.js' +import {formatStoreOperationInfo} from './graphql/common.js' +import {renderInfo, renderError, renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' +import {outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session' +import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin' + +const API_VERSION = '2026-01' + +interface StoreCancelBulkOperationOptions { + storeFqdn: string + operationId: string +} + +export async function storeCancelBulkOperation(options: StoreCancelBulkOperationOptions): Promise { + const {storeFqdn, operationId} = options + + renderInfo({ + headline: 'Canceling bulk operation.', + body: [ + { + list: { + items: [`ID: ${operationId}`, ...formatStoreOperationInfo({storeFqdn})], + }, + }, + ], + }) + + const adminSession = await ensureAuthenticatedAdmin(storeFqdn) + + const response = await adminRequestDoc({ + query: BulkOperationCancel, + session: adminSession, + variables: {id: operationId}, + version: API_VERSION, + }) + + if (response.bulkOperationCancel?.userErrors?.length) { + renderBulkOperationUserErrors(response.bulkOperationCancel.userErrors, 'Failed to cancel bulk operation.') + return + } + + const operation = response.bulkOperationCancel?.bulkOperation + if (operation) { + const result = formatBulkOperationCancellationResult(operation) + const renderOptions = { + headline: result.headline, + ...(result.body && {body: result.body}), + ...(result.customSections && {customSections: result.customSections}), + } + + switch (result.renderType) { + case 'success': + renderSuccess(renderOptions) + break + case 'warning': + renderWarning(renderOptions) + break + case 'info': + renderInfo(renderOptions) + break + } + } else { + renderError({ + headline: 'Bulk operation not found or could not be canceled.', + body: outputContent`ID: ${outputToken.yellow(operationId)}`.value, + }) + } +} diff --git a/packages/app/src/cli/services/store-bulk-execute-operation.ts b/packages/app/src/cli/services/store-bulk-execute-operation.ts new file mode 100644 index 00000000000..82762dfb6f4 --- /dev/null +++ b/packages/app/src/cli/services/store-bulk-execute-operation.ts @@ -0,0 +1,236 @@ +import {runBulkOperationQuery} from './bulk-operations/run-query.js' +import {runBulkOperationMutation} from './bulk-operations/run-mutation.js' +import {watchBulkOperation, shortBulkOperationPoll, type BulkOperation} from './bulk-operations/watch-bulk-operation.js' +import {formatBulkOperationStatus} from './bulk-operations/format-bulk-operation-status.js' +import {downloadBulkOperationResults} from './bulk-operations/download-bulk-operation-results.js' +import {extractBulkOperationId} from './bulk-operations/bulk-operation-status.js' +import {BULK_OPERATIONS_MIN_API_VERSION} from './bulk-operations/constants.js' +import {resolveApiVersion, formatStoreOperationInfo, isMutation} from './graphql/common.js' +import { + renderSuccess, + renderInfo, + renderError, + renderWarning, + renderSingleTask, + TokenItem, +} from '@shopify/cli-kit/node/ui' +import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output' +import {AbortError, BugError} from '@shopify/cli-kit/node/error' +import {AbortController} from '@shopify/cli-kit/node/abort' +import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session' +import {readFile, writeFile, fileExists} from '@shopify/cli-kit/node/fs' + +interface StoreExecuteBulkOperationInput { + storeFqdn: string + query: string + variables?: string[] + variableFile?: string + watch?: boolean + outputFile?: string + version?: string +} + +async function parseVariablesToJsonl(variables?: string[], variableFile?: string): Promise { + if (variables) { + return variables.join('\n') + } 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.`, + ) + } + return readFile(variableFile, {encoding: 'utf8'}) + } else { + return undefined + } +} + +export async function storeExecuteBulkOperation(input: StoreExecuteBulkOperationInput): Promise { + const {storeFqdn, query, variables, variableFile, outputFile, watch = false, version: userSpecifiedVersion} = input + + const {adminSession, version} = await renderSingleTask({ + title: outputContent`Authenticating`, + task: async () => { + const adminSession = await ensureAuthenticatedAdmin(storeFqdn) + const version = await resolveApiVersion({ + adminSession, + userSpecifiedVersion, + minimumDefaultVersion: BULK_OPERATIONS_MIN_API_VERSION, + }) + return {adminSession, version} + }, + renderOptions: {stdout: process.stderr}, + }) + + const variablesJsonl = await parseVariablesToJsonl(variables, variableFile) + + validateBulkOperationVariables(query, variablesJsonl) + + renderInfo({ + headline: 'Starting bulk operation.', + body: [ + { + list: { + items: formatStoreOperationInfo({storeFqdn, version}), + }, + }, + ], + }) + + const bulkOperationResponse = isMutation(query) + ? await runBulkOperationMutation({adminSession, query, variablesJsonl, version}) + : await runBulkOperationQuery({adminSession, query, version}) + + if (bulkOperationResponse?.userErrors?.length) { + renderError({ + headline: 'Error creating bulk operation.', + body: { + list: { + items: bulkOperationResponse.userErrors.map((error) => + error.field ? `${error.field.join('.')}: ${error.message}` : error.message, + ), + }, + }, + }) + return + } + + const createdOperation = bulkOperationResponse?.bulkOperation + if (createdOperation) { + if (watch) { + const abortController = new AbortController() + const operation = await watchBulkOperation(adminSession, createdOperation.id, abortController.signal, () => + abortController.abort(), + ) + + if (abortController.signal.aborted) { + renderInfo({ + headline: `Bulk operation ${operation.id} is still running in the background.`, + body: statusCommandHelpMessage(operation.id), + }) + } else { + await renderBulkOperationResult(operation, outputFile) + } + } else { + const operation = await shortBulkOperationPoll(adminSession, createdOperation.id) + const errorStatuses = ['FAILED', 'CANCELED', 'EXPIRED'] + if (errorStatuses.includes(operation.status)) { + await renderBulkOperationResult(operation, outputFile) + } else { + renderSuccess({ + headline: 'Bulk operation is running.', + body: statusCommandHelpMessage(operation.id), + customSections: [{body: [{list: {items: [outputContent`ID: ${outputToken.cyan(operation.id)}`.value]}}]}], + }) + } + } + } else { + renderWarning({ + headline: 'Bulk operation not created successfully.', + body: 'This is an unexpected error. Please try again later.', + }) + throw new BugError('Bulk operation response returned null with no error message.') + } +} + +async function renderBulkOperationResult(operation: BulkOperation, outputFile?: string): Promise { + const headline = formatBulkOperationStatus(operation).value + const items = [ + outputContent`ID: ${outputToken.cyan(operation.id)}`.value, + outputContent`Status: ${outputToken.yellow(operation.status)}`.value, + outputContent`Created at: ${outputToken.gray(String(operation.createdAt))}`.value, + ...(operation.completedAt + ? [outputContent`Completed at: ${outputToken.gray(String(operation.completedAt))}`.value] + : []), + ] + + const customSections = [{body: [{list: {items}}]}] + + switch (operation.status) { + case 'CREATED': + renderSuccess({ + headline: 'Bulk operation started.', + body: statusCommandHelpMessage(operation.id), + customSections, + }) + break + case 'RUNNING': + renderSuccess({ + headline: 'Bulk operation is running.', + body: statusCommandHelpMessage(operation.id), + customSections, + }) + break + case 'COMPLETED': + if (operation.url) { + const results = await downloadBulkOperationResults(operation.url) + const hasUserErrors = resultsContainUserErrors(results) + + if (outputFile) { + await writeFile(outputFile, results) + } else { + outputResult(results) + } + + if (hasUserErrors) { + renderWarning({ + headline: 'Bulk operation completed with errors.', + body: outputFile + ? `Results written to ${outputFile}. Check file for error details.` + : 'Check results for error details.', + customSections, + }) + } else { + renderSuccess({ + headline, + body: outputFile ? [`Results written to ${outputFile}`] : undefined, + customSections, + }) + } + } else { + renderSuccess({headline, customSections}) + } + break + default: + renderError({headline, customSections}) + break + } +} + +function resultsContainUserErrors(results: string): boolean { + const lines = results.trim().split('\n') + + return lines.some((line) => { + const parsed = JSON.parse(line) + if (!parsed.data) return false + const result = Object.values(parsed.data)[0] as {userErrors?: unknown[]} | undefined + return result?.userErrors !== undefined && result.userErrors.length > 0 + }) +} + +function validateBulkOperationVariables(graphqlOperation: string, variablesJsonl?: string): void { + if (isMutation(graphqlOperation) && !variablesJsonl) { + throw new AbortError( + outputContent`Bulk mutations require variables. Provide a JSONL file with ${outputToken.yellow( + '--variable-file', + )} or individual JSON objects with ${outputToken.yellow('--variables')}.`, + ) + } + + if (!isMutation(graphqlOperation) && variablesJsonl) { + throw new AbortError( + outputContent`The ${outputToken.yellow('--variables')} and ${outputToken.yellow( + '--variable-file', + )} flags can only be used with mutations, not queries.`, + ) + } +} + +function statusCommandHelpMessage(operationId: string): TokenItem { + return [ + 'Monitor its progress with:\n', + {command: `shopify store bulk status --id=${extractBulkOperationId(operationId)}`}, + ] +} diff --git a/packages/app/src/cli/services/store-bulk-operation-status.ts b/packages/app/src/cli/services/store-bulk-operation-status.ts new file mode 100644 index 00000000000..df41d773f81 --- /dev/null +++ b/packages/app/src/cli/services/store-bulk-operation-status.ts @@ -0,0 +1,170 @@ +import {BulkOperation} from './bulk-operations/watch-bulk-operation.js' +import {formatBulkOperationStatus} from './bulk-operations/format-bulk-operation-status.js' +import {BULK_OPERATIONS_MIN_API_VERSION} from './bulk-operations/constants.js' +import {extractBulkOperationId} from './bulk-operations/bulk-operation-status.js' +import { + GetBulkOperationById, + GetBulkOperationByIdQuery, +} from '../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js' +import {formatStoreOperationInfo, resolveApiVersion} from './graphql/common.js' +import { + ListBulkOperations, + ListBulkOperationsQuery, + ListBulkOperationsQueryVariables, +} from '../api/graphql/bulk-operations/generated/list-bulk-operations.js' +import {renderInfo, renderSuccess, renderError, renderTable} from '@shopify/cli-kit/node/ui' +import {outputContent, outputToken, outputNewline} from '@shopify/cli-kit/node/output' +import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session' +import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin' +import {timeAgo, formatDate} from '@shopify/cli-kit/common/string' +import colors from '@shopify/cli-kit/node/colors' + +interface StoreGetBulkOperationStatusOptions { + storeFqdn: string + operationId: string +} + +interface StoreListBulkOperationsOptions { + storeFqdn: string +} + +export async function storeGetBulkOperationStatus(options: StoreGetBulkOperationStatusOptions): Promise { + const {storeFqdn, operationId} = options + + renderInfo({ + headline: 'Checking bulk operation status.', + body: [ + { + list: { + items: formatStoreOperationInfo({storeFqdn}), + }, + }, + ], + }) + + const adminSession = await ensureAuthenticatedAdmin(storeFqdn) + + const response = await adminRequestDoc({ + query: GetBulkOperationById, + session: adminSession, + variables: {id: operationId}, + version: await resolveApiVersion({ + adminSession, + minimumDefaultVersion: BULK_OPERATIONS_MIN_API_VERSION, + }), + }) + + if (response.bulkOperation) { + renderBulkOperationStatus(response.bulkOperation) + } else { + renderError({ + headline: 'Bulk operation not found.', + body: outputContent`ID: ${outputToken.yellow(operationId)}`.value, + }) + } +} + +export async function storeListBulkOperations(options: StoreListBulkOperationsOptions): Promise { + const {storeFqdn} = options + + renderInfo({ + headline: 'Listing bulk operations.', + body: [ + { + list: { + items: formatStoreOperationInfo({storeFqdn}), + }, + }, + ], + }) + + const adminSession = await ensureAuthenticatedAdmin(storeFqdn) + + const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] + + const response = await adminRequestDoc({ + query: ListBulkOperations, + session: adminSession, + variables: { + query: `created_at:>=${sevenDaysAgo}`, + first: 100, + sortKey: 'CREATED_AT', + }, + version: await resolveApiVersion({ + adminSession, + minimumDefaultVersion: BULK_OPERATIONS_MIN_API_VERSION, + }), + }) + + const operations = response.bulkOperations.nodes.map((operation) => ({ + id: extractBulkOperationId(operation.id), + status: formatStatus(operation.status), + count: formatCount(operation.objectCount as number), + dateCreated: formatDate(new Date(String(operation.createdAt))), + dateFinished: operation.completedAt ? formatDate(new Date(String(operation.completedAt))) : '', + results: downloadLink(operation.url ?? operation.partialDataUrl), + })) + + outputNewline() + + if (operations.length === 0) { + renderInfo({body: 'No bulk operations found in the last 7 days.'}) + } else { + renderTable({ + rows: operations, + columns: { + id: {header: 'ID', color: 'yellow'}, + status: {header: 'STATUS'}, + count: {header: 'COUNT'}, + dateCreated: {header: 'DATE CREATED', color: 'cyan'}, + dateFinished: {header: 'DATE FINISHED', color: 'cyan'}, + results: {header: 'RESULTS'}, + }, + }) + } + + outputNewline() +} + +function renderBulkOperationStatus(operation: BulkOperation): void { + const {id, status, createdAt, completedAt, url, partialDataUrl} = operation + const statusDescription = formatBulkOperationStatus(operation).value + const timeDifference = formatTimeDifference(createdAt, completedAt) + const operationInfo = outputContent`ID: ${outputToken.yellow(id)}\n${timeDifference}`.value + + if (status === 'COMPLETED') { + const downloadLink = url ? outputToken.link('Download results', url) : '' + renderSuccess({headline: statusDescription, body: outputContent`${operationInfo}\n${downloadLink}`.value}) + } else if (status === 'FAILED') { + const downloadLink = partialDataUrl ? outputToken.link('Download partial results', partialDataUrl) : '' + renderError({headline: statusDescription, body: outputContent`${operationInfo}\n${downloadLink}`.value}) + } else { + renderInfo({headline: statusDescription, body: operationInfo}) + } +} + +function formatTimeDifference(createdAt: unknown, completedAt?: unknown): string { + const now = new Date() + + if (completedAt) { + return `Finished ${timeAgo(new Date(String(completedAt)), now)}` + } else { + return `Started ${timeAgo(new Date(String(createdAt)), now)}` + } +} + +function formatStatus(status: string): string { + if (status === 'COMPLETED') return colors.green(status) + if (status === 'FAILED') return colors.red(status) + return colors.dim(status) +} + +function formatCount(count: number): string { + if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M` + if (count >= 1000) return `${(count / 1000).toFixed(1)}K` + return String(count) +} + +function downloadLink(downloadUrl: string | null | undefined): string { + return downloadUrl ? outputContent`${outputToken.link('download', downloadUrl)}`.value : '' +} diff --git a/packages/cli/README.md b/packages/cli/README.md index e9a9e2f798b..4877060da0d 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -74,6 +74,9 @@ * [`shopify plugins unlink [PLUGIN]`](#shopify-plugins-unlink-plugin) * [`shopify plugins update`](#shopify-plugins-update) * [`shopify search [query]`](#shopify-search-query) +* [`shopify store bulk cancel`](#shopify-store-bulk-cancel) +* [`shopify store bulk execute`](#shopify-store-bulk-execute) +* [`shopify store bulk status`](#shopify-store-bulk-status) * [`shopify store execute`](#shopify-store-execute) * [`shopify theme check`](#shopify-theme-check) * [`shopify theme console`](#shopify-theme-console) @@ -2064,6 +2067,98 @@ EXAMPLES shopify search "" ``` +## `shopify store bulk cancel` + +Cancel a bulk operation on a store. + +``` +USAGE + $ shopify store bulk cancel --id -s [--no-color] [--verbose] + +FLAGS + -s, --store= (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store. + --id= (required) [env: SHOPIFY_FLAG_ID] The bulk operation ID to cancel (numeric ID or full GID). + --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. + --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. + +DESCRIPTION + Cancel a bulk operation on a store. + + Cancels a running bulk operation by ID, authenticated as the current user. +``` + +## `shopify store bulk execute` + +Execute bulk operations against a store. + +``` +USAGE + $ shopify store bulk execute -s [--no-color] [--output-file --watch] [-q ] [--query-file + ] [--variable-file | -v ...] [--verbose] [--version ] + +FLAGS + -q, --query= [env: SHOPIFY_FLAG_QUERY] The GraphQL query or mutation to run as a bulk operation. + -s, --store= (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store to execute + against. + -v, --variables=... [env: SHOPIFY_FLAG_VARIABLES] The values for any GraphQL variables in your mutation, in + JSON format. Can be specified multiple times. + --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. + --output-file= [env: SHOPIFY_FLAG_OUTPUT_FILE] The file path where results should be written if --watch + is specified. If not specified, results will be written to STDOUT. + --query-file= [env: SHOPIFY_FLAG_QUERY_FILE] Path to a file containing the GraphQL query or mutation. + Can't be used with --query. + --variable-file= [env: SHOPIFY_FLAG_VARIABLE_FILE] Path to a file containing GraphQL variables in JSONL + format (one JSON object per line). Can't be used with --variables. + --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. + --version= [env: SHOPIFY_FLAG_VERSION] The API version to use for the bulk operation. If not + specified, uses the latest stable version. + --watch [env: SHOPIFY_FLAG_WATCH] Wait for bulk operation results before exiting. Defaults to + false. + +DESCRIPTION + Execute bulk operations against a store. + + Executes an Admin API GraphQL query or mutation on the specified store as a bulk operation, authenticated as the + current user. + + Unlike "`app bulk execute`" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute), this command does not + require an app to be linked or installed on the target store. + + Bulk operations allow you to process large amounts of data asynchronously. Learn more about "bulk query operations" + (https://shopify.dev/docs/api/usage/bulk-operations/queries) and "bulk mutation operations" + (https://shopify.dev/docs/api/usage/bulk-operations/imports). + + Use "`store bulk status`" (https://shopify.dev/docs/api/shopify-cli/store/store-bulk-status) to check the status of + your bulk operations. +``` + +## `shopify store bulk status` + +Check the status of bulk operations on a store. + +``` +USAGE + $ shopify store bulk status -s [--id ] [--no-color] [--verbose] + +FLAGS + -s, --store= (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store. + --id= [env: SHOPIFY_FLAG_ID] The bulk operation ID (numeric ID or full GID). If not provided, lists all + bulk operations on this store in the last 7 days. + --no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output. + --verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output. + +DESCRIPTION + Check the status of bulk operations on a store. + + Check the status of a specific bulk operation by ID, or list all bulk operations on this store in the last 7 days. + + Unlike "`app bulk status`" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status), this command does not + require an app to be linked or installed on the target store. + + Use "`store bulk execute`" (https://shopify.dev/docs/api/shopify-cli/store/store-bulk-execute) to start a new bulk + operation. +``` + ## `shopify store execute` Execute GraphQL queries and mutations against a store. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 3eba0829731..de768a46cd7 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -5757,6 +5757,230 @@ "strict": true, "usage": "search [query]" }, + "store:bulk:cancel": { + "aliases": [ + ], + "args": { + }, + "customPluginName": "@shopify/app", + "description": "Cancels a running bulk operation by ID, authenticated as the current user.", + "enableJsonFlag": false, + "flags": { + "id": { + "description": "The bulk operation ID to cancel (numeric ID or full GID).", + "env": "SHOPIFY_FLAG_ID", + "hasDynamicHelp": false, + "multiple": false, + "name": "id", + "required": true, + "type": "option" + }, + "no-color": { + "allowNo": false, + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "type": "boolean" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain of the store.", + "env": "SHOPIFY_FLAG_STORE", + "hasDynamicHelp": false, + "multiple": false, + "name": "store", + "required": true, + "type": "option" + }, + "verbose": { + "allowNo": false, + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [ + ], + "id": "store:bulk:cancel", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Cancel a bulk operation on a store." + }, + "store:bulk:execute": { + "aliases": [ + ], + "args": { + }, + "customPluginName": "@shopify/app", + "description": "Executes an Admin API GraphQL query or mutation on the specified store as a bulk operation, authenticated as the current user.\n\n Unlike \"`app bulk execute`\" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute), this command does not require an app to be linked or installed on the target store.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about \"bulk query operations\" (https://shopify.dev/docs/api/usage/bulk-operations/queries) and \"bulk mutation operations\" (https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use \"`store bulk status`\" (https://shopify.dev/docs/api/shopify-cli/store/store-bulk-status) to check the status of your bulk operations.", + "descriptionWithMarkdown": "Executes an Admin API GraphQL query or mutation on the specified store as a bulk operation, authenticated as the current user.\n\n Unlike [`app bulk execute`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-execute), this command does not require an app to be linked or installed on the target store.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about [bulk query operations](https://shopify.dev/docs/api/usage/bulk-operations/queries) and [bulk mutation operations](https://shopify.dev/docs/api/usage/bulk-operations/imports).\n\n Use [`store bulk status`](https://shopify.dev/docs/api/shopify-cli/store/store-bulk-status) to check the status of your bulk operations.", + "enableJsonFlag": false, + "flags": { + "no-color": { + "allowNo": false, + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "type": "boolean" + }, + "output-file": { + "dependsOn": [ + "watch" + ], + "description": "The file path where results should be written if --watch is specified. If not specified, results will be written to STDOUT.", + "env": "SHOPIFY_FLAG_OUTPUT_FILE", + "hasDynamicHelp": false, + "multiple": false, + "name": "output-file", + "type": "option" + }, + "query": { + "char": "q", + "description": "The GraphQL query or mutation to run as a bulk operation.", + "env": "SHOPIFY_FLAG_QUERY", + "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", + "hasDynamicHelp": false, + "multiple": false, + "name": "query-file", + "type": "option" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain of the store to execute against.", + "env": "SHOPIFY_FLAG_STORE", + "hasDynamicHelp": false, + "multiple": false, + "name": "store", + "required": true, + "type": "option" + }, + "variable-file": { + "description": "Path to a file containing GraphQL variables in JSONL format (one JSON object per line). 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 mutation, in JSON format. Can be specified multiple times.", + "env": "SHOPIFY_FLAG_VARIABLES", + "exclusive": [ + "variable-file" + ], + "hasDynamicHelp": false, + "multiple": true, + "name": "variables", + "type": "option" + }, + "verbose": { + "allowNo": false, + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "type": "boolean" + }, + "version": { + "description": "The API version to use for the bulk operation. If not specified, uses the latest stable version.", + "env": "SHOPIFY_FLAG_VERSION", + "hasDynamicHelp": false, + "multiple": false, + "name": "version", + "type": "option" + }, + "watch": { + "allowNo": false, + "description": "Wait for bulk operation results before exiting. Defaults to false.", + "env": "SHOPIFY_FLAG_WATCH", + "name": "watch", + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [ + ], + "id": "store:bulk:execute", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Execute bulk operations against a store." + }, + "store:bulk:status": { + "aliases": [ + ], + "args": { + }, + "customPluginName": "@shopify/app", + "description": "Check the status of a specific bulk operation by ID, or list all bulk operations on this store in the last 7 days.\n\n Unlike \"`app bulk status`\" (https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status), this command does not require an app to be linked or installed on the target store.\n\n Use \"`store bulk execute`\" (https://shopify.dev/docs/api/shopify-cli/store/store-bulk-execute) to start a new bulk operation.", + "descriptionWithMarkdown": "Check the status of a specific bulk operation by ID, or list all bulk operations on this store in the last 7 days.\n\n Unlike [`app bulk status`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status), this command does not require an app to be linked or installed on the target store.\n\n Use [`store bulk execute`](https://shopify.dev/docs/api/shopify-cli/store/store-bulk-execute) to start a new bulk operation.", + "enableJsonFlag": false, + "flags": { + "id": { + "description": "The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations on this store in the last 7 days.", + "env": "SHOPIFY_FLAG_ID", + "hasDynamicHelp": false, + "multiple": false, + "name": "id", + "type": "option" + }, + "no-color": { + "allowNo": false, + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "type": "boolean" + }, + "store": { + "char": "s", + "description": "The myshopify.com domain of the store.", + "env": "SHOPIFY_FLAG_STORE", + "hasDynamicHelp": false, + "multiple": false, + "name": "store", + "required": true, + "type": "option" + }, + "verbose": { + "allowNo": false, + "description": "Increase the verbosity of the output.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [ + ], + "id": "store:bulk:status", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Check the status of bulk operations on a store." + }, "store:execute": { "aliases": [ ], From bc83d286ee1e89c70e7905d152613d69db23082a Mon Sep 17 00:00:00 2001 From: Nick Wesselman <27013789+nickwesselman@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:23:33 -0400 Subject: [PATCH 2/2] Fix lint errors, import order, regenerate docs and snapshots Fix prettier formatting in test files, import ordering in service files, regenerate dev docs and E2E snapshots. Co-Authored-By: Claude Opus 4.6 --- .../interfaces/auth-login.interface.ts | 12 + .../generated/generated_docs_data.json | 432 +++++++++++++++++- .../cli/commands/store/bulk/cancel.test.ts | 13 +- .../cli/commands/store/bulk/execute.test.ts | 26 +- .../cli/commands/store/bulk/status.test.ts | 10 +- .../app/src/cli/commands/store/bulk/status.ts | 5 +- .../services/store-bulk-cancel-operation.ts | 7 +- .../services/store-bulk-operation-status.ts | 2 +- packages/e2e/data/snapshots/commands.txt | 4 + 9 files changed, 473 insertions(+), 38 deletions(-) diff --git a/docs-shopify.dev/commands/interfaces/auth-login.interface.ts b/docs-shopify.dev/commands/interfaces/auth-login.interface.ts index a392b6c8ed2..1a897864f4d 100644 --- a/docs-shopify.dev/commands/interfaces/auth-login.interface.ts +++ b/docs-shopify.dev/commands/interfaces/auth-login.interface.ts @@ -5,4 +5,16 @@ export interface authlogin { * @environment SHOPIFY_FLAG_AUTH_ALIAS */ '--alias '?: string + + /** + * Start the login flow without polling. Prints the auth URL and exits immediately. + * @environment SHOPIFY_FLAG_AUTH_NO_POLLING + */ + '--no-polling'?: '' + + /** + * Resume a previously started login flow. + * @environment SHOPIFY_FLAG_AUTH_RESUME + */ + '--resume'?: '' } diff --git a/docs-shopify.dev/generated/generated_docs_data.json b/docs-shopify.dev/generated/generated_docs_data.json index c618d989337..9eda1ba88e9 100644 --- a/docs-shopify.dev/generated/generated_docs_data.json +++ b/docs-shopify.dev/generated/generated_docs_data.json @@ -3451,9 +3451,27 @@ "description": "Alias of the session you want to login to.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_AUTH_ALIAS" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/auth-login.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--no-polling", + "value": "\"\"", + "description": "Start the login flow without polling. Prints the auth URL and exits immediately.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_AUTH_NO_POLLING" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/auth-login.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--resume", + "value": "\"\"", + "description": "Resume a previously started login flow.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_AUTH_RESUME" } ], - "value": "export interface authlogin {\n /**\n * Alias of the session you want to login to.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--alias '?: string\n}" + "value": "export interface authlogin {\n /**\n * Alias of the session you want to login to.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--alias '?: string\n\n /**\n * Start the login flow without polling. Prints the auth URL and exits immediately.\n * @environment SHOPIFY_FLAG_AUTH_NO_POLLING\n */\n '--no-polling'?: ''\n\n /**\n * Resume a previously started login flow.\n * @environment SHOPIFY_FLAG_AUTH_RESUME\n */\n '--resume'?: ''\n}" } } } @@ -3483,6 +3501,28 @@ "category": "general commands", "related": [] }, + { + "name": "auth whoami", + "description": "Displays the currently logged-in Shopify account.", + "overviewPreviewDescription": "Displays the currently logged-in Shopify account.", + "type": "command", + "isVisualComponent": false, + "defaultExample": { + "codeblock": { + "tabs": [ + { + "title": "auth whoami", + "code": "shopify auth whoami", + "language": "bash" + } + ], + "title": "auth whoami" + } + }, + "definitions": [], + "category": "general commands", + "related": [] + }, { "name": "commands", "description": "List all shopify commands.", @@ -5765,6 +5805,396 @@ "category": "general commands", "related": [] }, + { + "name": "store bulk cancel", + "description": "Cancels a running bulk operation by ID, authenticated as the current user.", + "overviewPreviewDescription": "Cancel a bulk operation on a store.", + "type": "command", + "isVisualComponent": false, + "defaultExample": { + "codeblock": { + "tabs": [ + { + "title": "store bulk cancel", + "code": "shopify store bulk cancel [flags]", + "language": "bash" + } + ], + "title": "store bulk cancel" + } + }, + "definitions": [ + { + "title": "Flags", + "description": "The following flags are available for the `store bulk cancel` command:", + "type": "storebulkcancel", + "typeDefinitions": { + "storebulkcancel": { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-cancel.interface.ts", + "name": "storebulkcancel", + "description": "", + "members": [ + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-cancel.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--id ", + "value": "string", + "description": "The bulk operation ID to cancel (numeric ID or full GID).", + "environmentValue": "SHOPIFY_FLAG_ID" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-cancel.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--no-color", + "value": "\"\"", + "description": "Disable color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_NO_COLOR" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-cancel.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--verbose", + "value": "\"\"", + "description": "Increase the verbosity of the output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VERBOSE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-cancel.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-s, --store ", + "value": "string", + "description": "The myshopify.com domain of the store.", + "environmentValue": "SHOPIFY_FLAG_STORE" + } + ], + "value": "export interface storebulkcancel {\n /**\n * The bulk operation ID to cancel (numeric ID or full GID).\n * @environment SHOPIFY_FLAG_ID\n */\n '--id ': string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The myshopify.com domain of the store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store ': string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + } + } + } + ], + "category": "store", + "related": [] + }, + { + "name": "store bulk execute", + "description": "Executes an Admin API GraphQL query or mutation on the specified store as a bulk operation, authenticated as the current user.\n\n Unlike [`app bulk execute`](/docs/api/shopify-cli/app/app-bulk-execute), this command does not require an app to be linked or installed on the target store.\n\n Bulk operations allow you to process large amounts of data asynchronously. Learn more about [bulk query operations](/docs/api/usage/bulk-operations/queries) and [bulk mutation operations](/docs/api/usage/bulk-operations/imports).\n\n Use [`store bulk status`](/docs/api/shopify-cli/store/store-bulk-status) to check the status of your bulk operations.", + "overviewPreviewDescription": "Execute bulk operations against a store.", + "type": "command", + "isVisualComponent": false, + "defaultExample": { + "codeblock": { + "tabs": [ + { + "title": "store bulk execute", + "code": "shopify store bulk execute [flags]", + "language": "bash" + } + ], + "title": "store bulk execute" + } + }, + "definitions": [ + { + "title": "Flags", + "description": "The following flags are available for the `store bulk execute` command:", + "type": "storebulkexecute", + "typeDefinitions": { + "storebulkexecute": { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "name": "storebulkexecute", + "description": "", + "members": [ + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--no-color", + "value": "\"\"", + "description": "Disable color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_NO_COLOR" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--output-file ", + "value": "string", + "description": "The file path where results should be written if --watch is specified. If not specified, results will be written to STDOUT.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_OUTPUT_FILE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--query-file ", + "value": "string", + "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_QUERY_FILE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--variable-file ", + "value": "string", + "description": "Path to a file containing GraphQL variables in JSONL format (one JSON object per line). Can't be used with --variables.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VARIABLE_FILE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--verbose", + "value": "\"\"", + "description": "Increase the verbosity of the output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VERBOSE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--version ", + "value": "string", + "description": "The API version to use for the bulk operation. If not specified, uses the latest stable version.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VERSION" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--watch", + "value": "\"\"", + "description": "Wait for bulk operation results before exiting. Defaults to false.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_WATCH" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-q, --query ", + "value": "string", + "description": "The GraphQL query or mutation to run as a bulk operation.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_QUERY" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-s, --store ", + "value": "string", + "description": "The myshopify.com domain of the store to execute against.", + "environmentValue": "SHOPIFY_FLAG_STORE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-v, --variables ", + "value": "string", + "description": "The values for any GraphQL variables in your mutation, in JSON format. Can be specified multiple times.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VARIABLES" + } + ], + "value": "export interface storebulkexecute {\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The file path where results should be written if --watch is specified. If not specified, results will be written to STDOUT.\n * @environment SHOPIFY_FLAG_OUTPUT_FILE\n */\n '--output-file '?: string\n\n /**\n * The GraphQL query or mutation to run as a bulk operation.\n * @environment SHOPIFY_FLAG_QUERY\n */\n '-q, --query '?: string\n\n /**\n * Path to a file containing the GraphQL query or mutation. Can't be used with --query.\n * @environment SHOPIFY_FLAG_QUERY_FILE\n */\n '--query-file '?: string\n\n /**\n * The myshopify.com domain of the store to execute against.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store ': string\n\n /**\n * Path to a file containing GraphQL variables in JSONL format (one JSON object per line). Can't be used with --variables.\n * @environment SHOPIFY_FLAG_VARIABLE_FILE\n */\n '--variable-file '?: string\n\n /**\n * The values for any GraphQL variables in your mutation, in JSON format. Can be specified multiple times.\n * @environment SHOPIFY_FLAG_VARIABLES\n */\n '-v, --variables '?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The API version to use for the bulk operation. If not specified, uses the latest stable version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version '?: string\n\n /**\n * Wait for bulk operation results before exiting. Defaults to false.\n * @environment SHOPIFY_FLAG_WATCH\n */\n '--watch'?: ''\n}" + } + } + } + ], + "category": "store", + "related": [] + }, + { + "name": "store bulk status", + "description": "Check the status of a specific bulk operation by ID, or list all bulk operations on this store in the last 7 days.\n\n Unlike [`app bulk status`](/docs/api/shopify-cli/app/app-bulk-status), this command does not require an app to be linked or installed on the target store.\n\n Use [`store bulk execute`](/docs/api/shopify-cli/store/store-bulk-execute) to start a new bulk operation.", + "overviewPreviewDescription": "Check the status of bulk operations on a store.", + "type": "command", + "isVisualComponent": false, + "defaultExample": { + "codeblock": { + "tabs": [ + { + "title": "store bulk status", + "code": "shopify store bulk status [flags]", + "language": "bash" + } + ], + "title": "store bulk status" + } + }, + "definitions": [ + { + "title": "Flags", + "description": "The following flags are available for the `store bulk status` command:", + "type": "storebulkstatus", + "typeDefinitions": { + "storebulkstatus": { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-status.interface.ts", + "name": "storebulkstatus", + "description": "", + "members": [ + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-status.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--id ", + "value": "string", + "description": "The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations on this store in the last 7 days.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_ID" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-status.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--no-color", + "value": "\"\"", + "description": "Disable color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_NO_COLOR" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-status.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--verbose", + "value": "\"\"", + "description": "Increase the verbosity of the output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VERBOSE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-bulk-status.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-s, --store ", + "value": "string", + "description": "The myshopify.com domain of the store.", + "environmentValue": "SHOPIFY_FLAG_STORE" + } + ], + "value": "export interface storebulkstatus {\n /**\n * The bulk operation ID (numeric ID or full GID). If not provided, lists all bulk operations on this store in the last 7 days.\n * @environment SHOPIFY_FLAG_ID\n */\n '--id '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The myshopify.com domain of the store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store ': string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + } + } + } + ], + "category": "store", + "related": [] + }, + { + "name": "store execute", + "description": "Executes an Admin API GraphQL query or mutation on the specified store, authenticated as the current user.\n\n Unlike [`app execute`](/docs/api/shopify-cli/app/app-execute), this command does not require an app to be linked or installed on the target store.", + "overviewPreviewDescription": "Execute GraphQL queries and mutations against a store.", + "type": "command", + "isVisualComponent": false, + "defaultExample": { + "codeblock": { + "tabs": [ + { + "title": "store execute", + "code": "shopify store execute [flags]", + "language": "bash" + } + ], + "title": "store execute" + } + }, + "definitions": [ + { + "title": "Flags", + "description": "The following flags are available for the `store execute` command:", + "type": "storeexecute", + "typeDefinitions": { + "storeexecute": { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "name": "storeexecute", + "description": "", + "members": [ + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--no-color", + "value": "\"\"", + "description": "Disable color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_NO_COLOR" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--output-file ", + "value": "string", + "description": "The file name where results should be written, instead of STDOUT.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_OUTPUT_FILE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--query-file ", + "value": "string", + "description": "Path to a file containing the GraphQL query or mutation. Can't be used with --query.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_QUERY_FILE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--variable-file ", + "value": "string", + "description": "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VARIABLE_FILE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--verbose", + "value": "\"\"", + "description": "Increase the verbosity of the output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VERBOSE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--version ", + "value": "string", + "description": "The API version to use for the query or mutation. Defaults to the latest stable version.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VERSION" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-q, --query ", + "value": "string", + "description": "The GraphQL query or mutation, as a string.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_QUERY" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-s, --store ", + "value": "string", + "description": "The myshopify.com domain of the store to execute against.", + "environmentValue": "SHOPIFY_FLAG_STORE" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/store-execute.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-v, --variables ", + "value": "string", + "description": "The values for any GraphQL variables in your query or mutation, in JSON format.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_VARIABLES" + } + ], + "value": "export interface storeexecute {\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The file name where results should be written, instead of STDOUT.\n * @environment SHOPIFY_FLAG_OUTPUT_FILE\n */\n '--output-file '?: string\n\n /**\n * The GraphQL query or mutation, as a string.\n * @environment SHOPIFY_FLAG_QUERY\n */\n '-q, --query '?: string\n\n /**\n * Path to a file containing the GraphQL query or mutation. Can't be used with --query.\n * @environment SHOPIFY_FLAG_QUERY_FILE\n */\n '--query-file '?: string\n\n /**\n * The myshopify.com domain of the store to execute against.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store ': string\n\n /**\n * Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.\n * @environment SHOPIFY_FLAG_VARIABLE_FILE\n */\n '--variable-file '?: string\n\n /**\n * The values for any GraphQL variables in your query or mutation, in JSON format.\n * @environment SHOPIFY_FLAG_VARIABLES\n */\n '-v, --variables '?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The API version to use for the query or mutation. Defaults to the latest stable version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version '?: string\n}" + } + } + } + ], + "category": "store", + "related": [] + }, { "name": "theme check", "description": "Calls and runs [Theme Check](/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. [Learn more about the checks that Theme Check runs.](/docs/themes/tools/theme-check/checks)", diff --git a/packages/app/src/cli/commands/store/bulk/cancel.test.ts b/packages/app/src/cli/commands/store/bulk/cancel.test.ts index 0377bf4ea5f..dcefd34730e 100644 --- a/packages/app/src/cli/commands/store/bulk/cancel.test.ts +++ b/packages/app/src/cli/commands/store/bulk/cancel.test.ts @@ -6,17 +6,13 @@ vi.mock('../../../services/store-bulk-cancel-operation.js') describe('store bulk cancel command', () => { test('requires --store flag', async () => { - await expect( - StoreBulkCancel.run(['--id', '123'], import.meta.url), - ).rejects.toThrow() + await expect(StoreBulkCancel.run(['--id', '123'], import.meta.url)).rejects.toThrow() expect(storeCancelBulkOperation).not.toHaveBeenCalled() }) test('requires --id flag', async () => { - await expect( - StoreBulkCancel.run(['--store', 'test-store.myshopify.com'], import.meta.url), - ).rejects.toThrow() + await expect(StoreBulkCancel.run(['--store', 'test-store.myshopify.com'], import.meta.url)).rejects.toThrow() expect(storeCancelBulkOperation).not.toHaveBeenCalled() }) @@ -24,10 +20,7 @@ describe('store bulk cancel command', () => { test('calls storeCancelBulkOperation with correct arguments', async () => { vi.mocked(storeCancelBulkOperation).mockResolvedValue() - await StoreBulkCancel.run( - ['--store', 'test-store.myshopify.com', '--id', '123'], - import.meta.url, - ) + await StoreBulkCancel.run(['--store', 'test-store.myshopify.com', '--id', '123'], import.meta.url) expect(storeCancelBulkOperation).toHaveBeenCalledWith({ storeFqdn: 'test-store.myshopify.com', diff --git a/packages/app/src/cli/commands/store/bulk/execute.test.ts b/packages/app/src/cli/commands/store/bulk/execute.test.ts index d454759e419..9c2fd4a20a7 100644 --- a/packages/app/src/cli/commands/store/bulk/execute.test.ts +++ b/packages/app/src/cli/commands/store/bulk/execute.test.ts @@ -11,9 +11,7 @@ describe('store bulk execute command', () => { vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }') vi.mocked(storeExecuteBulkOperation).mockResolvedValue() - await expect( - StoreBulkExecute.run(['--query', 'query { shop { name } }'], import.meta.url), - ).rejects.toThrow() + await expect(StoreBulkExecute.run(['--query', 'query { shop { name } }'], import.meta.url)).rejects.toThrow() expect(storeExecuteBulkOperation).not.toHaveBeenCalled() }) @@ -27,9 +25,7 @@ describe('store bulk execute command', () => { import.meta.url, ) - expect(loadQuery).toHaveBeenCalledWith( - expect.objectContaining({query: 'query { shop { name } }'}), - ) + expect(loadQuery).toHaveBeenCalledWith(expect.objectContaining({query: 'query { shop { name } }'})) expect(storeExecuteBulkOperation).toHaveBeenCalledWith( expect.objectContaining({ storeFqdn: 'test-store.myshopify.com', @@ -61,10 +57,13 @@ describe('store bulk execute command', () => { await StoreBulkExecute.run( [ - '--store', 'test-store.myshopify.com', - '--query', 'query { shop { name } }', + '--store', + 'test-store.myshopify.com', + '--query', + 'query { shop { name } }', '--watch', - '--output-file', '/tmp/out.jsonl', + '--output-file', + '/tmp/out.jsonl', ], import.meta.url, ) @@ -83,9 +82,12 @@ describe('store bulk execute command', () => { await StoreBulkExecute.run( [ - '--store', 'test-store.myshopify.com', - '--query', 'mutation { productCreate { product { id } } }', - '--variables', '{"input": {"title": "test"}}', + '--store', + 'test-store.myshopify.com', + '--query', + 'mutation { productCreate { product { id } } }', + '--variables', + '{"input": {"title": "test"}}', ], import.meta.url, ) diff --git a/packages/app/src/cli/commands/store/bulk/status.test.ts b/packages/app/src/cli/commands/store/bulk/status.test.ts index 06b3abdadb8..f9b0bbe1d80 100644 --- a/packages/app/src/cli/commands/store/bulk/status.test.ts +++ b/packages/app/src/cli/commands/store/bulk/status.test.ts @@ -15,10 +15,7 @@ describe('store bulk status command', () => { test('calls storeGetBulkOperationStatus when --id is provided', async () => { vi.mocked(storeGetBulkOperationStatus).mockResolvedValue() - await StoreBulkStatus.run( - ['--store', 'test-store.myshopify.com', '--id', '123'], - import.meta.url, - ) + await StoreBulkStatus.run(['--store', 'test-store.myshopify.com', '--id', '123'], import.meta.url) expect(storeGetBulkOperationStatus).toHaveBeenCalledWith({ storeFqdn: 'test-store.myshopify.com', @@ -29,10 +26,7 @@ describe('store bulk status command', () => { test('calls storeListBulkOperations when --id is not provided', async () => { vi.mocked(storeListBulkOperations).mockResolvedValue() - await StoreBulkStatus.run( - ['--store', 'test-store.myshopify.com'], - import.meta.url, - ) + await StoreBulkStatus.run(['--store', 'test-store.myshopify.com'], import.meta.url) expect(storeListBulkOperations).toHaveBeenCalledWith({ storeFqdn: 'test-store.myshopify.com', diff --git a/packages/app/src/cli/commands/store/bulk/status.ts b/packages/app/src/cli/commands/store/bulk/status.ts index 44f50a5cb13..e42a193faae 100644 --- a/packages/app/src/cli/commands/store/bulk/status.ts +++ b/packages/app/src/cli/commands/store/bulk/status.ts @@ -1,7 +1,4 @@ -import { - storeGetBulkOperationStatus, - storeListBulkOperations, -} from '../../../services/store-bulk-operation-status.js' +import {storeGetBulkOperationStatus, storeListBulkOperations} from '../../../services/store-bulk-operation-status.js' import {normalizeBulkOperationId} from '../../../services/bulk-operations/bulk-operation-status.js' import {Flags} from '@oclif/core' import {globalFlags} from '@shopify/cli-kit/node/cli' diff --git a/packages/app/src/cli/services/store-bulk-cancel-operation.ts b/packages/app/src/cli/services/store-bulk-cancel-operation.ts index 50ae49deedb..f2ac926ed24 100644 --- a/packages/app/src/cli/services/store-bulk-cancel-operation.ts +++ b/packages/app/src/cli/services/store-bulk-cancel-operation.ts @@ -1,10 +1,13 @@ -import {renderBulkOperationUserErrors, formatBulkOperationCancellationResult} from './bulk-operations/format-bulk-operation-status.js' +import { + renderBulkOperationUserErrors, + formatBulkOperationCancellationResult, +} from './bulk-operations/format-bulk-operation-status.js' +import {formatStoreOperationInfo} from './graphql/common.js' import { BulkOperationCancel, BulkOperationCancelMutation, BulkOperationCancelMutationVariables, } from '../api/graphql/bulk-operations/generated/bulk-operation-cancel.js' -import {formatStoreOperationInfo} from './graphql/common.js' import {renderInfo, renderError, renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' import {outputContent, outputToken} from '@shopify/cli-kit/node/output' import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session' diff --git a/packages/app/src/cli/services/store-bulk-operation-status.ts b/packages/app/src/cli/services/store-bulk-operation-status.ts index df41d773f81..7a045d8bb17 100644 --- a/packages/app/src/cli/services/store-bulk-operation-status.ts +++ b/packages/app/src/cli/services/store-bulk-operation-status.ts @@ -2,11 +2,11 @@ import {BulkOperation} from './bulk-operations/watch-bulk-operation.js' import {formatBulkOperationStatus} from './bulk-operations/format-bulk-operation-status.js' import {BULK_OPERATIONS_MIN_API_VERSION} from './bulk-operations/constants.js' import {extractBulkOperationId} from './bulk-operations/bulk-operation-status.js' +import {formatStoreOperationInfo, resolveApiVersion} from './graphql/common.js' import { GetBulkOperationById, GetBulkOperationByIdQuery, } from '../api/graphql/bulk-operations/generated/get-bulk-operation-by-id.js' -import {formatStoreOperationInfo, resolveApiVersion} from './graphql/common.js' import { ListBulkOperations, ListBulkOperationsQuery, diff --git a/packages/e2e/data/snapshots/commands.txt b/packages/e2e/data/snapshots/commands.txt index c6778b234fa..20c6b306e9d 100644 --- a/packages/e2e/data/snapshots/commands.txt +++ b/packages/e2e/data/snapshots/commands.txt @@ -90,6 +90,10 @@ │ └─ update ├─ search ├─ store +│ ├─ bulk +│ │ ├─ cancel +│ │ ├─ execute +│ │ └─ status │ └─ execute ├─ theme │ ├─ check