Skip to content

Commit 437d91f

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

8 files changed

Lines changed: 517 additions & 0 deletions

File tree

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+
} | null
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: 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: 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+
})
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import {deleteDevStore} from '../../services/store/delete/dev.js'
2+
import {storeFlags} from '../../flags.js'
3+
import {selectOrg} from '@shopify/organizations'
4+
import Command from '@shopify/cli-kit/node/base-command'
5+
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
6+
import {AbortError} from '@shopify/cli-kit/node/error'
7+
import {outputResult} from '@shopify/cli-kit/node/output'
8+
9+
export default class StoreDelete extends Command {
10+
static hidden = true
11+
12+
static summary = 'Delete a development store.'
13+
14+
static descriptionWithMarkdown = 'Deletes a development store from your organization.'
15+
16+
static description = this.descriptionWithoutMarkdown()
17+
18+
static examples = [
19+
'<%= config.bin %> <%= command.id %> --store shop.myshopify.com --organization-id 1234567',
20+
'<%= config.bin %> <%= command.id %> --store shop.myshopify.com --organization-id 1234567 --json',
21+
]
22+
23+
static flags = {
24+
...globalFlags,
25+
...jsonFlag,
26+
store: storeFlags.store,
27+
'organization-id': storeFlags['organization-id'],
28+
}
29+
30+
async run(): Promise<void> {
31+
const {flags} = await this.parse(StoreDelete)
32+
this.failMissingNonTTYFlags(flags, ['store', 'organization-id'])
33+
34+
const organization = await selectOrg(flags['organization-id']?.toString())
35+
36+
try {
37+
await deleteDevStore({
38+
store: flags.store,
39+
organization,
40+
json: flags.json,
41+
})
42+
} catch (error) {
43+
if (flags.json && error instanceof AbortError) {
44+
outputResult(
45+
JSON.stringify(
46+
{
47+
error: true,
48+
message: error.message,
49+
nextSteps: error.nextSteps ?? [],
50+
exitCode: 1,
51+
},
52+
null,
53+
2,
54+
),
55+
)
56+
process.exit(1)
57+
}
58+
throw error
59+
}
60+
}
61+
}

0 commit comments

Comments
 (0)