From bcfaf041fd49ca9f9ba0c9b51c1978afecc11007 Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Wed, 12 Feb 2025 16:28:56 -0700 Subject: [PATCH] Add prompt before applying migration --- packages/app/src/cli/models/app/app.ts | 22 ++++++++++++++++--- .../models/extensions/extension-instance.ts | 5 +++++ .../cli/models/extensions/specification.ts | 3 +++ .../specifications/app_config_app_access.ts | 17 +++++++++++++- .../src/cli/prompts/deprecation-warnings.ts | 10 +++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/app/src/cli/models/app/app.ts b/packages/app/src/cli/models/app/app.ts index 84ed8ffc30..6859bd9c48 100644 --- a/packages/app/src/cli/models/app/app.ts +++ b/packages/app/src/cli/models/app/app.ts @@ -16,6 +16,7 @@ import {ApplicationURLs} from '../../services/dev/urls.js' import appHomeSpec from '../extensions/specifications/app_config_app_home.js' import appProxySpec from '../extensions/specifications/app_config_app_proxy.js' import {replaceScopesWithRequiredScopesInToml} from '../../services/app/patch-app-configuration-file.js' +import {confirmApplyPendingMigrations} from '../../prompts/deprecation-warnings.js' import {ZodObjectOf, zod} from '@shopify/cli-kit/node/schema' import {DotEnvFile} from '@shopify/cli-kit/node/dot-env' import {getDependencies, PackageManager, readAndParsePackageJson} from '@shopify/cli-kit/node/node-package-manager' @@ -25,6 +26,7 @@ import {AbortError} from '@shopify/cli-kit/node/error' import {normalizeDelimitedString} from '@shopify/cli-kit/common/string' import {JsonMapType} from '@shopify/cli-kit/node/toml' import {getArrayRejectingUndefined} from '@shopify/cli-kit/common/array' +import {renderText, renderSuccess} from '@shopify/cli-kit/node/ui' // Schemas for loading app configuration @@ -319,7 +321,8 @@ type AppConstructor< export class App< TConfig extends AppConfiguration = AppConfiguration, TModuleSpec extends ExtensionSpecification = ExtensionSpecification, -> implements AppInterface { +> implements AppInterface +{ name: string idEnvironmentVariableName: 'SHOPIFY_API_KEY' = 'SHOPIFY_API_KEY' as const directory: string @@ -424,12 +427,25 @@ export class App< } async migratePendingSchemaChanges() { - await this.migrateScopesToRequiredScopes() - await Promise.all([this.realExtensions.map((ext) => ext.migratePendingSchemaChanges())]) + const pendingMigrations = this.getPendingMigrationMessages() + if (pendingMigrations.length > 0) { + const shouldMigrate = await confirmApplyPendingMigrations(pendingMigrations) + if (shouldMigrate) { + await this.migrateScopesToRequiredScopes() + await Promise.all([this.realExtensions.map((ext) => ext.migratePendingSchemaChanges())]) + renderSuccess({headline: 'Migration completed locally, run `shopify app deploy` to push the changes.'}) + } + } + } + + getPendingMigrationMessages(): string[] { + return this.realExtensions.map((ext) => ext.pendingSchemaChanges()).flat() } async migrateScopesToRequiredScopes() { if (isCurrentAppSchema(this.configuration) && this.configuration.access_scopes?.scopes) { + renderText({text: 'Migration: Replacing `scopes` with `required_scopes` locally.'}) + const accessConfig = this.configuration as { access_scopes: {scopes?: string; required_scopes?: string[]} } diff --git a/packages/app/src/cli/models/extensions/extension-instance.ts b/packages/app/src/cli/models/extensions/extension-instance.ts index 8000c9f1a8..5e461577f5 100644 --- a/packages/app/src/cli/models/extensions/extension-instance.ts +++ b/packages/app/src/cli/models/extensions/extension-instance.ts @@ -225,6 +225,11 @@ export class ExtensionInstance { if (!this.specification.buildValidation) return Promise.resolve() return this.specification.buildValidation(this) diff --git a/packages/app/src/cli/models/extensions/specification.ts b/packages/app/src/cli/models/extensions/specification.ts index 14915336da..295dc82157 100644 --- a/packages/app/src/cli/models/extensions/specification.ts +++ b/packages/app/src/cli/models/extensions/specification.ts @@ -73,6 +73,7 @@ export interface ExtensionSpecification ExtensionFeature[] migratePendingSchemaChanges?: (extension: ExtensionInstance) => Promise + pendingSchemaChanges?: (extension: ExtensionInstance) => string[] getDevSessionActionUpdateMessage?: ( config: TConfiguration, appConfig: CurrentAppConfiguration, @@ -235,6 +236,7 @@ export function createConfigExtensionSpecification Promise migratePendingSchemaChanges?: (extension: ExtensionInstance) => Promise + pendingSchemaChanges?: (extension: ExtensionInstance) => string[] }): ExtensionSpecification { const appModuleFeatures = spec.appModuleFeatures ?? (() => []) return createExtensionSpecification({ @@ -249,6 +251,7 @@ export function createConfigExtensionSpecification { return migrateScopesToRequiredScopes(extension) }, + pendingSchemaChanges: (extension) => { + return pendingSchemaChanges(extension) + }, }) async function migrateScopesToRequiredScopes(extension: ExtensionInstance) { @@ -71,4 +73,17 @@ async function migrateScopesToRequiredScopes(extension: ExtensionInstance) { } } +function pendingSchemaChanges(extension: ExtensionInstance): string[] { + const accessConfig = extension.configuration as { + access_scopes?: {scopes?: string} + } + + const migrationMessages = [] + if (accessConfig.access_scopes?.scopes) { + migrationMessages.push('Replace `scopes(string)` with `required_scopes(string array)`.') + } + + return migrationMessages +} + export default appAccessSpec diff --git a/packages/app/src/cli/prompts/deprecation-warnings.ts b/packages/app/src/cli/prompts/deprecation-warnings.ts index 5deef46cae..67987c6f5e 100644 --- a/packages/app/src/cli/prompts/deprecation-warnings.ts +++ b/packages/app/src/cli/prompts/deprecation-warnings.ts @@ -9,3 +9,13 @@ export async function showApiKeyDeprecationWarning() { body: ['The flag', {command: 'api-key'}, 'has been deprecated in favor of', {command: 'client-id'}], }) } + +export async function confirmApplyPendingMigrations(migrations: string[]): Promise { + return renderConfirmationPrompt({ + message: `There are pending migrations, would you like to apply them now?`, + infoTable: {'': migrations}, + confirmationMessage: 'Yes, apply migrations', + cancellationMessage: 'No, apply migrations later', + defaultValue: true, + }) +}