From 80ae96cf7f6dc5fd5697ca4cd3ada7eaffe97744 Mon Sep 17 00:00:00 2001 From: River Date: Mon, 13 Jul 2026 20:52:35 +0000 Subject: [PATCH] Remove the Partners API developer platform client from app commands App Management is the only developer platform client now that all organizations are migrated, so drop the Partners client, its GraphQL operations, and the Partners branch of org/app discovery and selection. `app dev` and `deploy` no longer contact the Partners API, so local development no longer needs the `partners` dev service or SHOPIFY_CLI_NEVER_USE_PARTNERS_API. Also remove the Partners-era legacy extension migrations that ran during deploy: they only ever worked against the Partners API (App Management throws "Not implemented" for those mutations). Stacked on top of #8049 (top of Isaac's deploy-split stack). Co-authored-by: Craig Martin Co-authored-by: Nick Wesselman --- ...move-partners-developer-platform-client.md | 5 + .../graphql/partners/generated/all-orgs.ts | 49 -- .../generated/current-account-info.ts | 58 -- .../partners/generated/dev-stores-by-org.ts | 132 ---- .../api/graphql/partners/generated/types.d.ts | 33 - .../graphql/partners/queries/all-orgs.graphql | 8 - .../queries/current-account-info.graphql | 11 - .../queries/dev-stores-by-org.graphql | 17 - .../context/deploy-app-version-migrations.ts | 100 --- .../deploy-identifier-matching.test.ts | 108 +-- .../context/deploy-identifier-matching.ts | 5 +- .../app/src/cli/services/context/prompts.ts | 31 - .../app/src/cli/services/dev/fetch.test.ts | 31 +- .../services/dev/migrate-app-module.test.ts | 197 ----- .../cli/services/dev/migrate-app-module.ts | 149 ---- .../dev/migrate-flow-extension.test.ts | 73 -- .../services/dev/migrate-flow-extension.ts | 71 -- .../dev/migrate-to-ui-extension.test.ts | 74 -- .../services/dev/migrate-to-ui-extension.ts | 64 -- .../utilities/developer-platform-client.ts | 31 +- .../partners-client.test.ts | 239 ------- .../partners-client.ts | 670 ------------------ 22 files changed, 16 insertions(+), 2140 deletions(-) create mode 100644 .changeset/remove-partners-developer-platform-client.md delete mode 100644 packages/app/src/cli/api/graphql/partners/generated/all-orgs.ts delete mode 100644 packages/app/src/cli/api/graphql/partners/generated/current-account-info.ts delete mode 100644 packages/app/src/cli/api/graphql/partners/generated/dev-stores-by-org.ts delete mode 100644 packages/app/src/cli/api/graphql/partners/generated/types.d.ts delete mode 100644 packages/app/src/cli/api/graphql/partners/queries/all-orgs.graphql delete mode 100644 packages/app/src/cli/api/graphql/partners/queries/current-account-info.graphql delete mode 100644 packages/app/src/cli/api/graphql/partners/queries/dev-stores-by-org.graphql delete mode 100644 packages/app/src/cli/services/context/deploy-app-version-migrations.ts delete mode 100644 packages/app/src/cli/services/context/prompts.ts delete mode 100644 packages/app/src/cli/services/dev/migrate-app-module.test.ts delete mode 100644 packages/app/src/cli/services/dev/migrate-app-module.ts delete mode 100644 packages/app/src/cli/services/dev/migrate-flow-extension.test.ts delete mode 100644 packages/app/src/cli/services/dev/migrate-flow-extension.ts delete mode 100644 packages/app/src/cli/services/dev/migrate-to-ui-extension.test.ts delete mode 100644 packages/app/src/cli/services/dev/migrate-to-ui-extension.ts delete mode 100644 packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts delete mode 100644 packages/app/src/cli/utilities/developer-platform-client/partners-client.ts diff --git a/.changeset/remove-partners-developer-platform-client.md b/.changeset/remove-partners-developer-platform-client.md new file mode 100644 index 00000000000..74d68a14416 --- /dev/null +++ b/.changeset/remove-partners-developer-platform-client.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Remove the Partners API developer platform client from app commands. App Management is now the only developer platform client used by `app dev`, `deploy`, and related commands, so local development no longer needs the Partners dev service (`SHOPIFY_CLI_NEVER_USE_PARTNERS_API` is no longer required). The Partners-era legacy extension migrations that ran during `deploy` are also removed, since they only ever worked against the Partners API. diff --git a/packages/app/src/cli/api/graphql/partners/generated/all-orgs.ts b/packages/app/src/cli/api/graphql/partners/generated/all-orgs.ts deleted file mode 100644 index a0e4c8c245e..00000000000 --- a/packages/app/src/cli/api/graphql/partners/generated/all-orgs.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ -import * as Types from './types.js' - -import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' - -export type AllOrgsQueryVariables = Types.Exact<{[key: string]: never}> - -export type AllOrgsQuery = {organizations: {nodes?: ({id: string; businessName: string} | null)[] | null}} - -export const AllOrgs = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: {kind: 'Name', value: 'AllOrgs'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'organizations'}, - arguments: [ - {kind: 'Argument', name: {kind: 'Name', value: 'first'}, value: {kind: 'IntValue', value: '200'}}, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'nodes'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'id'}}, - {kind: 'Field', name: {kind: 'Name', value: 'businessName'}}, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode diff --git a/packages/app/src/cli/api/graphql/partners/generated/current-account-info.ts b/packages/app/src/cli/api/graphql/partners/generated/current-account-info.ts deleted file mode 100644 index ebbce6a1785..00000000000 --- a/packages/app/src/cli/api/graphql/partners/generated/current-account-info.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ -import * as Types from './types.js' - -import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' - -export type CurrentAccountInfoQueryVariables = Types.Exact<{[key: string]: never}> - -export type CurrentAccountInfoQuery = { - currentAccountInfo: {__typename: 'ServiceAccount'; orgName: string} | {__typename: 'UserAccount'; email: string} -} - -export const CurrentAccountInfo = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: {kind: 'Name', value: 'currentAccountInfo'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'currentAccountInfo'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - { - kind: 'InlineFragment', - typeCondition: {kind: 'NamedType', name: {kind: 'Name', value: 'ServiceAccount'}}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'orgName'}}, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - { - kind: 'InlineFragment', - typeCondition: {kind: 'NamedType', name: {kind: 'Name', value: 'UserAccount'}}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'email'}}, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode diff --git a/packages/app/src/cli/api/graphql/partners/generated/dev-stores-by-org.ts b/packages/app/src/cli/api/graphql/partners/generated/dev-stores-by-org.ts deleted file mode 100644 index 12381011f62..00000000000 --- a/packages/app/src/cli/api/graphql/partners/generated/dev-stores-by-org.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-definitions */ -import * as Types from './types.js' - -import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' - -export type DevStoresByOrgQueryVariables = Types.Exact<{ - id: Types.Scalars['ID']['input'] -}> - -export type DevStoresByOrgQuery = { - organizations: { - nodes?: - | ({ - id: string - stores: { - nodes?: - | ({ - shopId?: string | null - link: unknown - shopDomain: string - shopName: string - transferDisabled: boolean - convertableToPartnerTest: boolean - } | null)[] - | null - } - } | null)[] - | null - } -} - -export const DevStoresByOrg = { - kind: 'Document', - definitions: [ - { - kind: 'OperationDefinition', - operation: 'query', - name: {kind: 'Name', value: 'DevStoresByOrg'}, - variableDefinitions: [ - { - kind: 'VariableDefinition', - variable: {kind: 'Variable', name: {kind: 'Name', value: 'id'}}, - type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'organizations'}, - arguments: [ - { - kind: 'Argument', - name: {kind: 'Name', value: 'id'}, - value: {kind: 'Variable', name: {kind: 'Name', value: 'id'}}, - }, - {kind: 'Argument', name: {kind: 'Name', value: 'first'}, value: {kind: 'IntValue', value: '1'}}, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'nodes'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'id'}}, - { - kind: 'Field', - name: {kind: 'Name', value: 'stores'}, - arguments: [ - { - kind: 'Argument', - name: {kind: 'Name', value: 'first'}, - value: {kind: 'IntValue', value: '500'}, - }, - { - kind: 'Argument', - name: {kind: 'Name', value: 'archived'}, - value: {kind: 'BooleanValue', value: false}, - }, - { - kind: 'Argument', - name: {kind: 'Name', value: 'type'}, - value: { - kind: 'ListValue', - values: [ - {kind: 'EnumValue', value: 'DEVELOPMENT'}, - {kind: 'EnumValue', value: 'MANAGED'}, - {kind: 'EnumValue', value: 'PLUS_SANDBOX'}, - ], - }, - }, - ], - selectionSet: { - kind: 'SelectionSet', - selections: [ - { - kind: 'Field', - name: {kind: 'Name', value: 'nodes'}, - selectionSet: { - kind: 'SelectionSet', - selections: [ - {kind: 'Field', name: {kind: 'Name', value: 'shopId'}}, - {kind: 'Field', name: {kind: 'Name', value: 'link'}}, - {kind: 'Field', name: {kind: 'Name', value: 'shopDomain'}}, - {kind: 'Field', name: {kind: 'Name', value: 'shopName'}}, - {kind: 'Field', name: {kind: 'Name', value: 'transferDisabled'}}, - {kind: 'Field', name: {kind: 'Name', value: 'convertableToPartnerTest'}}, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode diff --git a/packages/app/src/cli/api/graphql/partners/generated/types.d.ts b/packages/app/src/cli/api/graphql/partners/generated/types.d.ts deleted file mode 100644 index 1ffd775f795..00000000000 --- a/packages/app/src/cli/api/graphql/partners/generated/types.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* eslint-disable @typescript-eslint/consistent-type-definitions, @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any, tsdoc/syntax, @typescript-eslint/no-duplicate-type-constituents, @typescript-eslint/no-redundant-type-constituents, @nx/enforce-module-boundaries */ -import {JsonMapType} from '@shopify/cli-kit/node/toml' -export type Maybe = T | null; -export type InputMaybe = Maybe; -export type Exact = { [K in keyof T]: T[K] }; -export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; -export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; -export type MakeEmpty = { [_ in K]?: never }; -export type Incremental = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never }; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: { input: string; output: string; } - String: { input: string; output: string; } - Boolean: { input: boolean; output: boolean; } - Int: { input: number; output: number; } - Float: { input: number; output: number; } - /** - * An [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601) encoded UTC date time - * string. Example value: `"2019-07-03T20:47:55.123456Z"`. - */ - DateTime: { input: any; output: any; } - /** - * Deprecated: Supported extension types. - * This used to include a list of extension types, we mantain it to keep compatibility with the old schema - */ - ExtensionType: { input: any; output: any; } - /** Represents untyped JSON */ - JSON: { input: JsonMapType | string; output: JsonMapType; } - /** A valid URL, transported as a string. */ - Url: { input: any; output: any; } - /** A valid webhook subscription endpoint string. */ - WebhookSubscriptionEndpoint: { input: any; output: any; } -}; diff --git a/packages/app/src/cli/api/graphql/partners/queries/all-orgs.graphql b/packages/app/src/cli/api/graphql/partners/queries/all-orgs.graphql deleted file mode 100644 index 30eb916c46d..00000000000 --- a/packages/app/src/cli/api/graphql/partners/queries/all-orgs.graphql +++ /dev/null @@ -1,8 +0,0 @@ -query AllOrgs { - organizations(first: 200) { - nodes { - id - businessName - } - } -} diff --git a/packages/app/src/cli/api/graphql/partners/queries/current-account-info.graphql b/packages/app/src/cli/api/graphql/partners/queries/current-account-info.graphql deleted file mode 100644 index 05d05fee2e2..00000000000 --- a/packages/app/src/cli/api/graphql/partners/queries/current-account-info.graphql +++ /dev/null @@ -1,11 +0,0 @@ -query currentAccountInfo { - currentAccountInfo { - __typename - ... on ServiceAccount { - orgName - } - ... on UserAccount { - email - } - } -} diff --git a/packages/app/src/cli/api/graphql/partners/queries/dev-stores-by-org.graphql b/packages/app/src/cli/api/graphql/partners/queries/dev-stores-by-org.graphql deleted file mode 100644 index a4f0dd56a46..00000000000 --- a/packages/app/src/cli/api/graphql/partners/queries/dev-stores-by-org.graphql +++ /dev/null @@ -1,17 +0,0 @@ -query DevStoresByOrg($id: ID!) { - organizations(id: $id, first: 1) { - nodes { - id - stores(first: 500, archived: false, type: [DEVELOPMENT, MANAGED, PLUS_SANDBOX]) { - nodes { - shopId - link - shopDomain - shopName - transferDisabled - convertableToPartnerTest - } - } - } - } -} diff --git a/packages/app/src/cli/services/context/deploy-app-version-migrations.ts b/packages/app/src/cli/services/context/deploy-app-version-migrations.ts deleted file mode 100644 index 9e862120cd4..00000000000 --- a/packages/app/src/cli/services/context/deploy-app-version-migrations.ts +++ /dev/null @@ -1,100 +0,0 @@ -import {EnsureDeploymentIdsPresenceOptions, RemoteSource} from './identifiers.js' -import {extensionMigrationPrompt} from './prompts.js' -import {migrateFlowExtensions} from '../dev/migrate-flow-extension.js' -import {migrateExtensionsToUIExtension} from '../dev/migrate-to-ui-extension.js' -import { - AdminLinkModulesMap, - FlowModulesMap, - getModulesToMigrate, - MarketingModulesMap, - migrateAppModules, - UIModulesMap, -} from '../dev/migrate-app-module.js' -import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {PartnersClient} from '../../utilities/developer-platform-client/partners-client.js' -import {AbortSilentError} from '@shopify/cli-kit/node/error' - -/** Returns a fresh active app version when migrations changed remote modules. */ -export async function activeAppVersionAfterMigrations(options: EnsureDeploymentIdsPresenceOptions) { - const didMigrateExtensions = await migrateExtensionsIfNeeded(options) - return didMigrateExtensions - ? options.developerPlatformClient.activeAppVersion(options.remoteApp) - : options.activeAppVersion -} - -/** Runs supported legacy extension migrations before deploy classification. */ -async function migrateExtensionsIfNeeded(options: EnsureDeploymentIdsPresenceOptions) { - const {app, appId, developerPlatformClient, envIdentifiers, remoteApp} = options - const registrations = await developerPlatformClient.appExtensionRegistrations(remoteApp, options.activeAppVersion) - const localExtensions = app.allExtensions - const identifiers = envIdentifiers - const remoteExtensions = registrations.app.extensionRegistrations - const dashboardExtensions = registrations.app.dashboardManagedExtensionRegistrations - const migrationClient = PartnersClient.getInstance() - - let didMigrate = false - let migratedRemoteExtensions = remoteExtensions - - const uiExtensionsToMigrate = getModulesToMigrate( - localExtensions, - migratedRemoteExtensions, - identifiers, - UIModulesMap, - ) - if (uiExtensionsToMigrate.length > 0) { - const confirmedMigration = await extensionMigrationPrompt(uiExtensionsToMigrate) - if (!confirmedMigration) throw new AbortSilentError() - migratedRemoteExtensions = await migrateExtensionsToUIExtension({ - extensionsToMigrate: uiExtensionsToMigrate, - appId, - remoteExtensions: migratedRemoteExtensions, - migrationClient, - }) - didMigrate = true - } - - const dashboardMigrations = [ - {typesMap: FlowModulesMap, migrate: migrateFlowExtensions}, - migrationGroup(MarketingModulesMap, 'marketing_activity'), - migrationGroup(AdminLinkModulesMap, 'admin_link'), - ] - - for (const migration of dashboardMigrations) { - const extensionsToMigrate = getModulesToMigrate( - localExtensions, - dashboardExtensions, - identifiers, - migration.typesMap, - ) - if (extensionsToMigrate.length === 0) continue - - // eslint-disable-next-line no-await-in-loop - const confirmedMigration = await extensionMigrationPrompt(extensionsToMigrate, false) - if (!confirmedMigration) throw new AbortSilentError() - - // eslint-disable-next-line no-await-in-loop - const newRemoteExtensions = await migration.migrate({ - extensionsToMigrate, - appId, - remoteExtensions: dashboardExtensions, - migrationClient, - }) - migratedRemoteExtensions = migratedRemoteExtensions.concat(newRemoteExtensions) - didMigrate = true - } - - return didMigrate -} - -/** Adapts app-module migrations to the shared dashboard migration loop. */ -function migrationGroup(typesMap: {[key: string]: string[]}, type: string) { - return { - typesMap, - migrate: (options: { - extensionsToMigrate: Parameters[0]['extensionsToMigrate'] - appId: string - remoteExtensions: RemoteSource[] - migrationClient: DeveloperPlatformClient - }) => migrateAppModules({...options, type}), - } -} diff --git a/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts b/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts index 1ae7b2fc8d3..c5f6c638058 100644 --- a/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts +++ b/packages/app/src/cli/services/context/deploy-identifier-matching.test.ts @@ -1,6 +1,5 @@ /* eslint-disable @shopify/prefer-module-scope-constants */ import {classifyDeployExtensionChanges, ensureDeployIdentifiersFromAppVersion} from './deploy-identifier-matching.js' -import {extensionMigrationPrompt} from './prompts.js' import {EnsureDeploymentIdsPresenceOptions} from './identifiers.js' import {AppInterface} from '../../models/app/app.js' import { @@ -15,16 +14,12 @@ import {OrganizationApp} from '../../models/organization.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {BaseConfigType} from '../../models/extensions/schemas.js' import {createConfigExtensionSpecification} from '../../models/extensions/specification.js' -import {AppModuleVersion, DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' +import {AppModuleVersion} from '../../utilities/developer-platform-client.js' import {deployOrReleaseConfirmationPrompt} from '../../prompts/deploy-release.js' -import {migrateExtensionsToUIExtension} from '../dev/migrate-to-ui-extension.js' import {beforeAll, beforeEach, describe, expect, test, vi} from 'vitest' -import {AbortSilentError} from '@shopify/cli-kit/node/error' import {zod} from '@shopify/cli-kit/node/schema' vi.mock('../../prompts/deploy-release.js') -vi.mock('./prompts.js') -vi.mock('../dev/migrate-to-ui-extension.js') const REMOTE_APP: OrganizationApp = testOrganizationApp({ id: 'app-id', @@ -36,7 +31,6 @@ const EMPTY_EXTENSION_BREAKDOWN = {onlyRemote: [], toCreate: [], toUpdate: [], u let EXTENSION_A: ExtensionInstance let EXTENSION_B: ExtensionInstance -let EXTENSION_TO_MIGRATE: ExtensionInstance let CONFIG_EXTENSION: ExtensionInstance let WEBHOOK_SUBSCRIPTION_EXTENSION: ExtensionInstance let APP: AppInterface @@ -121,13 +115,6 @@ beforeAll(async () => { type: 'checkout_post_purchase', uid: 'uid-b', }) - EXTENSION_TO_MIGRATE = await buildUiExtension({ - directory: '/extension-to-migrate', - name: 'Legacy UI', - type: 'ui_extension', - uid: 'uid-migration', - }) - CONFIG_EXTENSION = await testAppConfigExtensions() WEBHOOK_SUBSCRIPTION_EXTENSION = await testSingleWebhookSubscriptionExtension() @@ -191,8 +178,6 @@ beforeAll(async () => { beforeEach(() => { vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(true) - vi.mocked(extensionMigrationPrompt).mockResolvedValue(true) - vi.mocked(migrateExtensionsToUIExtension).mockResolvedValue([]) }) describe('classifyDeployExtensionChanges', () => { @@ -628,95 +613,4 @@ describe('ensureDeployIdentifiersFromAppVersion', () => { }, }) }) - - test('runs extension migrations before classifying the app version', async () => { - const legacyRemoteExtension = { - uuid: 'legacy-uuid-a', - id: '', - title: EXTENSION_TO_MIGRATE.localIdentifier, - type: 'CHECKOUT_UI_EXTENSION', - } - const migratedModule = { - registrationId: EXTENSION_TO_MIGRATE.uid, - registrationUuid: legacyRemoteExtension.uuid, - registrationTitle: 'Legacy UI', - type: 'ui_extension', - config: await EXTENSION_TO_MIGRATE.deployConfig({apiKey: REMOTE_APP.apiKey, appConfiguration: APP.configuration}), - specification: { - identifier: 'ui_extension', - name: 'UI Extension', - experience: 'extension', - options: {managementExperience: 'cli'}, - }, - } as AppModuleVersion - const activeAppVersion = {appModuleVersions: [migratedModule]} - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({ - appExtensionRegistrations: () => - Promise.resolve({ - app: { - extensionRegistrations: [legacyRemoteExtension], - dashboardManagedExtensionRegistrations: [], - configurationRegistrations: [], - }, - }), - activeAppVersion: () => Promise.resolve(activeAppVersion), - }) - - await ensureDeployIdentifiersFromAppVersion( - deployOptions({ - app: testApp({...APP, allExtensions: [EXTENSION_TO_MIGRATE]}), - developerPlatformClient, - activeAppVersion: {appModuleVersions: []}, - allowUpdates: true, - }), - ) - - expect(extensionMigrationPrompt).toHaveBeenCalledWith([ - {local: EXTENSION_TO_MIGRATE, remote: legacyRemoteExtension}, - ]) - expect(migrateExtensionsToUIExtension).toHaveBeenCalledWith({ - extensionsToMigrate: [{local: EXTENSION_TO_MIGRATE, remote: legacyRemoteExtension}], - appId: REMOTE_APP.apiKey, - remoteExtensions: [legacyRemoteExtension], - migrationClient: expect.objectContaining({clientName: 'partners'}), - }) - expect(deployOrReleaseConfirmationPrompt).toHaveBeenCalledWith( - expect.objectContaining({ - extensionIdentifiersBreakdown: expect.objectContaining({ - unchanged: [{title: EXTENSION_TO_MIGRATE.localIdentifier, uid: undefined, experience: 'extension'}], - }), - }), - ) - }) - - test('aborts when extension migration is declined', async () => { - const legacyRemoteExtension = { - uuid: 'legacy-uuid-a', - id: '', - title: EXTENSION_TO_MIGRATE.localIdentifier, - type: 'CHECKOUT_UI_EXTENSION', - } - const developerPlatformClient: DeveloperPlatformClient = testDeveloperPlatformClient({ - appExtensionRegistrations: () => - Promise.resolve({ - app: { - extensionRegistrations: [legacyRemoteExtension], - dashboardManagedExtensionRegistrations: [], - configurationRegistrations: [], - }, - }), - }) - vi.mocked(extensionMigrationPrompt).mockResolvedValue(false) - - await expect( - ensureDeployIdentifiersFromAppVersion( - deployOptions({ - app: testApp({...APP, allExtensions: [EXTENSION_TO_MIGRATE]}), - developerPlatformClient, - activeAppVersion: {appModuleVersions: []}, - }), - ), - ).rejects.toThrow(AbortSilentError) - expect(deployOrReleaseConfirmationPrompt).not.toHaveBeenCalled() - }) }) diff --git a/packages/app/src/cli/services/context/deploy-identifier-matching.ts b/packages/app/src/cli/services/context/deploy-identifier-matching.ts index 8728e26ae50..d2955b69397 100644 --- a/packages/app/src/cli/services/context/deploy-identifier-matching.ts +++ b/packages/app/src/cli/services/context/deploy-identifier-matching.ts @@ -4,7 +4,6 @@ import { buildExtensionBreakdownInfo, ExtensionIdentifiersBreakdown, } from './breakdown-extensions.js' -import {activeAppVersionAfterMigrations} from './deploy-app-version-migrations.js' import {EnsureDeploymentIdsPresenceOptions} from './identifiers.js' import {remoteAppConfigurationExtensionContent} from '../app/select-app.js' import {AppInterface} from '../../models/app/app.js' @@ -30,11 +29,11 @@ interface ClassifyDeployExtensionChangesOptions { activeAppVersion?: AppVersion } -/** Builds deploy identifiers after migration, classification, and confirmation. */ +/** Builds deploy identifiers after classification and confirmation. */ export async function ensureDeployIdentifiersFromAppVersion( options: EnsureDeploymentIdsPresenceOptions, ): Promise { - const activeAppVersion = await activeAppVersionAfterMigrations(options) + const {activeAppVersion} = options const changes = await classifyDeployExtensionChanges({options, activeAppVersion}) const extensionIdentifiersBreakdown = buildExtensionIdentifiersBreakdown(changes) const configExtensionIdentifiersBreakdown = await buildDeployConfigExtensionIdentifiersBreakdown( diff --git a/packages/app/src/cli/services/context/prompts.ts b/packages/app/src/cli/services/context/prompts.ts deleted file mode 100644 index 1364864b787..00000000000 --- a/packages/app/src/cli/services/context/prompts.ts +++ /dev/null @@ -1,31 +0,0 @@ -import {LocalRemoteSource} from '../dev/migrate-app-module.js' -import {renderConfirmationPrompt, renderInfo} from '@shopify/cli-kit/node/ui' - -export async function extensionMigrationPrompt( - toMigrate: LocalRemoteSource[], - includeRemoteType = true, -): Promise { - const migrationNames = toMigrate.map(({local}) => `"${local.handle}"`).join(', ') - const allMigrationTypes = toMigrate.map(({remote}) => remote.type.toLocaleLowerCase()) - const uniqueMigrationTypes = allMigrationTypes - .filter((type, i) => allMigrationTypes.indexOf(type) === i) - .map((name) => `"${name}"`) - .join(', ') - - const migrationEndType = toMigrate.map(({local}) => `"${local.type}"`).join(', ') - - renderInfo({ - headline: "Extension migrations can't be undone.", - body: `Your ${migrationNames} configuration has been updated. Migrating gives you access to new features and won't impact the end user experience. All previous extension versions will reflect this change.`, - }) - - const confirmMessage = includeRemoteType - ? `Yes, confirm migration from ${uniqueMigrationTypes} to ${migrationEndType}` - : 'Yes, confirm migration' - - return renderConfirmationPrompt({ - message: `Migrate ${migrationNames}?`, - confirmationMessage: confirmMessage, - cancellationMessage: 'No, cancel', - }) -} diff --git a/packages/app/src/cli/services/dev/fetch.test.ts b/packages/app/src/cli/services/dev/fetch.test.ts index 26e0d0870e8..1da638dde24 100644 --- a/packages/app/src/cli/services/dev/fetch.test.ts +++ b/packages/app/src/cli/services/dev/fetch.test.ts @@ -6,13 +6,11 @@ import { testDeveloperPlatformClient, } from '../../models/app/app.test-data.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {PartnersClient} from '../../utilities/developer-platform-client/partners-client.js' import {AppManagementClient} from '../../utilities/developer-platform-client/app-management-client.js' import {afterEach, describe, expect, test, vi} from 'vitest' import {renderFatalError} from '@shopify/cli-kit/node/ui' import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' import {AbortError} from '@shopify/cli-kit/node/error' -import {blockPartnersAccess} from '@shopify/cli-kit/node/environment' const ORG1: Organization = { id: '1', @@ -34,10 +32,7 @@ const STORE1: OrganizationStore = { provisionable: true, } -vi.mock('@shopify/cli-kit/node/api/partners') -vi.mock('../../utilities/developer-platform-client/partners-client.js') vi.mock('../../utilities/developer-platform-client/app-management-client.js') -vi.mock('@shopify/cli-kit/node/environment') afterEach(() => { mockAndCaptureOutput().clear() @@ -45,30 +40,8 @@ afterEach(() => { }) describe('fetchOrganizations', async () => { - test('returns fetched organizations from available developer platform clients', async () => { + test('returns fetched organizations from App Management', async () => { // Given - vi.mocked(blockPartnersAccess).mockReturnValue(false) - const partnersClient: PartnersClient = testDeveloperPlatformClient({ - organizations: () => Promise.resolve([ORG1]), - }) as PartnersClient - const appManagementClient: AppManagementClient = testDeveloperPlatformClient({ - organizations: () => Promise.resolve([ORG2]), - }) as AppManagementClient - vi.mocked(PartnersClient.getInstance).mockReturnValue(partnersClient) - vi.mocked(AppManagementClient.getInstance).mockReturnValue(appManagementClient) - - // When - const got = await fetchOrganizations() - - // Then - expect(got).toEqual([ORG2, ORG1]) - expect(partnersClient.organizations).toHaveBeenCalled() - expect(appManagementClient.organizations).toHaveBeenCalled() - }) - - test('returns fetched organizations from App Management for 3P development', async () => { - // Given - vi.mocked(blockPartnersAccess).mockReturnValue(true) const appManagementClient: AppManagementClient = testDeveloperPlatformClient({ organizations: () => Promise.resolve([ORG2]), }) as AppManagementClient @@ -79,13 +52,11 @@ describe('fetchOrganizations', async () => { // Then expect(got).toEqual([ORG2]) - expect(PartnersClient.getInstance).not.toHaveBeenCalled() expect(appManagementClient.organizations).toHaveBeenCalled() }) test('throws if there are no organizations', async () => { // Given - vi.mocked(blockPartnersAccess).mockReturnValue(true) const appManagementClient: AppManagementClient = testDeveloperPlatformClient({ organizations: () => Promise.resolve([]), }) as AppManagementClient diff --git a/packages/app/src/cli/services/dev/migrate-app-module.test.ts b/packages/app/src/cli/services/dev/migrate-app-module.test.ts deleted file mode 100644 index c19f5d706df..00000000000 --- a/packages/app/src/cli/services/dev/migrate-app-module.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import {getModulesToMigrate, migrateAppModules} from './migrate-app-module.js' -import {LocalSource, RemoteSource} from '../context/identifiers.js' -import {testDeveloperPlatformClient} from '../../models/app/app.test-data.js' -import {ClientName} from '../../utilities/developer-platform-client.js' -import {describe, expect, test} from 'vitest' - -function getLocalExtension(attributes: Partial = {}) { - return { - localIdentifier: 'my-action', - configuration: { - name: 'my-action', - }, - ...attributes, - } as unknown as LocalSource -} - -function getRemoteExtension(attributes: Partial = {}) { - return { - uuid: '1234', - title: 'a-different-extension', - ...attributes, - } as unknown as RemoteSource -} -const defaultMap = { - payments_extension: [ - 'payments_app', - 'payments_app_credit_card', - 'payments_app_custom_credit_card', - 'payments_app_custom_onsite', - 'payments_app_redeemable', - ], - marketing_activity: ['marketing_activity_extension'], - subscription_link_extension: ['subscription_link'], -} - -const defaultIdentifiers = { - 'module-A': '1234', - 'module-B': '', -} - -describe('getModulesToMigrate()', () => { - test('returns an empty array if no extensions are provided', () => { - const toMigrate = getModulesToMigrate([], [], {}, defaultMap) - expect(toMigrate).toStrictEqual([]) - }) - - test('matching by remote title and localIdentifier, without defaultIdentifiers', () => { - // Given - const localExtension = getLocalExtension({type: 'payments_extension', localIdentifier: 'module-A'}) - const localExtensionB = getLocalExtension({type: 'marketing_activity', localIdentifier: 'module-B'}) - const localExtensionC = getLocalExtension({type: 'subscription_link_extension', localIdentifier: 'module-C'}) - const remoteExtension = getRemoteExtension({type: 'payments_app_credit_card', title: 'module-A', uuid: 'yy'}) - const remoteExtensionB = getRemoteExtension({type: 'marketing_activity_extension', title: 'module-B', uuid: 'xx'}) - const remoteExtensionC = getRemoteExtension({type: 'subscription_link', title: 'module-C', uuid: 'zz'}) - - // When - const toMigrate = getModulesToMigrate( - [localExtension, localExtensionB, localExtensionC], - [remoteExtension, remoteExtensionB, remoteExtensionC], - {}, - defaultMap, - ) - - // Then - expect(toMigrate).toStrictEqual([ - {local: localExtension, remote: remoteExtension}, - {local: localExtensionB, remote: remoteExtensionB}, - {local: localExtensionC, remote: remoteExtensionC}, - ]) - }) - - test('matching by truncated remote title and localIdentifier, without defaultIdentifiers', () => { - // Given - const localExtension = getLocalExtension({ - type: 'payments_extension', - localIdentifier: 'ten-chars-ten-chars-ten-chars-ten-chars-ten-123456', - }) - const remoteExtension = getRemoteExtension({ - type: 'payments_app_credit_card', - title: 'Ten Chars Ten Chars Ten Chars Ten Chars Ten 123456789', - }) - - // When - const toMigrate = getModulesToMigrate([localExtension], [remoteExtension], {}, defaultMap) - - // Then - expect(toMigrate).toStrictEqual([{local: localExtension, remote: remoteExtension}]) - }) - - test('matching different title and localIdentifier, using uuid', () => { - // Given - const localExtension = getLocalExtension({type: 'payments_extension', localIdentifier: 'module-A'}) - const remoteExtension = getRemoteExtension({ - type: 'payments_app_credit_card', - title: 'random-title', - uuid: defaultIdentifiers['module-A'], - }) - - // When - const toMigrate = getModulesToMigrate([localExtension], [remoteExtension], defaultIdentifiers, defaultMap) - - // Then - expect(toMigrate).toStrictEqual([{local: localExtension, remote: remoteExtension}]) - }) - - test('does not return modules where local.type is not included in defaultMap', () => { - // Given - const localExtension = getLocalExtension({type: 'checkout_ui_extension'}) - const remoteExtension = getRemoteExtension({type: 'payments_app_credit_card'}) - - // When - const toMigrate = getModulesToMigrate([localExtension], [remoteExtension], defaultIdentifiers, defaultMap) - - // Then - expect(toMigrate).toStrictEqual([]) - }) - - test('does not return modules where remote.type is not included in defaultMap', () => { - // Given - const localExtension = getLocalExtension({type: 'payments_extension'}) - const remoteExtension = getRemoteExtension({type: 'marketing_activity_extension'}) - - // When - const toMigrate = getModulesToMigrate([localExtension], [remoteExtension], defaultIdentifiers, defaultMap) - - // Then - expect(toMigrate).toStrictEqual([]) - }) - - test('if neither title/name or ids match, does not return any extensions', () => { - // Given - const localExtension = getLocalExtension({type: 'payments_extension', localIdentifier: 'module-A'}) - const remoteExtension = getRemoteExtension({type: 'payments_app_credit_card', title: 'doesnt-match', uuid: '0000'}) - - // When - const toMigrate = getModulesToMigrate([localExtension], [remoteExtension], defaultIdentifiers, defaultMap) - - // Then - expect(toMigrate).toStrictEqual([]) - }) -}) - -describe('migrateAppModules()', () => { - test('uses registrationUuid for AppManagement client', async () => { - const extensionsToMigrate = [ - {local: getLocalExtension(), remote: getRemoteExtension({id: 'id1', uuid: 'uuid1'})}, - {local: getLocalExtension(), remote: getRemoteExtension({id: 'id2', uuid: 'uuid2'})}, - ] - const appId = '123abc' - const type = 'payments_extension' - const remoteExtensions = extensionsToMigrate.map(({remote}) => remote) - const appManagementClient = testDeveloperPlatformClient({clientName: ClientName.AppManagement}) - const migrationClient = testDeveloperPlatformClient() - - await migrateAppModules({ - extensionsToMigrate, - appId, - type, - remoteExtensions, - migrationClient, - }) - - expect(migrationClient.migrateAppModule).toHaveBeenCalledTimes(extensionsToMigrate.length) - expect(migrationClient.migrateAppModule).toHaveBeenCalledWith({ - apiKey: appId, - registrationId: undefined, - registrationUuid: extensionsToMigrate[0]!.remote.uuid, - type, - }) - expect(migrationClient.migrateAppModule).toHaveBeenCalledWith({ - apiKey: appId, - registrationId: undefined, - registrationUuid: extensionsToMigrate[1]!.remote.uuid, - type, - }) - }) - - test('Returns updated remoteExensions', async () => { - const extensionsToMigrate = [ - {local: getLocalExtension(), remote: getRemoteExtension()}, - {local: getLocalExtension(), remote: getRemoteExtension()}, - ] - const appId = '123abc' - const type = 'payments_extension' - const remoteExtensions = extensionsToMigrate.map(({remote}) => remote) - - const result = await migrateAppModules({ - extensionsToMigrate, - appId, - type, - remoteExtensions, - migrationClient: testDeveloperPlatformClient(), - }) - - expect(result).toStrictEqual(remoteExtensions.map((remote) => ({...remote, type: 'PAYMENTS_EXTENSION'}))) - }) -}) diff --git a/packages/app/src/cli/services/dev/migrate-app-module.ts b/packages/app/src/cli/services/dev/migrate-app-module.ts deleted file mode 100644 index c626040a0c7..00000000000 --- a/packages/app/src/cli/services/dev/migrate-app-module.ts +++ /dev/null @@ -1,149 +0,0 @@ -import {LocalSource, RemoteSource} from '../context/identifiers.js' -import {ExtensionUuidsByLocalIdentifier} from '../../models/app/identifiers.js' -import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {MigrateAppModuleSchema, MigrateAppModuleVariables} from '../../api/graphql/extension_migrate_app_module.js' -import {MAX_EXTENSION_HANDLE_LENGTH} from '../../models/extensions/schemas.js' -import {AbortError} from '@shopify/cli-kit/node/error' -import {slugify} from '@shopify/cli-kit/common/string' - -/** - * All ***ModulesMap define the migration mapping between local and remote extension types. - * - * When adding new mappings, follow this rule in the mapping object: - * - The key is the NEW type - * - The value is an array of OLD types that can be migrated to the new type - */ -export const MarketingModulesMap = { - marketing_activity: ['marketing_activity_extension'], -} - -export const FlowModulesMap = { - flow_action: ['flow_action_definition'], - flow_trigger: ['flow_trigger_definition'], - flow_trigger_lifecycle_callback: ['flow_trigger_discovery_webhook'], -} - -export const UIModulesMap = { - ui_extension: ['CHECKOUT_UI_EXTENSION', 'POS_UI_EXTENSION'], -} - -export const AdminLinkModulesMap = { - admin_link: ['app_link', 'bulk_action'], -} - -export interface LocalRemoteSource { - local: LocalSource - remote: RemoteSource -} - -function getExtensionIds( - localSources: LocalSource[], - identifiers: ExtensionUuidsByLocalIdentifier, -): ExtensionUuidsByLocalIdentifier { - const localSourceIds = new Set(localSources.map((source) => source.localIdentifier)) - return Object.fromEntries(Object.entries(identifiers).filter(([identifier]) => localSourceIds.has(identifier))) -} - -/** - * Returns a list of local and remote extensions that need to be migrated. - * - * @param localSources - The local extensions to migrate. - * @param remoteSources - The remote extensions to migrate. - * @param identifiers - The identifiers for the extensions. - * @param typesMap - A map of extension types to migrate. - * @returns A list of local and remote extensions that need to be migrated. - */ -export function getModulesToMigrate( - localSources: LocalSource[], - remoteSources: RemoteSource[], - identifiers: ExtensionUuidsByLocalIdentifier, - typesMap: {[key: string]: string[]}, -) { - const ids = getExtensionIds(localSources, identifiers) - const localExtensionTypesToMigrate = Object.keys(typesMap) - const remoteExtensionTypesToMigrate = Object.values(typesMap).flat() - - const local = localSources.filter((source) => localExtensionTypesToMigrate.includes(source.type)) - const remote = remoteSources.filter((source) => remoteExtensionTypesToMigrate.includes(source.type)) - - // Map remote sources by uuid and slugified title (the slugified title is used for matching with local folder) - const remoteSourcesMap = new Map() - remote.forEach((remoteSource) => { - remoteSourcesMap.set(remoteSource.uuid, remoteSource) - remoteSourcesMap.set(slugify(remoteSource.title.substring(0, MAX_EXTENSION_HANDLE_LENGTH)), remoteSource) - }) - - return local.reduce((accumulator, localSource) => { - const localSourceId = ids[localSource.localIdentifier] ?? 'unknown' - const remoteSource = - remoteSourcesMap.get(localSourceId) ?? remoteSourcesMap.get(localSource.localIdentifier.toLowerCase()) - const typeMatch = typesMap[localSource.type]?.includes(remoteSource?.type ?? 'undefined') - - if (remoteSource && typeMatch) { - accumulator.push({local: localSource, remote: remoteSource}) - } - return accumulator - }, []) -} - -export async function migrateAppModules(options: { - extensionsToMigrate: LocalRemoteSource[] - appId: string - type: string - remoteExtensions: RemoteSource[] - migrationClient: DeveloperPlatformClient -}) { - const {extensionsToMigrate, appId, type, remoteExtensions, migrationClient} = options - - await Promise.all( - extensionsToMigrate.map(({remote}) => - migrateAppModule({ - apiKey: appId, - registrationId: undefined, - registrationUuid: remote.uuid, - type, - migrationClient, - }), - ), - ) - - const migratedUUIDs = extensionsToMigrate.map(({remote}) => remote.uuid) - return remoteExtensions - .filter((extension) => migratedUUIDs.includes(extension.uuid)) - .map((extension) => { - return { - ...extension, - type: type.toUpperCase(), - } - }) -} - -async function migrateAppModule(options: { - apiKey: MigrateAppModuleVariables['apiKey'] - registrationId: MigrateAppModuleVariables['registrationId'] - registrationUuid: MigrateAppModuleVariables['registrationUuid'] - type: MigrateAppModuleVariables['type'] - migrationClient: DeveloperPlatformClient -}) { - const {apiKey, registrationId, registrationUuid, type, migrationClient} = options - - const variables: MigrateAppModuleVariables = { - apiKey, - registrationId, - registrationUuid, - type, - } - - const result: MigrateAppModuleSchema = await migrationClient.migrateAppModule(variables) - - if (result?.migrateAppModule?.userErrors?.length > 0) { - const errors = result.migrateAppModule.userErrors.map((error) => error.message).join(', ') - throw new AbortError(errors) - } - - if (!result?.migrateAppModule?.migratedAppModule) { - throw new AbortError(`Couldn't migrate to app module ${type}`) - } - - return registrationId -} diff --git a/packages/app/src/cli/services/dev/migrate-flow-extension.test.ts b/packages/app/src/cli/services/dev/migrate-flow-extension.test.ts deleted file mode 100644 index ffd57bbd9c5..00000000000 --- a/packages/app/src/cli/services/dev/migrate-flow-extension.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import {migrateFlowExtensions} from './migrate-flow-extension.js' -import {LocalSource, RemoteSource} from '../context/identifiers.js' -import {testDeveloperPlatformClient} from '../../models/app/app.test-data.js' -import {describe, expect, test} from 'vitest' - -function getLocalExtension(attributes: Partial = {}) { - return { - type: 'flow_action', - localIdentifier: 'my-action', - configuration: { - name: 'my-action', - }, - ...attributes, - } as unknown as LocalSource -} - -function getRemoteExtension(attributes: Partial = {}) { - return { - uuid: '1234', - type: 'flow_action_definition', - title: 'a-different-extension', - ...attributes, - } as unknown as RemoteSource -} - -describe('migrateExtensions()', () => { - test('uses registrationUuid for AppManagement client', async () => { - const extensionsToMigrate = [ - {local: getLocalExtension(), remote: getRemoteExtension({id: 'id1', uuid: 'uuid1'})}, - {local: getLocalExtension(), remote: getRemoteExtension({id: 'id2', uuid: 'uuid2'})}, - ] - const appId = '123abc' - const remoteExtensions = extensionsToMigrate.map(({remote}) => remote) - const migrationClient = testDeveloperPlatformClient() - - await migrateFlowExtensions({ - extensionsToMigrate, - appId, - remoteExtensions, - migrationClient, - }) - - expect(migrationClient.migrateFlowExtension).toHaveBeenCalledTimes(extensionsToMigrate.length) - expect(migrationClient.migrateFlowExtension).toHaveBeenCalledWith({ - apiKey: appId, - registrationId: undefined, - registrationUuid: extensionsToMigrate[0]!.remote.uuid, - }) - expect(migrationClient.migrateFlowExtension).toHaveBeenCalledWith({ - apiKey: appId, - registrationId: undefined, - registrationUuid: extensionsToMigrate[1]!.remote.uuid, - }) - }) - - test('Returns updated remoteExensions', async () => { - const extensionsToMigrate = [ - {local: getLocalExtension(), remote: getRemoteExtension()}, - {local: getLocalExtension(), remote: getRemoteExtension()}, - ] - const appId = '123abc' - const remoteExtensions = extensionsToMigrate.map(({remote}) => remote) - - const result = await migrateFlowExtensions({ - extensionsToMigrate, - appId, - remoteExtensions, - migrationClient: testDeveloperPlatformClient(), - }) - - expect(result).toStrictEqual(remoteExtensions.map((remote) => ({...remote, type: 'FLOW_ACTION'}))) - }) -}) diff --git a/packages/app/src/cli/services/dev/migrate-flow-extension.ts b/packages/app/src/cli/services/dev/migrate-flow-extension.ts deleted file mode 100644 index b5241e8f48f..00000000000 --- a/packages/app/src/cli/services/dev/migrate-flow-extension.ts +++ /dev/null @@ -1,71 +0,0 @@ -import {LocalRemoteSource} from './migrate-app-module.js' -import {RemoteSource} from '../context/identifiers.js' -import { - MigrateFlowExtensionSchema, - MigrateFlowExtensionVariables, -} from '../../api/graphql/extension_migrate_flow_extension.js' -import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {AbortError} from '@shopify/cli-kit/node/error' - -export async function migrateFlowExtensions(options: { - extensionsToMigrate: LocalRemoteSource[] - appId: string - remoteExtensions: RemoteSource[] - migrationClient: DeveloperPlatformClient -}) { - const {extensionsToMigrate, appId, remoteExtensions, migrationClient} = options - - await Promise.all( - extensionsToMigrate.map(({remote}) => - migrateFlowExtension({ - apiKey: appId, - registrationId: undefined, - registrationUuid: remote.uuid, - migrationClient, - }), - ), - ) - - const typesMap = new Map([ - ['flow_action_definition', 'FLOW_ACTION'], - ['flow_trigger_definition', 'FLOW_TRIGGER'], - ]) - - const migratedUUIDs = extensionsToMigrate.map(({remote}) => remote.uuid) - return remoteExtensions - .filter((extension) => migratedUUIDs.includes(extension.uuid)) - .map((extension) => { - return { - ...extension, - type: typesMap.get(extension.type) ?? extension.type, - } - }) -} - -async function migrateFlowExtension(options: { - apiKey: MigrateFlowExtensionVariables['apiKey'] - registrationId: MigrateFlowExtensionVariables['registrationId'] - registrationUuid: MigrateFlowExtensionVariables['registrationUuid'] - migrationClient: DeveloperPlatformClient -}) { - const {apiKey, registrationId, registrationUuid, migrationClient} = options - - const variables: MigrateFlowExtensionVariables = { - apiKey, - registrationId, - registrationUuid, - } - - const result: MigrateFlowExtensionSchema = await migrationClient.migrateFlowExtension(variables) - - if (result?.migrateFlowExtension?.userErrors?.length > 0) { - const errors = result.migrateFlowExtension.userErrors.map((error) => error.message).join(', ') - throw new AbortError(errors) - } - - if (!result?.migrateFlowExtension?.migratedFlowExtension) { - throw new AbortError("Couldn't migrate to Flow extension") - } - - return registrationId -} diff --git a/packages/app/src/cli/services/dev/migrate-to-ui-extension.test.ts b/packages/app/src/cli/services/dev/migrate-to-ui-extension.test.ts deleted file mode 100644 index cb5de06225d..00000000000 --- a/packages/app/src/cli/services/dev/migrate-to-ui-extension.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import {migrateExtensionsToUIExtension} from './migrate-to-ui-extension.js' -import {LocalSource, RemoteSource} from '../context/identifiers.js' -import {testDeveloperPlatformClient} from '../../models/app/app.test-data.js' -import {describe, expect, test} from 'vitest' - -function getLocalExtension(attributes: Partial = {}) { - return { - type: 'ui_extension', - localIdentifier: 'my-extension', - handle: 'my-extension', - configuration: { - name: 'my-extension', - }, - ...attributes, - } as unknown as LocalSource -} - -function getRemoteExtension(attributes: Partial = {}) { - return { - uuid: '1234', - type: 'CHECKOUT_UI_EXTENSION', - title: 'a-different-extension', - ...attributes, - } as unknown as RemoteSource -} - -describe('migrateExtensions()', () => { - test('uses registrationUuid for AppManagement client', async () => { - const extensionsToMigrate = [ - {local: getLocalExtension(), remote: getRemoteExtension({id: 'id1', uuid: 'uuid1'})}, - {local: getLocalExtension(), remote: getRemoteExtension({id: 'id2', uuid: 'uuid2'})}, - ] - const appId = '123abc' - const remoteExtensions = extensionsToMigrate.map(({remote}) => ({...remote, type: 'CHECKOUT_UI_EXTENSION'})) - const migrationClient = testDeveloperPlatformClient() - - await migrateExtensionsToUIExtension({ - extensionsToMigrate, - appId, - remoteExtensions, - migrationClient, - }) - - expect(migrationClient.migrateToUiExtension).toHaveBeenCalledTimes(extensionsToMigrate.length) - expect(migrationClient.migrateToUiExtension).toHaveBeenCalledWith({ - apiKey: appId, - registrationId: undefined, - registrationUuid: extensionsToMigrate[0]!.remote.uuid, - }) - expect(migrationClient.migrateToUiExtension).toHaveBeenCalledWith({ - apiKey: appId, - registrationId: undefined, - registrationUuid: extensionsToMigrate[1]!.remote.uuid, - }) - }) - - test('Returns updated remoteExensions', async () => { - const extensionsToMigrate = [ - {local: getLocalExtension(), remote: getRemoteExtension()}, - {local: getLocalExtension(), remote: getRemoteExtension()}, - ] - const appId = '123abc' - const remoteExtensions = extensionsToMigrate.map(({remote}) => ({...remote, type: 'CHECKOUT_UI_EXTENSION'})) - - const result = await migrateExtensionsToUIExtension({ - extensionsToMigrate, - appId, - remoteExtensions, - migrationClient: testDeveloperPlatformClient(), - }) - - expect(result).toStrictEqual(remoteExtensions.map((remote) => ({...remote, type: 'UI_EXTENSION'}))) - }) -}) diff --git a/packages/app/src/cli/services/dev/migrate-to-ui-extension.ts b/packages/app/src/cli/services/dev/migrate-to-ui-extension.ts deleted file mode 100644 index 92596967f88..00000000000 --- a/packages/app/src/cli/services/dev/migrate-to-ui-extension.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {LocalRemoteSource} from './migrate-app-module.js' -import { - MigrateToUiExtensionSchema, - MigrateToUiExtensionVariables, -} from '../../api/graphql/extension_migrate_to_ui_extension.js' -import {RemoteSource} from '../context/identifiers.js' -import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' -import {AbortError} from '@shopify/cli-kit/node/error' - -export async function migrateExtensionsToUIExtension(options: { - extensionsToMigrate: LocalRemoteSource[] - appId: string - remoteExtensions: RemoteSource[] - migrationClient: DeveloperPlatformClient -}) { - const {extensionsToMigrate, appId, remoteExtensions, migrationClient} = options - - await Promise.all( - extensionsToMigrate.map(({remote}) => - migrateExtensionToUIExtension({ - apiKey: appId, - registrationId: undefined, - registrationUuid: remote.uuid, - migrationClient, - }), - ), - ) - - return remoteExtensions.map((extension) => { - if (extensionsToMigrate.some(({remote}) => remote.uuid === extension.uuid)) { - return { - ...extension, - type: 'UI_EXTENSION', - } - } - return extension - }) -} - -async function migrateExtensionToUIExtension(options: { - apiKey: MigrateToUiExtensionVariables['apiKey'] - registrationId: MigrateToUiExtensionVariables['registrationId'] - registrationUuid: MigrateToUiExtensionVariables['registrationUuid'] - migrationClient: DeveloperPlatformClient -}) { - const {apiKey, registrationId, registrationUuid, migrationClient} = options - - const variables: MigrateToUiExtensionVariables = { - apiKey, - registrationId, - registrationUuid, - } - - const result: MigrateToUiExtensionSchema = await migrationClient.migrateToUiExtension(variables) - - if (result?.migrateToUiExtension?.userErrors?.length > 0) { - const errors = result.migrateToUiExtension.userErrors.map((error) => error.message).join(', ') - throw new AbortError(errors) - } - - if (!result?.migrateToUiExtension?.migratedToUiExtension) { - throw new AbortError("Couldn't migrate to UI extension") - } -} diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index f092e6d1d1b..1f52b09e135 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -1,4 +1,3 @@ -import {PartnersClient} from './developer-platform-client/partners-client.js' import {AppManagementClient} from './developer-platform-client/app-management-client.js' import { MinimalAppIdentifiers, @@ -49,7 +48,6 @@ import { import {Store} from '../api/graphql/business-platform-organizations/generated/types.js' import {Session} from '@shopify/cli-kit/node/session' import {TokenItem} from '@shopify/cli-kit/node/ui' -import {blockPartnersAccess} from '@shopify/cli-kit/node/environment' import {UnauthorizedHandler} from '@shopify/cli-kit/node/api/graphql' import {JsonMapType} from '@shopify/cli-kit/node/toml' @@ -57,6 +55,7 @@ export type {Store} from '../api/graphql/business-platform-organizations/generat export enum ClientName { AppManagement = 'app-management', + // Retained as a label for legacy fixtures; no client implements it anymore. Partners = 'partners', } @@ -74,30 +73,14 @@ export interface AppVersionIdentifiers { } export function allDeveloperPlatformClients(): DeveloperPlatformClient[] { - const clients: DeveloperPlatformClient[] = [] - - clients.push(AppManagementClient.getInstance()) - - if (!blockPartnersAccess()) { - clients.push(PartnersClient.getInstance()) - } - - return clients -} - -export function selectDeveloperPlatformClient({ - organization, -}: SelectDeveloperPlatformClientOptions = {}): DeveloperPlatformClient { - if (organization) return selectDeveloperPlatformClientByOrg(organization) - return defaultDeveloperPlatformClient() -} - -function selectDeveloperPlatformClientByOrg(organization: Organization): DeveloperPlatformClient { - if (organization.source === OrganizationSource.BusinessPlatform) return AppManagementClient.getInstance() - return PartnersClient.getInstance() + return [AppManagementClient.getInstance()] } -function defaultDeveloperPlatformClient(): DeveloperPlatformClient { +// All organizations are on the App Management platform, so App Management is the only +// client. The `organization` option is retained for call-site compatibility. +export function selectDeveloperPlatformClient( + _options: SelectDeveloperPlatformClientOptions = {}, +): DeveloperPlatformClient { return AppManagementClient.getInstance() } 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 deleted file mode 100644 index e2cce11bf64..00000000000 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import {PartnersClient} from './partners-client.js' -import {CreateAppQuery} from '../../api/graphql/create_app.js' -import {AppInterface, WebType} from '../../models/app/app.js' -import {Organization, OrganizationSource, OrganizationStore} from '../../models/organization.js' -import { - testPartnersUserSession, - testApp, - testAppWithConfig, - testOrganizationApp, -} from '../../models/app/app.test-data.js' -import {appNamePrompt} from '../../prompts/dev.js' -import {FindOrganizationQuery} from '../../api/graphql/find_org.js' -import {partnersRequest} from '@shopify/cli-kit/node/api/partners' -import {describe, expect, vi, test, beforeEach} from 'vitest' - -vi.mock('../../prompts/dev.js') -vi.mock('@shopify/cli-kit/node/api/partners') - -beforeEach(() => { - // Reset the singleton instance before each test - PartnersClient.resetInstance() -}) - -const LOCAL_APP: AppInterface = testApp({ - directory: '', - configuration: { - client_id: 'test-client-id', - name: 'my-app', - application_url: 'https://example.com', - embedded: true, - access_scopes: {scopes: 'read_products'}, - extension_directories: ['extensions/*'], - }, - webs: [ - { - directory: '', - configuration: { - roles: [WebType.Backend], - commands: {dev: ''}, - }, - }, - ], - name: 'my-app', -}) - -type OrganizationInPartnersResponse = Omit - -const ORG1: OrganizationInPartnersResponse = { - id: '1', - businessName: 'org1', -} -const ORG2: OrganizationInPartnersResponse = { - id: '2', - businessName: 'org2', -} - -const APP1 = testOrganizationApp({apiKey: 'key1'}) -const APP2 = testOrganizationApp({ - id: '2', - title: 'app2', - apiKey: 'key2', - apiSecretKeys: [{secret: 'secret2'}], -}) - -const STORE1: OrganizationStore = { - shopId: '1', - link: 'link1', - shopDomain: 'domain1', - shopName: 'store1', - transferDisabled: false, - convertableToPartnerTest: false, - provisionable: true, -} - -const FETCH_ORG_RESPONSE_VALUE = { - organizations: { - nodes: [ - { - id: ORG1.id, - businessName: ORG1.businessName, - apps: {nodes: [APP1, APP2], pageInfo: {hasNextPage: false}}, - stores: {nodes: [STORE1]}, - }, - ], - }, -} - -describe('createApp', () => { - test('sends request to create app with launchable defaults and returns it', async () => { - // Given - const partnersClient = PartnersClient.getInstance(testPartnersUserSession) - const localApp = testAppWithConfig({config: {access_scopes: {scopes: 'write_products'}}}) - vi.mocked(appNamePrompt).mockResolvedValue('app-name') - vi.mocked(partnersRequest).mockResolvedValueOnce({appCreate: {app: APP1, userErrors: []}}) - const variables = { - org: 1, - title: localApp.name, - appUrl: 'https://example.com', - redir: ['https://example.com/api/auth'], - requestedAccessScopes: ['write_products'], - type: 'undecided', - } - - // When - const got = await partnersClient.createApp( - {...ORG1, source: OrganizationSource.Partners}, - { - name: localApp.name, - scopesArray: ['write_products'], - isLaunchable: true, - directory: '', - }, - ) - - // Then - expect(got).toEqual({...APP1, newApp: true, developerPlatformClient: partnersClient}) - expect(partnersRequest).toHaveBeenCalledWith(CreateAppQuery, 'token', variables, undefined, undefined, { - type: 'token_refresh', - handler: expect.any(Function), - }) - }) - - test('creates an app with non-launchable defaults', async () => { - // Given - const partnersClient = PartnersClient.getInstance(testPartnersUserSession) - vi.mocked(appNamePrompt).mockResolvedValue('app-name') - vi.mocked(partnersRequest).mockResolvedValueOnce({appCreate: {app: APP1, userErrors: []}}) - const variables = { - org: 1, - title: LOCAL_APP.name, - appUrl: 'https://shopify.dev/apps/default-app-home', - redir: ['https://shopify.dev/apps/default-app-home/api/auth'], - requestedAccessScopes: ['write_products'], - type: 'undecided', - } - - // When - const got = await partnersClient.createApp( - {...ORG1, source: OrganizationSource.Partners}, - { - name: LOCAL_APP.name, - isLaunchable: false, - scopesArray: ['write_products'], - }, - ) - - // Then - expect(got).toEqual({...APP1, newApp: true, developerPlatformClient: partnersClient}) - expect(partnersRequest).toHaveBeenCalledWith(CreateAppQuery, 'token', variables, undefined, undefined, { - type: 'token_refresh', - handler: expect.any(Function), - }) - }) - - test('throws error if requests has a user error', async () => { - // Given - const partnersClient = PartnersClient.getInstance(testPartnersUserSession) - vi.mocked(appNamePrompt).mockResolvedValue('app-name') - vi.mocked(partnersRequest).mockResolvedValueOnce({ - appCreate: {app: {}, userErrors: [{message: 'some-error'}]}, - }) - - // When - const got = partnersClient.createApp({...ORG2, source: OrganizationSource.Partners}, {name: LOCAL_APP.name}) - - // Then - await expect(got).rejects.toThrow(`some-error`) - }) -}) - -describe('fetchApp', async () => { - test('returns fetched apps', async () => { - // Given - const partnersClient = PartnersClient.getInstance(testPartnersUserSession) - vi.mocked(partnersRequest).mockResolvedValue(FETCH_ORG_RESPONSE_VALUE) - const partnerMarkedOrg = {...ORG1, source: 'Partners'} - - // When - const got = await partnersClient.orgAndApps(ORG1.id) - - // 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), - }) - }) - - test('throws if there are no organizations', async () => { - // Given - const partnersClient = PartnersClient.getInstance(testPartnersUserSession) - vi.mocked(partnersRequest).mockResolvedValue({organizations: {nodes: []}}) - - // When - const got = () => partnersClient.orgAndApps(ORG1.id) - - // 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), - }) - }) -}) - -describe('PartnersClient', () => { - describe('bundleFormat', () => { - test('uses zip format', () => { - // Given - const client = PartnersClient.getInstance() - - // Then - expect(client.bundleFormat).toBe('zip') - }) - }) -}) - -describe('singleton pattern', () => { - test('getInstance returns the same instance', () => { - // Given/When - const instance1 = PartnersClient.getInstance() - const instance2 = PartnersClient.getInstance() - - // Then - expect(instance1).toBe(instance2) - }) - - test('resetInstance allows creating a new instance', () => { - // Given - const instance1 = PartnersClient.getInstance() - - // When - PartnersClient.resetInstance() - const instance2 = PartnersClient.getInstance() - - // Then - expect(instance1).not.toBe(instance2) - }) -}) 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 deleted file mode 100644 index fa2dafdc36e..00000000000 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts +++ /dev/null @@ -1,670 +0,0 @@ -import {CreateAppQuery, CreateAppQuerySchema, CreateAppQueryVariables} from '../../api/graphql/create_app.js' -import { - AppVersion, - AppDeployOptions, - AssetUrlSchema, - AppVersionIdentifiers, - DeveloperPlatformClient, - TemplateSpecificationsOptions, - Paginateable, - filterDisabledFlags, - ClientName, - AppVersionWithContext, - CreateAppOptions, - AppLogsResponse, - createUnauthorizedHandler, -} from '../developer-platform-client.js' -import {fetchCurrentAccountInformation} from '../../services/context/partner-account-info.js' -import { - MinimalAppIdentifiers, - MinimalOrganizationApp, - Organization, - OrganizationApp, - OrganizationSource, - OrganizationStore, -} from '../../models/organization.js' -import { - AllAppExtensionRegistrationsQuery, - AllAppExtensionRegistrationsQueryVariables, - AllAppExtensionRegistrationsQuerySchema, -} from '../../api/graphql/all_app_extension_registrations.js' -import { - ActiveAppVersionQuery, - ActiveAppVersionQuerySchema, - ActiveAppVersionQueryVariables, -} from '../../api/graphql/app_active_version.js' -import {AppDeploy, AppDeploySchema, AppDeployVariables} from '../../api/graphql/app_deploy.js' -import { - GenerateSignedUploadUrl, - GenerateSignedUploadUrlSchema, - GenerateSignedUploadUrlVariables, -} from '../../api/graphql/generate_signed_upload_url.js' -import { - ConvertDevToTransferDisabledStoreQuery, - ConvertDevToTransferDisabledSchema, - ConvertDevToTransferDisabledStoreVariables, -} from '../../api/graphql/convert_dev_to_transfer_disabled_store.js' -import { - FindStoreByDomainQuery, - FindStoreByDomainQueryVariables, - FindStoreByDomainSchema, -} from '../../api/graphql/find_store_by_domain.js' -import {Store} from '../../api/graphql/business-platform-organizations/generated/types.js' -import { - AppVersionsQuery, - AppVersionsQueryVariables, - AppVersionsQuerySchema, -} from '../../api/graphql/get_versions_list.js' -import { - AppVersionsDiffQuery, - AppVersionsDiffSchema, - AppVersionsDiffVariables, -} from '../../api/graphql/app_versions_diff.js' -import {AppRelease, AppReleaseSchema, AppReleaseVariables} from '../../api/graphql/app_release.js' -import { - AppVersionByTagQuery, - AppVersionByTagSchema, - AppVersionByTagVariables, -} from '../../api/graphql/app_version_by_tag.js' -import { - SendSampleWebhookSchema, - SendSampleWebhookVariables, - sendSampleWebhookMutation, -} from '../../services/webhook/request-sample.js' -import {PublicApiVersionsSchema, GetApiVersionsQuery} from '../../services/webhook/request-api-versions.js' -import {WebhookTopicsSchema, WebhookTopicsVariables, getTopicsQuery} from '../../services/webhook/request-topics.js' -import { - MigrateFlowExtensionVariables, - MigrateFlowExtensionSchema, - MigrateFlowExtensionMutation, -} from '../../api/graphql/extension_migrate_flow_extension.js' -import {UpdateURLsVariables, UpdateURLsSchema, UpdateURLsQuery} from '../../api/graphql/update_urls.js' -import {CurrentAccountInfo, CurrentAccountInfoQuery} from '../../api/graphql/partners/generated/current-account-info.js' -import { - RemoteTemplateSpecificationsQuery, - RemoteTemplateSpecificationsSchema, - RemoteTemplateSpecificationsVariables, -} from '../../api/graphql/template_specifications.js' -import {ExtensionTemplatesResult} from '../../models/app/template.js' -import { - TargetSchemaDefinitionQuerySchema, - TargetSchemaDefinitionQuery, -} from '../../api/graphql/functions/target_schema_definition.js' -import { - ApiSchemaDefinitionQuerySchema, - ApiSchemaDefinitionQuery, -} from '../../api/graphql/functions/api_schema_definition.js' -import { - MigrateToUiExtensionVariables, - MigrateToUiExtensionSchema, - MigrateToUiExtensionQuery, -} from '../../api/graphql/extension_migrate_to_ui_extension.js' -import { - ExtensionSpecificationsQuery, - ExtensionSpecificationsQuerySchema, - ExtensionSpecificationsQueryVariables, - RemoteSpecification, -} from '../../api/graphql/extension_specifications.js' -import { - FindOrganizationBasicQuery, - FindOrganizationBasicQuerySchema, - FindOrganizationBasicVariables, -} from '../../api/graphql/find_org_basic.js' -import { - MigrateAppModuleMutation, - MigrateAppModuleSchema, - MigrateAppModuleVariables, -} from '../../api/graphql/extension_migrate_app_module.js' -import {AppLogsSubscribeMutation, AppLogsSubscribeResponse} from '../../api/graphql/subscribe_to_app_logs.js' - -import {AllOrgs} from '../../api/graphql/partners/generated/all-orgs.js' -import {FindAppQuery, FindAppQuerySchema, FindAppQueryVariables} from '../../api/graphql/find_app.js' -import { - FindOrganizationQuery, - FindOrganizationQuerySchema, - FindOrganizationQueryVariables, -} from '../../api/graphql/find_org.js' -import {NoOrgError} from '../../services/dev/fetch.js' -import { - DevStoresByOrg, - DevStoresByOrgQuery, - DevStoresByOrgQueryVariables, -} from '../../api/graphql/partners/generated/dev-stores-by-org.js' -import {SchemaDefinitionByTargetQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-target.js' -import {SchemaDefinitionByApiTypeQueryVariables} from '../../api/graphql/functions/generated/schema-definition-by-api-type.js' -import {AppLogData} from '../../services/app-logs/types.js' -import {AppLogsOptions} from '../../services/app-logs/utils.js' -import {AppLogsSubscribeMutationVariables} from '../../api/graphql/app-management/generated/app-logs-subscribe.js' -import {TypedDocumentNode} from '@graphql-typed-document-node/core' -import {isUnitTest} from '@shopify/cli-kit/node/context/local' -import {AbortError} from '@shopify/cli-kit/node/error' -import {generateFetchAppLogUrl, partnersRequest, partnersRequestDoc} from '@shopify/cli-kit/node/api/partners' -import {CacheOptions, GraphQLVariables, UnauthorizedHandler} from '@shopify/cli-kit/node/api/graphql' -import {ensureAuthenticatedPartners, Session} from '@shopify/cli-kit/node/session' -import {partnersFqdn} from '@shopify/cli-kit/node/context/fqdn' -import {TokenItem} from '@shopify/cli-kit/node/ui' -import {RequestModeInput, Response, shopifyFetch} from '@shopify/cli-kit/node/http' -import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version' - -// this is a temporary solution for editions to support https://vault.shopify.io/gsd/projects/31406 -// read more here: https://vault.shopify.io/gsd/projects/31406 -const MAGIC_URL = 'https://shopify.dev/apps/default-app-home' -const MAGIC_REDIRECT_URL = 'https://shopify.dev/apps/default-app-home/api/auth' - -function getAppVars(org: Organization, options: CreateAppOptions): CreateAppQueryVariables { - const {name, isLaunchable = true, scopesArray} = options - const defaultAppUrl = isLaunchable ? 'https://example.com' : MAGIC_URL - const defaultRedirectUrl = isLaunchable ? 'https://example.com/api/auth' : MAGIC_REDIRECT_URL - - return { - org: parseInt(org.id, 10), - title: name, - appUrl: defaultAppUrl, - redir: [defaultRedirectUrl], - requestedAccessScopes: scopesArray ?? [], - type: 'undecided', - } -} - -interface OrganizationAppsResponse { - pageInfo: { - hasNextPage: boolean - } - nodes: MinimalOrganizationApp[] -} - -interface OrgAndAppsResponse { - organization: Organization - apps: OrganizationAppsResponse - stores: OrganizationStore[] -} - -export class PartnersClient implements DeveloperPlatformClient { - private static instance: PartnersClient | undefined - - static getInstance(session?: Session): PartnersClient { - PartnersClient.instance ??= new PartnersClient(session) - return PartnersClient.instance - } - - static resetInstance(): void { - PartnersClient.instance = undefined - } - - public readonly clientName = ClientName.Partners - public readonly webUiName = 'Partner Dashboard' - public readonly organizationSource = OrganizationSource.Partners - public readonly bundleFormat = 'zip' - private _session: Session | undefined - - private constructor(session?: Session) { - this._session = session - } - - async session(): Promise { - if (!this._session) { - if (isUnitTest()) { - throw new Error('PartnersClient.session() should not be invoked dynamically in a unit test') - } - const {token, userId} = await ensureAuthenticatedPartners() - this._session = { - token, - businessPlatformToken: '', - accountInfo: {type: 'UnknownAccount'}, - userId, - } - const accountInfo = await fetchCurrentAccountInformation(this, userId) - this._session = {token, businessPlatformToken: '', accountInfo, userId} - } - return this._session - } - - async request( - query: string, - variables: GraphQLVariables | undefined = undefined, - cacheOptions?: CacheOptions, - preferredBehaviour?: RequestModeInput, - ): Promise { - return partnersRequest( - query, - await this.token(), - variables, - cacheOptions, - preferredBehaviour, - this.createUnauthorizedHandler(), - ) - } - - async requestDoc( - document: TypedDocumentNode, - variables?: TVariables, - ): Promise { - return partnersRequestDoc(document, await this.token(), variables, undefined, this.createUnauthorizedHandler()) - } - - async token(): Promise { - return (await this.session()).token - } - - async unsafeRefreshToken(): Promise { - const {token} = await ensureAuthenticatedPartners([], process.env, {noPrompt: true, forceRefresh: true}) - const session = await this.session() - if (token) { - session.token = token - } - return session.token - } - - async accountInfo(): Promise { - return (await this.session()).accountInfo - } - - async appFromIdentifiers(apiKey: string): Promise { - const variables: FindAppQueryVariables = {apiKey} - const res: FindAppQuerySchema = await this.request(FindAppQuery, variables) - const app = res.app - if (app) { - const flags = filterDisabledFlags(app.disabledFlags) - return { - ...app, - flags, - developerPlatformClient: this, - } - } - } - - async organizations(): Promise { - try { - const result = await this.requestDoc(AllOrgs) - return result.organizations.nodes!.map((org) => ({ - id: org!.id, - businessName: `${org!.businessName} (Partner Dashboard)`, - source: this.organizationSource, - })) - } catch (error: unknown) { - if ((error as {statusCode?: number}).statusCode === 404) { - return [] - } else { - throw error - } - } - } - - async orgFromId(orgId: string): Promise { - const variables: FindOrganizationBasicVariables = {id: orgId} - const result: FindOrganizationBasicQuerySchema = await this.request(FindOrganizationBasicQuery, variables, { - cacheTTL: {hours: 6}, - }) - const org: Omit | undefined = result.organizations.nodes[0] - return org ? {...org, source: this.organizationSource} : undefined - } - - async orgAndApps(orgId: string): Promise> { - const result = await this.fetchOrgAndApps(orgId) - return { - organization: result.organization, - apps: result.apps.nodes, - hasMorePages: result.apps.pageInfo.hasNextPage, - } - } - - async appsForOrg(organizationId: string, term?: string): Promise> { - const result = await this.fetchOrgAndApps(organizationId, term) - return { - apps: result.apps.nodes, - hasMorePages: result.apps.pageInfo.hasNextPage, - } - } - - async specifications({apiKey}: MinimalAppIdentifiers): Promise { - const variables: ExtensionSpecificationsQueryVariables = {apiKey} - const result: ExtensionSpecificationsQuerySchema = await this.request(ExtensionSpecificationsQuery, variables) - // Partners API doesn't provide uidStrategy; derive it from experience. - return result.extensionSpecifications.map(({options, features, ...spec}) => ({ - ...spec, - uidStrategy: spec.experience === 'extension' ? 'uuid' : 'single', - registrationLimit: options.registrationLimit, - managementExperience: options.managementExperience, - surface: features?.argo?.surface, - })) - } - - async templateSpecifications( - {apiKey}: MinimalAppIdentifiers, - _options: TemplateSpecificationsOptions = {}, - ): Promise { - const variables: RemoteTemplateSpecificationsVariables = {apiKey} - const result: RemoteTemplateSpecificationsSchema = await this.request(RemoteTemplateSpecificationsQuery, variables) - const templates = result.templateSpecifications.map((template) => { - const {types, ...rest} = template - return { - ...rest, - ...types[0], - } - }) - - let counter = 0 - const templatesWithPriority = templates.map((template) => ({ - ...template, - sortPriority: template.sortPriority ?? counter++, - })) - - const groupOrder: string[] = [] - for (const template of templatesWithPriority) { - if (template.group && !groupOrder.includes(template.group)) { - groupOrder.push(template.group) - } - } - - return { - templates: templatesWithPriority, - groupOrder, - } - } - - async createApp(org: Organization, options: CreateAppOptions): Promise { - const variables: CreateAppQueryVariables = getAppVars(org, options) - const result: CreateAppQuerySchema = await this.request(CreateAppQuery, variables) - if (result.appCreate.userErrors.length > 0) { - const errors = result.appCreate.userErrors.map((error) => error.message).join(', ') - throw new AbortError(errors) - } - - const flags = filterDisabledFlags(result.appCreate.app.disabledFlags) - return {...result.appCreate.app, organizationId: org.id, newApp: true, flags, developerPlatformClient: this} - } - - async devStoresForOrg(orgId: string): Promise> { - const variables: DevStoresByOrgQueryVariables = {id: orgId} - const result: DevStoresByOrgQuery = await this.requestDoc(DevStoresByOrg, variables) - return { - stores: result.organizations.nodes![0]!.stores.nodes as OrganizationStore[], - hasMorePages: false, - } - } - - async appExtensionRegistrations( - {apiKey}: MinimalAppIdentifiers, - _activeAppVersion?: AppVersion, - ): Promise { - const variables: AllAppExtensionRegistrationsQueryVariables = {apiKey} - return this.request(AllAppExtensionRegistrationsQuery, variables) - } - - async appVersions({apiKey}: OrganizationApp): Promise { - const variables: AppVersionsQueryVariables = {apiKey} - return this.request(AppVersionsQuery, variables) - } - - async appInstallCount(_app: MinimalAppIdentifiers): Promise { - // Install count is not supported in partners client. - throw new Error('Unsupported operation') - } - - async appVersionByTag({apiKey}: MinimalOrganizationApp, versionTag: string): Promise { - const input: AppVersionByTagVariables = {apiKey, versionTag} - const result: AppVersionByTagSchema = await this.request(AppVersionByTagQuery, input) - const appVersion = result.app.appVersion - return { - ...appVersion, - appModuleVersions: appVersion.appModuleVersions.map((appModuleVersion) => ({ - ...appModuleVersion, - config: appModuleVersion.config ? JSON.parse(appModuleVersion.config) : undefined, - })), - } - } - - async appVersionsDiff( - {apiKey}: MinimalOrganizationApp, - {appVersionId}: AppVersionIdentifiers, - ): Promise { - const variables: AppVersionsDiffVariables = {apiKey, versionId: appVersionId} - return this.request(AppVersionsDiffQuery, variables) - } - - async activeAppVersion({apiKey}: MinimalAppIdentifiers): Promise { - const variables: ActiveAppVersionQueryVariables = {apiKey} - const result = await this.request(ActiveAppVersionQuery, variables) - const version = result.app.activeAppVersion - if (!version) return - return { - ...version, - appModuleVersions: version.appModuleVersions.map((mod) => { - return { - ...mod, - config: mod.config ? (JSON.parse(mod.config) as object) : {}, - } - }), - } - } - - async deploy(deployInput: AppDeployOptions): Promise { - const {organizationId, ...deployOptions} = deployInput - // Enforce the type - const variables: AppDeployVariables = deployOptions - // Exclude uid - variables.appModules = variables.appModules?.map((element) => { - const {uid, ...otherFields} = element - return otherFields - }) - return this.request(AppDeploy, variables, undefined, 'slow-request') - } - - async release({ - app: {apiKey}, - version: {appVersionId}, - }: { - app: MinimalOrganizationApp - version: AppVersionIdentifiers - }): Promise { - const input: AppReleaseVariables = {apiKey, appVersionId} - return this.request(AppRelease, input) - } - - async generateSignedUploadUrl(app: MinimalAppIdentifiers): Promise { - const variables: GenerateSignedUploadUrlVariables = {apiKey: app.apiKey, bundleFormat: 1} - const result = await this.request(GenerateSignedUploadUrl, variables) - return { - assetUrl: result.appVersionGenerateSignedUploadUrl.signedUploadUrl, - userErrors: result.appVersionGenerateSignedUploadUrl.userErrors, - } - } - - async convertToTransferDisabledStore( - input: ConvertDevToTransferDisabledStoreVariables, - ): Promise { - return this.request(ConvertDevToTransferDisabledStoreQuery, input) - } - - async storeByDomain(orgId: string, shopDomain: string, _storeTypes: Store[]): Promise { - // Note: storeTypes filtering not implemented for PartnersClient - const variables: FindStoreByDomainQueryVariables = {orgId, shopDomain} - const result: FindStoreByDomainSchema = await this.request(FindStoreByDomainQuery, variables) - - const node = result.organizations.nodes[0]?.stores.nodes[0] - if (!node) { - return undefined - } - return { - ...node, - provisionable: false, - } - } - - async ensureUserAccessToStore(_orgId: string, _store: OrganizationStore): Promise { - // This is a no-op for partners - } - - async sendSampleWebhook( - input: SendSampleWebhookVariables, - _organizationId: string, - ): Promise { - return this.request(sendSampleWebhookMutation, input) - } - - async apiVersions(_organizationId: string): Promise { - return this.request(GetApiVersionsQuery) - } - - async topics(input: WebhookTopicsVariables, _organizationId: string): Promise { - return this.request(getTopicsQuery, input) - } - - async migrateFlowExtension(input: MigrateFlowExtensionVariables): Promise { - return this.request(MigrateFlowExtensionMutation, input) - } - - async migrateAppModule(input: MigrateAppModuleVariables): Promise { - return this.request(MigrateAppModuleMutation, input) - } - - async updateURLs(input: UpdateURLsVariables): Promise { - return this.request(UpdateURLsQuery, input) - } - - async currentAccountInfo(): Promise { - return this.requestDoc(CurrentAccountInfo) - } - - async targetSchemaDefinition( - input: SchemaDefinitionByTargetQueryVariables, - apiKey: string, - _organizationId: string, - ): Promise { - // Ensures compatibility with existing partners requests - // Can remove once migrated to AMF - const transformedInput = { - target: input.handle, - version: input.version, - apiKey, - } - - const response: TargetSchemaDefinitionQuerySchema = await this.request( - TargetSchemaDefinitionQuery, - transformedInput, - ) - return response.definition - } - - async apiSchemaDefinition( - input: SchemaDefinitionByApiTypeQueryVariables & {apiKey?: string}, - apiKey: string, - _organizationId: string, - _appId?: string, - ): Promise { - const response: ApiSchemaDefinitionQuerySchema = await this.request(ApiSchemaDefinitionQuery, { - ...input, - apiKey, - }) - return response.definition - } - - async migrateToUiExtension(input: MigrateToUiExtensionVariables): Promise { - return this.request(MigrateToUiExtensionQuery, input) - } - - toExtensionGraphQLType(input: string) { - return input.toUpperCase() - } - - async subscribeToAppLogs( - input: AppLogsSubscribeMutationVariables, - _organizationId: string, - ): Promise { - return this.request(AppLogsSubscribeMutation, input) - } - - async appDeepLink({id, organizationId}: MinimalAppIdentifiers): Promise { - return `https://${await partnersFqdn()}/${organizationId}/apps/${id}` - } - - async appLogs(options: AppLogsOptions, _organizationId: string): Promise { - const response = await fetchAppLogs(options) - - try { - const data = (await response.json()) as { - app_logs?: AppLogData[] - cursor?: string - errors?: string[] - } - - if (!response.ok) { - return { - errors: data.errors ?? [`Request failed with status ${response.status}`], - status: response.status, - } - } - - return { - app_logs: data.app_logs ?? [], - cursor: data.cursor, - status: response.status, - } - // eslint-disable-next-line no-catch-all/no-catch-all - } catch (error) { - return { - errors: [`Failed to parse response: ${error}`], - status: response.status, - } - } - } - - async devSessionCreate(_input: unknown): Promise { - // Dev Sessions are not supported in partners client. - throw new Error('Unsupported operation') - } - - async devSessionUpdate(_input: unknown): Promise { - // Dev Sessions are not supported in partners client. - throw new Error('Unsupported operation') - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - async devSessionDelete(_input: unknown): Promise { - // Dev Sessions are not supported in partners client. - return Promise.resolve() - } - - async getCreateDevStoreLink(org: Organization): Promise { - const url = `https://${await partnersFqdn()}/${org.id}/stores` - return [ - `Looks like you don't have any dev stores associated with ${org.businessName}'s Partner Dashboard.`, - {link: {url, label: 'Create a store in Partner Dashboard'}}, - ] - } - - private async fetchOrgAndApps(orgId: string, title?: string): Promise { - const params: FindOrganizationQueryVariables = {id: orgId} - if (title) params.title = title - const result: FindOrganizationQuerySchema = await this.request(FindOrganizationQuery, params) - const org = result.organizations.nodes[0] - if (!org) { - const partnersSession = await this.session() - throw new NoOrgError(partnersSession.accountInfo, orgId) - } - const parsedOrg = {id: org.id, businessName: org.businessName, source: this.organizationSource} - const appsWithOrg = org.apps.nodes.map((app) => ({...app, organizationId: org.id})) - return {organization: parsedOrg, apps: {...org.apps, nodes: appsWithOrg}, stores: []} - } - - private createUnauthorizedHandler(): UnauthorizedHandler { - return createUnauthorizedHandler(this) - } -} - -const fetchAppLogs = async ({jwtToken, cursor, filters}: AppLogsOptions): Promise => { - const url = await generateFetchAppLogUrl(cursor, filters) - const userAgent = `Shopify CLI; v=${CLI_KIT_VERSION}` - const headers = { - Authorization: `Bearer ${jwtToken}`, - 'User-Agent': userAgent, - } - return shopifyFetch( - url, - { - method: 'GET', - headers, - }, - 'non-blocking', - ) -}