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
5 changes: 5 additions & 0 deletions .changeset/app-bulk-execute-json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli': minor
---

Add typed JSON output to app bulk execute.
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_APP_CONFIG"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-bulk-execute.interface.ts",
"syntaxKind": "PropertySignature",
"name": "-j, --json",
"value": "''",
"description": "Output the result as JSON. Automatically disables color output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_JSON"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-bulk-execute.interface.ts",
"syntaxKind": "PropertySignature",
Expand Down Expand Up @@ -379,7 +388,7 @@
"environmentValue": "SHOPIFY_FLAG_VARIABLES"
}
],
"value": "export interface appbulkexecute {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\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 <value>'?: string\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * The GraphQL query or mutation to run as a bulk operation.\n * @environment SHOPIFY_FLAG_QUERY\n */\n '-q, --query <value>'?: 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 <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * The store domain. Must be an existing dev store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: 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 <value>'?: 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 <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\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 <value>'?: string\n\n /**\n * Wait for bulk operation results before exiting. Defaults to false.\n * @environment SHOPIFY_FLAG_WATCH\n */\n '--watch'?: ''\n}"
"value": "export interface appbulkexecute {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\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 <value>'?: string\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * The GraphQL query or mutation to run as a bulk operation.\n * @environment SHOPIFY_FLAG_QUERY\n */\n '-q, --query <value>'?: 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 <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * The store domain. Must be an existing dev store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: 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 <value>'?: 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 <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\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 <value>'?: string\n\n /**\n * Wait for bulk operation results before exiting. Defaults to false.\n * @environment SHOPIFY_FLAG_WATCH\n */\n '--watch'?: ''\n}"
}
},
"appbulkstatus": {
Expand Down
38 changes: 26 additions & 12 deletions packages/app/src/cli/commands/app/bulk/execute.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BulkExecute from './execute.js'
import {executeBulkOperation} from '../../../services/bulk-operations/execute-bulk-operation.js'
import {renderExecuteBulkOperationResult} from '../../../services/bulk-operations/execute-result.js'
import {executeBulkOperation, prepareBulkOperation} from '../../../services/bulk-operations/execute-bulk-operation.js'
import {prepareExecuteContext} from '../../../utilities/execute-command-helpers.js'
import {
testAppLinked,
Expand All @@ -12,6 +13,8 @@ import {describe, expect, test, vi, beforeEach} from 'vitest'

vi.mock('../../../services/bulk-operations/execute-bulk-operation.js')
vi.mock('../../../utilities/execute-command-helpers.js')
vi.mock('../../../services/bulk-operations/execute-result.js')
vi.mock('../../../services/bulk-operations/progress.js')

describe('app bulk execute command', () => {
const app = testAppLinked()
Expand All @@ -33,33 +36,44 @@ describe('app bulk execute command', () => {
store,
query: 'query { shop { name } }',
})
vi.mocked(executeBulkOperation).mockResolvedValue()
vi.mocked(prepareBulkOperation).mockResolvedValue({
adminSession: {storeFqdn: store.shopDomain, token: 'token'},
version: '2026-01',
query: 'query { shop { name } }',
variablesJsonl: undefined,
watch: false,
})
vi.mocked(executeBulkOperation).mockResolvedValue({
query: 'query { shop { name } }',
operation: null,
userErrors: [],
watchAborted: false,
})
})

test('prepares execution context and calls executeBulkOperation', async () => {
// When
await BulkExecute.run(['--query', 'query { shop { name } }', '--store', 'shop.myshopify.com'])

// Then
expect(executeBulkOperation).toHaveBeenCalledWith(await vi.mocked(prepareBulkOperation).mock.results[0]!.value)
expect(prepareExecuteContext).toHaveBeenCalledWith(
expect.objectContaining({
query: 'query { shop { name } }',
store: 'shop.myshopify.com',
}),
)
expect(executeBulkOperation).toHaveBeenCalledWith({
organization,
expect(prepareBulkOperation).toHaveBeenCalledWith({
remoteApp,
store,
query: 'query { shop { name } }',
variables: undefined,
variableFile: undefined,
watch: false,
outputFile: undefined,
})
})

test('calls executeBulkOperation with variables flag', async () => {
test('passes input variables to preparation and output files to presentation', async () => {
// When
await BulkExecute.run([
'--query',
Expand All @@ -74,15 +88,17 @@ describe('app bulk execute command', () => {
])

// Then
expect(executeBulkOperation).toHaveBeenCalledWith({
organization,
expect(renderExecuteBulkOperationResult).toHaveBeenCalledWith(
{query: 'query { shop { name } }', operation: null, userErrors: [], watchAborted: false},
{format: 'text', watch: true, outputFile: 'output.json'},
)
expect(prepareBulkOperation).toHaveBeenCalledWith({
remoteApp,
store,
query: 'query { shop { name } }',
variables: ['{"key": "value"}'],
variableFile: undefined,
watch: true,
outputFile: 'output.json',
})
})

Expand All @@ -98,15 +114,13 @@ describe('app bulk execute command', () => {
])

// Then
expect(executeBulkOperation).toHaveBeenCalledWith({
organization,
expect(prepareBulkOperation).toHaveBeenCalledWith({
remoteApp,
store,
query: 'query { shop { name } }',
variables: undefined,
variableFile: expect.stringContaining('variables.jsonl'),
watch: false,
outputFile: undefined,
})
})
})
37 changes: 31 additions & 6 deletions packages/app/src/cli/commands/app/bulk/execute.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {appFlags, bulkOperationFlags} from '../../../flags.js'
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
import {executeBulkOperation} from '../../../services/bulk-operations/execute-bulk-operation.js'
import {executeBulkOperation, prepareBulkOperation} from '../../../services/bulk-operations/execute-bulk-operation.js'
import {executeBulkOperationJsonOutputSchema} from '../../../services/bulk-operations/types.js'
import {renderExecuteBulkOperationResult} from '../../../services/bulk-operations/execute-result.js'
import {logBulkOperationStart} from '../../../services/bulk-operations/progress.js'
import {prepareExecuteContext} from '../../../utilities/execute-command-helpers.js'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'

export default class BulkExecute extends AppLinkedCommand {
static summary = 'Execute bulk operations.'
Expand All @@ -11,32 +14,54 @@ export default class BulkExecute extends AppLinkedCommand {

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 [\`bulk status\`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status) to check the status of your bulk operations.`
Use [\`bulk status\`](https://shopify.dev/docs/api/shopify-cli/app/app-bulk-status) to check the status of your bulk operations.

With \`--watch\`, completed results are written as JSONL to stdout or \`--output-file\`. With \`--json\`, stdout contains one result object with operation details and downloaded JSONL in \`resultsJsonl\`. With \`--json --output-file\`, stdout contains only an absolute file receipt with \`path\` and \`format\`.`

static description = this.descriptionForHelp()

static flags = {
...globalFlags,
...jsonFlag,
...appFlags,
...bulkOperationFlags,
}

static get jsonOutputSchema() {
return executeBulkOperationJsonOutputSchema
}

async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(BulkExecute)

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

await executeBulkOperation({
organization: appContextResult.organization,
const format = flags.json ? 'json' : 'text'
const input = await prepareBulkOperation({
remoteApp: appContextResult.remoteApp,
store,
query,
variables: flags.variables,
variableFile: flags['variable-file'],
watch: flags.watch ?? false,
outputFile: flags['output-file'],
...(flags.version && {version: flags.version}),
})
logBulkOperationStart(
'Starting bulk operation.',
{
organization: appContextResult.organization,
remoteApp: appContextResult.remoteApp,
storeFqdn: store.shopDomain,
version: input.version,
},
format,
)
const result = await executeBulkOperation(input)
await renderExecuteBulkOperationResult(result, {
format,
watch: flags.watch ?? false,
outputFile: flags['output-file'],
})

return {app: appContextResult.app}
}
Expand Down
Loading
Loading