Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dnm] Prototype store execute command #4493

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
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
69 changes: 69 additions & 0 deletions example-script.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the best place, but I'd definitely add a few examples somewhere with the most common operations.

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node

async function main() {
try {
const store = process.env.STORE_FQDN
const accessToken = process.env.ACCESS_TOKEN
const apiVersion = process.env.API_VERSION

if (!store) {
throw new Error('STORE_FQDN environment variable is not set')
}

if (!accessToken) {
throw new Error('ACCESS_TOKEN environment variable is not set')
}

if (!apiVersion) {
throw new Error('API_VERSION environment variable is not set')
}
Comment on lines +9 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need so many checks when we are running this script and setting the variables ourselves.


const currentDate = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const productTitle = `script-${currentDate}`

const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': `Bearer ${accessToken}`,
authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
query: `
mutation {
productCreate(input: {
title: "${productTitle}"
}) {
product {
id
title
}
}
}
`,
}),
}

const url = `https://${store}/admin/api/${apiVersion}/graphql.json`
const response = await fetch(url, requestOptions)

const data = await response.json()

if (data.errors) {
console.error('GraphQL errors:', data.errors)
throw new Error('Failed to create product')
}

const productId = data.data.productCreate.product.id
const numericId = productId.split('/').pop()
const productUrl = `https://${store}/admin/products/${numericId}`

console.log('Product created successfully!')
console.log('Product URL:', productUrl)
} catch (error) {
console.error('An error occurred:', error)
process.exit(1)
}
}

main()
32 changes: 32 additions & 0 deletions packages/app/src/cli/commands/store/execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {executeStoreScript, ExecuteOptions} from '../../services/store/execute.js'
import {Command, Flags} from '@oclif/core'
import {globalFlags} from '@shopify/cli-kit/node/cli'

export default class StoreExecute extends Command {
static description = 'Execute a script in the context of a Shopify store'

static flags = {
...globalFlags,
'script-file': Flags.string({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about calling it script to simplify?

description: 'Path to the script file to execute',
required: true,
env: 'SHOPIFY_FLAG_SCRIPT_FILE',
}),
shop: Flags.string({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We call it store in other places

description: 'The shop domain to execute the script against',
required: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about making this flag optional and listing the available stores when it's not present?

env: 'SHOPIFY_FLAG_SHOP',
}),
}

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

const options: ExecuteOptions = {
scriptFile: flags['script-file'],
shop: flags.shop,
}

await executeStoreScript(options)
}
}
2 changes: 2 additions & 0 deletions packages/app/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import WebhookTriggerDeprecated from './commands/webhook/trigger.js'
import init from './hooks/clear_command_cache.js'
import gatherPublicMetadata from './hooks/public_metadata.js'
import gatherSensitiveMetadata from './hooks/sensitive_metadata.js'
import StoreExecute from './commands/store/execute.js'

export const commands = {
'app:build': Build,
Expand All @@ -50,6 +51,7 @@ export const commands = {
'app:versions:list': VersionsList,
'app:webhook:trigger': WebhookTrigger,
'webhook:trigger': WebhookTriggerDeprecated,
'store:execute': StoreExecute,
'demo:watcher': DemoWatcher,
}

Expand Down
63 changes: 63 additions & 0 deletions packages/app/src/cli/services/store/execute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {AdminSession, ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session'
import {exec} from '@shopify/cli-kit/node/system'
import {AbortError} from '@shopify/cli-kit/node/error'
import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn'
import {outputContent, outputInfo, outputSuccess, outputToken} from '@shopify/cli-kit/node/output'

export interface ExecuteOptions {
scriptFile: string
shop: string
}

export async function executeStoreScript(options: ExecuteOptions): Promise<void> {
const adminSession: AdminSession = await ensureAuthenticatedAdmin(options.shop)

outputInfo(
outputContent`Running script ${outputToken.path(options.scriptFile)} on store ${outputToken.raw(options.shop)}`,
)

const startOfExecution = new Date()

const env = {
...process.env,
STORE_FQDN: await normalizeStoreFqdn(adminSession.storeFqdn),
ACCESS_TOKEN: adminSession.token,
API_VERSION: '2024-07',
}

try {
await exec(process.execPath, [options.scriptFile], {
env,
stdio: 'inherit',
})

const endOfExecution = new Date()
const executionTime = endOfExecution.getTime() - startOfExecution.getTime()

outputSuccess(`Script ran in ${executionTime}ms`)
} catch (error) {
let errorMessage = 'An error occurred while executing the script.'
let errorDetails = ''

if (error instanceof Error) {
errorMessage = error.message
errorDetails = error.stack ?? ''
}

throw new AbortError(
`Script execution failed: ${errorMessage}`,
'There was an issue running your script. Please check the error details and your script for any issues.',
[
'Review your script for any syntax errors or logical issues.',
'Ensure all required dependencies are installed.',
'Check if the script has the necessary permissions to execute.',
],
[
{
title: 'Error Details',
body: errorDetails,
},
],
)
}
}
19 changes: 19 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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-themeconsole)
* [`shopify theme delete`](#shopify-theme-delete)
Expand Down Expand Up @@ -1661,6 +1662,24 @@ EXAMPLES
shopify search "<a search query separated by spaces>"
```

## `shopify store execute`

Execute a script in the context of a Shopify store

```
USAGE
$ shopify store execute --script-file <value> --shop <value> [--no-color] [--verbose]

FLAGS
--no-color Disable color output.
--script-file=<value> (required) Path to the script file to execute
--shop=<value> (required) The shop domain to execute the script against
--verbose Increase the verbosity of the output.

DESCRIPTION
Execute a script in the context of a Shopify store
```

## `shopify theme check`

Validate the theme.
Expand Down
53 changes: 53 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4595,6 +4595,59 @@
"pluginType": "core",
"strict": true
},
"store:execute": {
"aliases": [
],
"args": {
},
"customPluginName": "@shopify/app",
"description": "Execute a script in the context of a Shopify store",
"enableJsonFlag": false,
"flags": {
"no-color": {
"allowNo": false,
"description": "Disable color output.",
"env": "SHOPIFY_FLAG_NO_COLOR",
"hidden": false,
"name": "no-color",
"type": "boolean"
},
"script-file": {
"description": "Path to the script file to execute",
"env": "SHOPIFY_FLAG_SCRIPT_FILE",
"hasDynamicHelp": false,
"multiple": false,
"name": "script-file",
"required": true,
"type": "option"
},
"shop": {
"description": "The shop domain to execute the script against",
"env": "SHOPIFY_FLAG_SHOP",
"hasDynamicHelp": false,
"multiple": false,
"name": "shop",
"required": true,
"type": "option"
},
"verbose": {
"allowNo": false,
"description": "Increase the verbosity of the output.",
"env": "SHOPIFY_FLAG_VERBOSE",
"hidden": false,
"name": "verbose",
"type": "boolean"
}
},
"hasDynamicHelp": false,
"hiddenAliases": [
],
"id": "store:execute",
"pluginAlias": "@shopify/cli",
"pluginName": "@shopify/cli",
"pluginType": "core",
"strict": true
},
"theme:check": {
"aliases": [
],
Expand Down
Loading