Skip to content

Commit 11125b5

Browse files
Add store delete command
Assisted-By: devx/921acb10-d127-42c8-8a9a-0314a325d188
1 parent e98534a commit 11125b5

12 files changed

Lines changed: 852 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/store': patch
3+
---
4+
5+
Wait for development store cancellation confirmation after deletion is requested.

packages/cli/oclif.manifest.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6770,6 +6770,75 @@
67706770
"strict": true,
67716771
"summary": "Create a preview Shopify store."
67726772
},
6773+
"store:delete": {
6774+
"aliases": [
6775+
],
6776+
"args": {
6777+
},
6778+
"customPluginName": "@shopify/store",
6779+
"description": "Deletes a development store from your organization.",
6780+
"descriptionWithMarkdown": "Deletes a development store from your organization.",
6781+
"enableJsonFlag": false,
6782+
"examples": [
6783+
"<%= config.bin %> <%= command.id %> --store shop.myshopify.com --organization-id 1234567",
6784+
"<%= config.bin %> <%= command.id %> --store shop.myshopify.com --organization-id 1234567 --json"
6785+
],
6786+
"flags": {
6787+
"json": {
6788+
"allowNo": false,
6789+
"char": "j",
6790+
"description": "Output the result as JSON. Automatically disables color output.",
6791+
"env": "SHOPIFY_FLAG_JSON",
6792+
"hidden": false,
6793+
"name": "json",
6794+
"type": "boolean"
6795+
},
6796+
"no-color": {
6797+
"allowNo": false,
6798+
"description": "Disable color output.",
6799+
"env": "SHOPIFY_FLAG_NO_COLOR",
6800+
"hidden": false,
6801+
"name": "no-color",
6802+
"type": "boolean"
6803+
},
6804+
"organization-id": {
6805+
"description": "The numeric organization ID. Auto-selects if you belong to a single organization.",
6806+
"env": "SHOPIFY_FLAG_ORGANIZATION_ID",
6807+
"hasDynamicHelp": false,
6808+
"multiple": false,
6809+
"name": "organization-id",
6810+
"type": "option"
6811+
},
6812+
"store": {
6813+
"char": "s",
6814+
"description": "The myshopify.com domain of the store.",
6815+
"env": "SHOPIFY_FLAG_STORE",
6816+
"hasDynamicHelp": false,
6817+
"multiple": false,
6818+
"name": "store",
6819+
"required": true,
6820+
"type": "option"
6821+
},
6822+
"verbose": {
6823+
"allowNo": false,
6824+
"description": "Increase the verbosity of the output.",
6825+
"env": "SHOPIFY_FLAG_VERBOSE",
6826+
"hidden": false,
6827+
"name": "verbose",
6828+
"type": "boolean"
6829+
}
6830+
},
6831+
"hasDynamicHelp": false,
6832+
"hidden": true,
6833+
"hiddenAliases": [
6834+
],
6835+
"id": "store:delete",
6836+
"pluginAlias": "@shopify/cli",
6837+
"pluginName": "@shopify/cli",
6838+
"pluginType": "core",
6839+
"strict": true,
6840+
"summary": "Delete a development store."
6841+
},
67736842
"store:execute": {
67746843
"aliases": [
67756844
],
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* eslint-disable @typescript-eslint/consistent-type-definitions */
2+
import * as Types from './types.js'
3+
4+
import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'
5+
6+
export type DeleteAppDevelopmentStoreMutationVariables = Types.Exact<{
7+
storeFqdn: Types.Scalars['String']['input']
8+
}>
9+
10+
export type DeleteAppDevelopmentStoreMutation = {
11+
deleteAppDevelopmentStore: {
12+
success?: boolean | null
13+
userErrors?: {code?: string | null; field: string[]; message: string}[] | null
14+
}
15+
}
16+
17+
export const DeleteAppDevelopmentStore = {
18+
kind: 'Document',
19+
definitions: [
20+
{
21+
kind: 'OperationDefinition',
22+
operation: 'mutation',
23+
name: {kind: 'Name', value: 'DeleteAppDevelopmentStore'},
24+
variableDefinitions: [
25+
{
26+
kind: 'VariableDefinition',
27+
variable: {kind: 'Variable', name: {kind: 'Name', value: 'storeFqdn'}},
28+
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}},
29+
},
30+
],
31+
selectionSet: {
32+
kind: 'SelectionSet',
33+
selections: [
34+
{
35+
kind: 'Field',
36+
name: {kind: 'Name', value: 'deleteAppDevelopmentStore'},
37+
arguments: [
38+
{
39+
kind: 'Argument',
40+
name: {kind: 'Name', value: 'storeFqdn'},
41+
value: {kind: 'Variable', name: {kind: 'Name', value: 'storeFqdn'}},
42+
},
43+
],
44+
selectionSet: {
45+
kind: 'SelectionSet',
46+
selections: [
47+
{kind: 'Field', name: {kind: 'Name', value: 'success'}},
48+
{
49+
kind: 'Field',
50+
name: {kind: 'Name', value: 'userErrors'},
51+
selectionSet: {
52+
kind: 'SelectionSet',
53+
selections: [
54+
{kind: 'Field', name: {kind: 'Name', value: 'code'}},
55+
{kind: 'Field', name: {kind: 'Name', value: 'field'}},
56+
{kind: 'Field', name: {kind: 'Name', value: 'message'}},
57+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
58+
],
59+
},
60+
},
61+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
62+
],
63+
},
64+
},
65+
],
66+
},
67+
},
68+
],
69+
} as unknown as DocumentNode<DeleteAppDevelopmentStoreMutation, DeleteAppDevelopmentStoreMutationVariables>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* eslint-disable @typescript-eslint/consistent-type-definitions */
2+
import * as Types from './types.js'
3+
4+
import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'
5+
6+
export type OrganizationAccessibleShopQueryVariables = Types.Exact<{
7+
id: Types.Scalars['ShopifyShopID']['input']
8+
}>
9+
10+
export type OrganizationAccessibleShopQuery = {
11+
organization?: {
12+
accessibleShop?: {shopifyShopId?: string | null; planName?: string | null; storeType?: Types.Store | null} | null
13+
} | null
14+
}
15+
16+
export const OrganizationAccessibleShop = {
17+
kind: 'Document',
18+
definitions: [
19+
{
20+
kind: 'OperationDefinition',
21+
operation: 'query',
22+
name: {kind: 'Name', value: 'OrganizationAccessibleShop'},
23+
variableDefinitions: [
24+
{
25+
kind: 'VariableDefinition',
26+
variable: {kind: 'Variable', name: {kind: 'Name', value: 'id'}},
27+
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ShopifyShopID'}}},
28+
},
29+
],
30+
selectionSet: {
31+
kind: 'SelectionSet',
32+
selections: [
33+
{
34+
kind: 'Field',
35+
name: {kind: 'Name', value: 'organization'},
36+
selectionSet: {
37+
kind: 'SelectionSet',
38+
selections: [
39+
{
40+
kind: 'Field',
41+
name: {kind: 'Name', value: 'accessibleShop'},
42+
arguments: [
43+
{
44+
kind: 'Argument',
45+
name: {kind: 'Name', value: 'id'},
46+
value: {kind: 'Variable', name: {kind: 'Name', value: 'id'}},
47+
},
48+
],
49+
selectionSet: {
50+
kind: 'SelectionSet',
51+
selections: [
52+
{kind: 'Field', name: {kind: 'Name', value: 'shopifyShopId'}},
53+
{kind: 'Field', name: {kind: 'Name', value: 'planName'}},
54+
{kind: 'Field', name: {kind: 'Name', value: 'storeType'}},
55+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
56+
],
57+
},
58+
},
59+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
60+
],
61+
},
62+
},
63+
],
64+
},
65+
},
66+
],
67+
} as unknown as DocumentNode<OrganizationAccessibleShopQuery, OrganizationAccessibleShopQueryVariables>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
mutation DeleteAppDevelopmentStore($storeFqdn: String!) {
2+
deleteAppDevelopmentStore(storeFqdn: $storeFqdn) {
3+
success
4+
userErrors {
5+
code
6+
field
7+
message
8+
}
9+
}
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
query OrganizationAccessibleShop($id: ShopifyShopID!) {
2+
organization {
3+
accessibleShop(id: $id) {
4+
shopifyShopId
5+
planName
6+
storeType
7+
}
8+
}
9+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import StoreDelete from './delete.js'
2+
import {deleteDevStore} from '../../services/store/delete/dev.js'
3+
import {selectOrg} from '@shopify/organizations'
4+
import {AbortError} from '@shopify/cli-kit/node/error'
5+
import {outputResult} from '@shopify/cli-kit/node/output'
6+
import {terminalSupportsPrompting} from '@shopify/cli-kit/node/system'
7+
import {describe, expect, test, vi, beforeEach} from 'vitest'
8+
9+
vi.mock('../../services/store/delete/dev.js')
10+
vi.mock('@shopify/cli-kit/node/system')
11+
12+
vi.mock('@shopify/organizations', () => ({
13+
selectOrg: vi.fn(),
14+
}))
15+
16+
vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => {
17+
const actual: Record<string, unknown> = await importOriginal()
18+
return {
19+
...actual,
20+
outputResult: vi.fn(),
21+
}
22+
})
23+
24+
const defaultOrg = {id: '12345', businessName: 'Test Org'}
25+
26+
beforeEach(() => {
27+
vi.mocked(selectOrg).mockResolvedValue(defaultOrg)
28+
vi.mocked(terminalSupportsPrompting).mockReturnValue(true)
29+
})
30+
31+
describe('store delete command', () => {
32+
test('resolves the organization and passes parsed flags through to the service', async () => {
33+
await StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345'])
34+
35+
expect(selectOrg).toHaveBeenCalledWith('12345')
36+
expect(deleteDevStore).toHaveBeenCalledWith({
37+
store: 'my-store.myshopify.com',
38+
organization: defaultOrg,
39+
json: false,
40+
})
41+
})
42+
43+
test('normalizes the store flag before passing it to the service', async () => {
44+
await StoreDelete.run(['--store', 'my-store', '--organization-id', '12345'])
45+
46+
expect(deleteDevStore).toHaveBeenCalledWith(expect.objectContaining({store: 'my-store.myshopify.com'}))
47+
})
48+
49+
test('passes json flag through to the service', async () => {
50+
await StoreDelete.run(['--store', 'my-store.myshopify.com', '--json', '--organization-id', '12345'])
51+
52+
expect(deleteDevStore).toHaveBeenCalledWith({
53+
store: 'my-store.myshopify.com',
54+
organization: defaultOrg,
55+
json: true,
56+
})
57+
})
58+
59+
test('prompts for the organization when --organization-id is omitted in an interactive environment', async () => {
60+
await StoreDelete.run(['--store', 'my-store.myshopify.com'])
61+
62+
expect(selectOrg).toHaveBeenCalledWith(undefined)
63+
expect(deleteDevStore).toHaveBeenCalledWith(expect.objectContaining({organization: defaultOrg}))
64+
})
65+
66+
test('fails in a non-interactive environment when --organization-id is missing', async () => {
67+
vi.mocked(terminalSupportsPrompting).mockReturnValue(false)
68+
const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
69+
throw new Error('process.exit')
70+
}) as never)
71+
72+
await expect(StoreDelete.run(['--store', 'my-store.myshopify.com'])).rejects.toThrow()
73+
expect(deleteDevStore).not.toHaveBeenCalled()
74+
75+
mockExit.mockRestore()
76+
})
77+
78+
test('defines the expected flags', () => {
79+
expect(StoreDelete.flags.store).toBeDefined()
80+
expect(StoreDelete.flags['organization-id']).toBeDefined()
81+
expect(StoreDelete.flags.json).toBeDefined()
82+
})
83+
84+
test('outputs structured JSON error when --json is active and service throws AbortError', async () => {
85+
vi.mocked(deleteDevStore).mockRejectedValueOnce(new AbortError('Something went wrong'))
86+
const mockExit = vi.spyOn(process, 'exit').mockImplementation((() => {
87+
throw new Error('process.exit')
88+
}) as never)
89+
90+
await expect(
91+
StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345', '--json']),
92+
).rejects.toThrow('process.exit')
93+
94+
const call = vi.mocked(outputResult).mock.calls[0]![0] as string
95+
const parsed = JSON.parse(call)
96+
expect(parsed).toEqual({
97+
error: true,
98+
message: 'Something went wrong',
99+
nextSteps: [],
100+
exitCode: 1,
101+
})
102+
expect(mockExit).toHaveBeenCalledWith(1)
103+
104+
mockExit.mockRestore()
105+
})
106+
107+
test('does not output JSON for non-AbortError even when --json is active', async () => {
108+
vi.mocked(deleteDevStore).mockRejectedValueOnce(new Error('unexpected'))
109+
110+
await expect(
111+
StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345', '--json']),
112+
).rejects.toThrow()
113+
expect(vi.mocked(outputResult)).not.toHaveBeenCalled()
114+
})
115+
116+
test('does not output JSON for AbortError when --json is not active', async () => {
117+
vi.mocked(deleteDevStore).mockRejectedValueOnce(new AbortError('Something went wrong'))
118+
119+
await expect(StoreDelete.run(['--store', 'my-store.myshopify.com', '--organization-id', '12345'])).rejects.toThrow()
120+
expect(vi.mocked(outputResult)).not.toHaveBeenCalled()
121+
})
122+
})

0 commit comments

Comments
 (0)