-
Notifications
You must be signed in to change notification settings - Fork 125
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() |
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({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about calling it |
||
description: 'Path to the script file to execute', | ||
required: true, | ||
env: 'SHOPIFY_FLAG_SCRIPT_FILE', | ||
}), | ||
shop: Flags.string({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We call it |
||
description: 'The shop domain to execute the script against', | ||
required: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
} |
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, | ||
}, | ||
], | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
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.