diff --git a/packages/app/src/cli/commands/dev/execute.test.ts b/packages/app/src/cli/commands/dev/execute.test.ts new file mode 100644 index 00000000000..345680ae99b --- /dev/null +++ b/packages/app/src/cli/commands/dev/execute.test.ts @@ -0,0 +1,79 @@ +import Execute from './execute.js' +import {executeDevPlatformOperation} from '../../services/dev/execute.js' +import {outputResult} from '@shopify/cli-kit/node/output' +import {afterEach, beforeEach, expect, test, vi} from 'vitest' +import {Parser} from '@oclif/core' + +vi.mock('../../services/dev/execute.js') +vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => ({ + ...(await importOriginal()), + outputResult: vi.fn(), +})) + +const originalExitCode = process.exitCode + +beforeEach(() => { + process.exitCode = undefined +}) + +afterEach(() => { + process.exitCode = originalExitCode +}) + +test('forwards a GraphQL request without an app project or log-specific flags and prints the full response', async () => { + const query = '{ __schema { queryType { name } } }' + const response = {data: {__schema: {queryType: {name: 'QueryRoot'}}}} + vi.mocked(executeDevPlatformOperation).mockResolvedValue({response, failed: false}) + + await Execute.run(['--query', query], import.meta.url) + + expect(executeDevPlatformOperation).toHaveBeenCalledWith({ + query, + queryFile: undefined, + variables: undefined, + variableFile: undefined, + operationName: undefined, + demo: false, + }) + expect(outputResult).toHaveBeenCalledExactlyOnceWith(JSON.stringify(response, null, 2)) + expect(process.exitCode).toBeUndefined() +}) + +test('forwards stdin, variables, operation name and the local demo selection', async () => { + vi.mocked(executeDevPlatformOperation).mockResolvedValue({response: {data: {app: null}}, failed: false}) + + await Execute.run( + ['--query-file', '-', '--variables', '{"key":"test-app"}', '--operation-name', 'Logs', '--demo'], + import.meta.url, + ) + + expect(executeDevPlatformOperation).toHaveBeenCalledWith({ + query: undefined, + queryFile: '-', + variables: '{"key":"test-app"}', + variableFile: undefined, + operationName: 'Logs', + demo: true, + }) +}) + +test('preserves partial data and errors in stdout while setting a failing exit status', async () => { + const response = {data: {app: null}, errors: [{message: 'Access denied', path: ['app']}]} + vi.mocked(executeDevPlatformOperation).mockResolvedValue({response, failed: true}) + + await Execute.run(['--query', '{ app(key: "test-app") { key } }'], import.meta.url) + + expect(outputResult).toHaveBeenCalledExactlyOnceWith(JSON.stringify(response, null, 2)) + expect(process.exitCode).toBe(1) +}) + +test.each([ + {args: []}, + {args: ['--query', '{ __typename }', '--query-file', 'query.graphql']}, + {args: ['--query', '{ __typename }', '--variables', '{}', '--variable-file', 'variables.json']}, + {args: ['--query', '{ __typename }', '--minutes', '15']}, + {args: ['--query', '{ __typename }', '--client-id', 'test-app']}, + {args: ['--query', '{ __typename }', '--list-filters']}, +])('rejects conflicting, missing or log-specific flags %j', async ({args}) => { + await expect(Parser.parse(args, {flags: Execute.flags})).rejects.toThrow() +}) diff --git a/packages/app/src/cli/commands/dev/execute.ts b/packages/app/src/cli/commands/dev/execute.ts new file mode 100644 index 00000000000..6f0c2f3c352 --- /dev/null +++ b/packages/app/src/cli/commands/dev/execute.ts @@ -0,0 +1,49 @@ +import {executeDevPlatformOperation} from '../../services/dev/execute.js' +import {operationFlags} from '../../flags.js' +import BaseCommand from '@shopify/cli-kit/node/base-command' +import {globalFlags} from '@shopify/cli-kit/node/cli' +import {outputResult} from '@shopify/cli-kit/node/output' +import {Flags} from '@oclif/core' + +export default class Execute extends BaseCommand { + static hidden = true + static summary = 'Prototype only: execute a GraphQL request against the local Dev Platform API.' + static description = + 'Prints the complete GraphQL JSON response. The query selects apps, filters, and returned fields. ' + + 'GraphQL or HTTP errors produce a nonzero exit status, preserving partial data when available.' + + static flags = { + ...globalFlags, + query: operationFlags.query, + 'query-file': Flags.string({ + description: 'Path to a GraphQL document, or - to read from stdin.', + env: 'SHOPIFY_FLAG_QUERY_FILE', + exactlyOne: ['query', 'query-file'], + }), + variables: operationFlags.variables, + 'variable-file': operationFlags['variable-file'], + 'operation-name': Flags.string({ + description: 'The operation to execute when the document contains multiple operations.', + env: 'SHOPIFY_FLAG_OPERATION_NAME', + }), + demo: Flags.boolean({ + default: false, + env: 'SHOPIFY_FLAG_DEMO', + description: 'Use the loopback demo with seeded auth, not Identity login.', + }), + } + + public async run(): Promise { + const {flags} = await this.parse(Execute) + const {response, failed} = await executeDevPlatformOperation({ + query: flags.query, + queryFile: flags['query-file'], + variables: flags.variables, + variableFile: flags['variable-file'], + operationName: flags['operation-name'], + demo: flags.demo, + }) + outputResult(JSON.stringify(response, null, 2)) + if (failed) process.exitCode = 1 + } +} diff --git a/packages/app/src/cli/index.ts b/packages/app/src/cli/index.ts index a6289bba107..f2fe8e475ee 100644 --- a/packages/app/src/cli/index.ts +++ b/packages/app/src/cli/index.ts @@ -12,6 +12,7 @@ import DoctorSubmit from './commands/app/doctor/submit.js' import Doctor from './commands/app/doctor.js' import Logs from './commands/app/logs.js' import Sources from './commands/app/app-logs/sources.js' +import DevExecute from './commands/dev/execute.js' import EnvPull from './commands/app/env/pull.js' import EnvShow from './commands/app/env/show.js' import BulkExecute from './commands/app/bulk/execute.js' @@ -61,6 +62,7 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin 'app:doctor': Doctor, 'app:logs': Logs, 'app:logs:sources': Sources, + 'dev:execute': DevExecute, 'app:import-custom-data-definitions': ImportCustomDataDefinitions, 'app:import-extensions': ImportExtensions, 'app:info': AppInfo, diff --git a/packages/app/src/cli/services/dev/execute.test.ts b/packages/app/src/cli/services/dev/execute.test.ts new file mode 100644 index 00000000000..f35001f190b --- /dev/null +++ b/packages/app/src/cli/services/dev/execute.test.ts @@ -0,0 +1,256 @@ +import {executeDevPlatformOperation} from './execute.js' +import {developerDashboardFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {fetch, Response} from '@shopify/cli-kit/node/http' +import {ensureAuthenticatedAppManagementAndBusinessPlatform} from '@shopify/cli-kit/node/session' +import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs' +import {joinPath} from '@shopify/cli-kit/node/path' +import {readStdinString} from '@shopify/cli-kit/node/system' +import {afterEach, beforeEach, expect, test, vi} from 'vitest' + +vi.mock('@shopify/cli-kit/node/context/fqdn') +vi.mock('@shopify/cli-kit/node/http', async (importOriginal) => ({ + ...(await importOriginal()), + fetch: vi.fn(), +})) +vi.mock('@shopify/cli-kit/node/session') +vi.mock('@shopify/cli-kit/node/system', async (importOriginal) => ({ + ...(await importOriginal()), + readStdinString: vi.fn(), +})) + +const options = {query: '{ __typename }', demo: false} + +beforeEach(() => { + vi.stubEnv('SHOPIFY_APP_LOG_QUERY_PROTOTYPE', '1') + vi.stubEnv('SHOPIFY_SERVICE_ENV', 'local') + vi.mocked(developerDashboardFqdn).mockResolvedValue('dev.shop.dev') + vi.mocked(ensureAuthenticatedAppManagementAndBusinessPlatform).mockResolvedValue({ + appManagementToken: 'atkn_local-identity', + userId: 'local-user', + businessPlatformToken: 'unused', + }) +}) + +afterEach(() => { + vi.unstubAllEnvs() +}) + +test.each([ + ['SHOPIFY_SERVICE_ENV', 'production'], + ['SHOPIFY_APP_LOG_QUERY_PROTOTYPE', '0'], +])('refuses %s=%s before authenticating or sending a request', async (name, value) => { + vi.stubEnv(name, value) + await expect(executeDevPlatformOperation(options)).rejects.toThrow('Prototype only') + expect(ensureAuthenticatedAppManagementAndBusinessPlatform).not.toHaveBeenCalled() + expect(fetch).not.toHaveBeenCalled() +}) + +test('refuses an unexpected host before obtaining a token', async () => { + vi.mocked(developerDashboardFqdn).mockResolvedValue('dev.shopify.com') + await expect(executeDevPlatformOperation(options)).rejects.toThrow('only call dev.shop.dev') + expect(ensureAuthenticatedAppManagementAndBusinessPlatform).not.toHaveBeenCalled() + expect(fetch).not.toHaveBeenCalled() +}) + +test('forwards the exact document, variables and operation name without building a logs query', async () => { + const query = ` + query Logs($key: String!, $search: AppLogSearchInput!) { + chosenApp: app(key: $key) { + logs(input: $search) { events { ...Details } } + } + } + fragment Details on LogRecord { timestamp webhook { headers { name } } } + ` + const variables = {key: 'test-app', search: {limit: 10001, offset: 1000001}} + const response = {data: {chosenApp: {logs: {events: [{timestamp: '2026-09-16T20:00:00Z'}]}}}} + vi.mocked(fetch).mockResolvedValue(new Response(JSON.stringify(response))) + + await expect( + executeDevPlatformOperation({...options, query, variables: JSON.stringify(variables), operationName: 'Logs'}), + ).resolves.toEqual({response, failed: false}) + + expect(fetch).toHaveBeenCalledExactlyOnceWith( + 'https://dev.shop.dev/api/unstable/graphql', + expect.objectContaining({ + method: 'POST', + redirect: 'error', + headers: expect.objectContaining({authorization: 'Bearer atkn_local-identity'}), + body: JSON.stringify({query, variables, operationName: 'Logs'}), + }), + ) +}) + +test('supports introspection without an app key and preserves extensions', async () => { + const query = '{ __schema { queryType { name } } }' + const response = {data: {__schema: {queryType: {name: 'QueryRoot'}}}, extensions: {requestId: 'example'}} + vi.mocked(fetch).mockResolvedValue(new Response(JSON.stringify(response))) + + await expect(executeDevPlatformOperation({...options, query})).resolves.toEqual({response, failed: false}) + const request = JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string) + expect(request).toEqual({query}) +}) + +test('lets the server select an operation in a multi-operation document', async () => { + const query = 'query One { __typename } query Two { __schema { queryType { name } } }' + const response = {data: {__typename: 'QueryRoot'}} + vi.mocked(fetch).mockResolvedValue(new Response(JSON.stringify(response))) + + await executeDevPlatformOperation({...options, query, operationName: 'One'}) + + expect(JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string)).toEqual({query, operationName: 'One'}) +}) + +test('reads the document and variables from real files', async () => { + await inTemporaryDirectory(async (directory) => { + const queryFile = joinPath(directory, 'query.graphql') + const variableFile = joinPath(directory, 'variables.json') + const query = 'query App($key: String!) { app(key: $key) { key } }\n' + await writeFile(queryFile, query) + await writeFile(variableFile, '{"key":"test-app"}') + vi.mocked(fetch).mockResolvedValue(new Response('{"data":{"app":{"key":"test-app"}}}')) + + await executeDevPlatformOperation({queryFile, variableFile, demo: false}) + + expect(JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string)).toEqual({ + query, + variables: {key: 'test-app'}, + }) + }) +}) + +test('reads a document from stdin when query-file is a dash', async () => { + vi.mocked(readStdinString).mockResolvedValue(options.query) + vi.mocked(fetch).mockResolvedValue(new Response('{"data":{"__typename":"QueryRoot"}}')) + + await executeDevPlatformOperation({queryFile: '-', demo: false}) + + expect(readStdinString).toHaveBeenCalledOnce() + expect(JSON.parse(vi.mocked(fetch).mock.calls[0]![1]!.body as string)).toEqual({query: options.query}) +}) + +test('uses the temporary token only for the fixed loopback demo', async () => { + await inTemporaryDirectory(async (directory) => { + const path = joinPath(directory, 'token') + await writeFile(path, 'atkn_demo-only\n') + vi.stubEnv('APP_LOG_QUERY_DEMO_TOKEN_FILE', path) + vi.mocked(fetch).mockResolvedValue(new Response('{"data":{"__typename":"QueryRoot"}}')) + + await expect(executeDevPlatformOperation({...options, demo: true})).resolves.toEqual({ + response: {data: {__typename: 'QueryRoot'}}, + failed: false, + }) + expect(fetch).toHaveBeenCalledWith( + 'http://127.0.0.1:4387/api/unstable/graphql', + expect.objectContaining({headers: expect.objectContaining({authorization: 'Bearer atkn_demo-only'})}), + ) + expect(ensureAuthenticatedAppManagementAndBusinessPlatform).not.toHaveBeenCalled() + }) +}) + +test('requires a demo token file instead of falling back to Identity', async () => { + vi.stubEnv('APP_LOG_QUERY_DEMO_TOKEN_FILE', '') + await expect(executeDevPlatformOperation({...options, demo: true})).rejects.toThrow('APP_LOG_QUERY_DEMO_TOKEN_FILE') + expect(ensureAuthenticatedAppManagementAndBusinessPlatform).not.toHaveBeenCalled() + expect(fetch).not.toHaveBeenCalled() +}) + +test.each(['', 'not-a-token', `atkn_${'x'.repeat(4096)}`])('rejects an invalid demo token %#', async (token) => { + await inTemporaryDirectory(async (directory) => { + const path = joinPath(directory, 'token') + await writeFile(path, token) + vi.stubEnv('APP_LOG_QUERY_DEMO_TOKEN_FILE', path) + await expect(executeDevPlatformOperation({...options, demo: true})).rejects.toThrow('Invalid demo token') + expect(fetch).not.toHaveBeenCalled() + }) +}) + +test.each([200, 400])('preserves errors, paths, extensions and partial data for HTTP %s', async (status) => { + const response = { + data: {app: null}, + errors: [ + {message: 'Access denied', path: ['app'], locations: [{line: 1, column: 3}], extensions: {code: 'DENIED'}}, + ], + extensions: {requestId: 'example'}, + } + vi.mocked(fetch).mockResolvedValue(new Response(JSON.stringify(response), {status})) + + await expect(executeDevPlatformOperation(options)).resolves.toEqual({response, failed: true}) +}) + +test('preserves request errors without data', async () => { + const response = {errors: [{message: 'Unknown field'}]} + vi.mocked(fetch).mockResolvedValue(new Response(JSON.stringify(response))) + + await expect(executeDevPlatformOperation({...options, query: '{ unknownField }'})).resolves.toEqual({ + response, + failed: true, + }) +}) + +test('does not turn an HTTP failure with data into success', async () => { + const response = {data: null} + vi.mocked(fetch).mockResolvedValue(new Response(JSON.stringify(response), {status: 503})) + await expect(executeDevPlatformOperation(options)).resolves.toEqual({response, failed: true}) +}) + +test.each([null, [], {}, {data: []}, {errors: []}, {errors: [{}]}])( + 'rejects a malformed GraphQL envelope %j', + async (body) => { + vi.mocked(fetch).mockResolvedValue(new Response(JSON.stringify(body))) + await expect(executeDevPlatformOperation(options)).rejects.toThrow('invalid GraphQL response') + }, +) + +test('reports non-JSON HTTP failures without echoing their bodies', async () => { + vi.mocked(fetch).mockResolvedValue(new Response('private upstream diagnostic', {status: 502})) + await expect(executeDevPlatformOperation(options)).rejects.toThrow('invalid JSON (HTTP 502)') +}) + +test('propagates network failures without retrying', async () => { + const error = new Error('Connection refused') + vi.mocked(fetch).mockRejectedValue(error) + await expect(executeDevPlatformOperation(options)).rejects.toBe(error) + expect(fetch).toHaveBeenCalledOnce() +}) + +test('accepts responses larger than one MiB without a client byte cap', async () => { + const response = {data: {largeValue: 'x'.repeat(2 * 1024 * 1024)}} + vi.mocked(fetch).mockImplementation(async (_url, init) => { + const responseOptions = {status: 200, size: init?.size} + return new Response(JSON.stringify(response), responseOptions) + }) + + await expect(executeDevPlatformOperation(options)).resolves.toEqual({response, failed: false}) + expect(vi.mocked(fetch).mock.calls[0]![1]?.size).toBeUndefined() +}) + +test.each([ + {query: undefined}, + {queryFile: 'also.graphql'}, + {query: ''}, + {query: ' '}, + {variables: '{}', variableFile: 'also.json'}, + {variables: 'not json'}, + {variables: 'null'}, + {variables: '[]'}, + {variables: '1'}, +])('rejects invalid input %j before authentication', async (input) => { + await expect(executeDevPlatformOperation({...options, ...input})).rejects.toThrow() + expect(ensureAuthenticatedAppManagementAndBusinessPlatform).not.toHaveBeenCalled() + expect(fetch).not.toHaveBeenCalled() +}) + +test('rejects stdin without a document before authentication', async () => { + vi.mocked(readStdinString).mockResolvedValue(undefined) + await expect(executeDevPlatformOperation({queryFile: '-', demo: false})).rejects.toThrow('nonempty GraphQL') + expect(ensureAuthenticatedAppManagementAndBusinessPlatform).not.toHaveBeenCalled() +}) + +test('reports missing query files without making a request', async () => { + await inTemporaryDirectory(async (directory) => { + await expect( + executeDevPlatformOperation({queryFile: joinPath(directory, 'missing.graphql'), demo: false}), + ).rejects.toThrow() + expect(fetch).not.toHaveBeenCalled() + }) +}) diff --git a/packages/app/src/cli/services/dev/execute.ts b/packages/app/src/cli/services/dev/execute.ts new file mode 100644 index 00000000000..ee59fbe51e5 --- /dev/null +++ b/packages/app/src/cli/services/dev/execute.ts @@ -0,0 +1,102 @@ +import {appManagementHeaders} from '@shopify/cli-kit/node/api/app-management' +import {developerDashboardFqdn} from '@shopify/cli-kit/node/context/fqdn' +import {AbortError} from '@shopify/cli-kit/node/error' +import {readFile} from '@shopify/cli-kit/node/fs' +import {fetch} from '@shopify/cli-kit/node/http' +import {ensureAuthenticatedAppManagementAndBusinessPlatform} from '@shopify/cli-kit/node/session' +import {readStdinString} from '@shopify/cli-kit/node/system' +import {z} from 'zod' + +const responseSchema = z + .object({ + data: z.record(z.unknown()).nullable().optional(), + errors: z + .array(z.object({message: z.string()}).passthrough()) + .min(1) + .optional(), + extensions: z.record(z.unknown()).optional(), + }) + .passthrough() + .refine((response) => response.data !== undefined || response.errors !== undefined) + +interface ExecuteOptions { + query?: string + queryFile?: string + variables?: string + variableFile?: string + operationName?: string + demo: boolean +} + +interface ExecuteResult { + response: z.infer + failed: boolean +} + +export async function executeDevPlatformOperation(options: ExecuteOptions): Promise { + if (process.env.SHOPIFY_APP_LOG_QUERY_PROTOTYPE !== '1' || process.env.SHOPIFY_SERVICE_ENV !== 'local') { + throw new AbortError('Prototype only: set SHOPIFY_APP_LOG_QUERY_PROTOTYPE=1 and SHOPIFY_SERVICE_ENV=local.') + } + if ((options.query === undefined) === (options.queryFile === undefined)) { + throw new AbortError('Provide exactly one of --query or --query-file.') + } + if (options.variables !== undefined && options.variableFile !== undefined) { + throw new AbortError('Provide either --variables or --variable-file, not both.') + } + + const query = + options.query ?? (options.queryFile === '-' ? await readStdinString() : await readFile(options.queryFile!)) + if (!query?.trim()) throw new AbortError('Provide a nonempty GraphQL document.') + + const variableText = + options.variables ?? (options.variableFile === undefined ? undefined : await readFile(options.variableFile)) + let variables: Record | undefined + if (variableText !== undefined) { + let parsed: unknown + try { + parsed = JSON.parse(variableText) + } catch (error) { + if (!(error instanceof SyntaxError)) throw error + throw new AbortError('GraphQL variables must be a valid JSON object.') + } + const result = z.record(z.unknown()).safeParse(parsed) + if (!result.success) throw new AbortError('GraphQL variables must be a JSON object.') + variables = result.data + } + + const {origin, token} = await queryConnection(options.demo) + const response = await fetch(`${origin}/api/unstable/graphql`, { + method: 'POST', + redirect: 'error', + signal: AbortSignal.timeout(15000), + headers: appManagementHeaders(token), + body: JSON.stringify({query, variables, operationName: options.operationName}), + }) + let body: unknown + try { + body = await response.json() + } catch (error) { + if (!(error instanceof SyntaxError)) throw error + throw new AbortError(`Local Dev Platform API returned invalid JSON (HTTP ${response.status}).`) + } + const result = responseSchema.safeParse(body) + if (!result.success) { + throw new AbortError(`Local Dev Platform API returned an invalid GraphQL response (HTTP ${response.status}).`) + } + return {response: result.data, failed: !response.ok || Boolean(result.data.errors?.length)} +} + +async function queryConnection(demo: boolean): Promise<{origin: string; token: string}> { + if (demo) { + const path = process.env.APP_LOG_QUERY_DEMO_TOKEN_FILE + if (!path) throw new AbortError('Set APP_LOG_QUERY_DEMO_TOKEN_FILE to the file printed by the local demo server.') + const token = (await readFile(path)).trim() + if (!token.startsWith('atkn_') || token.length > 4096) throw new AbortError('Invalid demo token file.') + return {origin: 'http://127.0.0.1:4387', token} + } + + const host = await developerDashboardFqdn() + if (host !== 'dev.shop.dev') throw new AbortError('This prototype can only call dev.shop.dev.') + const {appManagementToken} = await ensureAuthenticatedAppManagementAndBusinessPlatform() + return {origin: `https://${host}`, token: appManagementToken} +} diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index e643cbb6194..4c6380da419 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -4733,6 +4733,99 @@ "strict": true, "summary": "Watch and prints out changes to an app." }, + "dev:execute": { + "aliases": [ + ], + "args": { + }, + "customPluginName": "@shopify/app", + "description": "Prints the complete GraphQL JSON response. The query selects apps, filters, and returned fields. GraphQL or HTTP errors produce a nonzero exit status, preserving partial data when available.", + "enableJsonFlag": false, + "flags": { + "demo": { + "allowNo": false, + "description": "Use the loopback demo with seeded auth, not Identity login.", + "env": "SHOPIFY_FLAG_DEMO", + "name": "demo", + "type": "boolean" + }, + "no-color": { + "allowNo": false, + "description": "Disable color output.", + "env": "SHOPIFY_FLAG_NO_COLOR", + "hidden": false, + "name": "no-color", + "type": "boolean" + }, + "operation-name": { + "description": "The operation to execute when the document contains multiple operations.", + "env": "SHOPIFY_FLAG_OPERATION_NAME", + "hasDynamicHelp": false, + "multiple": false, + "name": "operation-name", + "type": "option" + }, + "query": { + "char": "q", + "description": "The GraphQL query or mutation, as a string.", + "env": "SHOPIFY_FLAG_QUERY", + "hasDynamicHelp": false, + "multiple": false, + "name": "query", + "required": false, + "type": "option" + }, + "query-file": { + "description": "Path to a GraphQL document, or - to read from stdin.", + "env": "SHOPIFY_FLAG_QUERY_FILE", + "hasDynamicHelp": false, + "multiple": false, + "name": "query-file", + "type": "option" + }, + "variable-file": { + "description": "Path to a file containing GraphQL variables in JSON format. 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 query or mutation, in JSON format.", + "env": "SHOPIFY_FLAG_VARIABLES", + "exclusive": [ + "variable-file" + ], + "hasDynamicHelp": false, + "multiple": false, + "name": "variables", + "type": "option" + }, + "verbose": { + "allowNo": false, + "description": "Increase the verbosity of the output. May include sensitive data.", + "env": "SHOPIFY_FLAG_VERBOSE", + "hidden": false, + "name": "verbose", + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hidden": true, + "hiddenAliases": [ + ], + "id": "dev:execute", + "pluginAlias": "@shopify/cli", + "pluginName": "@shopify/cli", + "pluginType": "core", + "strict": true, + "summary": "Prototype only: execute a GraphQL request against the local Dev Platform API." + }, "doc:fetch": { "aliases": [ ],