Skip to content
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
55 changes: 55 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4204,6 +4204,33 @@ DESCRIPTION

Use `--json` for machine-readable output.

Output from `--json` conforms to the `StoreInfoResult` schema.

Use `--json-schema` to print the schema directly:

```ts
interface StoreInfoResult {
id?: string
displayName?: string
subdomain: string
organizationId?: string
organizationName?: string
storeOwner?: StoreInfoStoreOwner
type?: string
plan?: string
featurePreview?: string
adminUrl?: string
accessUrl?: string
saveUrl?: string
authScopes?: string[]
}

interface StoreInfoStoreOwner {
name?: string
email?: string
}
```

EXAMPLES
$ shopify store info --store shop.myshopify.com

Expand Down Expand Up @@ -4250,6 +4277,34 @@ DESCRIPTION

Run `shopify organization list` to find organization IDs.

Output from `--json` conforms to the `StoreListResult` schema.

Use `--json-schema` to print the schema directly:

```ts
interface StoreListResult {
stores: StoreListEntry[]
organization?: StoreListOrganization
notice?: string
truncated?: boolean
}

interface StoreListEntry {
id?: string
store: string
createdAt: string
organizationId: string
organizationName: string
name?: string
type?: string
}

interface StoreListOrganization {
id: string
name: string
}
```

EXAMPLES
$ shopify store list

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7803,7 +7803,7 @@
"args": {
},
"customPluginName": "@shopify/store",
"description": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and access and save URLs for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.",
"description": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and access and save URLs for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.\n\nOutput from `--json` conforms to the `StoreInfoResult` schema.\n\nUse `--json-schema` to print the schema directly:\n\n```ts\ninterface StoreInfoResult {\n id?: string\n displayName?: string\n subdomain: string\n organizationId?: string\n organizationName?: string\n storeOwner?: StoreInfoStoreOwner\n type?: string\n plan?: string\n featurePreview?: string\n adminUrl?: string\n accessUrl?: string\n saveUrl?: string\n authScopes?: string[]\n}\n\ninterface StoreInfoStoreOwner {\n name?: string\n email?: string\n}\n```",
"descriptionWithMarkdown": "Returns available metadata about a store you have access to, such as its id, display name, subdomain, organization, store owner, type, plan, feature preview, admin URL, and access and save URLs for preview stores.\n\nSome details may be omitted when they are not available for the store.\n\nUse `--json` for machine-readable output.",
"examples": [
"<%= config.bin %> <%= command.id %> --store shop.myshopify.com",
Expand Down Expand Up @@ -7869,7 +7869,7 @@
"args": {
},
"customPluginName": "@shopify/store",
"description": "Lists stores in a Shopify organization available to the current CLI account.\n\nWhen more than one organization is available, the command prompts you to pick one unless you provide `--organization-id`. In that case, `--organization-id` is required in non-interactive environments.\n\nRun `<%= config.bin %> organization list` to find organization IDs.",
"description": "Lists stores in a Shopify organization available to the current CLI account.\n\nWhen more than one organization is available, the command prompts you to pick one unless you provide `--organization-id`. In that case, `--organization-id` is required in non-interactive environments.\n\nRun `<%= config.bin %> organization list` to find organization IDs.\n\nOutput from `--json` conforms to the `StoreListResult` schema.\n\nUse `--json-schema` to print the schema directly:\n\n```ts\ninterface StoreListResult {\n stores: StoreListEntry[]\n organization?: StoreListOrganization\n notice?: string\n truncated?: boolean\n}\n\ninterface StoreListEntry {\n id?: string\n store: string\n createdAt: string\n organizationId: string\n organizationName: string\n name?: string\n type?: string\n}\n\ninterface StoreListOrganization {\n id: string\n name: string\n}\n```",
"descriptionWithMarkdown": "Lists stores in a Shopify organization available to the current CLI account.\n\nWhen more than one organization is available, the command prompts you to pick one unless you provide `--organization-id`. In that case, `--organization-id` is required in non-interactive environments.\n\nRun `<%= config.bin %> organization list` to find organization IDs.",
"examples": [
"<%= config.bin %> <%= command.id %>",
Expand Down
9 changes: 6 additions & 3 deletions packages/store/src/cli/commands/store/info.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import StoreInfo from './info.js'
import {getStoreInfo} from '../../services/store/info/index.js'
import {renderStoreInfoResult} from '../../services/store/info/result.js'
import {storeInfoJsonOutputSchema} from '../../services/store/info/types.js'
import {beforeEach, describe, expect, test, vi} from 'vitest'

vi.mock('../../services/store/info/index.js')
Expand All @@ -18,9 +19,7 @@ describe('store info command', () => {
test('passes the store flag through to the service', async () => {
await StoreInfo.run(['--store', 'shop.myshopify.com'])

expect(getStoreInfo).toHaveBeenCalledWith({
store: 'shop.myshopify.com',
})
expect(getStoreInfo).toHaveBeenCalledWith({store: 'shop.myshopify.com'})
expect(renderStoreInfoResult).toHaveBeenCalledWith(
expect.objectContaining({subdomain: 'shop.myshopify.com'}),
'text',
Expand All @@ -37,4 +36,8 @@ describe('store info command', () => {
expect(StoreInfo.flags.store).toBeDefined()
expect(StoreInfo.flags.json).toBeDefined()
})

test('exposes the JSON output schema', () => {
expect(StoreInfo.jsonOutputSchema).toBe(storeInfoJsonOutputSchema)
})
})
5 changes: 5 additions & 0 deletions packages/store/src/cli/commands/store/info.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {getStoreInfo} from '../../services/store/info/index.js'
import {renderStoreInfoResult} from '../../services/store/info/result.js'
import {storeInfoJsonOutputSchema} from '../../services/store/info/types.js'
import StoreCommand from '../../utilities/store-command.js'
import {storeFlags} from '../../flags.js'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
Expand All @@ -26,6 +27,10 @@ Use \`--json\` for machine-readable output.`
store: storeFlags.store,
}

static get jsonOutputSchema() {
return storeInfoJsonOutputSchema
}

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

Expand Down
17 changes: 11 additions & 6 deletions packages/store/src/cli/commands/store/list.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import StoreList from './list.js'
import {listStores} from '../../services/store/list.js'
import {writeStoreListResult} from '../../services/store/list/result.js'
import {presentStoreListResult} from '../../services/store/list/result.js'
import {storeListJsonOutputSchema} from '../../services/store/list/types.js'
import {describe, expect, test, vi} from 'vitest'

vi.mock('../../services/store/list.js')
Expand All @@ -9,34 +10,38 @@ vi.mock('../../services/store/attribution.js')

describe('store list command', () => {
test('runs the list service and writes text output by default', async () => {
vi.mocked(listStores).mockResolvedValue({stores: [], source: 'organization'})
vi.mocked(listStores).mockResolvedValue({stores: []})

await StoreList.run([])

expect(listStores).toHaveBeenCalledWith({organizationId: undefined})
expect(writeStoreListResult).toHaveBeenCalledWith({stores: [], source: 'organization'}, 'text')
expect(presentStoreListResult).toHaveBeenCalledWith({stores: []}, 'text')
})

test('passes the organization id through to the list service', async () => {
vi.mocked(listStores).mockResolvedValue({stores: [], source: 'organization'})
vi.mocked(listStores).mockResolvedValue({stores: []})

await StoreList.run(['--organization-id', '1234567'])

expect(listStores).toHaveBeenCalledWith({organizationId: 1234567})
})

test('writes json output when requested', async () => {
vi.mocked(listStores).mockResolvedValue({stores: [], source: 'organization'})
vi.mocked(listStores).mockResolvedValue({stores: []})

await StoreList.run(['--json'])

expect(listStores).toHaveBeenCalledWith({organizationId: undefined})
expect(writeStoreListResult).toHaveBeenCalledWith({stores: [], source: 'organization'}, 'json')
expect(presentStoreListResult).toHaveBeenCalledWith({stores: []}, 'json')
})

test('defines the expected flags', () => {
expect(StoreList.flags.json).toBeDefined()
expect(StoreList.flags['organization-id']).toBeDefined()
expect(StoreList.flags).not.toHaveProperty('from')
})

test('exposes the JSON output schema', () => {
expect(StoreList.jsonOutputSchema).toBe(storeListJsonOutputSchema)
})
})
9 changes: 7 additions & 2 deletions packages/store/src/cli/commands/store/list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {listStores} from '../../services/store/list.js'
import {writeStoreListResult} from '../../services/store/list/result.js'
import {presentStoreListResult} from '../../services/store/list/result.js'
import {storeListJsonOutputSchema} from '../../services/store/list/types.js'
import {storeFlags} from '../../flags.js'
import StoreCommand from '../../utilities/store-command.js'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
Expand Down Expand Up @@ -31,10 +32,14 @@ Run \`<%= config.bin %> organization list\` to find organization IDs.`
}),
}

static get jsonOutputSchema() {
return storeListJsonOutputSchema
}

public async run(): Promise<void> {
const {flags} = await this.parse(StoreList)
const result = await listStores({organizationId: flags['organization-id']})

writeStoreListResult(result, flags.json ? 'json' : 'text')
presentStoreListResult(result, flags.json ? 'json' : 'text')
}
}
2 changes: 2 additions & 0 deletions packages/store/src/cli/services/store/info/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {getStoreInfo} from './index.js'
import {storeInfoJsonOutputSchema} from './types.js'
import {StoreLookupStoreNotFoundError, fetchDestinationsContext} from '../../../utilities/store-lookup/destinations.js'
import {fetchOrganizationShop} from '../../../utilities/store-lookup/organization-shop.js'
import {STORE_AUTH_APP_CLIENT_ID} from '../auth/config.js'
Expand Down Expand Up @@ -149,6 +150,7 @@ describe('getStoreInfo', () => {
featurePreview: 'extended_variants',
adminUrl: 'https://admin.shopify.com/store/shop',
})
expect(storeInfoJsonOutputSchema.validate(result)).toEqual(result)
})

test('returns fresh access and save URLs for locally stored preview stores', async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/store/src/cli/services/store/info/result.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {storeInfoJsonOutputSchema, type StoreInfoResult, type StoreInfoStoreOwner} from './types.js'
import {outputResult} from '@shopify/cli-kit/node/output'
import {renderInfo, type InlineToken, type LinkToken} from '@shopify/cli-kit/node/ui'
import {capitalizeWords} from '@shopify/cli-kit/common/string'
import type {StoreInfoResult, StoreInfoStoreOwner} from './types.js'

type StoreInfoOutputFormat = 'text' | 'json'

export function renderStoreInfoResult(result: StoreInfoResult, format: StoreInfoOutputFormat): void {
if (format === 'json') {
outputResult(JSON.stringify(result, null, 2))
outputResult(storeInfoJsonOutputSchema.encode(result))
return
}
const actions = storeActions(result)
Expand Down
55 changes: 32 additions & 23 deletions packages/store/src/cli/services/store/info/types.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
export interface StoreInfoStoreOwner {
name?: string
email?: string
}
import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema'
import {zod} from '@shopify/cli-kit/node/schema'

export interface StoreInfoResult {
id?: string
displayName?: string
subdomain: string
organizationId?: string
organizationName?: string
storeOwner?: StoreInfoStoreOwner
type?: string
// Admin API public display name for store-auth stores, or public plan handle for BP-backed stores.
plan?: string
featurePreview?: string
adminUrl?: string
accessUrl?: string
saveUrl?: string
// Preapproved Admin API access scopes for the store (currently only preview stores, which
// cache the scopes granted at creation time). Preview stores aren't a logged-in experience, so
// there's no way to grant additional scopes later.
authScopes?: string[]
}
const StoreInfoStoreOwnerSchema = zod.object({
name: zod.string().optional(),
email: zod.string().optional(),
})

export const storeInfoJsonOutputSchema = defineJsonOutputSchema({
name: 'StoreInfoResult',
schema: zod.object({
id: zod.string().optional(),
displayName: zod.string().optional(),
subdomain: zod.string(),
organizationId: zod.string().optional(),
organizationName: zod.string().optional(),
storeOwner: StoreInfoStoreOwnerSchema.optional(),
type: zod.string().optional(),
// Admin API public display name for store-auth stores, or public plan handle for BP-backed stores.
plan: zod.string().optional(),
featurePreview: zod.string().optional(),
adminUrl: zod.string().optional(),
accessUrl: zod.string().optional(),
saveUrl: zod.string().optional(),
// Preapproved Admin API access scopes for preview stores. Preview stores aren't a logged-in
// experience, so there's no way to grant additional scopes later.
authScopes: zod.array(zod.string()).optional(),
}),
definitions: {StoreInfoStoreOwner: StoreInfoStoreOwnerSchema},
})

export type StoreInfoStoreOwner = zod.infer<typeof StoreInfoStoreOwnerSchema>
export type StoreInfoResult = InferJsonOutputSchema<typeof storeInfoJsonOutputSchema>
6 changes: 3 additions & 3 deletions packages/store/src/cli/services/store/list.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {listStores} from './list.js'
import * as bpSource from './list/bp-source.js'
import {storeListJsonOutputSchema} from './list/types.js'
import {describe, expect, test, vi} from 'vitest'
import {ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session'
import {AbortError} from '@shopify/cli-kit/node/error'
Expand Down Expand Up @@ -42,9 +43,9 @@ describe('listStores', () => {
expect(renderAutocompletePrompt).not.toHaveBeenCalled()
expect(result).toEqual({
stores: [orgEntry],
source: 'organization',
organization: {id: '1234', name: 'Acme'},
})
expect(storeListJsonOutputSchema.validate(result)).toEqual(result)
})

test('uses the requested organization id when provided', async () => {
Expand Down Expand Up @@ -107,7 +108,6 @@ describe('listStores', () => {

expect(result).toEqual({
stores: [],
source: 'organization',
notice: "Couldn't resolve a Shopify account for the current CLI session.",
})
})
Expand All @@ -117,7 +117,7 @@ describe('listStores', () => {

const result = await listStores()

expect(result).toEqual({stores: [], source: 'organization'})
expect(result).toEqual({stores: []})
})

test('propagates store listing failures', async () => {
Expand Down
8 changes: 3 additions & 5 deletions packages/store/src/cli/services/store/list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {listBusinessPlatformStores} from './list/bp-source.js'
import {STORE_LIST_LIMIT} from './list/constants.js'
import {type ListStoresResult, type StoreListEntry, type StoreListOrganization} from './list/types.js'
import {type StoreListEntry, type StoreListOrganization, type StoreListResult} from './list/types.js'
import {AbortError} from '@shopify/cli-kit/node/error'
import {ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session'
import {isTTY, renderAutocompletePrompt} from '@shopify/cli-kit/node/ui'
Expand All @@ -10,20 +10,19 @@ interface ListStoresOptions {
organizationId?: number
}

export async function listStores(options: ListStoresOptions = {}): Promise<ListStoresResult> {
export async function listStores(options: ListStoresOptions = {}): Promise<StoreListResult> {
const token = await ensureAuthenticatedBusinessPlatform()
const organizationsResult = await fetchOrganizationsWithAccessInfo(token)

if (!organizationsResult.currentUserResolved) {
return {
stores: [],
source: 'organization',
notice: "Couldn't resolve a Shopify account for the current CLI session.",
}
}

if (organizationsResult.organizations.length === 0) {
return {stores: [], source: 'organization'}
return {stores: []}
}

if (!options.organizationId && organizationsResult.organizations.length > 1 && !isTTY()) {
Expand All @@ -43,7 +42,6 @@ export async function listStores(options: ListStoresOptions = {}): Promise<ListS

return {
stores,
source: 'organization',
organization: storeListOrganization(selectedOrganization),
...(truncated ? {truncated: true} : {}),
}
Expand Down
Loading
Loading