Skip to content
Open
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
12 changes: 12 additions & 0 deletions docs-shopify.dev/commands/interfaces/auth-login.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,16 @@ export interface authlogin {
* @environment SHOPIFY_FLAG_AUTH_ALIAS
*/
'--alias <value>'?: 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'?: ''
}
432 changes: 431 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data.json

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions packages/app/src/cli/commands/store/bulk/cancel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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',
})
})
})
37 changes: 37 additions & 0 deletions packages/app/src/cli/commands/store/bulk/cancel.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const {flags} = await this.parse(StoreBulkCancel)

await storeCancelBulkOperation({
storeFqdn: flags.store,
operationId: normalizeBulkOperationId(flags.id),
})
}
}
101 changes: 101 additions & 0 deletions packages/app/src/cli/commands/store/bulk/execute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
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"}}'],
}),
)
})
})
38 changes: 38 additions & 0 deletions packages/app/src/cli/commands/store/bulk/execute.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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}),
})
}
}
49 changes: 49 additions & 0 deletions packages/app/src/cli/commands/store/bulk/status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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',
})
})
})
49 changes: 49 additions & 0 deletions packages/app/src/cli/commands/store/bulk/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
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<void> {
const {flags} = await this.parse(StoreBulkStatus)

if (flags.id) {
await storeGetBulkOperationStatus({
storeFqdn: flags.store,
operationId: normalizeBulkOperationId(flags.id),
})
} else {
await storeListBulkOperations({
storeFqdn: flags.store,
})
}
}
}
52 changes: 52 additions & 0 deletions packages/app/src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading
Loading