Skip to content

Commit bf44c96

Browse files
Use Singleton for developer platform clients
1 parent fa48688 commit bf44c96

File tree

7 files changed

+130
-47
lines changed

7 files changed

+130
-47
lines changed

packages/app/src/cli/services/context/identifiers-extensions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ export async function ensureExtensionsIds(
6161

6262
// Migration is only supported in partners client
6363
const clientName = options.developerPlatformClient.clientName
64-
const migrationClient = clientName === ClientName.Partners ? options.developerPlatformClient : new PartnersClient()
64+
const migrationClient =
65+
clientName === ClientName.Partners ? options.developerPlatformClient : PartnersClient.getInstance()
6566

6667
let didMigrateDashboardExtensions = false
6768

packages/app/src/cli/services/dev/fetch.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ describe('fetchOrganizations', async () => {
5151
const appManagementClient: AppManagementClient = testDeveloperPlatformClient({
5252
organizations: () => Promise.resolve([ORG2]),
5353
}) as AppManagementClient
54-
vi.mocked(PartnersClient).mockReturnValue(partnersClient)
55-
vi.mocked(AppManagementClient).mockReturnValue(appManagementClient)
54+
vi.mocked(PartnersClient.getInstance).mockReturnValue(partnersClient)
55+
vi.mocked(AppManagementClient.getInstance).mockReturnValue(appManagementClient)
5656

5757
// When
5858
const got = await fetchOrganizations()
@@ -71,8 +71,8 @@ describe('fetchOrganizations', async () => {
7171
const appManagementClient: AppManagementClient = testDeveloperPlatformClient({
7272
organizations: () => Promise.resolve([]),
7373
}) as AppManagementClient
74-
vi.mocked(PartnersClient).mockReturnValue(partnersClient)
75-
vi.mocked(AppManagementClient).mockReturnValue(appManagementClient)
74+
vi.mocked(PartnersClient.getInstance).mockReturnValue(partnersClient)
75+
vi.mocked(AppManagementClient.getInstance).mockReturnValue(appManagementClient)
7676

7777
// When
7878
const got = fetchOrganizations()

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ export interface AppVersionIdentifiers {
8383
export function allDeveloperPlatformClients(): DeveloperPlatformClient[] {
8484
const clients: DeveloperPlatformClient[] = []
8585

86-
clients.push(new AppManagementClient())
86+
clients.push(AppManagementClient.getInstance())
8787

8888
if (!blockPartnersAccess()) {
89-
clients.push(new PartnersClient())
89+
clients.push(PartnersClient.getInstance())
9090
}
9191

9292
return clients
@@ -96,12 +96,12 @@ export function selectDeveloperPlatformClient({
9696
organization,
9797
}: SelectDeveloperPlatformClientOptions = {}): DeveloperPlatformClient {
9898
if (organization) return selectDeveloperPlatformClientByOrg(organization)
99-
return new PartnersClient()
99+
return PartnersClient.getInstance()
100100
}
101101

102102
function selectDeveloperPlatformClientByOrg(organization: Organization): DeveloperPlatformClient {
103-
if (organization.source === OrganizationSource.BusinessPlatform) return new AppManagementClient()
104-
return new PartnersClient()
103+
if (organization.source === OrganizationSource.BusinessPlatform) return AppManagementClient.getInstance()
104+
return PartnersClient.getInstance()
105105
}
106106

107107
export interface CreateAppOptions {

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

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {MinimalAppIdentifiers} from '../../models/organization.js'
3131
import {CreateAssetUrl} from '../../api/graphql/app-management/generated/create-asset-url.js'
3232
import {SourceExtension} from '../../api/graphql/app-management/generated/types.js'
3333
import {ListOrganizations} from '../../api/graphql/business-platform-destinations/generated/organizations.js'
34-
import {describe, expect, test, vi} from 'vitest'
34+
import {describe, expect, test, vi, beforeEach} from 'vitest'
3535
import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'
3636
import {fetch} from '@shopify/cli-kit/node/http'
3737
import {
@@ -49,6 +49,11 @@ vi.mock('@shopify/cli-kit/node/api/business-platform')
4949
vi.mock('@shopify/cli-kit/node/api/app-management')
5050
vi.mock('@shopify/cli-kit/node/api/webhooks')
5151

52+
beforeEach(() => {
53+
// Reset the singleton instance before each test
54+
AppManagementClient.resetInstance()
55+
})
56+
5257
const extensionA = await testUIExtension({uid: 'extension-a-uuid'})
5358
const extensionB = await testUIExtension({uid: 'extension-b-uuid'})
5459
const extensionC = await testUIExtension({uid: 'extension-c-uuid'})
@@ -160,7 +165,7 @@ describe('templateSpecifications', () => {
160165
vi.mocked(businessPlatformOrganizationsRequest).mockResolvedValueOnce(mockedFetchFlagsResponse)
161166

162167
// When
163-
const client = new AppManagementClient()
168+
const client = AppManagementClient.getInstance()
164169
client.businessPlatformToken = () => Promise.resolve('business-platform-token')
165170
const {templates: got} = await client.templateSpecifications(orgApp)
166171
const gotLabels = got.map((template) => template.name)
@@ -188,7 +193,7 @@ describe('templateSpecifications', () => {
188193
vi.mocked(businessPlatformOrganizationsRequest).mockResolvedValueOnce(mockedFetchFlagsResponse)
189194

190195
// When
191-
const client = new AppManagementClient()
196+
const client = AppManagementClient.getInstance()
192197
client.businessPlatformToken = () => Promise.resolve('business-platform-token')
193198
const {templates: got} = await client.templateSpecifications(orgApp)
194199
const gotLabels = got.map((template) => template.name)
@@ -236,7 +241,7 @@ describe('templateSpecifications', () => {
236241
vi.mocked(businessPlatformOrganizationsRequest).mockResolvedValueOnce(mockedFetchFlagsResponse)
237242

238243
// When
239-
const client = new AppManagementClient()
244+
const client = AppManagementClient.getInstance()
240245
client.businessPlatformToken = () => Promise.resolve('business-platform-token')
241246
const {groupOrder} = await client.templateSpecifications(orgApp)
242247

@@ -249,7 +254,7 @@ describe('templateSpecifications', () => {
249254
vi.mocked(fetch).mockRejectedValueOnce(new Error('Failed to fetch'))
250255

251256
// When
252-
const client = new AppManagementClient()
257+
const client = AppManagementClient.getInstance()
253258
const got = client.templateSpecifications(testOrganizationApp())
254259

255260
// Then
@@ -346,7 +351,7 @@ describe('searching for apps', () => {
346351
vi.mocked(appManagementRequestDoc).mockResolvedValueOnce(mockedFetchAppsResponse)
347352

348353
// When
349-
const client = new AppManagementClient()
354+
const client = AppManagementClient.getInstance()
350355
client.token = () => Promise.resolve('token')
351356
const got = await client.appsForOrg(orgId, query)
352357

@@ -377,7 +382,7 @@ describe('searching for apps', () => {
377382
vi.mocked(appManagementRequestDoc).mockResolvedValueOnce({})
378383

379384
// When
380-
const client = new AppManagementClient()
385+
const client = AppManagementClient.getInstance()
381386
client.token = () => Promise.resolve('token')
382387

383388
// Then
@@ -388,7 +393,7 @@ describe('searching for apps', () => {
388393
describe('createApp', () => {
389394
test('fetches latest stable API version for webhooks module', async () => {
390395
// Given
391-
const client = new AppManagementClient()
396+
const client = AppManagementClient.getInstance()
392397
const org = testOrganization()
393398
const mockedApiVersionResult: PublicApiVersionsQuery = {
394399
publicApiVersions: [{handle: '2024-07'}, {handle: '2024-10'}, {handle: '2025-01'}, {handle: 'unstable'}],
@@ -442,7 +447,7 @@ describe('createApp', () => {
442447
test('creates app successfully and returns expected app structure', async () => {
443448
// Given
444449
const appName = 'app-name'
445-
const client = new AppManagementClient()
450+
const client = AppManagementClient.getInstance()
446451
const org = testOrganization()
447452
const expectedApp = {
448453
id: '1',
@@ -485,7 +490,7 @@ describe('createApp', () => {
485490

486491
test('sets embedded to true in app home module', async () => {
487492
// Given
488-
const client = new AppManagementClient()
493+
const client = AppManagementClient.getInstance()
489494
const org = testOrganization()
490495
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce({
491496
publicApiVersions: [{handle: '2024-07'}, {handle: '2024-10'}, {handle: '2025-01'}, {handle: 'unstable'}],
@@ -540,7 +545,7 @@ describe('apiVersions', () => {
540545
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce(mockedResponse)
541546

542547
// When
543-
const client = new AppManagementClient()
548+
const client = AppManagementClient.getInstance()
544549
client.token = () => Promise.resolve('token')
545550
const apiVersions = await client.apiVersions(orgId)
546551

@@ -558,7 +563,7 @@ describe('topics', () => {
558563
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce(mockedResponse)
559564

560565
// When
561-
const client = new AppManagementClient()
566+
const client = AppManagementClient.getInstance()
562567
client.token = () => Promise.resolve('token')
563568
const topics = await client.topics({api_version: '2024-07'}, orgId)
564569

@@ -574,7 +579,7 @@ describe('topics', () => {
574579
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce(mockedResponse)
575580

576581
// When
577-
const client = new AppManagementClient()
582+
const client = AppManagementClient.getInstance()
578583
client.token = () => Promise.resolve('token')
579584
const topics = await client.topics({api_version: 'invalid'}, orgId)
580585

@@ -615,7 +620,7 @@ describe('sendSampleWebhook', () => {
615620
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce(mockedResponse)
616621

617622
// When
618-
const client = new AppManagementClient()
623+
const client = AppManagementClient.getInstance()
619624
client.token = () => Promise.resolve(token)
620625
const result = await client.sendSampleWebhook(input, orgId)
621626

@@ -664,7 +669,7 @@ describe('sendSampleWebhook', () => {
664669
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce(mockedResponse)
665670

666671
// When
667-
const client = new AppManagementClient()
672+
const client = AppManagementClient.getInstance()
668673
client.token = () => Promise.resolve(token)
669674
const result = await client.sendSampleWebhook(input, orgId)
670675

@@ -704,7 +709,7 @@ describe('sendSampleWebhook', () => {
704709
vi.mocked(webhooksRequestDoc).mockResolvedValueOnce(mockedResponse)
705710

706711
// When
707-
const client = new AppManagementClient()
712+
const client = AppManagementClient.getInstance()
708713
client.token = () => Promise.resolve('token')
709714
const result = await client.sendSampleWebhook(input, orgId)
710715

@@ -718,7 +723,7 @@ describe('sendSampleWebhook', () => {
718723

719724
describe('deploy', () => {
720725
// Given
721-
const client = new AppManagementClient()
726+
const client = AppManagementClient.getInstance()
722727
client.token = () => Promise.resolve('token')
723728

724729
test('creates version with correct metadata and modules', async () => {
@@ -933,7 +938,7 @@ describe('deploy', () => {
933938
test('queries for versions list', async () => {
934939
// Given
935940
const appId = 'gid://shopify/App/123'
936-
const client = new AppManagementClient()
941+
const client = AppManagementClient.getInstance()
937942
client.token = () => Promise.resolve('token')
938943
const mockResponse: AppVersionsQuery = {
939944
app: {
@@ -1036,7 +1041,7 @@ describe('AppManagementClient', () => {
10361041
describe('generateSignedUploadUrl', () => {
10371042
test('passes Brotli format for uploads', async () => {
10381043
// Given
1039-
const client = new AppManagementClient()
1044+
const client = AppManagementClient.getInstance()
10401045
const mockResponse = {
10411046
appRequestSourceUploadUrl: {
10421047
sourceUploadUrl: 'https://example.com/upload-url',
@@ -1079,7 +1084,7 @@ describe('AppManagementClient', () => {
10791084
describe('bundleFormat', () => {
10801085
test('returns br for Brotli compression format', () => {
10811086
// Given
1082-
const client = new AppManagementClient()
1087+
const client = AppManagementClient.getInstance()
10831088

10841089
// Then
10851090
expect(client.bundleFormat).toBe('br')
@@ -1094,7 +1099,7 @@ describe('ensureUserAccessToStore', () => {
10941099
const store = testOrganizationStore({shopId: '456'})
10951100
const token = 'business-platform-token'
10961101

1097-
const client = new AppManagementClient()
1102+
const client = AppManagementClient.getInstance()
10981103
client.businessPlatformToken = () => Promise.resolve(token)
10991104

11001105
const mockResponse = {
@@ -1127,7 +1132,7 @@ describe('ensureUserAccessToStore', () => {
11271132
// Given
11281133
const store = testOrganizationStore({})
11291134
store.provisionable = false
1130-
const client = new AppManagementClient()
1135+
const client = AppManagementClient.getInstance()
11311136

11321137
// When
11331138
await client.ensureUserAccessToStore('123', store)
@@ -1138,7 +1143,7 @@ describe('ensureUserAccessToStore', () => {
11381143

11391144
test('handles failure', async () => {
11401145
const store = testOrganizationStore({})
1141-
const client = new AppManagementClient()
1146+
const client = AppManagementClient.getInstance()
11421147
client.businessPlatformToken = () => Promise.resolve('business-platform-token')
11431148

11441149
const mockResponse = {
@@ -1156,7 +1161,7 @@ describe('ensureUserAccessToStore', () => {
11561161
})
11571162

11581163
describe('appExtensionRegistrations', () => {
1159-
const client = new AppManagementClient()
1164+
const client = AppManagementClient.getInstance()
11601165
const organizationId = 'org123'
11611166
const apiKey = 'api-key-123'
11621167
const appId = 'app-id-123'
@@ -1534,7 +1539,7 @@ describe('appExtensionRegistrations', () => {
15341539
describe('organizations', () => {
15351540
test('returns empty array when currentUserAccount is null', async () => {
15361541
// Given
1537-
const client = new AppManagementClient()
1542+
const client = AppManagementClient.getInstance()
15381543
client.businessPlatformToken = () => Promise.resolve('business-platform-token')
15391544

15401545
vi.mocked(businessPlatformRequestDoc).mockResolvedValueOnce({
@@ -1556,7 +1561,7 @@ describe('organizations', () => {
15561561

15571562
test('returns organizations with unique names', async () => {
15581563
// Given
1559-
const client = new AppManagementClient()
1564+
const client = AppManagementClient.getInstance()
15601565
client.businessPlatformToken = () => Promise.resolve('business-platform-token')
15611566
const mockResponse = {
15621567
currentUserAccount: {
@@ -1585,7 +1590,7 @@ describe('organizations', () => {
15851590

15861591
test('appends ID to businessName when organizations have duplicate names', async () => {
15871592
// Given
1588-
const client = new AppManagementClient()
1593+
const client = AppManagementClient.getInstance()
15891594
client.businessPlatformToken = () => Promise.resolve('business-platform-token')
15901595
const mockResponse = {
15911596
currentUserAccount: {
@@ -1632,7 +1637,7 @@ describe('organizations', () => {
16321637

16331638
test('returns empty array when organizationsWithAccessToDestination is empty', async () => {
16341639
// Given
1635-
const client = new AppManagementClient()
1640+
const client = AppManagementClient.getInstance()
16361641
client.businessPlatformToken = () => Promise.resolve('business-platform-token')
16371642
const mockResponse = {
16381643
currentUserAccount: {
@@ -1651,3 +1656,26 @@ describe('organizations', () => {
16511656
expect(result).toEqual([])
16521657
})
16531658
})
1659+
1660+
describe('singleton pattern', () => {
1661+
test('getInstance returns the same instance', () => {
1662+
// Given/When
1663+
const instance1 = AppManagementClient.getInstance()
1664+
const instance2 = AppManagementClient.getInstance()
1665+
1666+
// Then
1667+
expect(instance1).toBe(instance2)
1668+
})
1669+
1670+
test('resetInstance allows creating a new instance', () => {
1671+
// Given
1672+
const instance1 = AppManagementClient.getInstance()
1673+
1674+
// When
1675+
AppManagementClient.resetInstance()
1676+
const instance2 = AppManagementClient.getInstance()
1677+
1678+
// Then
1679+
expect(instance1).not.toBe(instance2)
1680+
})
1681+
})

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ export interface GatedExtensionTemplate extends ExtensionTemplate {
183183
}
184184

185185
export class AppManagementClient implements DeveloperPlatformClient {
186+
private static instance: AppManagementClient | undefined
187+
186188
public readonly clientName = ClientName.AppManagement
187189
public readonly webUiName = 'Developer Dashboard'
188190
public readonly supportsAtomicDeployments = true
@@ -193,10 +195,21 @@ export class AppManagementClient implements DeveloperPlatformClient {
193195
public readonly supportsDashboardManagedExtensions = false
194196
private _session: PartnersSession | undefined
195197

196-
constructor(session?: PartnersSession) {
198+
private constructor(session?: PartnersSession) {
197199
this._session = session
198200
}
199201

202+
static getInstance(session?: PartnersSession): AppManagementClient {
203+
if (!AppManagementClient.instance) {
204+
AppManagementClient.instance = new AppManagementClient(session)
205+
}
206+
return AppManagementClient.instance
207+
}
208+
209+
static resetInstance(): void {
210+
AppManagementClient.instance = undefined
211+
}
212+
200213
async subscribeToAppLogs(
201214
input: AppLogsSubscribeMutationVariables,
202215
_organizationId: string,

0 commit comments

Comments
 (0)