Skip to content

Commit 9b9a51f

Browse files
Allow Partners API for extension migration queries
1 parent e794f12 commit 9b9a51f

File tree

8 files changed

+292
-22
lines changed

8 files changed

+292
-22
lines changed

packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {PartnersClient} from './partners-client.js'
22
import {CreateAppQuery} from '../../api/graphql/create_app.js'
3+
import {MigrateToUiExtensionQuery} from '../../api/graphql/extension_migrate_to_ui_extension.js'
4+
import {MigrateFlowExtensionMutation} from '../../api/graphql/extension_migrate_flow_extension.js'
5+
import {MigrateAppModuleMutation} from '../../api/graphql/extension_migrate_app_module.js'
36
import {AppInterface, WebType} from '../../models/app/app.js'
47
import {Organization, OrganizationSource, OrganizationStore} from '../../models/organization.js'
58
import {
@@ -110,7 +113,7 @@ describe('createApp', () => {
110113
expect(partnersRequest).toHaveBeenCalledWith(CreateAppQuery, 'token', variables, undefined, undefined, {
111114
type: 'token_refresh',
112115
handler: expect.any(Function),
113-
})
116+
}, false)
114117
})
115118

116119
test('creates an app with non-launchable defaults', async () => {
@@ -142,7 +145,7 @@ describe('createApp', () => {
142145
expect(partnersRequest).toHaveBeenCalledWith(CreateAppQuery, 'token', variables, undefined, undefined, {
143146
type: 'token_refresh',
144147
handler: expect.any(Function),
145-
})
148+
}, false)
146149
})
147150

148151
test('throws error if requests has a user error', async () => {
@@ -176,7 +179,7 @@ describe('fetchApp', async () => {
176179
expect(partnersRequest).toHaveBeenCalledWith(FindOrganizationQuery, 'token', {id: ORG1.id}, undefined, undefined, {
177180
type: 'token_refresh',
178181
handler: expect.any(Function),
179-
})
182+
}, false)
180183
})
181184

182185
test('throws if there are no organizations', async () => {
@@ -192,7 +195,7 @@ describe('fetchApp', async () => {
192195
expect(partnersRequest).toHaveBeenCalledWith(FindOrganizationQuery, 'token', {id: ORG1.id}, undefined, undefined, {
193196
type: 'token_refresh',
194197
handler: expect.any(Function),
195-
})
198+
}, false)
196199
})
197200
})
198201

@@ -230,3 +233,101 @@ describe('singleton pattern', () => {
230233
expect(instance1).not.toBe(instance2)
231234
})
232235
})
236+
237+
describe('request with forceUsePartnersApi', () => {
238+
test('passes forceUsePartnersApi=true for MigrateToUiExtensionQuery', async () => {
239+
// Given
240+
const partnersClient = PartnersClient.getInstance(testPartnersUserSession)
241+
const mockResponse = {migrateToUiExtension: {userErrors: [], migratedToUiExtension: true}}
242+
vi.mocked(partnersRequest).mockResolvedValueOnce(mockResponse)
243+
244+
// When
245+
await partnersClient.request(MigrateToUiExtensionQuery, {extensionId: '123'})
246+
247+
// Then
248+
expect(partnersRequest).toHaveBeenCalledWith(
249+
MigrateToUiExtensionQuery,
250+
testPartnersUserSession.token,
251+
{extensionId: '123'},
252+
undefined,
253+
undefined,
254+
expect.objectContaining({
255+
type: 'token_refresh',
256+
handler: expect.any(Function),
257+
}),
258+
true, // forceUsePartnersApi should be true
259+
)
260+
})
261+
262+
test('passes forceUsePartnersApi=true for MigrateFlowExtensionMutation', async () => {
263+
// Given
264+
const partnersClient = PartnersClient.getInstance(testPartnersUserSession)
265+
const mockResponse = {migrateFlowExtension: {userErrors: [], migratedFlowExtension: true}}
266+
vi.mocked(partnersRequest).mockResolvedValueOnce(mockResponse)
267+
268+
// When
269+
await partnersClient.request(MigrateFlowExtensionMutation, {extensionId: '123'})
270+
271+
// Then
272+
expect(partnersRequest).toHaveBeenCalledWith(
273+
MigrateFlowExtensionMutation,
274+
testPartnersUserSession.token,
275+
{extensionId: '123'},
276+
undefined,
277+
undefined,
278+
expect.objectContaining({
279+
type: 'token_refresh',
280+
handler: expect.any(Function),
281+
}),
282+
true, // forceUsePartnersApi should be true
283+
)
284+
})
285+
286+
test('passes forceUsePartnersApi=true for MigrateAppModuleMutation', async () => {
287+
// Given
288+
const partnersClient = PartnersClient.getInstance(testPartnersUserSession)
289+
const mockResponse = {migrateAppModule: {userErrors: [], migratedAppModule: true}}
290+
vi.mocked(partnersRequest).mockResolvedValueOnce(mockResponse)
291+
292+
// When
293+
await partnersClient.request(MigrateAppModuleMutation, {appModuleId: '123'})
294+
295+
// Then
296+
expect(partnersRequest).toHaveBeenCalledWith(
297+
MigrateAppModuleMutation,
298+
testPartnersUserSession.token,
299+
{appModuleId: '123'},
300+
undefined,
301+
undefined,
302+
expect.objectContaining({
303+
type: 'token_refresh',
304+
handler: expect.any(Function),
305+
}),
306+
true, // forceUsePartnersApi should be true
307+
)
308+
})
309+
310+
test('passes forceUsePartnersApi=false for other queries', async () => {
311+
// Given
312+
const partnersClient = PartnersClient.getInstance(testPartnersUserSession)
313+
const mockResponse = {organizations: {nodes: []}}
314+
vi.mocked(partnersRequest).mockResolvedValueOnce(mockResponse)
315+
316+
// When
317+
await partnersClient.request(FindOrganizationQuery, {id: '1'})
318+
319+
// Then
320+
expect(partnersRequest).toHaveBeenCalledWith(
321+
FindOrganizationQuery,
322+
testPartnersUserSession.token,
323+
{id: '1'},
324+
undefined,
325+
undefined,
326+
expect.objectContaining({
327+
type: 'token_refresh',
328+
handler: expect.any(Function),
329+
}),
330+
false, // forceUsePartnersApi should be false for non-migration queries
331+
)
332+
})
333+
})

packages/app/src/cli/utilities/developer-platform-client/partners-client.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,16 @@ export class PartnersClient implements DeveloperPlatformClient {
262262
cacheOptions?: CacheOptions,
263263
preferredBehaviour?: RequestModeInput,
264264
): Promise<T> {
265+
const allowedQueries = [MigrateToUiExtensionQuery, MigrateFlowExtensionMutation, MigrateAppModuleMutation]
266+
const forceUsePartnersApi = allowedQueries.some((allowedQuery) => query === allowedQuery)
265267
return partnersRequest(
266268
query,
267269
await this.token(),
268270
variables,
269271
cacheOptions,
270272
preferredBehaviour,
271273
this.createUnauthorizedHandler(),
274+
forceUsePartnersApi,
272275
)
273276
}
274277

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import {partnersRequest, handleDeprecations} from './partners.js'
22
import {graphqlRequest, GraphQLResponse} from './graphql.js'
33
import {partnersFqdn} from '../context/fqdn.js'
4+
import {blockPartnersAccess} from '../environment.js'
5+
import {BugError} from '../error.js'
46
import {setNextDeprecationDate} from '../../../private/node/context/deprecations-store.js'
57
import {test, vi, expect, describe, beforeEach, beforeAll} from 'vitest'
68

79
vi.mock('./graphql.js')
810
vi.mock('../../../private/node/context/deprecations-store.js')
911
vi.mock('../context/fqdn.js')
12+
vi.mock('../environment.js')
1013

1114
const mockedResult = 'OK'
1215
const partnersFQDN = 'partners.shopify.com'
@@ -16,6 +19,7 @@ const mockedToken = 'token'
1619

1720
beforeEach(() => {
1821
vi.mocked(partnersFqdn).mockResolvedValue(partnersFQDN)
22+
vi.mocked(blockPartnersAccess).mockReturnValue(false)
1923
})
2024

2125
describe('partnersRequest', () => {
@@ -36,6 +40,50 @@ describe('partnersRequest', () => {
3640
responseOptions: {onResponse: handleDeprecations},
3741
})
3842
})
43+
44+
test('throws BugError when blockPartnersAccess returns true without forceUsePartnersApi', async () => {
45+
// Given
46+
vi.mocked(blockPartnersAccess).mockReturnValue(true)
47+
48+
// When/Then
49+
await expect(partnersRequest('query', mockedToken, {variables: 'variables'})).rejects.toThrow(BugError)
50+
expect(blockPartnersAccess).toHaveBeenCalledWith(false)
51+
})
52+
53+
test('throws BugError when blockPartnersAccess returns true even with forceUsePartnersApi=false', async () => {
54+
// Given
55+
vi.mocked(blockPartnersAccess).mockReturnValue(true)
56+
57+
// When/Then
58+
await expect(
59+
partnersRequest('query', mockedToken, {variables: 'variables'}, undefined, undefined, undefined, false),
60+
).rejects.toThrow(BugError)
61+
expect(blockPartnersAccess).toHaveBeenCalledWith(false)
62+
})
63+
64+
test('does not throw when forceUsePartnersApi=true even if blockPartnersAccess would normally block', async () => {
65+
// Given
66+
vi.mocked(blockPartnersAccess).mockReturnValue(false)
67+
vi.mocked(graphqlRequest).mockResolvedValue(mockedResult)
68+
69+
// When
70+
await partnersRequest('query', mockedToken, {variables: 'variables'}, undefined, undefined, undefined, true)
71+
72+
// Then
73+
expect(blockPartnersAccess).toHaveBeenCalledWith(true)
74+
expect(graphqlRequest).toHaveBeenCalled()
75+
})
76+
77+
test('passes forceUsePartnersApi to blockPartnersAccess', async () => {
78+
// Given
79+
vi.mocked(graphqlRequest).mockResolvedValue(mockedResult)
80+
81+
// When
82+
await partnersRequest('query', mockedToken, {variables: 'variables'}, undefined, undefined, undefined, true)
83+
84+
// Then
85+
expect(blockPartnersAccess).toHaveBeenCalledWith(true)
86+
})
3987
})
4088

4189
describe('handleDeprecations', () => {

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ import {partnersFqdn} from '../context/fqdn.js'
1111
import {setNextDeprecationDate} from '../../../private/node/context/deprecations-store.js'
1212
import {getPackageManager} from '../node-package-manager.js'
1313
import {cwd} from '../path.js'
14-
import {AbortError} from '../error.js'
14+
import {AbortError, BugError} from '../error.js'
1515
import {formatPackageManagerCommand} from '../output.js'
1616
import {RequestModeInput} from '../http.js'
17+
import {blockPartnersAccess} from '../environment.js'
1718
import Bottleneck from 'bottleneck'
1819
import {Variables} from 'graphql-request'
1920
import {TypedDocumentNode} from '@graphql-typed-document-node/core'
@@ -30,8 +31,13 @@ const limiter = new Bottleneck({
3031
* Sets up the request to the Partners API.
3132
*
3233
* @param token - Partners token.
34+
* @param forceUsePartnersApi - If true, the request will be forced to use the Partners API.
3335
*/
34-
async function setupRequest(token: string) {
36+
async function setupRequest(token: string, forceUsePartnersApi = false) {
37+
if (blockPartnersAccess(forceUsePartnersApi)) {
38+
throw new BugError('Partners API is no longer available.')
39+
}
40+
3541
const api = 'Partners'
3642
const fqdn = await partnersFqdn()
3743
const url = `https://${fqdn}/api/cli/graphql`
@@ -52,6 +58,7 @@ async function setupRequest(token: string) {
5258
* @param cacheOptions - Cache options.
5359
* @param preferredBehaviour - Preferred behaviour for the request.
5460
* @param unauthorizedHandler - Optional handler for unauthorized requests.
61+
* @param forceUsePartnersApi - If true, the request will be forced to use the Partners API.
5562
* @returns The response of the query of generic type <T>.
5663
*/
5764
export async function partnersRequest<T>(
@@ -61,8 +68,9 @@ export async function partnersRequest<T>(
6168
cacheOptions?: CacheOptions,
6269
preferredBehaviour?: RequestModeInput,
6370
unauthorizedHandler?: UnauthorizedHandler,
71+
forceUsePartnersApi = false,
6472
): Promise<T> {
65-
const opts = await setupRequest(token)
73+
const opts = await setupRequest(token, forceUsePartnersApi)
6674
const result = limiter.schedule(() =>
6775
graphqlRequest<T>({
6876
...opts,

packages/cli-kit/src/public/node/context/fqdn.test.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import {
88
adminFqdn,
99
} from './fqdn.js'
1010
import {Environment, serviceEnvironment} from '../../../private/node/context/service.js'
11-
import {blockPartnersAccess} from '../environment.js'
1211
import {expect, describe, test, vi} from 'vitest'
1312

1413
vi.mock('../../../private/node/context/service.js')
15-
vi.mock('../environment.js')
1614

1715
vi.mock('../vendor/dev_server/index.js', () => {
1816
return {
@@ -34,7 +32,6 @@ describe('partners', () => {
3432
test('returns the local fqdn when the environment is local', async () => {
3533
// Given
3634
vi.mocked(serviceEnvironment).mockReturnValue(Environment.Local)
37-
vi.mocked(blockPartnersAccess).mockReturnValue(false)
3835

3936
// When
4037
const got = await partnersFqdn()
@@ -46,7 +43,6 @@ describe('partners', () => {
4643
test('returns the production fqdn when the environment is production', async () => {
4744
// Given
4845
vi.mocked(serviceEnvironment).mockReturnValue(Environment.Production)
49-
vi.mocked(blockPartnersAccess).mockReturnValue(false)
5046

5147
// When
5248
const got = await partnersFqdn()

packages/cli-kit/src/public/node/context/fqdn.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import {AbortError, BugError} from '../error.js'
1+
import {AbortError} from '../error.js'
22
import {serviceEnvironment} from '../../../private/node/context/service.js'
33
import {DevServer, DevServerCore} from '../vendor/dev_server/index.js'
4-
import {blockPartnersAccess} from '../environment.js'
54

65
export const NotProvidedStoreFQDNError = new AbortError(
76
"Couldn't obtain the Shopify FQDN because the store FQDN was not provided.",
@@ -13,9 +12,6 @@ export const NotProvidedStoreFQDNError = new AbortError(
1312
* @returns Fully-qualified domain of the partners service we should interact with.
1413
*/
1514
export async function partnersFqdn(): Promise<string> {
16-
if (blockPartnersAccess()) {
17-
throw new BugError('Partners API is is no longer available.')
18-
}
1915
const environment = serviceEnvironment()
2016
const productionFqdn = 'partners.shopify.com'
2117
switch (environment) {

0 commit comments

Comments
 (0)