Skip to content

Commit

Permalink
Merge pull request #4217 from Shopify/add-fetch-store-bp-queries
Browse files Browse the repository at this point in the history
list and select dev stores
  • Loading branch information
gracejychang authored Jul 31, 2024
2 parents 7cd5e46 + 74671fb commit 204542c
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
/* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/ban-types */
import * as Types from './types.js'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'
Expand All @@ -14,17 +14,16 @@ export type FetchDevStoreByDomainQuery = {
properties?: {
edges: {
node:
| {__typename: 'BillingAccount'; id: string; externalId?: string | null}
| {__typename: 'Property'; id: string; externalId?: string | null}
| {
__typename: 'Shop'
id: string
externalId?: string | null
name: string
storeType?: Types.Store | null
primaryDomain?: string | null
shortName?: string | null
id: string
externalId?: string | null
}
| {}
}[]
} | null
} | null
Expand Down Expand Up @@ -109,23 +108,23 @@ export const FetchDevStoreByDomain = {
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
{kind: 'Field', name: {kind: 'Name', value: 'id'}},
{kind: 'Field', name: {kind: 'Name', value: 'externalId'}},
{
kind: 'InlineFragment',
typeCondition: {kind: 'NamedType', name: {kind: 'Name', value: 'Shop'}},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
{kind: 'Field', name: {kind: 'Name', value: 'id'}},
{kind: 'Field', name: {kind: 'Name', value: 'externalId'}},
{kind: 'Field', name: {kind: 'Name', value: 'name'}},
{kind: 'Field', name: {kind: 'Name', value: 'storeType'}},
{kind: 'Field', name: {kind: 'Name', value: 'primaryDomain'}},
{kind: 'Field', name: {kind: 'Name', value: 'shortName'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/consistent-type-definitions */
/* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/ban-types */
import * as Types from './types.js'

import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'
Expand All @@ -12,17 +12,16 @@ export type ListAppDevStoresQuery = {
properties?: {
edges: {
node:
| {__typename: 'BillingAccount'; id: string; externalId?: string | null}
| {__typename: 'Property'; id: string; externalId?: string | null}
| {
__typename: 'Shop'
id: string
externalId?: string | null
name: string
storeType?: Types.Store | null
primaryDomain?: string | null
shortName?: string | null
id: string
externalId?: string | null
}
| {}
}[]
} | null
} | null
Expand Down Expand Up @@ -95,23 +94,23 @@ export const ListAppDevStores = {
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
{kind: 'Field', name: {kind: 'Name', value: 'id'}},
{kind: 'Field', name: {kind: 'Name', value: 'externalId'}},
{
kind: 'InlineFragment',
typeCondition: {kind: 'NamedType', name: {kind: 'Name', value: 'Shop'}},
selectionSet: {
kind: 'SelectionSet',
selections: [
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
{kind: 'Field', name: {kind: 'Name', value: 'id'}},
{kind: 'Field', name: {kind: 'Name', value: 'externalId'}},
{kind: 'Field', name: {kind: 'Name', value: 'name'}},
{kind: 'Field', name: {kind: 'Name', value: 'storeType'}},
{kind: 'Field', name: {kind: 'Name', value: 'primaryDomain'}},
{kind: 'Field', name: {kind: 'Name', value: 'shortName'}},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
properties(filters: {field: STORE_TYPE, operator: EQUALS, value: "app_development"}, search: $domain, offeringHandles: ["shop"]) {
edges {
node {
__typename
id
externalId
... on Shop {
__typename
id
externalId
name
storeType
primaryDomain
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
properties(filters: {field: STORE_TYPE, operator: EQUALS, value: "app_development"}, offeringHandles: ["shop"]) {
edges {
node {
__typename
id
externalId
... on Shop {
__typename
id
externalId
name
storeType
primaryDomain
Expand Down
20 changes: 11 additions & 9 deletions packages/app/src/cli/services/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,32 +175,34 @@ export async function ensureDevContext(options: DevContextOptions): Promise<DevC
orgId = org.id
}

let {app: selectedApp, store: selectedStore} = await fetchDevDataFromOptions(options, orgId, developerPlatformClient)
const organization = await fetchOrgFromId(orgId, developerPlatformClient)

// we select an app or a dev store from a command flag
let {app: selectedApp, store: selectedStore} = await fetchDevDataFromOptions(options, orgId, developerPlatformClient)

// if no stores or apps were selected previously from a command,
// we try to load the app or the dev store from the current config or cache
// if that's not available, we prompt the user to choose an existing one or create a new one
if (!selectedApp || !selectedStore) {
// if we have selected an app or a dev store from a command flag, we keep them
// if not, we try to load the app or the dev store from the current config or cache
// if that's not available, we prompt the user to choose an existing one or create a new one
const [_selectedApp, _selectedStore] = await Promise.all([
const [cachedApp, cachedStore] = await Promise.all([
selectedApp ||
remoteApp ||
(cachedInfo?.appId &&
appFromId({id: cachedInfo.appGid, apiKey: cachedInfo.appId, organizationId: orgId, developerPlatformClient})),
selectedStore || (cachedInfo?.storeFqdn && storeFromFqdn(cachedInfo.storeFqdn, orgId, developerPlatformClient)),
])

if (_selectedApp) {
selectedApp = _selectedApp
if (cachedApp) {
selectedApp = cachedApp
} else {
const {apps, hasMorePages} = await developerPlatformClient.appsForOrg(orgId)
// get toml names somewhere close to here
const localAppName = await loadAppName(options.directory)
selectedApp = await selectOrCreateApp(localAppName, apps, hasMorePages, organization, developerPlatformClient)
}

if (_selectedStore) {
selectedStore = _selectedStore
if (cachedStore) {
selectedStore = cachedStore
} else {
const allStores = await developerPlatformClient.devStoresForOrg(orgId)
selectedStore = await selectStore(allStores, organization, developerPlatformClient)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,20 +109,36 @@ import {CONFIG_EXTENSION_IDS} from '../../models/extensions/extension-instance.j
import {DevSessionCreate, DevSessionCreateMutation} from '../../api/graphql/app-dev/generated/dev-session-create.js'
import {DevSessionUpdate, DevSessionUpdateMutation} from '../../api/graphql/app-dev/generated/dev-session-update.js'
import {DevSessionDelete, DevSessionDeleteMutation} from '../../api/graphql/app-dev/generated/dev-session-delete.js'
import {
FetchDevStoreByDomain,
FetchDevStoreByDomainQueryVariables,
} from '../../api/graphql/business-platform-organizations/generated/fetch_dev_store_by_domain.js'
import {
ListAppDevStores,
ListAppDevStoresQuery,
} from '../../api/graphql/business-platform-organizations/generated/list_app_dev_stores.js'
import {ensureAuthenticatedAppManagement, ensureAuthenticatedBusinessPlatform} from '@shopify/cli-kit/node/session'
import {FunctionUploadUrlGenerateResponse} from '@shopify/cli-kit/node/api/partners'
import {isUnitTest} from '@shopify/cli-kit/node/context/local'
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
import {fetch} from '@shopify/cli-kit/node/http'
import {appManagementRequest} from '@shopify/cli-kit/node/api/app-management'
import {appDevRequest} from '@shopify/cli-kit/node/api/app-dev'
import {businessPlatformRequest, businessPlatformRequestDoc} from '@shopify/cli-kit/node/api/business-platform'
import {
businessPlatformOrganizationsRequest,
businessPlatformRequest,
businessPlatformRequestDoc,
} from '@shopify/cli-kit/node/api/business-platform'
import {developerDashboardFqdn} from '@shopify/cli-kit/node/context/fqdn'
import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'
import {versionSatisfies} from '@shopify/cli-kit/node/node-package-manager'

const TEMPLATE_JSON_URL = 'https://raw.githubusercontent.com/Shopify/extensions-templates/main/templates.json'

type OrgType = NonNullable<ListAppDevStoresQuery['organization']>
type Properties = NonNullable<OrgType['properties']>
type ShopEdge = NonNullable<Properties['edges'][number]>
type ShopNode = Exclude<ShopEdge['node'], {[key: string]: never}>
export interface GatedExtensionTemplate extends ExtensionTemplate {
organizationBetaFlags?: string[]
minimumCliVersion?: string
Expand Down Expand Up @@ -360,8 +376,23 @@ export class AppManagementClient implements DeveloperPlatformClient {
}
}

async devStoresForOrg(_orgId: string): Promise<OrganizationStore[]> {
return []
// we are returning OrganizationStore type here because we want to keep types consistent btwn
// partners-client and app-management-client. Since we need transferDisabled and convertableToPartnerTest values
// from the Partners OrganizationStore schema, we will return this type for now
async devStoresForOrg(orgId: string): Promise<OrganizationStore[]> {
const storesResult = await businessPlatformOrganizationsRequest<ListAppDevStoresQuery>(
ListAppDevStores,
await this.businessPlatformToken(),
orgId,
)
const organization = storesResult.organization

if (!organization) {
throw new AbortError(`No organization found`)
}

const shopArray = organization.properties?.edges.map((value) => value.node as ShopNode) ?? []
return mapBusinessPlatformStoresToOrganizationStores(shopArray)
}

async appExtensionRegistrations(
Expand Down Expand Up @@ -694,8 +725,41 @@ export class AppManagementClient implements DeveloperPlatformClient {
}
}

async storeByDomain(_orgId: string, _shopDomain: string): Promise<FindStoreByDomainSchema> {
throw new BugError('Not implemented: storeByDomain')
// we are using FindStoreByDomainSchema type here because we want to keep types consistent btwn
// partners-client and app-management-client. Since we need transferDisabled and convertableToPartnerTest values
// from the Partners FindByStoreDomainSchema, we will return this type for now
async storeByDomain(orgId: string, shopDomain: string): Promise<FindStoreByDomainSchema> {
const queryVariables: FetchDevStoreByDomainQueryVariables = {domain: shopDomain}
const storesResult = await businessPlatformOrganizationsRequest(
FetchDevStoreByDomain,
await this.businessPlatformToken(),
orgId,
queryVariables,
)

const organization = storesResult.organization

if (!organization) {
throw new AbortError(`No organization found`)
}

const bpStoresArray = organization.properties?.edges.map((value) => value.node as ShopNode) ?? []
const storesArray = mapBusinessPlatformStoresToOrganizationStores(bpStoresArray)

return {
organizations: {
nodes: [
{
id: organization.id,
businessName: organization.name,
website: 'N/A',
stores: {
nodes: storesArray,
},
},
],
},
}
}

async createExtension(_input: ExtensionCreateVariables): Promise<ExtensionCreateSchema> {
Expand Down Expand Up @@ -917,3 +981,17 @@ export async function allowedTemplates(
function experience(identifier: string): 'configuration' | 'extension' {
return CONFIG_EXTENSION_IDS.includes(identifier) ? 'configuration' : 'extension'
}

function mapBusinessPlatformStoresToOrganizationStores(storesArray: ShopNode[]): OrganizationStore[] {
return storesArray.map((store: ShopNode) => {
const {externalId, primaryDomain, name} = store
return {
shopId: externalId,
link: primaryDomain,
shopDomain: primaryDomain,
shopName: name,
transferDisabled: true,
convertableToPartnerTest: true,
} as OrganizationStore
})
}
3 changes: 2 additions & 1 deletion packages/cli-kit/src/private/node/session/scopes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('allDefaultScopes', () => {
])
})

test('includes the App Management one when the required env var is defined', async () => {
test('includes App Management and Store Management when the required env var is defined', async () => {
// Given
const envVars = {USE_APP_MANAGEMENT_API: 'true'}

Expand All @@ -39,6 +39,7 @@ describe('allDefaultScopes', () => {
'https://api.shopify.com/auth/shop.storefront-renderer.devtools',
'https://api.shopify.com/auth/partners.app.cli.access',
'https://api.shopify.com/auth/destinations.readonly',
'https://api.shopify.com/auth/organization.store-management',
'https://api.shopify.com/auth/organization.apps.manage',
])
})
Expand Down
6 changes: 5 additions & 1 deletion packages/cli-kit/src/private/node/session/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ function defaultApiScopes(api: API, systemEnvironment = process.env): string[] {
case 'partners':
return ['cli']
case 'business-platform':
return ['destinations']
return isTruthy(systemEnvironment.USE_APP_MANAGEMENT_API)
? ['destinations', 'store-management']
: ['destinations']
case 'app-management':
return isTruthy(systemEnvironment.USE_APP_MANAGEMENT_API) ? ['app-management'] : []
default:
Expand All @@ -57,6 +59,8 @@ function scopeTransform(scope: string): string {
return 'https://api.shopify.com/auth/shop.storefront-renderer.devtools'
case 'destinations':
return 'https://api.shopify.com/auth/destinations.readonly'
case 'store-management':
return 'https://api.shopify.com/auth/organization.store-management'
case 'app-management':
return 'https://api.shopify.com/auth/organization.apps.manage'
default:
Expand Down
30 changes: 29 additions & 1 deletion packages/cli-kit/src/public/node/api/business-platform.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {GraphQLVariables, graphqlRequest, graphqlRequestDoc} from './graphql.js'
import {Exact, GraphQLVariables, graphqlRequest, graphqlRequestDoc} from './graphql.js'
import {handleDeprecations} from './partners.js'
import {businessPlatformFqdn} from '../context/fqdn.js'
import {TypedDocumentNode} from '@graphql-typed-document-node/core'
Expand Down Expand Up @@ -60,3 +60,31 @@ export async function businessPlatformRequestDoc<TResult, TVariables extends Var
variables,
})
}

/**
* Executes a GraphQL query against the Business Platform Organizations API.
*
* @param query - GraphQL query to execute.
* @param token - Business Platform token.
* @param organizationId - Organization ID as a numeric value.
* @param variables - GraphQL variables to pass to the query.
* @returns The response of the query of generic type <T>.
*/
export async function businessPlatformOrganizationsRequest<TResult>(
query: TypedDocumentNode<TResult, GraphQLVariables> | TypedDocumentNode<TResult, Exact<{[key: string]: never}>>,
token: string,
organizationId: string,
variables?: GraphQLVariables,
): Promise<TResult> {
const api = 'BusinessPlatform'
const fqdn = await businessPlatformFqdn()
const url = `https://${fqdn}/organizations/api/unstable/organization/${organizationId}/graphql`
return graphqlRequestDoc({
query,
api,
url,
token,
variables,
responseOptions: {onResponse: handleDeprecations},
})
}
Loading

0 comments on commit 204542c

Please sign in to comment.