Skip to content

Commit 95b42ed

Browse files
committed
Port theme file deletes to graphQL
1 parent da30052 commit 95b42ed

File tree

11 files changed

+276
-95
lines changed

11 files changed

+276
-95
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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 ThemeFilesDeleteMutationVariables = Types.Exact<{
7+
themeId: Types.Scalars['ID']['input']
8+
files: Types.Scalars['String']['input'][] | Types.Scalars['String']['input']
9+
}>
10+
11+
export type ThemeFilesDeleteMutation = {
12+
themeFilesDelete?: {
13+
deletedThemeFiles?: {filename: string}[] | null
14+
userErrors: {
15+
filename?: string | null
16+
code?: Types.OnlineStoreThemeFilesUserErrorsCode | null
17+
message: string
18+
}[]
19+
} | null
20+
}
21+
22+
export const ThemeFilesDelete = {
23+
kind: 'Document',
24+
definitions: [
25+
{
26+
kind: 'OperationDefinition',
27+
operation: 'mutation',
28+
name: {kind: 'Name', value: 'themeFilesDelete'},
29+
variableDefinitions: [
30+
{
31+
kind: 'VariableDefinition',
32+
variable: {kind: 'Variable', name: {kind: 'Name', value: 'themeId'}},
33+
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}},
34+
},
35+
{
36+
kind: 'VariableDefinition',
37+
variable: {kind: 'Variable', name: {kind: 'Name', value: 'files'}},
38+
type: {
39+
kind: 'NonNullType',
40+
type: {
41+
kind: 'ListType',
42+
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}},
43+
},
44+
},
45+
},
46+
],
47+
selectionSet: {
48+
kind: 'SelectionSet',
49+
selections: [
50+
{
51+
kind: 'Field',
52+
name: {kind: 'Name', value: 'themeFilesDelete'},
53+
arguments: [
54+
{
55+
kind: 'Argument',
56+
name: {kind: 'Name', value: 'themeId'},
57+
value: {kind: 'Variable', name: {kind: 'Name', value: 'themeId'}},
58+
},
59+
{
60+
kind: 'Argument',
61+
name: {kind: 'Name', value: 'files'},
62+
value: {kind: 'Variable', name: {kind: 'Name', value: 'files'}},
63+
},
64+
],
65+
selectionSet: {
66+
kind: 'SelectionSet',
67+
selections: [
68+
{
69+
kind: 'Field',
70+
name: {kind: 'Name', value: 'deletedThemeFiles'},
71+
selectionSet: {
72+
kind: 'SelectionSet',
73+
selections: [
74+
{kind: 'Field', name: {kind: 'Name', value: 'filename'}},
75+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
76+
],
77+
},
78+
},
79+
{
80+
kind: 'Field',
81+
name: {kind: 'Name', value: 'userErrors'},
82+
selectionSet: {
83+
kind: 'SelectionSet',
84+
selections: [
85+
{kind: 'Field', name: {kind: 'Name', value: 'filename'}},
86+
{kind: 'Field', name: {kind: 'Name', value: 'code'}},
87+
{kind: 'Field', name: {kind: 'Name', value: 'message'}},
88+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
89+
],
90+
},
91+
},
92+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
93+
],
94+
},
95+
},
96+
],
97+
},
98+
},
99+
],
100+
} as unknown as DocumentNode<ThemeFilesDeleteMutation, ThemeFilesDeleteMutationVariables>

packages/cli-kit/src/cli/api/graphql/admin/generated/types.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,27 @@ export type OnlineStoreThemeFilesUpsertFileInput = {
223223
filename: Scalars['String']['input']
224224
}
225225

226+
/** Possible error codes that can be returned by `OnlineStoreThemeFilesUserErrors`. */
227+
export type OnlineStoreThemeFilesUserErrorsCode =
228+
/** Access denied. */
229+
| 'ACCESS_DENIED'
230+
/** There are files with the same filename. */
231+
| 'DUPLICATE_FILE_INPUT'
232+
/** Error. */
233+
| 'ERROR'
234+
/** The file is invalid. */
235+
| 'FILE_VALIDATION_ERROR'
236+
/** The input value should be less than or equal to the maximum value allowed. */
237+
| 'LESS_THAN_OR_EQUAL_TO'
238+
/** The record with the ID used as the input value couldn't be found. */
239+
| 'NOT_FOUND'
240+
/** There are theme files with conflicts. */
241+
| 'THEME_FILES_CONFLICT'
242+
/** This action is not available on your current plan. Please upgrade to access theme editing features. */
243+
| 'THEME_LIMITED_PLAN'
244+
/** Too many updates in a short period. Please try again later. */
245+
| 'THROTTLED'
246+
226247
/** The input fields for Theme attributes to update. */
227248
export type OnlineStoreThemeInput = {
228249
/** The new name of the theme. */
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
mutation themeFilesDelete($themeId: ID!, $files: [String!]!) {
2+
themeFilesDelete(themeId: $themeId, files: $files) {
3+
deletedThemeFiles {
4+
filename
5+
}
6+
userErrors {
7+
filename
8+
code
9+
message
10+
}
11+
}
12+
}

packages/cli-kit/src/public/node/themes/api.test.ts

Lines changed: 25 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ import {
99
fetchChecksums,
1010
bulkUploadThemeAssets,
1111
AssetParams,
12-
deleteThemeAsset,
12+
deleteThemeAssets,
1313
} from './api.js'
1414
import {Operation} from './types.js'
1515
import {ThemeDelete} from '../../../cli/api/graphql/admin/generated/theme_delete.js'
1616
import {ThemeUpdate} from '../../../cli/api/graphql/admin/generated/theme_update.js'
1717
import {ThemePublish} from '../../../cli/api/graphql/admin/generated/theme_publish.js'
1818
import {GetThemeFileChecksums} from '../../../cli/api/graphql/admin/generated/get_theme_file_checksums.js'
1919
import {ThemeFilesUpsert} from '../../../cli/api/graphql/admin/generated/theme_files_upsert.js'
20+
import {ThemeFilesDelete} from '../../../cli/api/graphql/admin/generated/theme_files_delete.js'
2021
import {OnlineStoreThemeFileBodyInputType} from '../../../cli/api/graphql/admin/generated/types.js'
2122
import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js'
2223
import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js'
@@ -318,63 +319,51 @@ describe('themePublish', () => {
318319
}
319320
})
320321

321-
describe('deleteThemeAsset', () => {
322+
describe('deleteThemeAssets', () => {
322323
test('deletes a theme asset', async () => {
323324
// Given
324325
const id = 123
325326
const key = 'snippets/product-variant-picker.liquid'
326327

327-
vi.mocked(restRequest).mockResolvedValue({
328-
json: {message: 'snippets/product-variant-picker.liquid was succesfully deleted'},
329-
status: 200,
330-
headers: {},
328+
vi.mocked(adminRequestDoc).mockResolvedValue({
329+
themeFilesDelete: {
330+
deletedThemeFiles: [{filename: key}],
331+
userErrors: [],
332+
},
331333
})
332334

333335
// When
334-
const output = await deleteThemeAsset(id, key, session)
336+
const output = await deleteThemeAssets(id, [key], session)
335337

336338
// Then
337-
expect(restRequest).toHaveBeenCalledWith('DELETE', `/themes/${id}/assets`, session, undefined, {'asset[key]': key})
338-
expect(output).toBe(true)
339+
expect(adminRequestDoc).toHaveBeenCalledWith(ThemeFilesDelete, session, {
340+
themeId: `gid://shopify/OnlineStoreTheme/${id}`,
341+
files: [key],
342+
})
343+
expect(output).toEqual([{key, success: true, operation: 'DELETE'}])
339344
})
340345

341-
test('returns true when attemping to delete an nonexistent asset', async () => {
346+
test('returns success when attempting to delete nonexistent assets', async () => {
342347
// Given
343348
const id = 123
344349
const key = 'snippets/product-variant-picker.liquid'
345350

346-
vi.mocked(restRequest).mockResolvedValue({
347-
json: {},
348-
status: 200,
349-
headers: {},
351+
vi.mocked(adminRequestDoc).mockResolvedValue({
352+
themeFilesDelete: {
353+
deletedThemeFiles: [{filename: key}],
354+
userErrors: [],
355+
},
350356
})
351357

352358
// When
353-
const output = await deleteThemeAsset(id, key, session)
359+
const output = await deleteThemeAssets(id, [key], session)
354360

355361
// Then
356-
expect(restRequest).toHaveBeenCalledWith('DELETE', `/themes/${id}/assets`, session, undefined, {'asset[key]': key})
357-
expect(output).toBe(true)
358-
})
359-
360-
test('throws an AbortError when the server responds with a 403', async () => {
361-
// Given
362-
const id = 123
363-
const key = 'config/settings_data.json'
364-
const message = 'You are not authorized to edit themes on "my-shop.myshopify.com".'
365-
366-
vi.mocked(restRequest).mockResolvedValue({
367-
json: {message},
368-
status: 403,
369-
headers: {},
362+
expect(adminRequestDoc).toHaveBeenCalledWith(ThemeFilesDelete, session, {
363+
themeId: `gid://shopify/OnlineStoreTheme/${id}`,
364+
files: [key],
370365
})
371-
372-
// When
373-
const deletePromise = () => deleteThemeAsset(id, key, session)
374-
375-
// Then
376-
await expect(deletePromise).rejects.toThrow(new AbortError(message))
377-
expect(restRequest).toHaveBeenCalledWith('DELETE', `/themes/${id}/assets`, session, undefined, {'asset[key]': key})
366+
expect(output).toEqual([{key, success: true, operation: 'DELETE'}])
378367
})
379368
})
380369

@@ -383,7 +372,6 @@ describe('themeDelete', () => {
383372
test(`deletes a theme with graphql with a ${sessionType} session`, async () => {
384373
// Given
385374
const id = 123
386-
const name = 'store theme'
387375

388376
vi.mocked(adminRequestDoc).mockResolvedValue({
389377
themeDelete: {

packages/cli-kit/src/public/node/themes/api.ts

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
ThemeFilesUpsert,
1111
ThemeFilesUpsertMutation,
1212
} from '../../../cli/api/graphql/admin/generated/theme_files_upsert.js'
13+
import {ThemeFilesDelete} from '../../../cli/api/graphql/admin/generated/theme_files_delete.js'
1314
import {
1415
OnlineStoreThemeFileBodyInputType,
1516
OnlineStoreThemeFilesUpsertFileInput,
@@ -147,11 +148,47 @@ export async function fetchThemeAssets(id: number, filenames: Key[], session: Ad
147148
}
148149
}
149150

150-
export async function deleteThemeAsset(id: number, key: Key, session: AdminSession): Promise<boolean> {
151-
const response = await request('DELETE', `/themes/${id}/assets`, session, undefined, {
152-
'asset[key]': key,
153-
})
154-
return response.status === 200
151+
export async function deleteThemeAssets(id: number, filenames: Key[], session: AdminSession): Promise<Result[]> {
152+
const batchSize = 50
153+
const results: Result[] = []
154+
155+
for (let i = 0; i < filenames.length; i += batchSize) {
156+
const batch = filenames.slice(i, i + batchSize)
157+
// eslint-disable-next-line no-await-in-loop
158+
const {themeFilesDelete} = await adminRequestDoc(ThemeFilesDelete, session, {
159+
themeId: composeThemeGid(id),
160+
files: batch,
161+
})
162+
163+
if (!themeFilesDelete) {
164+
unexpectedGraphQLError('Failed to delete theme assets')
165+
}
166+
167+
const {deletedThemeFiles, userErrors} = themeFilesDelete
168+
169+
if (deletedThemeFiles) {
170+
deletedThemeFiles.forEach((file) => {
171+
results.push({key: file.filename, success: true, operation: Operation.Delete})
172+
})
173+
}
174+
175+
if (userErrors.length > 0) {
176+
userErrors.forEach((error) => {
177+
if (error.filename) {
178+
results.push({
179+
key: error.filename,
180+
success: false,
181+
operation: Operation.Delete,
182+
errors: {asset: [error.message]},
183+
})
184+
} else {
185+
unexpectedGraphQLError(`Failed to delete theme assets: ${error.message}`)
186+
}
187+
})
188+
}
189+
}
190+
191+
return results
155192
}
156193

157194
export async function bulkUploadThemeAssets(

packages/theme/src/cli/utilities/theme-environment/theme-reconciliation.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {reconcileJsonFiles} from './theme-reconciliation.js'
22
import {REMOTE_STRATEGY, LOCAL_STRATEGY} from './remote-theme-watcher.js'
33
import {fakeThemeFileSystem} from '../theme-fs/theme-fs-mock-factory.js'
4-
import {deleteThemeAsset, fetchThemeAssets} from '@shopify/cli-kit/node/themes/api'
4+
import {deleteThemeAssets, fetchThemeAssets} from '@shopify/cli-kit/node/themes/api'
55
import {buildTheme} from '@shopify/cli-kit/node/themes/factories'
66
import {Checksum, ThemeAsset, ThemeFileSystem} from '@shopify/cli-kit/node/themes/types'
77
import {DEVELOPMENT_THEME_ROLE} from '@shopify/cli-kit/node/themes/utils'
@@ -143,7 +143,7 @@ describe('reconcileJsonFiles', () => {
143143
)
144144

145145
// Then
146-
expect(deleteThemeAsset).toHaveBeenCalledWith(developmentTheme.id, assetToBeDeleted.key, adminSession)
146+
expect(deleteThemeAssets).toHaveBeenCalledWith(developmentTheme.id, [assetToBeDeleted.key], adminSession)
147147
expect(defaultThemeFileSystem.files.get('templates/asset.json')).toBeUndefined()
148148
})
149149
})

packages/theme/src/cli/utilities/theme-environment/theme-reconciliation.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {batchedRequests} from '../batching.js'
33
import {MAX_GRAPHQL_THEME_FILES} from '../../constants.js'
44
import {outputDebug} from '@shopify/cli-kit/node/output'
55
import {AdminSession} from '@shopify/cli-kit/node/session'
6-
import {deleteThemeAsset, fetchThemeAssets} from '@shopify/cli-kit/node/themes/api'
6+
import {deleteThemeAssets, fetchThemeAssets} from '@shopify/cli-kit/node/themes/api'
77
import {Checksum, ThemeFileSystem, ThemeAsset, Theme} from '@shopify/cli-kit/node/themes/types'
88
import {renderInfo, renderSelectPrompt} from '@shopify/cli-kit/node/ui'
99

@@ -179,9 +179,13 @@ async function performFileReconciliation(
179179
)
180180
})
181181

182-
const deleteRemoteFiles = remoteFilesToDelete.map((file) => deleteThemeAsset(targetTheme.id, file.key, session))
182+
const deleteRemoteFiles = deleteThemeAssets(
183+
targetTheme.id,
184+
remoteFilesToDelete.map((file) => file.key),
185+
session,
186+
)
183187

184-
await Promise.all([...deleteLocalFiles, ...downloadRemoteFiles, ...deleteRemoteFiles])
188+
await Promise.all([...deleteLocalFiles, ...downloadRemoteFiles, deleteRemoteFiles])
185189
}
186190

187191
async function partitionFilesByReconciliationStrategy(

0 commit comments

Comments
 (0)