Skip to content

Commit 4ef8ece

Browse files
amcaplanclaude
andcommitted
Extract @Shopify/organizations package with org listing and selection utilities
Creates a standalone package for organization fetching and selection, calling the Business Platform Destinations API directly. Includes GraphQL codegen pipeline, fetchOrganizations (with GID decoding), selectOrganizationPrompt (auto-select for single org, duplicate name disambiguation), and selectOrg helper. Wires packages/app to use @Shopify/organizations for org fetching in AppManagementClient.organizations() and for the org selection prompt, eliminating duplicated Destinations API and prompt logic. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent acc94a4 commit 4ef8ece

36 files changed

Lines changed: 1272 additions & 169 deletions

.changeset/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"@shopify/cli",
88
"@shopify/app",
99
"@shopify/store",
10+
"@shopify/organizations",
1011
"@shopify/create-app",
1112
"@shopify/cli-kit",
1213
"@shopify/theme",

bin/get-graphql-schemas.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ const schemas = [
4848
owner: 'shop',
4949
repo: 'world',
5050
pathToFile: 'areas/platforms/organizations/db/graphql/destinations_schema.graphql',
51-
localPaths: ['./packages/app/src/cli/api/graphql/business-platform-destinations/destinations_schema.graphql'],
51+
localPaths: [
52+
'./packages/app/src/cli/api/graphql/business-platform-destinations/destinations_schema.graphql',
53+
'./packages/organizations/src/cli/api/graphql/business-platform-destinations/destinations_schema.graphql',
54+
],
5255
},
5356
{
5457
owner: 'shop',

configurations/vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,7 @@ export const aliases = (packagePath: string) => {
8282
},
8383
},
8484
{find: '@shopify/theme', replacement: path.join(packagePath, '../theme/src/index')},
85+
{find: '@shopify/organizations', replacement: path.join(packagePath, '../organizations/src/index')},
86+
{find: '@shopify/store', replacement: path.join(packagePath, '../store/src/index')},
8587
]
8688
}

graphql.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,6 @@ export default {
8585
webhooks: projectFactory('webhooks', 'webhooks_schema.graphql'),
8686
functions: projectFactory('functions', 'functions_cli_schema.graphql', 'app'),
8787
adminAsApp: projectFactory('admin', 'admin_schema.graphql'),
88+
organizationsDestinations: projectFactory('business-platform-destinations', 'destinations_schema.graphql', 'organizations'),
8889
},
8990
}

package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,23 @@
203203
]
204204
}
205205
},
206+
"packages/organizations": {
207+
"entry": [
208+
"**/index.ts!"
209+
],
210+
"project": "**/*.ts!",
211+
"ignore": [
212+
"**/graphql/**/generated/*.ts"
213+
],
214+
"ignoreDependencies": [
215+
"@graphql-typed-document-node/core"
216+
],
217+
"vite": {
218+
"config": [
219+
"vite.config.ts"
220+
]
221+
}
222+
},
206223
"packages/cli": {
207224
"entry": [
208225
"**/{commands,hooks}/**/*.ts!",

packages/app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"@luckycatfactory/esbuild-graphql-loader": "3.8.1",
5252
"@oclif/core": "4.5.3",
5353
"@shopify/cli-kit": "3.93.0",
54+
"@shopify/organizations": "3.93.0",
5455
"@shopify/plugin-cloudflare": "3.93.0",
5556
"@shopify/polaris": "12.27.0",
5657
"@shopify/polaris-icons": "8.11.1",

packages/app/src/cli/models/organization.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {AppConfigurationUsedByCli} from './extensions/specifications/types/app_config.js'
22
import {Flag, DeveloperPlatformClient} from '../utilities/developer-platform-client.js'
3+
import {Organization as BaseOrganization} from '@shopify/organizations'
34

45
export enum OrganizationSource {
56
Partners = 'Partners',
67
BusinessPlatform = 'BusinessPlatform',
78
}
89

9-
export interface Organization {
10-
id: string
11-
businessName: string
10+
export interface Organization extends BaseOrganization {
1211
source: OrganizationSource
1312
}
1413

packages/app/src/cli/prompts/dev.test.ts

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
createAsNewAppPrompt,
44
reloadStoreListPrompt,
55
selectAppPrompt,
6-
selectOrganizationPrompt,
76
selectStorePrompt,
87
updateURLsPrompt,
98
} from './dev.js'
@@ -68,85 +67,6 @@ beforeEach(() => {
6867
vi.mocked(getTomls).mockResolvedValue({})
6968
})
7069

71-
describe('selectOrganization', () => {
72-
test('request org selection if passing more than 1 org', async () => {
73-
// Given
74-
vi.mocked(renderAutocompletePrompt).mockResolvedValue('1')
75-
76-
// When
77-
const got = await selectOrganizationPrompt([ORG1, ORG2])
78-
79-
// Then
80-
expect(got).toEqual(ORG1)
81-
expect(renderAutocompletePrompt).toHaveBeenCalledWith({
82-
message: 'Which organization is this work for?',
83-
choices: [
84-
{label: 'org1', value: '1'},
85-
{label: 'org2', value: '2'},
86-
],
87-
})
88-
})
89-
90-
test('returns directly if passing only 1 org', async () => {
91-
// Given
92-
const orgs = [ORG2]
93-
94-
// When
95-
const got = await selectOrganizationPrompt(orgs)
96-
97-
// Then
98-
expect(got).toEqual(ORG2)
99-
expect(renderAutocompletePrompt).not.toBeCalled()
100-
})
101-
102-
// Intentional: when ANY duplicates exist, ALL orgs get ID suffix for consistent formatting
103-
test('appends ID to label when duplicate names exist', async () => {
104-
// Given
105-
const orgsWithDuplicates = [
106-
{id: '1', businessName: 'My Org', source: OrganizationSource.BusinessPlatform},
107-
{id: '2', businessName: 'My Org', source: OrganizationSource.BusinessPlatform},
108-
{id: '3', businessName: 'Other Org', source: OrganizationSource.BusinessPlatform},
109-
]
110-
vi.mocked(renderAutocompletePrompt).mockResolvedValue('1')
111-
112-
// When
113-
await selectOrganizationPrompt(orgsWithDuplicates)
114-
115-
// Then - note: Other Org also gets ID suffix for consistency
116-
expect(renderAutocompletePrompt).toHaveBeenCalledWith({
117-
message: 'Which organization is this work for?',
118-
choices: [
119-
{label: 'My Org (1)', value: '1'},
120-
{label: 'My Org (2)', value: '2'},
121-
{label: 'Other Org (3)', value: '3'},
122-
],
123-
})
124-
})
125-
126-
test('appends ID to all labels when all names are identical', async () => {
127-
// Given
128-
const orgsAllSameName = [
129-
{id: '1', businessName: 'Same Org', source: OrganizationSource.BusinessPlatform},
130-
{id: '2', businessName: 'Same Org', source: OrganizationSource.BusinessPlatform},
131-
{id: '3', businessName: 'Same Org', source: OrganizationSource.BusinessPlatform},
132-
]
133-
vi.mocked(renderAutocompletePrompt).mockResolvedValue('2')
134-
135-
// When
136-
await selectOrganizationPrompt(orgsAllSameName)
137-
138-
// Then
139-
expect(renderAutocompletePrompt).toHaveBeenCalledWith({
140-
message: 'Which organization is this work for?',
141-
choices: [
142-
{label: 'Same Org (1)', value: '1'},
143-
{label: 'Same Org (2)', value: '2'},
144-
{label: 'Same Org (3)', value: '3'},
145-
],
146-
})
147-
})
148-
})
149-
15070
describe('selectApp', () => {
15171
test('returns app if user selects one', async () => {
15272
// Given

packages/app/src/cli/prompts/dev.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,6 @@ import {
1212
} from '@shopify/cli-kit/node/ui'
1313
import {outputCompleted} from '@shopify/cli-kit/node/output'
1414

15-
export async function selectOrganizationPrompt(organizations: Organization[]): Promise<Organization> {
16-
if (organizations.length === 1) {
17-
return organizations[0]!
18-
}
19-
20-
// Add ID suffix to disambiguate when duplicate names exist
21-
const uniqueNames = new Set(organizations.map((org) => org.businessName))
22-
const hasDuplicates = uniqueNames.size < organizations.length
23-
const orgList = organizations.map((org) => ({
24-
label: hasDuplicates ? `${org.businessName} (${org.id})` : org.businessName,
25-
value: org.id,
26-
}))
27-
const id = await renderAutocompletePrompt({
28-
message: `Which organization is this work for?`,
29-
choices: orgList,
30-
})
31-
return organizations.find((org) => org.id === id)!
32-
}
33-
3415
export async function selectAppPrompt(
3516
onSearchForAppsByName: (term: string) => Promise<{apps: MinimalOrganizationApp[]; hasMorePages: boolean}>,
3617
apps: MinimalOrganizationApp[],

packages/app/src/cli/services/app/config/link-service.test.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import link from './link.js'
22
import {testOrganizationApp, testDeveloperPlatformClient} from '../../../models/app/app.test-data.js'
33
import {DeveloperPlatformClient, selectDeveloperPlatformClient} from '../../../utilities/developer-platform-client.js'
44
import {OrganizationApp, OrganizationSource} from '../../../models/organization.js'
5-
import {appNamePrompt, createAsNewAppPrompt, selectOrganizationPrompt} from '../../../prompts/dev.js'
5+
import {appNamePrompt, createAsNewAppPrompt} from '../../../prompts/dev.js'
66
import {selectConfigName} from '../../../prompts/config.js'
7+
import {selectOrganizationPrompt} from '@shopify/organizations'
78
import {beforeEach, describe, expect, test, vi} from 'vitest'
89
import {inTemporaryDirectory, readFile, writeFileSync} from '@shopify/cli-kit/node/fs'
910
import {joinPath} from '@shopify/cli-kit/node/path'
1011

1112
vi.mock('./use.js')
1213
vi.mock('../../../prompts/dev.js')
14+
vi.mock('@shopify/organizations')
1315
vi.mock('../../../prompts/config.js')
1416
vi.mock('../../local-storage')
1517
vi.mock('@shopify/cli-kit/node/ui')
@@ -84,7 +86,6 @@ api_version = "2024-01"
8486
vi.mocked(selectOrganizationPrompt).mockResolvedValue({
8587
id: '12345',
8688
businessName: 'test',
87-
source: OrganizationSource.BusinessPlatform,
8889
})
8990
vi.mocked(selectConfigName).mockResolvedValue('shopify.app.toml')
9091

@@ -178,7 +179,6 @@ api_version = "2025-07"
178179
vi.mocked(selectOrganizationPrompt).mockResolvedValue({
179180
id: '12345',
180181
businessName: 'test',
181-
source: OrganizationSource.BusinessPlatform,
182182
})
183183

184184
const options = {
@@ -227,7 +227,6 @@ required = true
227227
vi.mocked(selectOrganizationPrompt).mockResolvedValue({
228228
id: '12345',
229229
businessName: 'test',
230-
source: OrganizationSource.BusinessPlatform,
231230
})
232231

233232
const options = {

0 commit comments

Comments
 (0)