From c16ea38dc8b900ac696fe2c76c49a679f142d1db Mon Sep 17 00:00:00 2001 From: ajaz Date: Wed, 5 Nov 2025 14:21:18 +0530 Subject: [PATCH 1/6] feat: update deploy command to provision db --- README.md | 10 +-- package.json | 2 +- src/commands/app/deploy.js | 31 ++++++++ test/commands/app/deploy.test.js | 130 +++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 85dd50dc..e749166b 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ USAGE $ aio app create [PATH] [-v] [--version] [-i ] ARGUMENTS - PATH [default: .] Path to the app directory + [PATH] [default: .] Path to the app directory FLAGS -i, --import= Import an Adobe I/O Developer Console configuration file @@ -337,7 +337,7 @@ USAGE $ aio app delete action [ACTION-NAME] [-v] [--version] [-y] ARGUMENTS - ACTION-NAME Action `pkg/name` to delete, you can specify multiple actions via a comma separated list + [ACTION-NAME] Action `pkg/name` to delete, you can specify multiple actions via a comma separated list FLAGS -v, --verbose Verbose output @@ -543,7 +543,7 @@ USAGE | | ] [--confirm-new-workspace] [--use-jwt] [--github-pat ] [--linter none|basic|adobe-recommended] ARGUMENTS - PATH [default: .] Path to the app directory + [PATH] [default: .] Path to the app directory FLAGS -e, --extension=... Extension point(s) to implement @@ -674,7 +674,7 @@ USAGE $ aio app pack [PATH] [-v] [--version] [--lock-file] [-o ] ARGUMENTS - PATH [default: .] Path to the app directory to package + [PATH] [default: .] Path to the app directory to package FLAGS -o, --output= [default: dist/app.zip] The packaged app output file path @@ -773,7 +773,7 @@ USAGE [--confirm-new-workspace] [--no-service-sync | --confirm-service-sync] [--no-input] [--use-jwt] ARGUMENTS - CONFIG_FILE_PATH path to an Adobe I/O Developer Console configuration file + [CONFIG_FILE_PATH] path to an Adobe I/O Developer Console configuration file FLAGS -g, --global Use the global Adobe Developer Console Org / Project / Workspace configuration, diff --git a/package.json b/package.json index 90ae9bdb..a239af93 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Adobe Inc.", "bugs": "https://github.com/adobe/aio-cli-plugin-app/issues", "dependencies": { - "@adobe/aio-cli-lib-app-config": "^4.0.3", + "@adobe/aio-cli-lib-app-config": "../aio-cli-lib-app-config", "@adobe/aio-cli-lib-console": "^5.0.3", "@adobe/aio-lib-core-config": "^5", "@adobe/aio-lib-core-logging": "^3", diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 84b400f9..91a03a65 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -203,6 +203,11 @@ class Deploy extends BuildCommand { this.error(err) } + // provision database if configured + if (config.manifest?.full?.database?.['auto-provision'] === true) { + await this.provisionDatabase(config, spinner, flags) + } + if (flags.actions) { if (config.app.hasBackend) { let filterEntities @@ -299,6 +304,32 @@ class Deploy extends BuildCommand { } } + async provisionDatabase (config, spinner, flags) { + const region = config.manifest?.full?.database?.region || 'amer' + + if (!config.manifest?.full?.database?.region) { + spinner.info(chalk.green('No region is configured for the database, deploying in default region amer')) + } + + const message = `Deploying database for region '${region}'` + + try { + spinner.start(message) + + if (flags.verbose) { + spinner.info(chalk.dim(`Running: aio app db provision --region ${region} --yes`)) + spinner.start(message) + } + + await this.config.runCommand('app:db:provision', ['--region', region, '--yes']) + + spinner.succeed(chalk.green(`Deployed database for application in region '${region}'`)) + } catch (error) { + spinner.fail(chalk.red('Database deployment failed')) + throw error + } + } + async publishExtensionPoints (deployConfigs, aioConfig, force) { const libConsoleCLI = await this.getLibConsoleCLI() diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index c6b7ada3..fe55faf5 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -18,6 +18,7 @@ const helpersActual = jest.requireActual('../../../src/lib/app-helper.js') const authHelpersActual = jest.requireActual('../../../src/lib/auth-helper') const open = require('open') +const ora = require('ora') const mockBundleFunc = jest.fn() jest.mock('../../../src/lib/app-helper.js') @@ -203,6 +204,7 @@ beforeEach(() => { command = new TheCommand([]) command.error = jest.fn() command.log = jest.fn() + command.warn = jest.fn() command.appConfig = cloneDeep(mockConfigData) command.appConfig.actions = { dist: 'actions' } command.appConfig.web.distProd = 'dist' @@ -1628,4 +1630,132 @@ describe('run', () => { expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) }) + + test('should deploy successfully when auto-provision database is configured', async () => { + // Create app config with database auto-provision enabled + const appConfigWithDb = createAppConfig({ + ...command.appConfig, + manifest: { + full: { + database: { + 'auto-provision': true + } + }, + database: { + region: 'emea' + } + } + }) + + command.getAppExtConfigs.mockResolvedValueOnce(appConfigWithDb) + command.provisionDatabase = jest.fn().mockResolvedValue() + + await command.run() + + expect(command.error).toHaveBeenCalledTimes(0) + expect(command.provisionDatabase).toHaveBeenCalledTimes(1) + expect(command.provisionDatabase).toHaveBeenCalledWith( + appConfigWithDb.application, + expect.any(Object), // spinner + expect.objectContaining({ 'force-build': true }) // flags + ) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + }) +}) + +describe('database provisioning', () => { + const createDatabaseConfig = (region = null, autoProvision = true) => ({ + manifest: { + full: { + database: { + 'auto-provision': autoProvision, + ...(region && { region }) + } + } + } + }) + + // Helper function to set up and run test + const runProvisionTest = async (config, flags, mockResult) => { + const spinner = ora() + + if (mockResult instanceof Error) { + command.config.runCommand.mockRejectedValueOnce(mockResult) + } else { + command.config.runCommand.mockResolvedValueOnce(mockResult) + } + + return { + result: await command.provisionDatabase(config, spinner, flags).catch(e => { throw e }), + spinner + } + } + + test('should use default region when not specified in manifest', async () => { + const config = createDatabaseConfig() + const flags = { verbose: false } + + const { spinner } = await runProvisionTest(config, flags) + + expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('No region is configured for the database, deploying in default region amer')) + expect(spinner.start).toHaveBeenCalledWith('Deploying database for region \'amer\'') + expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--region', 'amer', '--yes']) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'amer\'')) + }) + + test('should use configured region when specified in manifest', async () => { + const config = createDatabaseConfig('emea') + const flags = { verbose: false } + + const { spinner } = await runProvisionTest(config, flags) + + expect(spinner.info).not.toHaveBeenCalledWith(expect.stringContaining('No region is configured for the database')) + expect(spinner.start).toHaveBeenCalledWith('Deploying database for region \'emea\'') + expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--region', 'emea', '--yes']) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'emea\'')) + }) + + test('should show verbose output when verbose flag is true', async () => { + const config = createDatabaseConfig('amer') + const flags = { verbose: true } + + const { spinner } = await runProvisionTest(config, flags) + + expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('Running: aio app db provision --region amer --yes')) + expect(spinner.info).not.toHaveBeenCalledWith(expect.stringContaining('No region is configured for the database')) + expect(spinner.start).toHaveBeenCalledTimes(2) // Once initially, once after verbose info + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'amer\'')) + }) + + test('should handle provision command failure', async () => { + const config = createDatabaseConfig('amer') + const flags = { verbose: false } + const error = new Error('Database provision failed') + + await expect(runProvisionTest(config, flags, error)) + .rejects.toThrow('Database provision failed') + + expect(command.warn).not.toHaveBeenCalled() + }) + + test('should show verbose error details when verbose flag is true and provision fails', async () => { + const config = createDatabaseConfig('amer') + const flags = { verbose: true } + const error = new Error('Database provision failed') + + await expect(runProvisionTest(config, flags, error)) + .rejects.toThrow('Database provision failed') + + expect(command.warn).not.toHaveBeenCalled() + }) + + test('should fail if invalid region specified', async () => { + const config = createDatabaseConfig('invalid-region') + const flags = { verbose: false } + const error = new Error('Invalid region: invalid-region') + + await expect(runProvisionTest(config, flags, error)) + .rejects.toThrow('Invalid region: invalid-region') + }) }) From 2e44d03e903d97c41f794e98f7a6deaca6c175c1 Mon Sep 17 00:00:00 2001 From: ajaz Date: Wed, 5 Nov 2025 17:41:05 +0530 Subject: [PATCH 2/6] chore: revert package.json README.md --- README.md | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e749166b..85dd50dc 100644 --- a/README.md +++ b/README.md @@ -297,7 +297,7 @@ USAGE $ aio app create [PATH] [-v] [--version] [-i ] ARGUMENTS - [PATH] [default: .] Path to the app directory + PATH [default: .] Path to the app directory FLAGS -i, --import= Import an Adobe I/O Developer Console configuration file @@ -337,7 +337,7 @@ USAGE $ aio app delete action [ACTION-NAME] [-v] [--version] [-y] ARGUMENTS - [ACTION-NAME] Action `pkg/name` to delete, you can specify multiple actions via a comma separated list + ACTION-NAME Action `pkg/name` to delete, you can specify multiple actions via a comma separated list FLAGS -v, --verbose Verbose output @@ -543,7 +543,7 @@ USAGE | | ] [--confirm-new-workspace] [--use-jwt] [--github-pat ] [--linter none|basic|adobe-recommended] ARGUMENTS - [PATH] [default: .] Path to the app directory + PATH [default: .] Path to the app directory FLAGS -e, --extension=... Extension point(s) to implement @@ -674,7 +674,7 @@ USAGE $ aio app pack [PATH] [-v] [--version] [--lock-file] [-o ] ARGUMENTS - [PATH] [default: .] Path to the app directory to package + PATH [default: .] Path to the app directory to package FLAGS -o, --output= [default: dist/app.zip] The packaged app output file path @@ -773,7 +773,7 @@ USAGE [--confirm-new-workspace] [--no-service-sync | --confirm-service-sync] [--no-input] [--use-jwt] ARGUMENTS - [CONFIG_FILE_PATH] path to an Adobe I/O Developer Console configuration file + CONFIG_FILE_PATH path to an Adobe I/O Developer Console configuration file FLAGS -g, --global Use the global Adobe Developer Console Org / Project / Workspace configuration, diff --git a/package.json b/package.json index a239af93..90ae9bdb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "author": "Adobe Inc.", "bugs": "https://github.com/adobe/aio-cli-plugin-app/issues", "dependencies": { - "@adobe/aio-cli-lib-app-config": "../aio-cli-lib-app-config", + "@adobe/aio-cli-lib-app-config": "^4.0.3", "@adobe/aio-cli-lib-console": "^5.0.3", "@adobe/aio-lib-core-config": "^5", "@adobe/aio-lib-core-logging": "^3", From 6c8bca34441baab6e059100548532e7719f8fb31 Mon Sep 17 00:00:00 2001 From: ajaz Date: Thu, 6 Nov 2025 18:19:05 +0530 Subject: [PATCH 3/6] address review comment: remove default region --- src/commands/app/deploy.js | 17 ++++++++++------- test/commands/app/deploy.test.js | 22 +++++++++++++++------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 91a03a65..0ccec731 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -305,25 +305,28 @@ class Deploy extends BuildCommand { } async provisionDatabase (config, spinner, flags) { - const region = config.manifest?.full?.database?.region || 'amer' + const region = config.manifest?.full?.database?.region + const args = ['--yes'] - if (!config.manifest?.full?.database?.region) { - spinner.info(chalk.green('No region is configured for the database, deploying in default region amer')) + if (region) { + args.unshift('--region', region) } - const message = `Deploying database for region '${region}'` + const message = region ? `Deploying database in region '${region}'` : 'Deploying database in default region' try { spinner.start(message) if (flags.verbose) { - spinner.info(chalk.dim(`Running: aio app db provision --region ${region} --yes`)) + const commandStr = region ? `aio app db provision --region ${region} --yes` : 'aio app db provision --yes' + spinner.info(chalk.dim(`Running: ${commandStr}`)) spinner.start(message) } - await this.config.runCommand('app:db:provision', ['--region', region, '--yes']) + await this.config.runCommand('app:db:provision', args) - spinner.succeed(chalk.green(`Deployed database for application in region '${region}'`)) + const successMessage = region ? `Deployed database for application in region '${region}'` : 'Deployed database for application' + spinner.succeed(chalk.green(successMessage)) } catch (error) { spinner.fail(chalk.red('Database deployment failed')) throw error diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index fe55faf5..6d8ab0ad 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -1698,10 +1698,9 @@ describe('database provisioning', () => { const { spinner } = await runProvisionTest(config, flags) - expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('No region is configured for the database, deploying in default region amer')) - expect(spinner.start).toHaveBeenCalledWith('Deploying database for region \'amer\'') - expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--region', 'amer', '--yes']) - expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'amer\'')) + expect(spinner.start).toHaveBeenCalledWith('Deploying database in default region') + expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--yes']) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application')) }) test('should use configured region when specified in manifest', async () => { @@ -1710,8 +1709,7 @@ describe('database provisioning', () => { const { spinner } = await runProvisionTest(config, flags) - expect(spinner.info).not.toHaveBeenCalledWith(expect.stringContaining('No region is configured for the database')) - expect(spinner.start).toHaveBeenCalledWith('Deploying database for region \'emea\'') + expect(spinner.start).toHaveBeenCalledWith('Deploying database in region \'emea\'') expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--region', 'emea', '--yes']) expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'emea\'')) }) @@ -1723,11 +1721,21 @@ describe('database provisioning', () => { const { spinner } = await runProvisionTest(config, flags) expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('Running: aio app db provision --region amer --yes')) - expect(spinner.info).not.toHaveBeenCalledWith(expect.stringContaining('No region is configured for the database')) expect(spinner.start).toHaveBeenCalledTimes(2) // Once initially, once after verbose info expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'amer\'')) }) + test('should show verbose output without region when no region configured', async () => { + const config = createDatabaseConfig() + const flags = { verbose: true } + + const { spinner } = await runProvisionTest(config, flags) + + expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('Running: aio app db provision --yes')) + expect(spinner.start).toHaveBeenCalledTimes(2) // Once initially, once after verbose info + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application')) + }) + test('should handle provision command failure', async () => { const config = createDatabaseConfig('amer') const flags = { verbose: false } From 445e9d8afd5ed04945cb7fcba8dab132094eb798 Mon Sep 17 00:00:00 2001 From: ajaz Date: Fri, 7 Nov 2025 17:59:48 +0530 Subject: [PATCH 4/6] fix: region level --- test/commands/app/deploy.test.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 6d8ab0ad..72033e09 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -1638,11 +1638,9 @@ describe('run', () => { manifest: { full: { database: { - 'auto-provision': true + 'auto-provision': true, + region: 'emea' } - }, - database: { - region: 'emea' } } }) From 8972415c2435ab1c4a7077ea205f5b107130d44d Mon Sep 17 00:00:00 2001 From: ajaz Date: Fri, 7 Nov 2025 18:45:37 +0530 Subject: [PATCH 5/6] fix: test --- test/commands/app/deploy.test.js | 130 +++++++++++++++---------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 72033e09..4f4a1b99 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -1630,51 +1630,20 @@ describe('run', () => { expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) }) - - test('should deploy successfully when auto-provision database is configured', async () => { - // Create app config with database auto-provision enabled - const appConfigWithDb = createAppConfig({ - ...command.appConfig, - manifest: { - full: { - database: { - 'auto-provision': true, - region: 'emea' - } - } - } - }) - - command.getAppExtConfigs.mockResolvedValueOnce(appConfigWithDb) - command.provisionDatabase = jest.fn().mockResolvedValue() - - await command.run() - - expect(command.error).toHaveBeenCalledTimes(0) - expect(command.provisionDatabase).toHaveBeenCalledTimes(1) - expect(command.provisionDatabase).toHaveBeenCalledWith( - appConfigWithDb.application, - expect.any(Object), // spinner - expect.objectContaining({ 'force-build': true }) // flags - ) - expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) - expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) - }) }) describe('database provisioning', () => { - const createDatabaseConfig = (region = null, autoProvision = true) => ({ + // Helper functions for unit tests + const createDatabaseConfig = (region = null) => ({ manifest: { full: { database: { - 'auto-provision': autoProvision, ...(region && { region }) } } } }) - // Helper function to set up and run test const runProvisionTest = async (config, flags, mockResult) => { const spinner = ora() @@ -1690,18 +1659,60 @@ describe('database provisioning', () => { } } - test('should use default region when not specified in manifest', async () => { - const config = createDatabaseConfig() - const flags = { verbose: false } + test('should provision database when auto-provision is true', async () => { + const appConfigWithDb = createAppConfig({ + ...command.appConfig, + manifest: { + full: { + database: { + 'auto-provision': true, + region: 'emea' + } + } + } + }) - const { spinner } = await runProvisionTest(config, flags) + command.getAppExtConfigs.mockResolvedValueOnce(appConfigWithDb) + command.config.runCommand.mockResolvedValue() - expect(spinner.start).toHaveBeenCalledWith('Deploying database in default region') - expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--yes']) - expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application')) + await command.run() + + expect(command.error).toHaveBeenCalledTimes(0) + expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--region', 'emea', '--yes']) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + expect(command.log).toHaveBeenCalledWith( + expect.stringContaining('Successful deployment 🏄') + ) + }) + + test('should not provision database when auto-provision is false', async () => { + const appConfigWithoutDb = createAppConfig({ + ...command.appConfig, + manifest: { + full: { + database: { + 'auto-provision': false + } + } + } + }) + + command.getAppExtConfigs.mockResolvedValueOnce(appConfigWithoutDb) + + await command.run() + + expect(command.error).toHaveBeenCalledTimes(0) + expect(command.config.runCommand).not.toHaveBeenCalledWith('app:db:provision', expect.anything()) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) + expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) + expect(command.log).toHaveBeenCalledWith( + expect.stringContaining('Successful deployment 🏄') + ) }) - test('should use configured region when specified in manifest', async () => { + // tests for provisionDatabase method behavior + test('should run provision command correctly with region', async () => { const config = createDatabaseConfig('emea') const flags = { verbose: false } @@ -1712,7 +1723,18 @@ describe('database provisioning', () => { expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'emea\'')) }) - test('should show verbose output when verbose flag is true', async () => { + test('should run provision command correctly without region', async () => { + const config = createDatabaseConfig() + const flags = { verbose: false } + + const { spinner } = await runProvisionTest(config, flags) + + expect(spinner.start).toHaveBeenCalledWith('Deploying database in default region') + expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--yes']) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application')) + }) + + test('should show verbose output with region', async () => { const config = createDatabaseConfig('amer') const flags = { verbose: true } @@ -1723,7 +1745,7 @@ describe('database provisioning', () => { expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'amer\'')) }) - test('should show verbose output without region when no region configured', async () => { + test('should show verbose output without region', async () => { const config = createDatabaseConfig() const flags = { verbose: true } @@ -1741,27 +1763,5 @@ describe('database provisioning', () => { await expect(runProvisionTest(config, flags, error)) .rejects.toThrow('Database provision failed') - - expect(command.warn).not.toHaveBeenCalled() - }) - - test('should show verbose error details when verbose flag is true and provision fails', async () => { - const config = createDatabaseConfig('amer') - const flags = { verbose: true } - const error = new Error('Database provision failed') - - await expect(runProvisionTest(config, flags, error)) - .rejects.toThrow('Database provision failed') - - expect(command.warn).not.toHaveBeenCalled() - }) - - test('should fail if invalid region specified', async () => { - const config = createDatabaseConfig('invalid-region') - const flags = { verbose: false } - const error = new Error('Invalid region: invalid-region') - - await expect(runProvisionTest(config, flags, error)) - .rejects.toThrow('Invalid region: invalid-region') }) }) From c475862c05d1f156478ef6297180b4d4ac7d1e6f Mon Sep 17 00:00:00 2001 From: Peter Dohogne Date: Tue, 23 Dec 2025 13:04:59 -0500 Subject: [PATCH 6/6] CEXT-5618: Use @adobe/aio-lib-db for database provisioning --- package.json | 1 + src/commands/app/deploy.js | 69 +++++-- src/lib/defaults.js | 13 +- test/commands/app/deploy.test.js | 302 +++++++++++++++++++++++++------ 4 files changed, 318 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index 15dc0bd5..4352b05a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@adobe/aio-lib-core-config": "^5", "@adobe/aio-lib-core-logging": "^3", "@adobe/aio-lib-core-networking": "^5", + "@adobe/aio-lib-db": "^0.1.0-beta.4", "@adobe/aio-lib-env": "^3", "@adobe/aio-lib-ims": "^7", "@adobe/aio-lib-runtime": "^7.1.3", diff --git a/src/commands/app/deploy.js b/src/commands/app/deploy.js index 3afa9a19..b5eed90c 100644 --- a/src/commands/app/deploy.js +++ b/src/commands/app/deploy.js @@ -24,6 +24,8 @@ const { getFilesCountWithExtension } = require('../../lib/app-helper') const rtLib = require('@adobe/aio-lib-runtime') +const dbLib = require('@adobe/aio-lib-db') +const { DB_STATUS } = require('../../lib/defaults') const LogForwarding = require('../../lib/log-forwarding') const { sendAppAssetsDeployedAuditLog, sendAppDeployAuditLog } = require('../../lib/audit-logger') const { setRuntimeApiHostAndAuthHandler, getAccessToken } = require('../../lib/auth-helper') @@ -306,32 +308,71 @@ class Deploy extends BuildCommand { } async provisionDatabase (config, spinner, flags) { + const { namespace, auth } = config.ow || {} + if (!(namespace && auth)) { + throw new Error('Database deployment requires OW auth configuration.') + } const region = config.manifest?.full?.database?.region - const args = ['--yes'] + const regionMess = region ? `'${region}'` : 'default' - if (region) { - args.unshift('--region', region) + const progress = ({ next = undefined, status = undefined, verboseOnly = false }, statusMethod = spinner.info) => { + if (flags.verbose) { + const method = statusMethod.bind(spinner) + method(status) + spinner.start(next) + } else if (next && !verboseOnly) { + spinner.text = next + } } - const message = region ? `Deploying database in region '${region}'` : 'Deploying database in default region' - + let provRes try { - spinner.start(message) - - if (flags.verbose) { - const commandStr = region ? `aio app db provision --region ${region} --yes` : 'aio app db provision --yes' - spinner.info(chalk.dim(`Running: ${commandStr}`)) - spinner.start(message) + spinner.start(`Deploying database in the ${regionMess} region...`) + + const db = await dbLib.init({ ow: { namespace, auth }, region }) + progress({ next: 'Checking existing database deployment status...', verboseOnly: true }) + + let prevStatus + let statusRegion + const next = `Submitting database provisioning request in the ${regionMess} region...` + try { + const statusRes = await db.provisionStatus() + prevStatus = statusRes.status.toUpperCase() + statusRegion = statusRes.region + const regionMessage = statusRegion ? ` in region '${statusRegion}'` : '' + progress({ status: chalk.dim(`Existing database provisioning status: ${prevStatus}${regionMessage}`), next }) + } catch (err) { + progress({ status: chalk.red(`Database status check failed: ${err.message}`), next }, spinner.warn) + prevStatus = null } - await this.config.runCommand('app:db:provision', args) + if (prevStatus === DB_STATUS.PROVISIONED) { + spinner.succeed(chalk.green(`Database is deployed and ready for use in the '${statusRegion}' region`)) + return + } else if (prevStatus === DB_STATUS.REQUESTED || prevStatus === DB_STATUS.PROCESSING) { + spinner.succeed(chalk.green(`Database provisioning request has already been submitted in the '${statusRegion}' region and is pending`)) + return + } - const successMessage = region ? `Deployed database for application in region '${region}'` : 'Deployed database for application' - spinner.succeed(chalk.green(successMessage)) + provRes = await db.provisionRequest() + progress({ status: chalk.dim(`Database provisioning result:\n${JSON.stringify(provRes, null, 2)}`) }) } catch (error) { spinner.fail(chalk.red('Database deployment failed')) throw error } + + const resultStatus = provRes?.status?.toUpperCase() || DB_STATUS.UNKNOWN + if (resultStatus === DB_STATUS.PROVISIONED) { + spinner.succeed(chalk.green(`Database is deployed and ready for use in the '${provRes.region}' region`)) + } else if (resultStatus === DB_STATUS.REQUESTED || resultStatus === DB_STATUS.PROCESSING) { + spinner.succeed(chalk.green(`Database provisioning request submitted in the '${provRes.region}' region, database deployment is now pending`)) + } else if (resultStatus === DB_STATUS.FAILED || resultStatus === DB_STATUS.REJECTED) { + const message = `Database provisioning request failed with status '${resultStatus}'` + spinner.fail(chalk.red(message)) + throw new Error(`${message}: ${provRes.message || 'Unknown error'}`) + } else { + spinner.warn(chalk.yellow(`Database provisioning request returned unexpected status '${resultStatus}', an update to the aio cli tool may be necessary.`)) + } } async publishExtensionPoints (deployConfigs, aioConfig, force) { diff --git a/src/lib/defaults.js b/src/lib/defaults.js index c24d5440..c68a54e4 100644 --- a/src/lib/defaults.js +++ b/src/lib/defaults.js @@ -40,5 +40,16 @@ module.exports = { EXTENSIONS_CONFIG_KEY: 'extensions', // Adding tracking file constants LAST_BUILT_ACTIONS_FILENAME: 'last-built-actions.json', - LAST_DEPLOYED_ACTIONS_FILENAME: 'last-deployed-actions.json' + LAST_DEPLOYED_ACTIONS_FILENAME: 'last-deployed-actions.json', + // Database constants + DB_STATUS: { + PROVISIONED: 'PROVISIONED', + REQUESTED: 'REQUESTED', + PROCESSING: 'PROCESSING', + FAILED: 'FAILED', + REJECTED: 'REJECTED', + NOT_PROVISIONED: 'NOT_PROVISIONED', + DELETED: 'DELETED', + UNKNOWN: 'UNKNOWN' + } } diff --git a/test/commands/app/deploy.test.js b/test/commands/app/deploy.test.js index 71b9cee3..d7942ef4 100644 --- a/test/commands/app/deploy.test.js +++ b/test/commands/app/deploy.test.js @@ -33,6 +33,9 @@ const authHelper = require('../../../src/lib/auth-helper') const mockWebLib = require('@adobe/aio-lib-web') const mockRuntimeLib = require('@adobe/aio-lib-runtime') +jest.mock('@adobe/aio-lib-db') +const mockDbLib = require('@adobe/aio-lib-db') + jest.mock('@adobe/aio-lib-core-config') const mockConfig = require('@adobe/aio-lib-core-config') @@ -56,6 +59,7 @@ jest.mock('../../../src/lib/log-forwarding', () => { } }) const LogForwarding = require('../../../src/lib/log-forwarding') +const { DB_STATUS } = require('../../../src/lib/defaults') const createWebExportAnnotation = (value) => ({ annotations: { 'web-export': value } @@ -1634,52 +1638,67 @@ describe('run', () => { }) describe('database provisioning', () => { + let mockDb + beforeEach(() => { + mockDb = { + provisionStatus: jest.fn(), + provisionRequest: jest.fn() + } + mockDbLib.init.mockResolvedValue(mockDb) + }) + // Helper functions for unit tests - const createDatabaseConfig = (region = null) => ({ + const createDatabaseConfig = (region = null, provision = true) => ({ + ow: { namespace: 'test_ns', auth: 'user:pass' }, manifest: { full: { database: { + 'auto-provision': provision, ...(region && { region }) } } } }) - const runProvisionTest = async (config, flags, mockResult) => { - const spinner = ora() + const runProvisionTest = async ( + config, + flags, + spinner, + mockResult = { status: DB_STATUS.PROVISIONED, region: 'amer' }, + mockStatus = { status: DB_STATUS.NOT_PROVISIONED } + ) => { + if (mockStatus instanceof Error) { + mockDb.provisionStatus.mockRejectedValueOnce(mockStatus) + } else { + mockDb.provisionStatus.mockResolvedValueOnce(mockStatus) + } if (mockResult instanceof Error) { - command.config.runCommand.mockRejectedValueOnce(mockResult) + mockDb.provisionRequest.mockRejectedValueOnce(mockResult) } else { - command.config.runCommand.mockResolvedValueOnce(mockResult) + mockDb.provisionRequest.mockResolvedValueOnce(mockResult) } - return { - result: await command.provisionDatabase(config, spinner, flags).catch(e => { throw e }), - spinner - } + await command.provisionDatabase(config, spinner, flags).catch(e => { throw e }) + return spinner } test('should provision database when auto-provision is true', async () => { - const appConfigWithDb = createAppConfig({ - ...command.appConfig, - manifest: { - full: { - database: { - 'auto-provision': true, - region: 'emea' - } - } - } - }) + mockDb.provisionStatus.mockResolvedValue({ status: DB_STATUS.NOT_PROVISIONED }) + mockDb.provisionRequest.mockResolvedValue({ status: DB_STATUS.PROVISIONED, region: 'amer' }) + const appConfigWithDb = createAppConfig({ ...command.appConfig, ...createDatabaseConfig() }) command.getAppExtConfigs.mockResolvedValueOnce(appConfigWithDb) - command.config.runCommand.mockResolvedValue() await command.run() expect(command.error).toHaveBeenCalledTimes(0) - expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--region', 'emea', '--yes']) + + const expectedInitConfig = { ow: { namespace: 'test_ns', auth: 'user:pass' } } + expect(mockDbLib.init).toHaveBeenCalledWith(expect.objectContaining(expectedInitConfig)) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) expect(command.log).toHaveBeenCalledWith( @@ -1688,23 +1707,19 @@ describe('database provisioning', () => { }) test('should not provision database when auto-provision is false', async () => { - const appConfigWithoutDb = createAppConfig({ - ...command.appConfig, - manifest: { - full: { - database: { - 'auto-provision': false - } - } - } - }) + mockDb.provisionStatus.mockResolvedValue({ status: DB_STATUS.NOT_PROVISIONED }) + mockDb.provisionRequest.mockResolvedValue({ status: DB_STATUS.PROVISIONED, region: 'emea' }) + + const appConfigWithoutDb = createAppConfig({ ...command.appConfig, ...createDatabaseConfig(null, false) }) command.getAppExtConfigs.mockResolvedValueOnce(appConfigWithoutDb) await command.run() expect(command.error).toHaveBeenCalledTimes(0) - expect(command.config.runCommand).not.toHaveBeenCalledWith('app:db:provision', expect.anything()) + expect(mockDbLib.init).not.toHaveBeenCalled() + expect(mockDb.provisionStatus).not.toHaveBeenCalled() + expect(mockDb.provisionRequest).not.toHaveBeenCalled() expect(mockRuntimeLib.deployActions).toHaveBeenCalledTimes(1) expect(mockWebLib.deployWeb).toHaveBeenCalledTimes(1) expect(command.log).toHaveBeenCalledWith( @@ -1714,55 +1729,238 @@ describe('database provisioning', () => { // tests for provisionDatabase method behavior test('should run provision command correctly with region', async () => { - const config = createDatabaseConfig('emea') + const config = createDatabaseConfig('amer') const flags = { verbose: false } + const spinner = ora() + + await runProvisionTest(config, flags, spinner) - const { spinner } = await runProvisionTest(config, flags) + expect(spinner.start).toHaveBeenCalledWith(expect.stringContaining('Deploying database in the \'amer\' region')) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Database is deployed and ready for use')) - expect(spinner.start).toHaveBeenCalledWith('Deploying database in region \'emea\'') - expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--region', 'emea', '--yes']) - expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'emea\'')) + const expectedInitConfig = { + ow: { namespace: 'test_ns', auth: 'user:pass' }, + region: 'amer' + } + expect(mockDbLib.init).toHaveBeenCalledWith(expect.objectContaining(expectedInitConfig)) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) }) test('should run provision command correctly without region', async () => { const config = createDatabaseConfig() const flags = { verbose: false } + const spinner = ora() + + await runProvisionTest(config, flags, spinner) - const { spinner } = await runProvisionTest(config, flags) + expect(spinner.start).toHaveBeenCalledWith(expect.stringContaining('Deploying database in the default region')) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Database is deployed and ready for use')) - expect(spinner.start).toHaveBeenCalledWith('Deploying database in default region') - expect(command.config.runCommand).toHaveBeenCalledWith('app:db:provision', ['--yes']) - expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application')) + const expectedInitConfig = { ow: { namespace: 'test_ns', auth: 'user:pass' } } + expect(mockDbLib.init).toHaveBeenCalledWith(expect.objectContaining(expectedInitConfig)) + expect(mockDbLib.init).not.toHaveBeenCalledWith(expect.objectContaining({ region: expect.any(String) })) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) }) test('should show verbose output with region', async () => { - const config = createDatabaseConfig('amer') + const config = createDatabaseConfig('apac') const flags = { verbose: true } + const spinner = ora() - const { spinner } = await runProvisionTest(config, flags) + await runProvisionTest(config, flags, spinner, { status: DB_STATUS.PROVISIONED, region: 'apac' }, { status: DB_STATUS.DELETED }) - expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('Running: aio app db provision --region amer --yes')) - expect(spinner.start).toHaveBeenCalledTimes(2) // Once initially, once after verbose info - expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application in region \'amer\'')) + // Existing database status + expect(spinner.start).toHaveBeenCalledWith(expect.stringContaining('Checking existing database deployment status...')) + expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('status: DELETED')) + // Provision request + expect(spinner.start).toHaveBeenCalledWith(expect.stringContaining('in the \'apac\' region...')) + expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('Database provisioning result:')) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Database is deployed and ready for use')) }) test('should show verbose output without region', async () => { const config = createDatabaseConfig() const flags = { verbose: true } + const spinner = ora() - const { spinner } = await runProvisionTest(config, flags) + await runProvisionTest(config, flags, spinner, { status: DB_STATUS.PROVISIONED, region: 'emea' }) - expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('Running: aio app db provision --yes')) - expect(spinner.start).toHaveBeenCalledTimes(2) // Once initially, once after verbose info - expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Deployed database for application')) + // Existing database status + expect(spinner.start).toHaveBeenCalledWith(expect.stringContaining('Checking existing database deployment status...')) + expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('status: NOT_PROVISIONED')) + // Provision request + expect(spinner.start).toHaveBeenCalledWith(expect.stringContaining('in the default region...')) + expect(spinner.info).toHaveBeenCalledWith(expect.stringContaining('Database provisioning result:')) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Database is deployed and ready for use')) }) test('should handle provision command failure', async () => { const config = createDatabaseConfig('amer') const flags = { verbose: false } const error = new Error('Database provision failed') + const spinner = ora() - await expect(runProvisionTest(config, flags, error)) + await expect(runProvisionTest(config, flags, spinner, error)) .rejects.toThrow('Database provision failed') + expect(spinner.fail).toHaveBeenCalled() + }) + + test('should not provision if database is already provisioned', async () => { + const config = createDatabaseConfig('apac') + const spinner = ora() + + await runProvisionTest(config, {}, spinner, null, { status: DB_STATUS.PROVISIONED, region: 'apac' }) + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).not.toHaveBeenCalled() + + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('Database is deployed and ready for use')) + }) + + test('should not provision database if request is pending', async () => { + const config = createDatabaseConfig('apac') + const spinner = ora() + + await runProvisionTest(config, {}, spinner, null, { status: DB_STATUS.REQUESTED, region: 'apac' }) + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).not.toHaveBeenCalled() + + expect(spinner.succeed).toHaveBeenCalledWith( + expect.stringContaining('already been submitted') + ) + }) + + test('should try to provision if previous request failed', async () => { + const config = createDatabaseConfig('apac') + + await runProvisionTest(config, { }, ora(), undefined, { status: DB_STATUS.FAILED, region: 'apac' }) + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + }) + + test('should try to provision if database status check fails with verbose flag', async () => { + const config = createDatabaseConfig('apac') + const spinner = ora() + + await runProvisionTest(config, { verbose: true }, spinner, undefined, new Error('Status check failure')) + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + expect(spinner.warn).toHaveBeenCalledWith(expect.stringContaining('status check failed')) + }) + + test('should try to provision if database status check fails without verbose flag', async () => { + const config = createDatabaseConfig('apac') + const spinner = ora() + + await runProvisionTest(config, { }, spinner, undefined, new Error('Status check failure')) + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + expect(spinner.warn).not.toHaveBeenCalledWith(expect.stringContaining('status check failed')) + }) + + test('should report success when provision request gets submitted and responds as pending', async () => { + const config = createDatabaseConfig() + const spinner = ora() + + await runProvisionTest(config, {}, spinner, { status: DB_STATUS.PROCESSING, region: 'apac' }) + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + expect(spinner.succeed).toHaveBeenCalledWith(expect.stringContaining('request submitted')) + }) + + test('should report warning when provision request reports unrecognized status', async () => { + const config = createDatabaseConfig() + const spinner = ora() + + await runProvisionTest(config, {}, spinner, { status: 'OTHER_STATUS' }) + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + expect(spinner.warn).toHaveBeenCalledWith(expect.stringContaining('unexpected status')) + }) + + test('should report warning when provision request responds without status', async () => { + const config = createDatabaseConfig() + const spinner = ora() + + await runProvisionTest(config, {}, spinner, {}) + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + expect(spinner.warn).toHaveBeenCalledWith(expect.stringContaining('unexpected status')) + }) + + test('should throw error if provision request returns failure status with known message', async () => { + const config = createDatabaseConfig() + const spinner = ora() + + await expect(runProvisionTest(config, {}, spinner, { status: DB_STATUS.FAILED, message: 'Could not be provisioned' })) + .rejects.toThrow('Could not be provisioned') + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + expect(spinner.fail).toHaveBeenCalled() + }) + + test('should throw error if provision request returns failure status with unknown message', async () => { + const config = createDatabaseConfig() + const spinner = ora() + + await expect(runProvisionTest(config, {}, spinner, { status: DB_STATUS.REJECTED })) + .rejects.toThrow('Unknown error') + + expect(mockDbLib.init).toHaveBeenCalledTimes(1) + expect(mockDb.provisionStatus).toHaveBeenCalledTimes(1) + expect(mockDb.provisionRequest).toHaveBeenCalledTimes(1) + expect(spinner.fail).toHaveBeenCalled() + }) + + test('should throw error if OW auth is missing in config', async () => { + const config = createDatabaseConfig() + config.ow = { namespace: 'test_ns' } // missing auth + + await expect(runProvisionTest(config, {}, ora())).rejects.toThrow() + + expect(mockDbLib.init).not.toHaveBeenCalled() + expect(mockDb.provisionStatus).not.toHaveBeenCalled() + expect(mockDb.provisionRequest).not.toHaveBeenCalled() + }) + + test('should throw error if OW namespace is missing in config', async () => { + const config = createDatabaseConfig() + config.ow = { auth: 'user:pass' } // missing namespace + + await expect(runProvisionTest(config, {}, ora())).rejects.toThrow() + + expect(mockDbLib.init).not.toHaveBeenCalled() + expect(mockDb.provisionStatus).not.toHaveBeenCalled() + expect(mockDb.provisionRequest).not.toHaveBeenCalled() + }) + + test('should throw error if OW config is missing entirely', async () => { + const config = createDatabaseConfig() + delete config.ow + + await expect(runProvisionTest(config, {}, ora())).rejects.toThrow() + + expect(mockDbLib.init).not.toHaveBeenCalled() + expect(mockDb.provisionStatus).not.toHaveBeenCalled() + expect(mockDb.provisionRequest).not.toHaveBeenCalled() }) })