From 1579dbf09947f4c527541c3dd8a107aeede1928c Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Wed, 10 Dec 2025 14:10:52 +0100 Subject: [PATCH 1/2] Allow Partners API for extension migration queries --- .../partners-client.test.ts | 169 ++++++++++++++++-- .../partners-client.ts | 3 + .../src/public/node/api/partners.test.ts | 48 +++++ .../cli-kit/src/public/node/api/partners.ts | 14 +- .../src/public/node/context/fqdn.test.ts | 4 - .../cli-kit/src/public/node/context/fqdn.ts | 6 +- .../src/public/node/environment.test.ts | 116 ++++++++++++ .../cli-kit/src/public/node/environment.ts | 14 +- 8 files changed, 340 insertions(+), 34 deletions(-) create mode 100644 packages/cli-kit/src/public/node/environment.test.ts diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts index b75607313a4..68d018ae2ca 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts @@ -1,5 +1,8 @@ import {PartnersClient} from './partners-client.js' import {CreateAppQuery} from '../../api/graphql/create_app.js' +import {MigrateToUiExtensionQuery} from '../../api/graphql/extension_migrate_to_ui_extension.js' +import {MigrateFlowExtensionMutation} from '../../api/graphql/extension_migrate_flow_extension.js' +import {MigrateAppModuleMutation} from '../../api/graphql/extension_migrate_app_module.js' import {AppInterface, WebType} from '../../models/app/app.js' import {Organization, OrganizationSource, OrganizationStore} from '../../models/organization.js' import { @@ -107,10 +110,18 @@ describe('createApp', () => { // Then expect(got).toEqual({...APP1, newApp: true, developerPlatformClient: partnersClient}) - expect(partnersRequest).toHaveBeenCalledWith(CreateAppQuery, 'token', variables, undefined, undefined, { - type: 'token_refresh', - handler: expect.any(Function), - }) + expect(partnersRequest).toHaveBeenCalledWith( + CreateAppQuery, + 'token', + variables, + undefined, + undefined, + { + type: 'token_refresh', + handler: expect.any(Function), + }, + false, + ) }) test('creates an app with non-launchable defaults', async () => { @@ -139,10 +150,18 @@ describe('createApp', () => { // Then expect(got).toEqual({...APP1, newApp: true, developerPlatformClient: partnersClient}) - expect(partnersRequest).toHaveBeenCalledWith(CreateAppQuery, 'token', variables, undefined, undefined, { - type: 'token_refresh', - handler: expect.any(Function), - }) + expect(partnersRequest).toHaveBeenCalledWith( + CreateAppQuery, + 'token', + variables, + undefined, + undefined, + { + type: 'token_refresh', + handler: expect.any(Function), + }, + false, + ) }) test('throws error if requests has a user error', async () => { @@ -173,10 +192,18 @@ describe('fetchApp', async () => { // Then expect(got).toEqual({organization: partnerMarkedOrg, apps: [APP1, APP2], hasMorePages: false}) - expect(partnersRequest).toHaveBeenCalledWith(FindOrganizationQuery, 'token', {id: ORG1.id}, undefined, undefined, { - type: 'token_refresh', - handler: expect.any(Function), - }) + expect(partnersRequest).toHaveBeenCalledWith( + FindOrganizationQuery, + 'token', + {id: ORG1.id}, + undefined, + undefined, + { + type: 'token_refresh', + handler: expect.any(Function), + }, + false, + ) }) test('throws if there are no organizations', async () => { @@ -189,10 +216,18 @@ describe('fetchApp', async () => { // Then await expect(got).rejects.toThrow('No Organization found') - expect(partnersRequest).toHaveBeenCalledWith(FindOrganizationQuery, 'token', {id: ORG1.id}, undefined, undefined, { - type: 'token_refresh', - handler: expect.any(Function), - }) + expect(partnersRequest).toHaveBeenCalledWith( + FindOrganizationQuery, + 'token', + {id: ORG1.id}, + undefined, + undefined, + { + type: 'token_refresh', + handler: expect.any(Function), + }, + false, + ) }) }) @@ -230,3 +265,105 @@ describe('singleton pattern', () => { expect(instance1).not.toBe(instance2) }) }) + +describe('request with forceUsePartnersApi', () => { + test('passes forceUsePartnersApi=true for MigrateToUiExtensionQuery', async () => { + // Given + const partnersClient = PartnersClient.getInstance(testPartnersUserSession) + const mockResponse = {migrateToUiExtension: {userErrors: [], migratedToUiExtension: true}} + vi.mocked(partnersRequest).mockResolvedValueOnce(mockResponse) + + // When + await partnersClient.request(MigrateToUiExtensionQuery, {extensionId: '123'}) + + // Then + expect(partnersRequest).toHaveBeenCalledWith( + MigrateToUiExtensionQuery, + testPartnersUserSession.token, + {extensionId: '123'}, + undefined, + undefined, + expect.objectContaining({ + type: 'token_refresh', + handler: expect.any(Function), + }), + // forceUsePartnersApi should be true + true, + ) + }) + + test('passes forceUsePartnersApi=true for MigrateFlowExtensionMutation', async () => { + // Given + const partnersClient = PartnersClient.getInstance(testPartnersUserSession) + const mockResponse = {migrateFlowExtension: {userErrors: [], migratedFlowExtension: true}} + vi.mocked(partnersRequest).mockResolvedValueOnce(mockResponse) + + // When + await partnersClient.request(MigrateFlowExtensionMutation, {extensionId: '123'}) + + // Then + expect(partnersRequest).toHaveBeenCalledWith( + MigrateFlowExtensionMutation, + testPartnersUserSession.token, + {extensionId: '123'}, + undefined, + undefined, + expect.objectContaining({ + type: 'token_refresh', + handler: expect.any(Function), + }), + // forceUsePartnersApi should be true + true, + ) + }) + + test('passes forceUsePartnersApi=true for MigrateAppModuleMutation', async () => { + // Given + const partnersClient = PartnersClient.getInstance(testPartnersUserSession) + const mockResponse = {migrateAppModule: {userErrors: [], migratedAppModule: true}} + vi.mocked(partnersRequest).mockResolvedValueOnce(mockResponse) + + // When + await partnersClient.request(MigrateAppModuleMutation, {appModuleId: '123'}) + + // Then + expect(partnersRequest).toHaveBeenCalledWith( + MigrateAppModuleMutation, + testPartnersUserSession.token, + {appModuleId: '123'}, + undefined, + undefined, + expect.objectContaining({ + type: 'token_refresh', + handler: expect.any(Function), + }), + // forceUsePartnersApi should be true + true, + ) + }) + + test('passes forceUsePartnersApi=false for other queries', async () => { + // Given + const partnersClient = PartnersClient.getInstance(testPartnersUserSession) + const mockResponse = {organizations: {nodes: []}} + vi.mocked(partnersRequest).mockResolvedValueOnce(mockResponse) + + // When + await partnersClient.request(FindOrganizationQuery, {id: '1'}) + + // Then + expect(partnersRequest).toHaveBeenCalledWith( + FindOrganizationQuery, + testPartnersUserSession.token, + {id: '1'}, + undefined, + undefined, + expect.objectContaining({ + type: 'token_refresh', + handler: expect.any(Function), + }), + // forceUsePartnersApi should be false for non-migration queries + false, + ) + }) +}) diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts index 59a680465da..739cf859ff7 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts @@ -262,6 +262,8 @@ export class PartnersClient implements DeveloperPlatformClient { cacheOptions?: CacheOptions, preferredBehaviour?: RequestModeInput, ): Promise { + const allowedQueries = [MigrateToUiExtensionQuery, MigrateFlowExtensionMutation, MigrateAppModuleMutation] + const forceUsePartnersApi = allowedQueries.some((allowedQuery) => query === allowedQuery) return partnersRequest( query, await this.token(), @@ -269,6 +271,7 @@ export class PartnersClient implements DeveloperPlatformClient { cacheOptions, preferredBehaviour, this.createUnauthorizedHandler(), + forceUsePartnersApi, ) } diff --git a/packages/cli-kit/src/public/node/api/partners.test.ts b/packages/cli-kit/src/public/node/api/partners.test.ts index e3597f2b6db..cb1578f8863 100644 --- a/packages/cli-kit/src/public/node/api/partners.test.ts +++ b/packages/cli-kit/src/public/node/api/partners.test.ts @@ -1,12 +1,15 @@ import {partnersRequest, handleDeprecations} from './partners.js' import {graphqlRequest, GraphQLResponse} from './graphql.js' import {partnersFqdn} from '../context/fqdn.js' +import {blockPartnersAccess} from '../environment.js' +import {BugError} from '../error.js' import {setNextDeprecationDate} from '../../../private/node/context/deprecations-store.js' import {test, vi, expect, describe, beforeEach, beforeAll} from 'vitest' vi.mock('./graphql.js') vi.mock('../../../private/node/context/deprecations-store.js') vi.mock('../context/fqdn.js') +vi.mock('../environment.js') const mockedResult = 'OK' const partnersFQDN = 'partners.shopify.com' @@ -16,6 +19,7 @@ const mockedToken = 'token' beforeEach(() => { vi.mocked(partnersFqdn).mockResolvedValue(partnersFQDN) + vi.mocked(blockPartnersAccess).mockReturnValue(false) }) describe('partnersRequest', () => { @@ -36,6 +40,50 @@ describe('partnersRequest', () => { responseOptions: {onResponse: handleDeprecations}, }) }) + + test('throws BugError when blockPartnersAccess returns true without forceUsePartnersApi', async () => { + // Given + vi.mocked(blockPartnersAccess).mockReturnValue(true) + + // When/Then + await expect(partnersRequest('query', mockedToken, {variables: 'variables'})).rejects.toThrow(BugError) + expect(blockPartnersAccess).toHaveBeenCalledWith(false) + }) + + test('throws BugError when blockPartnersAccess returns true even with forceUsePartnersApi=false', async () => { + // Given + vi.mocked(blockPartnersAccess).mockReturnValue(true) + + // When/Then + await expect( + partnersRequest('query', mockedToken, {variables: 'variables'}, undefined, undefined, undefined, false), + ).rejects.toThrow(BugError) + expect(blockPartnersAccess).toHaveBeenCalledWith(false) + }) + + test('does not throw when forceUsePartnersApi=true even if blockPartnersAccess would normally block', async () => { + // Given + vi.mocked(blockPartnersAccess).mockReturnValue(false) + vi.mocked(graphqlRequest).mockResolvedValue(mockedResult) + + // When + await partnersRequest('query', mockedToken, {variables: 'variables'}, undefined, undefined, undefined, true) + + // Then + expect(blockPartnersAccess).toHaveBeenCalledWith(true) + expect(graphqlRequest).toHaveBeenCalled() + }) + + test('passes forceUsePartnersApi to blockPartnersAccess', async () => { + // Given + vi.mocked(graphqlRequest).mockResolvedValue(mockedResult) + + // When + await partnersRequest('query', mockedToken, {variables: 'variables'}, undefined, undefined, undefined, true) + + // Then + expect(blockPartnersAccess).toHaveBeenCalledWith(true) + }) }) describe('handleDeprecations', () => { diff --git a/packages/cli-kit/src/public/node/api/partners.ts b/packages/cli-kit/src/public/node/api/partners.ts index 8da3c7ac11b..9082b5774ea 100644 --- a/packages/cli-kit/src/public/node/api/partners.ts +++ b/packages/cli-kit/src/public/node/api/partners.ts @@ -11,9 +11,10 @@ import {partnersFqdn} from '../context/fqdn.js' import {setNextDeprecationDate} from '../../../private/node/context/deprecations-store.js' import {getPackageManager} from '../node-package-manager.js' import {cwd} from '../path.js' -import {AbortError} from '../error.js' +import {AbortError, BugError} from '../error.js' import {formatPackageManagerCommand} from '../output.js' import {RequestModeInput} from '../http.js' +import {blockPartnersAccess} from '../environment.js' import Bottleneck from 'bottleneck' import {Variables} from 'graphql-request' import {TypedDocumentNode} from '@graphql-typed-document-node/core' @@ -30,8 +31,13 @@ const limiter = new Bottleneck({ * Sets up the request to the Partners API. * * @param token - Partners token. + * @param forceUsePartnersApi - If true, the request will be forced to use the Partners API. */ -async function setupRequest(token: string) { +async function setupRequest(token: string, forceUsePartnersApi = false) { + if (blockPartnersAccess(forceUsePartnersApi)) { + throw new BugError('Partners API is no longer available.') + } + const api = 'Partners' const fqdn = await partnersFqdn() const url = `https://${fqdn}/api/cli/graphql` @@ -52,6 +58,7 @@ async function setupRequest(token: string) { * @param cacheOptions - Cache options. * @param preferredBehaviour - Preferred behaviour for the request. * @param unauthorizedHandler - Optional handler for unauthorized requests. + * @param forceUsePartnersApi - If true, the request will be forced to use the Partners API. * @returns The response of the query of generic type . */ export async function partnersRequest( @@ -61,8 +68,9 @@ export async function partnersRequest( cacheOptions?: CacheOptions, preferredBehaviour?: RequestModeInput, unauthorizedHandler?: UnauthorizedHandler, + forceUsePartnersApi = false, ): Promise { - const opts = await setupRequest(token) + const opts = await setupRequest(token, forceUsePartnersApi) const result = limiter.schedule(() => graphqlRequest({ ...opts, diff --git a/packages/cli-kit/src/public/node/context/fqdn.test.ts b/packages/cli-kit/src/public/node/context/fqdn.test.ts index c867540fc31..7159e051fc7 100644 --- a/packages/cli-kit/src/public/node/context/fqdn.test.ts +++ b/packages/cli-kit/src/public/node/context/fqdn.test.ts @@ -8,11 +8,9 @@ import { adminFqdn, } from './fqdn.js' import {Environment, serviceEnvironment} from '../../../private/node/context/service.js' -import {blockPartnersAccess} from '../environment.js' import {expect, describe, test, vi} from 'vitest' vi.mock('../../../private/node/context/service.js') -vi.mock('../environment.js') vi.mock('../vendor/dev_server/index.js', () => { return { @@ -34,7 +32,6 @@ describe('partners', () => { test('returns the local fqdn when the environment is local', async () => { // Given vi.mocked(serviceEnvironment).mockReturnValue(Environment.Local) - vi.mocked(blockPartnersAccess).mockReturnValue(false) // When const got = await partnersFqdn() @@ -46,7 +43,6 @@ describe('partners', () => { test('returns the production fqdn when the environment is production', async () => { // Given vi.mocked(serviceEnvironment).mockReturnValue(Environment.Production) - vi.mocked(blockPartnersAccess).mockReturnValue(false) // When const got = await partnersFqdn() diff --git a/packages/cli-kit/src/public/node/context/fqdn.ts b/packages/cli-kit/src/public/node/context/fqdn.ts index c45d1999c64..a41be33e161 100644 --- a/packages/cli-kit/src/public/node/context/fqdn.ts +++ b/packages/cli-kit/src/public/node/context/fqdn.ts @@ -1,7 +1,6 @@ -import {AbortError, BugError} from '../error.js' +import {AbortError} from '../error.js' import {serviceEnvironment} from '../../../private/node/context/service.js' import {DevServer, DevServerCore} from '../vendor/dev_server/index.js' -import {blockPartnersAccess} from '../environment.js' export const NotProvidedStoreFQDNError = new AbortError( "Couldn't obtain the Shopify FQDN because the store FQDN was not provided.", @@ -13,9 +12,6 @@ export const NotProvidedStoreFQDNError = new AbortError( * @returns Fully-qualified domain of the partners service we should interact with. */ export async function partnersFqdn(): Promise { - if (blockPartnersAccess()) { - throw new BugError('Partners API is is no longer available.') - } const environment = serviceEnvironment() const productionFqdn = 'partners.shopify.com' switch (environment) { diff --git a/packages/cli-kit/src/public/node/environment.test.ts b/packages/cli-kit/src/public/node/environment.test.ts new file mode 100644 index 00000000000..ba4bd6b2e6f --- /dev/null +++ b/packages/cli-kit/src/public/node/environment.test.ts @@ -0,0 +1,116 @@ +import {blockPartnersAccess} from './environment.js' +import {describe, expect, test, beforeEach, afterEach} from 'vitest' + +describe('blockPartnersAccess', () => { + const originalEnv = process.env + + beforeEach(() => { + process.env = {...originalEnv} + }) + + afterEach(() => { + process.env = originalEnv + }) + + test('returns true when SHOPIFY_CLI_NEVER_USE_PARTNERS_API is set', () => { + // Given + process.env.SHOPIFY_CLI_NEVER_USE_PARTNERS_API = '1' + + // When + const result = blockPartnersAccess() + + // Then + expect(result).toBe(true) + }) + + test('returns false when SHOPIFY_CLI_USE_PARTNERS_API is set', () => { + // Given + process.env.SHOPIFY_CLI_USE_PARTNERS_API = '1' + + // When + const result = blockPartnersAccess() + + // Then + expect(result).toBe(false) + }) + + test('returns false when forceUsePartnersApi is true regardless of environment variables', () => { + // Given + process.env.SHOPIFY_CLI_NEVER_USE_PARTNERS_API = '1' + + // When + const result = blockPartnersAccess(true) + + // Then + expect(result).toBe(false) + }) + + test('returns false when forceUsePartnersApi is true even without env vars', () => { + // Given + // No environment variables set + + // When + const result = blockPartnersAccess(true) + + // Then + expect(result).toBe(false) + }) + + test('returns true for 3P devs (without SHOPIFY_CLI_1P_DEV)', () => { + // Given + // SHOPIFY_CLI_1P_DEV is not set + + // When + const result = blockPartnersAccess() + + // Then + expect(result).toBe(true) + }) + + test('returns false for 1P devs (with SHOPIFY_CLI_1P_DEV)', () => { + // Given + process.env.SHOPIFY_CLI_1P_DEV = '1' + + // When + const result = blockPartnersAccess() + + // Then + expect(result).toBe(false) + }) + + test('respects forceUsePartnersApi even for 3P devs', () => { + // Given + // SHOPIFY_CLI_1P_DEV is not set (3P dev) + + // When + const result = blockPartnersAccess(true) + + // Then + expect(result).toBe(false) + }) + + test('SHOPIFY_CLI_USE_PARTNERS_API takes precedence over SHOPIFY_CLI_NEVER_USE_PARTNERS_API', () => { + // Given - both environment variables are set + process.env.SHOPIFY_CLI_NEVER_USE_PARTNERS_API = '1' + process.env.SHOPIFY_CLI_USE_PARTNERS_API = '1' + + // When + const result = blockPartnersAccess() + + // Then - should return false because USE_PARTNERS_API takes precedence + expect(result).toBe(false) + }) + + test('forceUsePartnersApi takes precedence over all environment variables', () => { + // Given - all conflicting environment variables are set + process.env.SHOPIFY_CLI_NEVER_USE_PARTNERS_API = '1' + process.env.SHOPIFY_CLI_USE_PARTNERS_API = '0' + // SHOPIFY_CLI_1P_DEV is not set (3P dev) + + // When + const result = blockPartnersAccess(true) + + // Then - should return false because forceUsePartnersApi takes highest precedence + expect(result).toBe(false) + }) +}) diff --git a/packages/cli-kit/src/public/node/environment.ts b/packages/cli-kit/src/public/node/environment.ts index ad1ce55c7e0..5d5ffd68160 100644 --- a/packages/cli-kit/src/public/node/environment.ts +++ b/packages/cli-kit/src/public/node/environment.ts @@ -86,19 +86,21 @@ export function jsonOutputEnabled(environment = getEnvironmentVariables()): bool /** * If true, the CLI should not use the Partners API. * + * @param forceUsePartnersApi - If true, the CLI will force to use the Partners API. * @returns True when the CLI should not use the Partners API. */ -export function blockPartnersAccess(): boolean { +export function blockPartnersAccess(forceUsePartnersApi?: boolean): boolean { + // If explicitly forcing to use Partners API, do not block + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (forceUsePartnersApi || isTruthy(getEnvironmentVariables()[environmentVariables.usePartnersApi])) { + return false + } + // Block if explicitly set to never use Partners API if (isTruthy(getEnvironmentVariables()[environmentVariables.neverUsePartnersApi])) { return true } - // If explicitly forcing to use Partners API, do not block - if (isTruthy(getEnvironmentVariables()[environmentVariables.usePartnersApi])) { - return false - } - // Block for 3P devs return !isTruthy(getEnvironmentVariables()[environmentVariables.firstPartyDev]) } From 4d08d656bed5d1e8ce22a1811412e01d30e48849 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Wed, 10 Dec 2025 14:28:28 +0100 Subject: [PATCH 2/2] Add changeset --- .changeset/afraid-parents-clap.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/afraid-parents-clap.md diff --git a/.changeset/afraid-parents-clap.md b/.changeset/afraid-parents-clap.md new file mode 100644 index 00000000000..a3c40a58f61 --- /dev/null +++ b/.changeset/afraid-parents-clap.md @@ -0,0 +1,6 @@ +--- +'@shopify/cli-kit': patch +'@shopify/app': patch +--- + +Fix import-extensions by allowing some queries against Partners API