Skip to content

Commit 25dfd57

Browse files
committed
refactored to share logic between app execute and app bulk execute and app bulk status
1 parent f492b21 commit 25dfd57

File tree

7 files changed

+123
-93
lines changed

7 files changed

+123
-93
lines changed

packages/app/src/cli/commands/app/bulk/execute.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import {appFlags, bulkOperationFlags} from '../../../flags.js'
22
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
3-
import {linkedAppContext} from '../../../services/app-context.js'
4-
import {storeContext} from '../../../services/store-context.js'
53
import {executeBulkOperation} from '../../../services/bulk-operations/execute-bulk-operation.js'
4+
import {prepareExecuteContext} from '../../../utilities/execute-command-helpers.js'
65
import {globalFlags} from '@shopify/cli-kit/node/cli'
7-
import {readStdinString} from '@shopify/cli-kit/node/system'
8-
import {AbortError} from '@shopify/cli-kit/node/error'
96

107
export default class BulkExecute extends AppLinkedCommand {
118
static summary = 'Execute bulk operations.'
@@ -23,26 +20,7 @@ export default class BulkExecute extends AppLinkedCommand {
2320
async run(): Promise<AppLinkedCommandOutput> {
2421
const {flags} = await this.parse(BulkExecute)
2522

26-
const query = flags.query ?? (await readStdinString())
27-
if (!query) {
28-
throw new AbortError(
29-
'No query provided. Use the --query flag or pipe input via stdin.',
30-
'Example: echo "query { ... }" | shopify app bulk execute',
31-
)
32-
}
33-
34-
const appContextResult = await linkedAppContext({
35-
directory: flags.path,
36-
clientId: flags['client-id'],
37-
forceRelink: flags.reset,
38-
userProvidedConfigName: flags.config,
39-
})
40-
41-
const store = await storeContext({
42-
appContextResult,
43-
storeFqdn: flags.store,
44-
forceReselectStore: flags.reset,
45-
})
23+
const {query, appContextResult, store} = await prepareExecuteContext(flags, 'bulk execute')
4624

4725
await executeBulkOperation({
4826
remoteApp: appContextResult.remoteApp,

packages/app/src/cli/commands/app/bulk/status.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {appFlags} from '../../../flags.js'
22
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
3-
import {linkedAppContext} from '../../../services/app-context.js'
4-
import {storeContext} from '../../../services/store-context.js'
3+
import {prepareAppStoreContext} from '../../../utilities/execute-command-helpers.js'
54
import {getBulkOperationStatus, listBulkOperations} from '../../../services/bulk-operations/bulk-operation-status.js'
65
import {Flags} from '@oclif/core'
76
import {globalFlags} from '@shopify/cli-kit/node/cli'
@@ -33,18 +32,7 @@ export default class BulkStatus extends AppLinkedCommand {
3332
async run(): Promise<AppLinkedCommandOutput> {
3433
const {flags} = await this.parse(BulkStatus)
3534

36-
const appContextResult = await linkedAppContext({
37-
directory: flags.path,
38-
clientId: flags['client-id'],
39-
forceRelink: flags.reset,
40-
userProvidedConfigName: flags.config,
41-
})
42-
43-
const store = await storeContext({
44-
appContextResult,
45-
storeFqdn: flags.store,
46-
forceReselectStore: flags.reset,
47-
})
35+
const {appContextResult, store} = await prepareAppStoreContext(flags)
4836

4937
if (flags.id) {
5038
await getBulkOperationStatus({

packages/app/src/cli/commands/app/execute.ts

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import {appFlags, operationFlags} from '../../flags.js'
22
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../utilities/app-linked-command.js'
3-
import {linkedAppContext} from '../../services/app-context.js'
4-
import {storeContext} from '../../services/store-context.js'
53
import {executeOperation} from '../../services/execute-operation.js'
4+
import {prepareExecuteContext} from '../../utilities/execute-command-helpers.js'
65
import {globalFlags} from '@shopify/cli-kit/node/cli'
7-
import {readStdinString} from '@shopify/cli-kit/node/system'
8-
import {AbortError} from '@shopify/cli-kit/node/error'
96

107
export default class Execute extends AppLinkedCommand {
118
static summary = 'Execute GraphQL queries and mutations.'
@@ -22,26 +19,7 @@ export default class Execute extends AppLinkedCommand {
2219
async run(): Promise<AppLinkedCommandOutput> {
2320
const {flags} = await this.parse(Execute)
2421

25-
const query = flags.query ?? (await readStdinString())
26-
if (!query) {
27-
throw new AbortError(
28-
'No query provided. Use the --query flag or pipe input via stdin.',
29-
'Example: echo "query { shop { name } }" | shopify app execute',
30-
)
31-
}
32-
33-
const appContextResult = await linkedAppContext({
34-
directory: flags.path,
35-
clientId: flags['client-id'],
36-
forceRelink: flags.reset,
37-
userProvidedConfigName: flags.config,
38-
})
39-
40-
const store = await storeContext({
41-
appContextResult,
42-
storeFqdn: flags.store,
43-
forceReselectStore: flags.reset,
44-
})
22+
const {query, appContextResult, store} = await prepareExecuteContext(flags, 'execute')
4523

4624
await executeOperation({
4725
remoteApp: appContextResult.remoteApp,

packages/app/src/cli/services/bulk-operations/execute-bulk-operation.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import {runBulkOperationMutation} from './run-mutation.js'
33
import {watchBulkOperation, type BulkOperation} from './watch-bulk-operation.js'
44
import {formatBulkOperationStatus} from './format-bulk-operation-status.js'
55
import {downloadBulkOperationResults} from './download-bulk-operation-results.js'
6+
import {createAdminSessionAsApp, validateSingleOperation} from '../graphql/common.js'
67
import {OrganizationApp} from '../../models/organization.js'
78
import {renderSuccess, renderInfo, renderError, renderWarning} from '@shopify/cli-kit/node/ui'
89
import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output'
9-
import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session'
1010
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
1111
import {parse} from 'graphql'
1212
import {readFile, writeFile, fileExists} from '@shopify/cli-kit/node/fs'
@@ -46,10 +46,7 @@ export async function executeBulkOperation(input: ExecuteBulkOperationInput): Pr
4646
body: `App: ${remoteApp.title}\nStore: ${storeFqdn}`,
4747
})
4848

49-
const appSecret = remoteApp.apiSecretKeys[0]?.secret
50-
if (!appSecret) throw new BugError('No API secret keys found for app')
51-
52-
const adminSession = await ensureAuthenticatedAdminAsApp(storeFqdn, remoteApp.apiKey, appSecret)
49+
const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn)
5350

5451
const variablesJsonl = await parseVariablesToJsonl(variables, variableFile)
5552

@@ -129,14 +126,7 @@ async function renderBulkOperationResult(operation: BulkOperation, outputFile?:
129126
}
130127

131128
function validateGraphQLDocument(graphqlOperation: string, variablesJsonl?: string): void {
132-
const document = parse(graphqlOperation)
133-
const operationDefinitions = document.definitions.filter((def) => def.kind === 'OperationDefinition')
134-
135-
if (operationDefinitions.length !== 1) {
136-
throw new AbortError(
137-
'GraphQL document must contain exactly one operation definition. Multiple operations are not supported.',
138-
)
139-
}
129+
validateSingleOperation(graphqlOperation)
140130

141131
if (!isMutation(graphqlOperation) && variablesJsonl) {
142132
throw new AbortError(

packages/app/src/cli/services/execute-operation.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import {createAdminSessionAsApp, validateSingleOperation} from './graphql/common.js'
12
import {OrganizationApp} from '../models/organization.js'
23
import {renderSuccess, renderError, renderInfo} from '@shopify/cli-kit/node/ui'
34
import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output'
4-
import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session'
5-
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
5+
import {AbortError} from '@shopify/cli-kit/node/error'
66
import {adminRequestDoc} from '@shopify/cli-kit/node/api/admin'
77
import {parse} from 'graphql'
88
import {writeFile} from '@shopify/cli-kit/node/fs'
@@ -37,14 +37,11 @@ export async function executeOperation(input: ExecuteOperationInput): Promise<vo
3737
body: `App: ${remoteApp.title}\nStore: ${storeFqdn}`,
3838
})
3939

40-
const appSecret = remoteApp.apiSecretKeys[0]?.secret
41-
if (!appSecret) throw new BugError('No API secret keys found for app')
42-
43-
const adminSession = await ensureAuthenticatedAdminAsApp(storeFqdn, remoteApp.apiKey, appSecret)
40+
const adminSession = await createAdminSessionAsApp(remoteApp, storeFqdn)
4441

4542
const parsedVariables = await parseVariables(variables)
4643

47-
validateGraphQLDocument(query)
44+
validateSingleOperation(query)
4845

4946
try {
5047
const result = await adminRequestDoc({
@@ -79,14 +76,3 @@ export async function executeOperation(input: ExecuteOperationInput): Promise<vo
7976
throw error
8077
}
8178
}
82-
83-
function validateGraphQLDocument(graphqlOperation: string): void {
84-
const document = parse(graphqlOperation)
85-
const operationDefinitions = document.definitions.filter((def) => def.kind === 'OperationDefinition')
86-
87-
if (operationDefinitions.length !== 1) {
88-
throw new AbortError(
89-
'GraphQL document must contain exactly one operation definition. Multiple operations are not supported.',
90-
)
91-
}
92-
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {OrganizationApp} from '../../models/organization.js'
2+
import {ensureAuthenticatedAdminAsApp, AdminSession} from '@shopify/cli-kit/node/session'
3+
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
4+
import {parse} from 'graphql'
5+
6+
/**
7+
* Creates an Admin API session authenticated as an app using client credentials.
8+
*
9+
* @param remoteApp - The organization app containing API credentials.
10+
* @param storeFqdn - The fully qualified domain name of the store.
11+
* @returns Admin session for making authenticated API requests.
12+
*/
13+
export async function createAdminSessionAsApp(remoteApp: OrganizationApp, storeFqdn: string): Promise<AdminSession> {
14+
const appSecret = remoteApp.apiSecretKeys[0]?.secret
15+
if (!appSecret) throw new BugError('No API secret keys found for app')
16+
17+
return ensureAuthenticatedAdminAsApp(storeFqdn, remoteApp.apiKey, appSecret)
18+
}
19+
20+
/**
21+
* Validates that a GraphQL document contains exactly one operation definition.
22+
*
23+
* @param graphqlOperation - The GraphQL query or mutation string to validate.
24+
* @throws AbortError if the document doesn't contain exactly one operation.
25+
*/
26+
export function validateSingleOperation(graphqlOperation: string): void {
27+
const document = parse(graphqlOperation)
28+
const operationDefinitions = document.definitions.filter((def) => def.kind === 'OperationDefinition')
29+
30+
if (operationDefinitions.length !== 1) {
31+
throw new AbortError(
32+
'GraphQL document must contain exactly one operation definition. Multiple operations are not supported.',
33+
)
34+
}
35+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {linkedAppContext, LoadedAppContextOutput} from '../services/app-context.js'
2+
import {storeContext} from '../services/store-context.js'
3+
import {OrganizationStore} from '../models/organization.js'
4+
import {readStdinString} from '@shopify/cli-kit/node/system'
5+
import {AbortError} from '@shopify/cli-kit/node/error'
6+
7+
export interface AppStoreContextFlags {
8+
path: string
9+
'client-id'?: string
10+
reset: boolean
11+
config?: string
12+
store?: string
13+
}
14+
15+
export interface AppStoreContext {
16+
appContextResult: LoadedAppContextOutput
17+
store: OrganizationStore
18+
}
19+
20+
export interface ExecuteCommandFlags extends AppStoreContextFlags {
21+
query?: string
22+
}
23+
24+
export interface ExecuteContext extends AppStoreContext {
25+
query: string
26+
}
27+
28+
/**
29+
* Prepares the app and store context for commands.
30+
* Sets up app linking and store selection without query handling.
31+
*
32+
* @param flags - Command flags containing configuration options.
33+
* @returns Context object containing app context and store information.
34+
*/
35+
export async function prepareAppStoreContext(flags: AppStoreContextFlags): Promise<AppStoreContext> {
36+
const appContextResult = await linkedAppContext({
37+
directory: flags.path,
38+
clientId: flags['client-id'],
39+
forceRelink: flags.reset,
40+
userProvidedConfigName: flags.config,
41+
})
42+
43+
const store = await storeContext({
44+
appContextResult,
45+
storeFqdn: flags.store,
46+
forceReselectStore: flags.reset,
47+
})
48+
49+
return {appContextResult, store}
50+
}
51+
52+
/**
53+
* Prepares the execution context for GraphQL operations.
54+
* Handles query input from flag or stdin, and sets up app and store contexts.
55+
*
56+
* @param flags - Command flags containing configuration options.
57+
* @param commandName - Name of the command for error messages (e.g., 'execute', 'bulk execute').
58+
* @returns Context object containing query, app context, and store information.
59+
*/
60+
export async function prepareExecuteContext(
61+
flags: ExecuteCommandFlags,
62+
commandName = 'execute',
63+
): Promise<ExecuteContext> {
64+
const query = flags.query ?? (await readStdinString())
65+
if (!query) {
66+
throw new AbortError(
67+
'No query provided. Use the --query flag or pipe input via stdin.',
68+
`Example: echo "query { shop { name } }" | shopify app ${commandName}`,
69+
)
70+
}
71+
72+
const {appContextResult, store} = await prepareAppStoreContext(flags)
73+
74+
return {query, appContextResult, store}
75+
}

0 commit comments

Comments
 (0)