Skip to content
Draft
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
68 changes: 68 additions & 0 deletions packages/app/src/cli/commands/store/execute.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import StoreExecute from './execute.js'
import {storeExecuteOperation} from '../../services/store-execute-operation.js'
import {loadQuery} from '../../utilities/execute-command-helpers.js'
import {describe, expect, test, vi} from 'vitest'

vi.mock('../../services/store-execute-operation.js')
vi.mock('../../utilities/execute-command-helpers.js')

describe('store execute command', () => {
test('requires --store flag', async () => {
vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }')
vi.mocked(storeExecuteOperation).mockResolvedValue()

await expect(StoreExecute.run(['--query', 'query { shop { name } }'], import.meta.url)).rejects.toThrow()

expect(storeExecuteOperation).not.toHaveBeenCalled()
})

test('calls storeExecuteOperation with correct arguments', async () => {
vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }')
vi.mocked(storeExecuteOperation).mockResolvedValue()

await StoreExecute.run(
['--store', 'test-store.myshopify.com', '--query', 'query { shop { name } }'],
import.meta.url,
)

expect(loadQuery).toHaveBeenCalledWith(expect.objectContaining({query: 'query { shop { name } }'}))
expect(storeExecuteOperation).toHaveBeenCalledWith(
expect.objectContaining({
storeFqdn: 'test-store.myshopify.com',
query: 'query { shop { name } }',
}),
)
})

test('passes version flag when provided', async () => {
vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }')
vi.mocked(storeExecuteOperation).mockResolvedValue()

await StoreExecute.run(
['--store', 'test-store.myshopify.com', '--query', 'query { shop { name } }', '--version', '2024-01'],
import.meta.url,
)

expect(storeExecuteOperation).toHaveBeenCalledWith(
expect.objectContaining({
version: '2024-01',
}),
)
})

test('passes output-file flag when provided', async () => {
vi.mocked(loadQuery).mockResolvedValue('query { shop { name } }')
vi.mocked(storeExecuteOperation).mockResolvedValue()

await StoreExecute.run(
['--store', 'test-store.myshopify.com', '--query', 'query { shop { name } }', '--output-file', '/tmp/out.json'],
import.meta.url,
)

expect(storeExecuteOperation).toHaveBeenCalledWith(
expect.objectContaining({
outputFile: '/tmp/out.json',
}),
)
})
})
33 changes: 33 additions & 0 deletions packages/app/src/cli/commands/store/execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {storeOperationFlags} from '../../flags.js'
import {storeExecuteOperation} from '../../services/store-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 StoreExecute extends BaseCommand {
static summary = 'Execute GraphQL queries and mutations against a store.'

static descriptionWithMarkdown = `Executes an Admin API GraphQL query or mutation on the specified store, authenticated as the current user.

Unlike [\`app execute\`](https://shopify.dev/docs/api/shopify-cli/app/app-execute), this command does not require an app to be linked or installed on the target store.`

static description = this.descriptionWithoutMarkdown()

static flags = {
...globalFlags,
...storeOperationFlags,
}

async run(): Promise<void> {
const {flags} = await this.parse(StoreExecute)
const query = await loadQuery(flags)
await storeExecuteOperation({
storeFqdn: flags.store,
query,
variables: flags.variables,
variableFile: flags['variable-file'],
outputFile: flags['output-file'],
...(flags.version && {version: flags.version}),
})
}
}
43 changes: 43 additions & 0 deletions packages/app/src/cli/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,49 @@ export const bulkOperationFlags = {
}),
}

export const storeOperationFlags = {
query: Flags.string({
char: 'q',
description: 'The GraphQL query or mutation, as a string.',
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 query or mutation, in JSON format.',
env: 'SHOPIFY_FLAG_VARIABLES',
exclusive: ['variable-file'],
}),
'variable-file': Flags.string({
description: "Path to a file containing GraphQL variables in JSON format. Can't be used with --variables.",
env: 'SHOPIFY_FLAG_VARIABLE_FILE',
parse: async (input) => resolvePath(input),
exclusive: ['variables'],
}),
store: Flags.string({
char: 's',
description: 'The myshopify.com domain of the store to execute against.',
env: 'SHOPIFY_FLAG_STORE',
parse: async (input) => normalizeStoreFqdn(input),
required: true,
}),
version: Flags.string({
description: 'The API version to use for the query or mutation. Defaults to the latest stable version.',
env: 'SHOPIFY_FLAG_VERSION',
}),
'output-file': Flags.string({
description: 'The file name where results should be written, instead of STDOUT.',
env: 'SHOPIFY_FLAG_OUTPUT_FILE',
}),
}

export const operationFlags = {
query: Flags.string({
char: 'q',
Expand Down
2 changes: 2 additions & 0 deletions packages/app/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import AppUnlinkedCommand from './utilities/app-unlinked-command.js'
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 BaseCommand from '@shopify/cli-kit/node/base-command'

/**
Expand Down Expand Up @@ -78,6 +79,7 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin
'webhook:trigger': WebhookTriggerDeprecated,
'demo:watcher': DemoWatcher,
'organization:list': OrganizationList,
'store:execute': StoreExecute,
}

export const AppSensitiveMetadataHook = gatherSensitiveMetadata
Expand Down
91 changes: 91 additions & 0 deletions packages/app/src/cli/services/store-execute-operation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {storeExecuteOperation} from './store-execute-operation.js'
import {resolveApiVersion} from './graphql/common.js'
import {runGraphQLExecution} from './execute-operation.js'
import {renderSingleTask} from '@shopify/cli-kit/node/ui'
import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session'
import {describe, test, expect, vi, beforeEach} from 'vitest'

vi.mock('./graphql/common.js')
vi.mock('./execute-operation.js')
vi.mock('@shopify/cli-kit/node/ui')
vi.mock('@shopify/cli-kit/node/session')

describe('storeExecuteOperation', () => {
const storeFqdn = 'test-store.myshopify.com'
const mockAdminSession = {token: 'user-token', storeFqdn}

beforeEach(() => {
vi.mocked(ensureAuthenticatedAdmin).mockResolvedValue(mockAdminSession)
vi.mocked(resolveApiVersion).mockResolvedValue('2024-07')
vi.mocked(renderSingleTask).mockImplementation(async ({task}) => {
return task(() => {})
})
vi.mocked(runGraphQLExecution).mockResolvedValue(undefined)
})

test('authenticates as user via ensureAuthenticatedAdmin', async () => {
await storeExecuteOperation({
storeFqdn,
query: 'query { shop { name } }',
})

expect(ensureAuthenticatedAdmin).toHaveBeenCalledWith(storeFqdn)
})

test('resolves API version', async () => {
await storeExecuteOperation({
storeFqdn,
query: 'query { shop { name } }',
})

expect(resolveApiVersion).toHaveBeenCalledWith({adminSession: mockAdminSession})
})

test('passes user-specified version to resolveApiVersion', async () => {
await storeExecuteOperation({
storeFqdn,
query: 'query { shop { name } }',
version: '2024-01',
})

expect(resolveApiVersion).toHaveBeenCalledWith({
adminSession: mockAdminSession,
userSpecifiedVersion: '2024-01',
})
})

test('delegates to runGraphQLExecution with correct args', async () => {
await storeExecuteOperation({
storeFqdn,
query: 'query { shop { name } }',
variables: '{"key":"value"}',
variableFile: '/path/to/vars.json',
outputFile: '/path/to/output.json',
})

expect(runGraphQLExecution).toHaveBeenCalledWith({
adminSession: mockAdminSession,
query: 'query { shop { name } }',
variables: '{"key":"value"}',
variableFile: '/path/to/vars.json',
outputFile: '/path/to/output.json',
version: '2024-07',
})
})

test('passes undefined optional fields when not provided', async () => {
await storeExecuteOperation({
storeFqdn,
query: 'query { shop { name } }',
})

expect(runGraphQLExecution).toHaveBeenCalledWith({
adminSession: mockAdminSession,
query: 'query { shop { name } }',
variables: undefined,
variableFile: undefined,
outputFile: undefined,
version: '2024-07',
})
})
})
30 changes: 30 additions & 0 deletions packages/app/src/cli/services/store-execute-operation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {resolveApiVersion} from './graphql/common.js'
import {runGraphQLExecution} from './execute-operation.js'
import {renderSingleTask} from '@shopify/cli-kit/node/ui'
import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session'
import {outputContent} from '@shopify/cli-kit/node/output'

interface StoreExecuteOperationInput {
storeFqdn: string
query: string
variables?: string
variableFile?: string
outputFile?: string
version?: string
}

export async function storeExecuteOperation(input: StoreExecuteOperationInput): Promise<void> {
const {storeFqdn, query, variables, variableFile, outputFile, version: userSpecifiedVersion} = input

const adminSession = await ensureAuthenticatedAdmin(storeFqdn)

const version = await renderSingleTask({
title: outputContent`Resolving API version`,
task: async (): Promise<string> => {
return resolveApiVersion({adminSession, userSpecifiedVersion})
},
renderOptions: {stdout: process.stderr},
})

await runGraphQLExecution({adminSession, query, variables, variableFile, outputFile, version})
}
36 changes: 36 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
* [`shopify plugins unlink [PLUGIN]`](#shopify-plugins-unlink-plugin)
* [`shopify plugins update`](#shopify-plugins-update)
* [`shopify search [query]`](#shopify-search-query)
* [`shopify store execute`](#shopify-store-execute)
* [`shopify theme check`](#shopify-theme-check)
* [`shopify theme console`](#shopify-theme-console)
* [`shopify theme delete`](#shopify-theme-delete)
Expand Down Expand Up @@ -2063,6 +2064,41 @@ EXAMPLES
shopify search "<a search query separated by spaces>"
```

## `shopify store execute`

Execute GraphQL queries and mutations against a store.

```
USAGE
$ shopify store execute -s <value> [--no-color] [--output-file <value>] [-q <value>] [--query-file <value>]
[--variable-file <value> | -v <value>] [--verbose] [--version <value>]

FLAGS
-q, --query=<value> [env: SHOPIFY_FLAG_QUERY] The GraphQL query or mutation, as a string.
-s, --store=<value> (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store to execute
against.
-v, --variables=<value> [env: SHOPIFY_FLAG_VARIABLES] The values for any GraphQL variables in your query or
mutation, in JSON format.
--no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output.
--output-file=<value> [env: SHOPIFY_FLAG_OUTPUT_FILE] The file name where results should be written, instead of
STDOUT.
--query-file=<value> [env: SHOPIFY_FLAG_QUERY_FILE] Path to a file containing the GraphQL query or mutation.
Can't be used with --query.
--variable-file=<value> [env: SHOPIFY_FLAG_VARIABLE_FILE] Path to a file containing GraphQL variables in JSON
format. Can't be used with --variables.
--verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output.
--version=<value> [env: SHOPIFY_FLAG_VERSION] The API version to use for the query or mutation. Defaults to
the latest stable version.

DESCRIPTION
Execute GraphQL queries and mutations against a store.

Executes an Admin API GraphQL query or mutation on the specified store, authenticated as the current user.

Unlike "`app execute`" (https://shopify.dev/docs/api/shopify-cli/app/app-execute), this command does not require an
app to be linked or installed on the target store.
```

## `shopify theme check`

Validate the theme.
Expand Down
Loading
Loading