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
5 changes: 5 additions & 0 deletions .changeset/store-list-filter-by-store-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/store': minor
---

Add `store list --type` to list only dev, production, client transfer, or collaborator stores
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -5356,6 +5356,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_ORGANIZATION_ID"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/store-list.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--type <value>",
"value": "string",
"description": "List only stores of this type.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_STORE_TYPE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/store-list.interface.ts",
"syntaxKind": "PropertySignature",
Expand All @@ -5375,7 +5384,7 @@
"environmentValue": "SHOPIFY_FLAG_JSON"
}
],
"value": "export interface storelist {\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The numeric organization ID. Auto-selects if you belong to a single organization. Required if non interactive when more than one organization is available.\n * @environment SHOPIFY_FLAG_ORGANIZATION_ID\n */\n '--organization-id <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface storelist {\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The numeric organization ID. Auto-selects if you belong to a single organization. Required if non interactive when more than one organization is available.\n * @environment SHOPIFY_FLAG_ORGANIZATION_ID\n */\n '--organization-id <value>'?: string\n\n /**\n * List only stores of this type.\n * @environment SHOPIFY_FLAG_STORE_TYPE\n */\n '--type <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"storeopen": {
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3872,7 +3872,8 @@ List stores in a Shopify organization.

```
USAGE
$ shopify store list [-j] [--no-color] [--organization-id <value>] [--verbose]
$ shopify store list [-j] [--no-color] [--organization-id <value>] [--type
dev|production|client_transfer|collaborator] [--verbose]

FLAGS
-j, --json
Expand All @@ -3888,6 +3889,11 @@ FLAGS
more than one organization is available.
[env: SHOPIFY_FLAG_ORGANIZATION_ID]

--type=<option>
List only stores of this type.
[env: SHOPIFY_FLAG_STORE_TYPE]
<options: dev|production|client_transfer|collaborator>

--verbose
Increase the verbosity of the output. May include sensitive data.
[env: SHOPIFY_FLAG_VERBOSE]
Expand All @@ -3907,6 +3913,8 @@ EXAMPLES

$ shopify store list --organization-id 1234567

$ shopify store list --type dev

$ shopify store list --json
```

Expand Down
15 changes: 15 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7710,6 +7710,7 @@
"examples": [
"<%= config.bin %> <%= command.id %>",
"<%= config.bin %> <%= command.id %> --organization-id 1234567",
"<%= config.bin %> <%= command.id %> --type dev",
"<%= config.bin %> <%= command.id %> --json"
],
"flags": {
Expand Down Expand Up @@ -7738,6 +7739,20 @@
"name": "organization-id",
"type": "option"
},
"type": {
"description": "List only stores of this type.",
"env": "SHOPIFY_FLAG_STORE_TYPE",
"hasDynamicHelp": false,
"multiple": false,
"name": "type",
"options": [
"dev",
"production",
"client_transfer",
"collaborator"
],
"type": "option"
},
"verbose": {
"allowNo": false,
"description": "Increase the verbosity of the output. May include sensitive data.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/co

export type ListAccessibleShopsQueryVariables = Types.Exact<{
first: Types.Scalars['Int']['input']
filters?: Types.InputMaybe<Types.ShopFilterInput[] | Types.ShopFilterInput>
}>

export type ListAccessibleShopsQuery = {
Expand Down Expand Up @@ -41,6 +42,14 @@ export const ListAccessibleShops = {
variable: {kind: 'Variable', name: {kind: 'Name', value: 'first'}},
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'Int'}}},
},
{
kind: 'VariableDefinition',
variable: {kind: 'Variable', name: {kind: 'Name', value: 'filters'}},
type: {
kind: 'ListType',
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ShopFilterInput'}}},
},
},
],
selectionSet: {
kind: 'SelectionSet',
Expand Down Expand Up @@ -70,31 +79,7 @@ export const ListAccessibleShops = {
{
kind: 'Argument',
name: {kind: 'Name', value: 'filters'},
value: {
kind: 'ListValue',
values: [
{
kind: 'ObjectValue',
fields: [
{
kind: 'ObjectField',
name: {kind: 'Name', value: 'field'},
value: {kind: 'EnumValue', value: 'STORE_STATUS'},
},
{
kind: 'ObjectField',
name: {kind: 'Name', value: 'operator'},
value: {kind: 'EnumValue', value: 'EQUALS'},
},
{
kind: 'ObjectField',
name: {kind: 'Name', value: 'value'},
value: {kind: 'StringValue', value: 'active', block: false},
},
],
},
],
},
value: {kind: 'Variable', name: {kind: 'Name', value: 'filters'}},
},
],
selectionSet: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,62 @@ export type Scalars = {
URL: { input: string; output: string; }
};

/** Operators for filter queries. */
export type Operator =
/** Between operator. */
| 'BETWEEN'
/** Equals operator. */
| 'EQUALS'
/**
* In operator. Accepts a comma-separated string of values (e.g.
* "value1,value2,value3"). Not supported for all filter fields.
*/
| 'IN';

/** Field options for filtering shop queries. */
export type ShopFilterField =
/**
* The phase of the client transfer process. Requires
* `store_type=client_transfer`. Values: `in_development`, `pending`, `completed`.
*/
| 'CLIENT_TRANSFER_PHASE'
/**
* The status of the collaborator relationship. Requires
* `store_type=collaborator`. Values: `active`, `access_pending`, `expired`.
*/
| 'COLLABORATOR_RELATIONSHIP_STATUS'
/** The GID of the counterpart organization. Requires `store_type=client_transfer` or `store_type=collaborator`. */
| 'COUNTERPART_ORGANIZATION_ID'
/** The GID of the owning organization of the shop. */
| 'OWNER_ORGANIZATION_ID'
/**
* The plan of the shop. Values: `basic`, `grow`, `plus`, `frozen`, `advanced`,
* `inactive`, `cancelled`, `client_transfer`, `plus_client_transfer`,
* `development_legacy`, `custom`, `fraudulent`, `staff`, `trial`,
* `plus_development`, `retail`, `shop_pay_commerce_components`, `non_profit`.
* With the `In` operator, use raw plan names (e.g. "professional,shopify_plus").
*/
| 'SHOP_PLAN'
/** The active/inactive status of the shop. Values: `active`, `inactive`. */
| 'STORE_STATUS'
/**
* The type of the shop. Does not support the `In` operator. Values:
* `development`, `production`, `app_development`, `development_superset`,
* `client_transfer`, `collaborator`.
*/
| 'STORE_TYPE';

/**
* Represents a single filter option for shop queries. When using the `In`
* operator, pass a comma-separated string of values (e.g. "value1,value2").
* Maximum 20 values.
*/
export type ShopFilterInput = {
field: ShopFilterField;
operator: Operator;
value: Scalars['String']['input'];
};

export type Store =
| 'APP_DEVELOPMENT'
| 'CLIENT_TRANSFER'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
query ListAccessibleShops($first: Int!) {
query ListAccessibleShops($first: Int!, $filters: [ShopFilterInput!]) {
organization {
id
name
accessibleShops(
first: $first
sort: SHOP_CREATED_AT_DESC
filters: [{field: STORE_STATUS, operator: EQUALS, value: "active"}]
) {
accessibleShops(first: $first, sort: SHOP_CREATED_AT_DESC, filters: $filters) {
edges {
node {
id
Expand Down
15 changes: 12 additions & 3 deletions packages/store/src/cli/commands/store/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('store list command', () => {

await StoreList.run([])

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

Expand All @@ -22,21 +22,30 @@ describe('store list command', () => {

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

expect(listStores).toHaveBeenCalledWith({organizationId: 1234567})
expect(listStores).toHaveBeenCalledWith({organizationId: 1234567, storeType: undefined})
})

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

await StoreList.run(['--type', 'client_transfer'])

expect(listStores).toHaveBeenCalledWith({organizationId: undefined, storeType: 'client_transfer'})
})

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

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

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

test('defines the expected flags', () => {
expect(StoreList.flags.json).toBeDefined()
expect(StoreList.flags['organization-id']).toBeDefined()
expect(StoreList.flags.type?.options).toEqual(['dev', 'production', 'client_transfer', 'collaborator'])
expect(StoreList.flags).not.toHaveProperty('from')
})
})
13 changes: 12 additions & 1 deletion 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 {storeTypeFilters, type StoreTypeFilter} from '../../services/store/store-type.js'
import {storeFlags} from '../../flags.js'
import StoreCommand from '../../utilities/store-command.js'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
Expand All @@ -19,6 +20,7 @@ Run \`<%= config.bin %> organization list\` to find organization IDs.`
static examples = [
'<%= config.bin %> <%= command.id %>',
'<%= config.bin %> <%= command.id %> --organization-id 1234567',
'<%= config.bin %> <%= command.id %> --type dev',
'<%= config.bin %> <%= command.id %> --json',
]

Expand All @@ -29,11 +31,20 @@ Run \`<%= config.bin %> organization list\` to find organization IDs.`
description: `${storeFlags['organization-id'].description} Required if non interactive when more than one organization is available.`,
env: 'SHOPIFY_FLAG_ORGANIZATION_ID',
}),
type: Flags.string({
description: 'List only stores of this type.',
options: storeTypeFilters,
env: 'SHOPIFY_FLAG_STORE_TYPE',
}),
}

public async run(): Promise<void> {
const {flags} = await this.parse(StoreList)
const result = await listStores({organizationId: flags['organization-id']})
const result = await listStores({
organizationId: flags['organization-id'],
// oclif validates the value against `storeTypeFilters`, so the cast is safe.
storeType: flags.type as StoreTypeFilter | undefined,
})

writeStoreListResult(result, flags.json ? 'json' : 'text')
}
Expand Down
41 changes: 38 additions & 3 deletions packages/store/src/cli/services/store/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ describe('listStores', () => {

const result = await listStores()

expect(bpSource.listBusinessPlatformStores).toHaveBeenCalledWith({token: 'bp-token', organization: acme})
expect(bpSource.listBusinessPlatformStores).toHaveBeenCalledWith({
token: 'bp-token',
organization: acme,
storeType: undefined,
})
expect(renderAutocompletePrompt).not.toHaveBeenCalled()
expect(result).toEqual({
stores: [orgEntry],
Expand All @@ -47,13 +51,40 @@ describe('listStores', () => {
})
})

test('passes the requested store type to the source and echoes it in the result', async () => {
mockOrganizations([acme])
vi.spyOn(bpSource, 'listBusinessPlatformStores').mockResolvedValue({entries: [orgEntry], hasMore: false})

const result = await listStores({storeType: 'dev'})

expect(bpSource.listBusinessPlatformStores).toHaveBeenCalledWith({
token: 'bp-token',
organization: acme,
storeType: 'dev',
})
expect(result.storeType).toBe('dev')
})

test('omits the store type from the result when the listing was not filtered', async () => {
mockOrganizations([acme])
vi.spyOn(bpSource, 'listBusinessPlatformStores').mockResolvedValue({entries: [orgEntry], hasMore: false})

const result = await listStores()

expect(result).not.toHaveProperty('storeType')
})

test('uses the requested organization id when provided', async () => {
mockOrganizations([acme, beta])
vi.spyOn(bpSource, 'listBusinessPlatformStores').mockResolvedValue({entries: [], hasMore: false})

await listStores({organizationId: 5678})

expect(bpSource.listBusinessPlatformStores).toHaveBeenCalledWith({token: 'bp-token', organization: beta})
expect(bpSource.listBusinessPlatformStores).toHaveBeenCalledWith({
token: 'bp-token',
organization: beta,
storeType: undefined,
})
expect(renderAutocompletePrompt).not.toHaveBeenCalled()
})

Expand All @@ -72,7 +103,11 @@ describe('listStores', () => {
{label: 'Beta', value: '5678'},
],
})
expect(bpSource.listBusinessPlatformStores).toHaveBeenCalledWith({token: 'bp-token', organization: beta})
expect(bpSource.listBusinessPlatformStores).toHaveBeenCalledWith({
token: 'bp-token',
organization: beta,
storeType: undefined,
})
expect(result.organization).toEqual({id: '5678', name: 'Beta'})
})

Expand Down
Loading
Loading