diff --git a/packages/cli-kit/src/cli/api/graphql/admin/generated/get_theme.ts b/packages/cli-kit/src/cli/api/graphql/admin/generated/get_theme.ts new file mode 100644 index 00000000000..deeaff22b89 --- /dev/null +++ b/packages/cli-kit/src/cli/api/graphql/admin/generated/get_theme.ts @@ -0,0 +1,54 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +import * as Types from './types.js' + +import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' + +export type GetThemeQueryVariables = Types.Exact<{ + id: Types.Scalars['ID']['input'] +}> + +export type GetThemeQuery = {theme?: {id: string; name: string; role: Types.ThemeRole; processing: boolean} | null} + +export const GetTheme = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: {kind: 'Name', value: 'getTheme'}, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'id'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'theme'}, + arguments: [ + { + kind: 'Argument', + name: {kind: 'Name', value: 'id'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'id'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'id'}}, + {kind: 'Field', name: {kind: 'Name', value: 'name'}}, + {kind: 'Field', name: {kind: 'Name', value: 'role'}}, + {kind: 'Field', name: {kind: 'Name', value: 'processing'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode diff --git a/packages/cli-kit/src/cli/api/graphql/admin/generated/get_themes.ts b/packages/cli-kit/src/cli/api/graphql/admin/generated/get_themes.ts new file mode 100644 index 00000000000..5a6f78152f6 --- /dev/null +++ b/packages/cli-kit/src/cli/api/graphql/admin/generated/get_themes.ts @@ -0,0 +1,82 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +import * as Types from './types.js' + +import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' + +export type GetThemesQueryVariables = Types.Exact<{ + after?: Types.InputMaybe +}> + +export type GetThemesQuery = { + themes?: { + nodes: {id: string; name: string; role: Types.ThemeRole; processing: boolean}[] + pageInfo: {hasNextPage: boolean; endCursor?: string | null} + } | null +} + +export const GetThemes = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: {kind: 'Name', value: 'getThemes'}, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'after'}}, + type: {kind: 'NamedType', name: {kind: 'Name', value: 'String'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'themes'}, + arguments: [ + {kind: 'Argument', name: {kind: 'Name', value: 'first'}, value: {kind: 'IntValue', value: '50'}}, + { + kind: 'Argument', + name: {kind: 'Name', value: 'after'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'after'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'nodes'}, + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'id'}}, + {kind: 'Field', name: {kind: 'Name', value: 'name'}}, + {kind: 'Field', name: {kind: 'Name', value: 'role'}}, + {kind: 'Field', name: {kind: 'Name', value: 'processing'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + { + kind: 'Field', + name: {kind: 'Name', value: 'pageInfo'}, + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'hasNextPage'}}, + {kind: 'Field', name: {kind: 'Name', value: 'endCursor'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode diff --git a/packages/cli-kit/src/cli/api/graphql/admin/queries/get_theme.graphql b/packages/cli-kit/src/cli/api/graphql/admin/queries/get_theme.graphql new file mode 100644 index 00000000000..10e760dce81 --- /dev/null +++ b/packages/cli-kit/src/cli/api/graphql/admin/queries/get_theme.graphql @@ -0,0 +1,9 @@ + +query getTheme($id: ID!) { + theme(id: $id) { + id + name + role + processing + } +} diff --git a/packages/cli-kit/src/cli/api/graphql/admin/queries/get_themes.graphql b/packages/cli-kit/src/cli/api/graphql/admin/queries/get_themes.graphql new file mode 100644 index 00000000000..b8d15dfbea7 --- /dev/null +++ b/packages/cli-kit/src/cli/api/graphql/admin/queries/get_themes.graphql @@ -0,0 +1,15 @@ + +query getThemes($after: String) { + themes(first: 50, after: $after) { + nodes { + id + name + role + processing + } + pageInfo { + hasNextPage + endCursor + } + } +} diff --git a/packages/cli-kit/src/public/node/themes/api.test.ts b/packages/cli-kit/src/public/node/themes/api.test.ts index e74ba0159ba..960f638cdb6 100644 --- a/packages/cli-kit/src/public/node/themes/api.test.ts +++ b/packages/cli-kit/src/public/node/themes/api.test.ts @@ -17,6 +17,7 @@ import {ThemePublish} from '../../../cli/api/graphql/admin/generated/theme_publi import {GetThemeFileChecksums} from '../../../cli/api/graphql/admin/generated/get_theme_file_checksums.js' import {ThemeFilesUpsert} from '../../../cli/api/graphql/admin/generated/theme_files_upsert.js' import {OnlineStoreThemeFileBodyInputType} from '../../../cli/api/graphql/admin/generated/types.js' +import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js' import {test, vi, expect, describe} from 'vitest' import {adminRequestDoc, restRequest, supportedApiVersions} from '@shopify/cli-kit/node/api/admin' import {AbortError} from '@shopify/cli-kit/node/error' @@ -31,22 +32,21 @@ const sessions = {CLI: session, 'Theme Access': themeAccessSession} describe('fetchThemes', () => { test('returns store themes', async () => { // Given - vi.mocked(restRequest).mockResolvedValue({ - json: { - themes: [ - {id: 123, name: 'store theme 1', processing: false}, - {id: 456, name: 'store theme 2', processing: true}, + vi.mocked(adminRequestDoc).mockResolvedValue({ + themes: { + nodes: [ + {id: 'gid://shopify/OnlineStoreTheme/123', name: 'store theme 1', role: 'UNPUBLISHED', processing: false}, + {id: 'gid://shopify/OnlineStoreTheme/456', name: 'store theme 2', role: 'MAIN', processing: true}, ], + pageInfo: {hasNextPage: false, endCursor: null}, }, - status: 200, - headers: {}, }) // When const themes = await fetchThemes(session) // Then - expect(restRequest).toHaveBeenCalledWith('GET', '/themes', session, undefined, {fields: 'id,name,role,processing'}) + expect(adminRequestDoc).toHaveBeenCalledWith(GetThemes, session, {after: null}) expect(themes).toHaveLength(2) expect(themes[0]!.id).toEqual(123) diff --git a/packages/cli-kit/src/public/node/themes/api.ts b/packages/cli-kit/src/public/node/themes/api.ts index 32fba672c97..b4cd03d2041 100644 --- a/packages/cli-kit/src/public/node/themes/api.ts +++ b/packages/cli-kit/src/public/node/themes/api.ts @@ -16,6 +16,8 @@ import { MetafieldOwnerType, } from '../../../cli/api/graphql/admin/generated/types.js' import {MetafieldDefinitionsByOwnerType} from '../../../cli/api/graphql/admin/generated/metafield_definitions_by_owner_type.js' +import {GetThemes} from '../../../cli/api/graphql/admin/generated/get_themes.js' +import {GetTheme} from '../../../cli/api/graphql/admin/generated/get_theme.js' import {restRequest, RestResponse, adminRequestDoc} from '@shopify/cli-kit/node/api/admin' import {AdminSession} from '@shopify/cli-kit/node/session' import {AbortError} from '@shopify/cli-kit/node/error' @@ -28,15 +30,48 @@ export type ThemeParams = Partial & Partial> export async function fetchTheme(id: number, session: AdminSession): Promise { - const response = await request('GET', `/themes/${id}`, session, undefined, {fields: 'id,name,role,processing'}) - return buildTheme(response.json.theme) + const response = await adminRequestDoc(GetTheme, session, {id: composeThemeGid(id)}) + + const {theme} = response + if (!theme) { + return undefined + } + return buildTheme({ + id: parseGid(theme.id), + processing: theme.processing, + role: theme.role.toLowerCase(), + name: theme.name, + }) } export async function fetchThemes(session: AdminSession): Promise { - const response = await request('GET', '/themes', session, undefined, {fields: 'id,name,role,processing'}) - const themes = response.json?.themes - if (themes?.length > 0) return themes.map(buildTheme) - return [] + const themes: Theme[] = [] + let after: string | null = null + + while (true) { + // eslint-disable-next-line no-await-in-loop + const response = await adminRequestDoc(GetThemes, session, {after}) + if (!response.themes) { + unexpectedGraphQLError('Failed to fetch themes') + } + const {nodes, pageInfo} = response.themes + nodes.forEach((theme) => { + const t = buildTheme({ + id: parseGid(theme.id), + processing: theme.processing, + role: theme.role.toLowerCase(), + name: theme.name, + }) + if (t) { + themes.push(t) + } + }) + if (!pageInfo.hasNextPage) { + return themes + } + + after = pageInfo.endCursor as string + } } export async function createTheme(params: ThemeParams, session: AdminSession): Promise {