Skip to content
Open
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
110 changes: 110 additions & 0 deletions docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -2364,6 +2364,116 @@
"value": "export interface appgraphiql {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Local port for the GraphiQL server. Must be between 1 and 65535.\n * @environment SHOPIFY_FLAG_PORT\n */\n '--port <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * The myshopify.com domain of the store to open GraphiQL against. The app must be installed on the store. If not specified, you will be prompted to select a store.\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * The values for any GraphQL variables in your query or mutation, in JSON format.\n * @environment SHOPIFY_FLAG_VARIABLES\n */\n '-v, --variables <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The API version to use in GraphiQL. Defaults to the latest stable version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version <value>'?: string\n}"
}
},
"appimportchannelconfig": {
"docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts": {
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"name": "appimportchannelconfig",
"description": "The following flags are available for the `app import channel-config` command:",
"isPublicDocs": true,
"members": [
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--auth-alias <value>",
"value": "string",
"description": "Alias of the Shopify account to use for authentication.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_AUTH_ALIAS"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--client-id <value>",
"value": "string",
"description": "The Client ID of your app.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_CLIENT_ID"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--json-schema",
"value": "''",
"description": "Print the command's JSON schemas.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_JSON_SCHEMA"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--no-color",
"value": "''",
"description": "Disable color output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_NO_COLOR"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--overwrite",
"value": "''",
"description": "Overwrite the existing channel spec file if one already exists.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_OVERWRITE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--path <value>",
"value": "string",
"description": "The path to your app directory.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_PATH"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--reset",
"value": "''",
"description": "Reset all your settings.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_RESET"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--stdout",
"value": "''",
"description": "Print the generated TOML to stdout instead of writing it to a file. For piped output, use an already-linked app: first-time linking prompts may interleave with the output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_STDOUT"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--verbose",
"value": "''",
"description": "Increase the verbosity of the output. May include sensitive data.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_VERBOSE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "-c, --config <value>",
"value": "string",
"description": "The name of the app configuration.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_APP_CONFIG"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-import-channel-config.interface.ts",
"syntaxKind": "PropertySignature",
"name": "-j, --json",
"value": "''",
"description": "Output the result as JSON. Automatically disables color output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_JSON"
}
],
"value": "export interface appimportchannelconfig {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Overwrite the existing channel spec file if one already exists.\n * @environment SHOPIFY_FLAG_OVERWRITE\n */\n '--overwrite'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Print the generated TOML to stdout instead of writing it to a file. For piped output, use an already-linked app: first-time linking prompts may interleave with the output.\n * @environment SHOPIFY_FLAG_STDOUT\n */\n '--stdout'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"appimportcustomdatadefinitions": {
"docs-shopify.dev/commands/interfaces/app-import-custom-data-definitions.interface.ts": {
"filePath": "docs-shopify.dev/commands/interfaces/app-import-custom-data-definitions.interface.ts",
Expand Down
61 changes: 61 additions & 0 deletions packages/app/src/cli/commands/app/import/channel-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {appFlags} from '../../../flags.js'
import {linkedAppContext} from '../../../services/app-context.js'
import {importChannelConfig} from '../../../services/import-channel-config/import.js'
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
import {importChannelConfigJsonOutputSchema} from '../../../services/import-channel-config/types.js'
import {Flags} from '@oclif/core'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'

export default class ImportChannelConfig extends AppLinkedCommand {
static summary = 'Import the Shopify-authored default channel spec as a channel_config TOML file.'

static descriptionWithMarkdown = `Imports the Shopify-authored default channel specification for your app as a deployable \`channel_config\` extension spec.

The generated TOML file contains only public \`channel_config\` fields. Review it, commit it to your app, then deploy it with \`shopify app deploy\`. This command never deploys the spec itself.`

static description = this.descriptionWithoutMarkdown()

static get jsonOutputSchema() {
return importChannelConfigJsonOutputSchema
}

static flags = {
...globalFlags,
...appFlags,
...jsonFlag,
stdout: Flags.boolean({
description:
'Print the generated TOML to stdout instead of writing it to a file. For piped output, use an already-linked app: first-time linking prompts may interleave with the output.',
env: 'SHOPIFY_FLAG_STDOUT',
default: false,
exclusive: ['json'],
}),
overwrite: Flags.boolean({
description: 'Overwrite the existing channel spec file if one already exists.',
env: 'SHOPIFY_FLAG_OVERWRITE',
default: false,
}),
}

public async run(): Promise<AppLinkedCommandOutput> {
const {flags} = await this.parse(ImportChannelConfig)

const {app, remoteApp, developerPlatformClient} = await linkedAppContext({
directory: flags.path,
clientId: flags['client-id'],
forceRelink: flags.reset,
userProvidedConfigName: flags.config,
})

await importChannelConfig({
app,
remoteApp,
developerPlatformClient,
stdout: flags.stdout,
overwrite: flags.overwrite,
json: flags.json,
})

return {app}
}
}
2 changes: 2 additions & 0 deletions packages/app/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import AppLinkedCommand from './utilities/app-linked-command.js'
import DevClean from './commands/app/dev/clean.js'
import AppUnlinkedCommand from './utilities/app-unlinked-command.js'
import FunctionInfo from './commands/app/function/info.js'
import ImportChannelConfig from './commands/app/import/channel-config.js'
import ImportCustomDataDefinitions from './commands/app/import/custom-data-definitions.js'
import ImportCustomDataDefinitionsDeprecated from './commands/app/import-custom-data-definitions.js'
import OrganizationList from './commands/organization/list.js'
Expand All @@ -63,6 +64,7 @@ export const commands: {[key: string]: typeof AppLinkedCommand | typeof AppUnlin
'app:doctor': Doctor,
'app:logs': Logs,
'app:logs:sources': Sources,
'app:import:channel-config': ImportChannelConfig,
'app:import:custom-data-definitions': ImportCustomDataDefinitions,
'app:import:dashboard-extensions': ImportDashboardExtensions,
// Deprecated paths, kept registered (and hidden) so existing scripts keep working.
Expand Down
131 changes: 131 additions & 0 deletions packages/app/src/cli/services/import-channel-config/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {fetchChannelSpecExport} from './fetch.js'
import {testDeveloperPlatformClient, testOrganizationApp} from '../../models/app/app.test-data.js'
import {describe, expect, test, vi} from 'vitest'
import {shopifyFetch} from '@shopify/cli-kit/node/http'
import {appManagementFqdn} from '@shopify/cli-kit/node/context/fqdn'

vi.mock('@shopify/cli-kit/node/http')
vi.mock('@shopify/cli-kit/node/context/fqdn')

const SUCCESS_PAYLOAD = {
success: true,
handle: 'example',
filename: 'example.toml',
toml: 'handle = "example"\n',
warnings: [],
}

function mockResponse({status = 200, json}: {status?: number; json?: unknown} = {}) {
return {
status,
ok: status >= 200 && status < 300,
json: json === undefined ? () => Promise.reject(new Error('invalid json')) : () => Promise.resolve(json),
} as unknown as Awaited<ReturnType<typeof shopifyFetch>>
}

function testOptions() {
return {
remoteApp: testOrganizationApp({id: 'gid://shopify/App/123', organizationId: '42'}),
developerPlatformClient: testDeveloperPlatformClient(),
}
}

describe('fetchChannelSpecExport', () => {
test('extracts the numeric app id from a GID when building the endpoint URL', async () => {
// Given
vi.mocked(appManagementFqdn).mockResolvedValue('app.shopify.com')
vi.mocked(shopifyFetch).mockResolvedValue(mockResponse({json: SUCCESS_PAYLOAD}))

// When
await fetchChannelSpecExport(testOptions())

// Then
expect(shopifyFetch).toHaveBeenCalledWith(
'https://app.shopify.com/app_management/unstable/organizations/42/apps/123/channel_spec_export.json',
expect.anything(),
)
})

test('returns the parsed export on success', async () => {
// Given
vi.mocked(appManagementFqdn).mockResolvedValue('app.shopify.com')
vi.mocked(shopifyFetch).mockResolvedValue(mockResponse({json: SUCCESS_PAYLOAD}))

// When
const result = await fetchChannelSpecExport(testOptions())

// Then
expect(result).toEqual({
success: true,
handle: 'example',
filename: 'example.toml',
toml: 'handle = "example"\n',
warnings: [],
})
})

test('treats a 422 as a well-formed export failure with a reason', async () => {
// Given
vi.mocked(appManagementFqdn).mockResolvedValue('app.shopify.com')
vi.mocked(shopifyFetch).mockResolvedValue(
mockResponse({status: 422, json: {success: false, error: 'not_exportable_yet', reason: 'not_allowlisted'}}),
)

// When
const result = await fetchChannelSpecExport(testOptions())

// Then
expect(result).toEqual({success: false, reason: 'not_allowlisted'})
})

test('aborts with endpoint-unavailable guidance on 404', async () => {
// Given
vi.mocked(appManagementFqdn).mockResolvedValue('app.shopify.com')
vi.mocked(shopifyFetch).mockResolvedValue(mockResponse({status: 404, json: {}}))

// When/Then
await expect(fetchChannelSpecExport(testOptions())).rejects.toThrow(
'The channel spec export endpoint is not available for this app.',
)
})

test.each([401, 403])('aborts with re-auth guidance on %i instead of reporting an export failure', async (status) => {
// Given
vi.mocked(appManagementFqdn).mockResolvedValue('app.shopify.com')
vi.mocked(shopifyFetch).mockResolvedValue(mockResponse({status, json: {}}))

// When/Then
await expect(fetchChannelSpecExport(testOptions())).rejects.toThrow('authentication failed')
})

test('aborts with retry guidance on 5xx JSON responses instead of reporting an export failure', async () => {
// Given
vi.mocked(appManagementFqdn).mockResolvedValue('app.shopify.com')
vi.mocked(shopifyFetch).mockResolvedValue(mockResponse({status: 500, json: {message: 'oops'}}))

// When/Then
await expect(fetchChannelSpecExport(testOptions())).rejects.toThrow('responded with status 500')
})

test.each([
['null', null],
['an array', ['not', 'an', 'object']],
['a primitive', 'nope'],
])('aborts with a controlled error when the body is %s', async (_label, json) => {
// Given
vi.mocked(appManagementFqdn).mockResolvedValue('app.shopify.com')
vi.mocked(shopifyFetch).mockResolvedValue(mockResponse({json}))

// When/Then
await expect(fetchChannelSpecExport(testOptions())).rejects.toThrow('unexpected response')
})

test('aborts when required fields are missing from the response', async () => {
// Given
vi.mocked(appManagementFqdn).mockResolvedValue('app.shopify.com')
vi.mocked(shopifyFetch).mockResolvedValue(mockResponse({json: {handle: 'example'}}))

// When/Then
await expect(fetchChannelSpecExport(testOptions())).rejects.toThrow('missing required fields')
})
})
Loading
Loading