From 9a329f43f5961ba7195e62be99d069e445223d5d Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Tue, 8 Jul 2025 10:44:54 +0530 Subject: [PATCH 01/11] feat:cext-4860 added standalone commands --- src/commands/app/db/getCollectionInfos.js | 47 ++++++++++++ src/commands/app/db/getCollectionNames.js | 69 ++++++++++++++++++ src/commands/app/db/showCollections.js | 20 ++++++ src/commands/app/db/stats.js | 88 +++++++++++++++++++++++ 4 files changed, 224 insertions(+) create mode 100644 src/commands/app/db/getCollectionInfos.js create mode 100644 src/commands/app/db/getCollectionNames.js create mode 100644 src/commands/app/db/showCollections.js create mode 100644 src/commands/app/db/stats.js diff --git a/src/commands/app/db/getCollectionInfos.js b/src/commands/app/db/getCollectionInfos.js new file mode 100644 index 0000000..1ca427a --- /dev/null +++ b/src/commands/app/db/getCollectionInfos.js @@ -0,0 +1,47 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { DBBaseCommand } from '../../../DBBaseCommand.js' +import chalk from 'chalk' + +export class GetCollectionInfos extends DBBaseCommand { + async run () { + this.debugLogger?.info?.('Fetching collection info') + + try { + this.log(chalk.blue('Fetching collection info...')) + + const client = await this.db.connect() + const collectionInfo = await client.listCollections() + + this.log(collectionInfo) + } catch(error){ + this.debugLogger?.error?.('Error fetching collection info', error) + this.log(chalk.red('Error fetching collection info')) + this.log(error) + this.exit(1) + } + } +} + +GetCollectionInfos.description = 'Get details about your App Builder database' + +GetCollectionInfos.examples = [ + '$ aio app db getCollectionInfos', + '$ aio app db getCollectionInfos --json' +] + +GetCollectionInfos.flags = { + ...DBBaseCommand.flags +} + +GetCollectionInfos.args = {} \ No newline at end of file diff --git a/src/commands/app/db/getCollectionNames.js b/src/commands/app/db/getCollectionNames.js new file mode 100644 index 0000000..23dd9a0 --- /dev/null +++ b/src/commands/app/db/getCollectionNames.js @@ -0,0 +1,69 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { DBBaseCommand } from '../../../DBBaseCommand.js' +import chalk from 'chalk' + +export class GetCollectionNames extends DBBaseCommand { + async run () { + this.debugLogger?.info?.('Fetching collection names') + + try { + this.log(chalk.blue('Fetching collection names...')) + + const client = await this.db.connect() + const collectionInfo = await client.listCollections() + + // Extract only the names from the collection info + const collectionNames = collectionInfo.map(collection => collection.name) + + if (this.flags.json) { + return collectionNames + } + + this.log(chalk.green('Collection names:')) + this.log(JSON.stringify(collectionNames, null, 2)) + + return collectionNames + } catch(error){ + this.debugLogger?.error?.('Error fetching collection names', error) + this.log(chalk.red('Error fetching collection names')) + this.log(error) + this.exit(1) + } + } +} + +GetCollectionNames.description = 'Get collection names from your App Builder database' + +GetCollectionNames.examples = [ + '$ aio app db GetCollectionNames', + '$ aio app db GetCollectionNames --json' +] + +GetCollectionNames.flags = { + ...DBBaseCommand.flags +} + +GetCollectionNames.args = {} \ No newline at end of file diff --git a/src/commands/app/db/showCollections.js b/src/commands/app/db/showCollections.js new file mode 100644 index 0000000..ad11842 --- /dev/null +++ b/src/commands/app/db/showCollections.js @@ -0,0 +1,20 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { DBBaseCommand } from '../../../DBBaseCommand.js' +import chalk from 'chalk' + +export class ShowCollections extends DBBaseCommand { + async run () { + this.log(chalk.green('ShowCollections command not implemented yet')) + } +} \ No newline at end of file diff --git a/src/commands/app/db/stats.js b/src/commands/app/db/stats.js new file mode 100644 index 0000000..45cd92f --- /dev/null +++ b/src/commands/app/db/stats.js @@ -0,0 +1,88 @@ +/* +Copyright 2024 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { DBBaseCommand } from '../../../DBBaseCommand.js' +import chalk from 'chalk' + +export class Stats extends DBBaseCommand { + async run () { + this.debugLogger?.info?.('Fetching database statistics') + + try { + this.log(chalk.blue('Fetching database statistics...')) + + const client = await this.db.connect() + const stats = await client.dbStats() + + this.debugLogger?.info?.('Database stats retrieved:', stats) + + this.displayStats(stats) + + const result = { + ...stats, + namespace: this.rtNamespace, + timestamp: new Date().toISOString() + } + + return result + } catch (error) { + this.debugLogger?.error?.('Stats command error:', error) + + this.log(chalk.red('Failed to retrieve database statistics')) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + this.log(chalk.dim(` Error: ${error.message}`)) + + this.error(`Failed to fetch database statistics: ${error.message}`) + } + } + + displayStats (stats) { + this.log(chalk.green('Database Statistics:')) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + + if (stats && typeof stats === 'object') { + // Format and display stats in a readable way + Object.entries(stats).forEach(([key, value]) => { + const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()) + this.log(chalk.dim(` ${formattedKey}: ${this.formatValue(value)}`)) + }) + } else { + this.log(chalk.dim(` Raw Stats: ${JSON.stringify(stats, null, 2)}`)) + } + + this.log(chalk.dim(` Retrieved: ${new Date().toLocaleString()}`)) + } + + formatValue (value) { + if (typeof value === 'number') { + // Format large numbers with commas + return value.toLocaleString() + } + if (typeof value === 'object' && value !== null) { + return JSON.stringify(value, null, 2) + } + return String(value) + } +} + +Stats.description = 'Get statistics about your App Builder database' + +Stats.examples = [ + '$ aio app db stats', + '$ aio app db stats --json' +] + +Stats.flags = { + ...DBBaseCommand.flags +} + +Stats.args = {} \ No newline at end of file From ecdd1e5d34e79dcb4c6e96906c40a6c67dfa1f1f Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Tue, 8 Jul 2025 13:11:16 +0530 Subject: [PATCH 02/11] feat:cext-4860 added tests for standalone db commands --- src/commands/app/db/getCollectionInfos.js | 2 +- src/commands/app/db/getCollectionNames.js | 14 +- src/commands/app/db/showCollections.js | 2 +- src/commands/app/db/stats.js | 2 +- .../app/db/getCollectionInfos.test.js | 147 ++++++++++++ .../app/db/getCollectionNames.test.js | 150 ++++++++++++ test/commands/app/db/showCollections.test.js | 43 ++++ test/commands/app/db/stats.test.js | 215 ++++++++++++++++++ 8 files changed, 559 insertions(+), 16 deletions(-) create mode 100644 test/commands/app/db/getCollectionInfos.test.js create mode 100644 test/commands/app/db/getCollectionNames.test.js create mode 100644 test/commands/app/db/showCollections.test.js create mode 100644 test/commands/app/db/stats.test.js diff --git a/src/commands/app/db/getCollectionInfos.js b/src/commands/app/db/getCollectionInfos.js index 1ca427a..190b035 100644 --- a/src/commands/app/db/getCollectionInfos.js +++ b/src/commands/app/db/getCollectionInfos.js @@ -1,5 +1,5 @@ /* -Copyright 2024 Adobe. All rights reserved. +Copyright 2025 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/src/commands/app/db/getCollectionNames.js b/src/commands/app/db/getCollectionNames.js index 23dd9a0..a212f3c 100644 --- a/src/commands/app/db/getCollectionNames.js +++ b/src/commands/app/db/getCollectionNames.js @@ -1,17 +1,5 @@ /* -Copyright 2024 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -/* -Copyright 2024 Adobe. All rights reserved. +Copyright 2025 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/src/commands/app/db/showCollections.js b/src/commands/app/db/showCollections.js index ad11842..fb9c74f 100644 --- a/src/commands/app/db/showCollections.js +++ b/src/commands/app/db/showCollections.js @@ -1,5 +1,5 @@ /* -Copyright 2024 Adobe. All rights reserved. +Copyright 2025 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/src/commands/app/db/stats.js b/src/commands/app/db/stats.js index 45cd92f..84f051e 100644 --- a/src/commands/app/db/stats.js +++ b/src/commands/app/db/stats.js @@ -1,5 +1,5 @@ /* -Copyright 2024 Adobe. All rights reserved. +Copyright 2025 Adobe. All rights reserved. This file is licensed to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 diff --git a/test/commands/app/db/getCollectionInfos.test.js b/test/commands/app/db/getCollectionInfos.test.js new file mode 100644 index 0000000..70d47ea --- /dev/null +++ b/test/commands/app/db/getCollectionInfos.test.js @@ -0,0 +1,147 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { GetCollectionInfos } from '../../../../src/commands/app/db/getCollectionInfos.js' +import { expect, jest } from '@jest/globals' +import { stdout } from 'stdout-stderr' +import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' + +// Use the global DB mock +const mockConnect = global.mockDBInstance.connect +const mockListCollections = jest.fn() + +describe('prototype', () => { + test('extends DBBaseCommand', () => { + expect(GetCollectionInfos.prototype instanceof DBBaseCommand).toBe(true) + }) + test('args', () => { + expect(Object.keys(GetCollectionInfos.args)).toEqual([]) + }) + test('flags', () => { + expect(Object.keys(GetCollectionInfos.flags).sort()).toEqual(['region']) + expect(GetCollectionInfos.enableJsonFlag).toEqual(true) + }) +}) + +describe('run', () => { + let command + beforeEach(async () => { + command = new GetCollectionInfos([]) + command.config = { + runHook: jest.fn().mockResolvedValue({}) + } + + // Reset mocks + mockConnect.mockReset() + mockListCollections.mockReset() + + // Mock the db client + mockConnect.mockResolvedValue({ + listCollections: mockListCollections + }) + }) + + describe('successful collection info retrieval', () => { + test('returns full collection information', async () => { + command.argv = [] + await command.init() + + const collectionInfo = [ + { name: 'users', documentCount: 100, size: 1024 }, + { name: 'products', documentCount: 50, size: 512 }, + { name: 'orders', documentCount: 200, size: 2048 } + ] + mockListCollections.mockResolvedValue(collectionInfo) + + await command.run() + + expect(mockConnect).toHaveBeenCalled() + expect(mockListCollections).toHaveBeenCalled() + + expect(stdout.output).toContain('Fetching collection info...') + expect(stdout.output).toContain('users') + expect(stdout.output).toContain('products') + expect(stdout.output).toContain('orders') + expect(stdout.output).toContain('100') + expect(stdout.output).toContain('1024') + }) + + test('handles empty collection list', async () => { + command.argv = [] + await command.init() + + mockListCollections.mockResolvedValue([]) + + await command.run() + + expect(stdout.output).toContain('Fetching collection info...') + expect(stdout.output).toContain('[]') + }) + }) + + describe('error handling', () => { + test('connection error', async () => { + command.argv = [] + await command.init() + + mockConnect.mockRejectedValue(new Error('Connection failed')) + + // Mock process.exit to prevent test from actually exiting + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + + await command.run() + + expect(stdout.output).toContain('Error fetching collection info') + expect(stdout.output).toContain('Connection failed') + expect(mockExit).toHaveBeenCalledWith(1) + + mockExit.mockRestore() + }) + + test('listCollections error', async () => { + command.argv = [] + await command.init() + + mockConnect.mockResolvedValue({ + listCollections: mockListCollections + }) + mockListCollections.mockRejectedValue(new Error('Query failed')) + + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + + await command.run() + + expect(stdout.output).toContain('Error fetching collection info') + expect(stdout.output).toContain('Query failed') + expect(mockExit).toHaveBeenCalledWith(1) + + mockExit.mockRestore() + }) + + test('authentication error', async () => { + command.argv = [] + await command.init() + + mockConnect.mockRejectedValue(new Error('401 Unauthorized')) + + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + + await command.run() + + expect(stdout.output).toContain('Error fetching collection info') + expect(stdout.output).toContain('401 Unauthorized') + expect(mockExit).toHaveBeenCalledWith(1) + + mockExit.mockRestore() + }) + }) +}) diff --git a/test/commands/app/db/getCollectionNames.test.js b/test/commands/app/db/getCollectionNames.test.js new file mode 100644 index 0000000..0a1e101 --- /dev/null +++ b/test/commands/app/db/getCollectionNames.test.js @@ -0,0 +1,150 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { GetCollectionNames } from '../../../../src/commands/app/db/getCollectionNames.js' +import { expect, jest } from '@jest/globals' +import { stdout } from 'stdout-stderr' +import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' + +// Use the global DB mock +const mockConnect = global.mockDBInstance.connect +const mockListCollections = jest.fn() + +describe('prototype', () => { + test('extends DBBaseCommand', () => { + expect(GetCollectionNames.prototype instanceof DBBaseCommand).toBe(true) + }) + test('args', () => { + expect(Object.keys(GetCollectionNames.args)).toEqual([]) + }) + test('flags', () => { + expect(Object.keys(GetCollectionNames.flags).sort()).toEqual(['region']) + expect(GetCollectionNames.enableJsonFlag).toEqual(true) + }) +}) + +describe('run', () => { + let command + beforeEach(async () => { + command = new GetCollectionNames([]) + command.config = { + runHook: jest.fn().mockResolvedValue({}) + } + + // Reset mocks + mockConnect.mockReset() + mockListCollections.mockReset() + + // Mock the db client + mockConnect.mockResolvedValue({ + listCollections: mockListCollections + }) + }) + + describe('successful collection names retrieval', () => { + test('returns array of collection names', async () => { + command.argv = [] + await command.init() + + const collectionInfo = [ + { name: 'users', documentCount: 100 }, + { name: 'products', documentCount: 50 }, + { name: 'orders', documentCount: 200 } + ] + mockListCollections.mockResolvedValue(collectionInfo) + + const result = await command.run() + + expect(mockConnect).toHaveBeenCalled() + expect(mockListCollections).toHaveBeenCalled() + expect(result).toEqual(['users', 'products', 'orders']) + + expect(stdout.output).toContain('Fetching collection names...') + expect(stdout.output).toContain('Collection names:') + expect(stdout.output).toContain('users') + expect(stdout.output).toContain('products') + expect(stdout.output).toContain('orders') + }) + + test('handles empty collection list', async () => { + command.argv = [] + await command.init() + + mockListCollections.mockResolvedValue([]) + + const result = await command.run() + + expect(result).toEqual([]) + expect(stdout.output).toContain('Collection names:') + expect(stdout.output).toContain('[]') + }) + }) + + describe('json output', () => { + test('json flag returns raw array', async () => { + command.argv = ['--json'] + await command.init() + + const collectionInfo = [ + { name: 'test1', documentCount: 10 }, + { name: 'test2', documentCount: 20 } + ] + mockListCollections.mockResolvedValue(collectionInfo) + + const result = await command.run() + + expect(result).toEqual(['test1', 'test2']) + expect(stdout.output).toContain('Fetching collection names...') + expect(stdout.output).not.toContain('Collection names:') + }) + }) + + describe('error handling', () => { + test('connection error', async () => { + command.argv = [] + await command.init() + + mockConnect.mockRejectedValue(new Error('Connection failed')) + + // Mock process.exit to prevent test from actually exiting + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + + await command.run() + + expect(stdout.output).toContain('Error fetching collection names') + expect(stdout.output).toContain('Connection failed') + expect(mockExit).toHaveBeenCalledWith(1) + + mockExit.mockRestore() + }) + + test('listCollections error', async () => { + command.argv = [] + await command.init() + + mockConnect.mockResolvedValue({ + listCollections: mockListCollections + }) + mockListCollections.mockRejectedValue(new Error('Query failed')) + + const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + + await command.run() + + expect(stdout.output).toContain('Error fetching collection names') + expect(stdout.output).toContain('Query failed') + expect(mockExit).toHaveBeenCalledWith(1) + + mockExit.mockRestore() + }) + }) +}) diff --git a/test/commands/app/db/showCollections.test.js b/test/commands/app/db/showCollections.test.js new file mode 100644 index 0000000..925e800 --- /dev/null +++ b/test/commands/app/db/showCollections.test.js @@ -0,0 +1,43 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { ShowCollections } from '../../../../src/commands/app/db/showCollections.js' +import { expect, jest } from '@jest/globals' +import { stdout } from 'stdout-stderr' +import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' + +describe('prototype', () => { + test('extends DBBaseCommand', () => { + expect(ShowCollections.prototype instanceof DBBaseCommand).toBe(true) + }) +}) + +describe('run', () => { + let command + beforeEach(async () => { + command = new ShowCollections([]) + command.config = { + runHook: jest.fn().mockResolvedValue({}) + } + }) + + describe('current implementation', () => { + test('shows not implemented message', async () => { + command.argv = [] + await command.init() + + await command.run() + + expect(stdout.output).toContain('ShowCollections command not implemented yet') + }) + }) +}) diff --git a/test/commands/app/db/stats.test.js b/test/commands/app/db/stats.test.js new file mode 100644 index 0000000..63f01a1 --- /dev/null +++ b/test/commands/app/db/stats.test.js @@ -0,0 +1,215 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { Stats } from '../../../../src/commands/app/db/stats.js' +import { expect, jest } from '@jest/globals' +import { stdout } from 'stdout-stderr' +import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' + +// Use the global DB mock +const mockConnect = global.mockDBInstance.connect +const mockDbStats = jest.fn() + +describe('prototype', () => { + test('extends DBBaseCommand', () => { + expect(Stats.prototype instanceof DBBaseCommand).toBe(true) + }) + test('args', () => { + expect(Object.keys(Stats.args)).toEqual([]) + }) + test('flags', () => { + expect(Object.keys(Stats.flags).sort()).toEqual(['region']) + expect(Stats.enableJsonFlag).toEqual(true) + }) +}) + +describe('run', () => { + let command + beforeEach(async () => { + command = new Stats([]) + command.config = { + runHook: jest.fn().mockResolvedValue({}) + } + + // Reset mocks + mockConnect.mockReset() + mockDbStats.mockReset() + + // Mock the db client + mockConnect.mockResolvedValue({ + dbStats: mockDbStats + }) + }) + + describe('successful stats retrieval', () => { + test('returns and displays database statistics', async () => { + command.argv = [] + await command.init() + + const statsData = { + totalCollections: 5, + totalDocuments: 1000, + totalSize: 2048, + avgDocumentSize: 2.048, + lastUpdated: '2025-01-01T00:00:00Z' + } + mockDbStats.mockResolvedValue(statsData) + + const result = await command.run() + + expect(mockConnect).toHaveBeenCalled() + expect(mockDbStats).toHaveBeenCalled() + expect(result).toEqual({ + ...statsData, + namespace: 'test-namespace', + timestamp: expect.any(String) + }) + + expect(stdout.output).toContain('Fetching database statistics...') + expect(stdout.output).toContain('Database Statistics:') + expect(stdout.output).toContain('Namespace: test-namespace') + expect(stdout.output).toContain('Total Collections: 5') + expect(stdout.output).toContain('Total Documents: 1,000') + expect(stdout.output).toContain('Total Size: 2,048') + expect(stdout.output).toContain('Avg Document Size: 2.048') + expect(stdout.output).toContain('Retrieved:') + }) + + test('handles empty/null stats', async () => { + command.argv = [] + await command.init() + + mockDbStats.mockResolvedValue(null) + + const result = await command.run() + + expect(result).toEqual({ + namespace: 'test-namespace', + timestamp: expect.any(String) + }) + + expect(stdout.output).toContain('Raw Stats: null') + }) + + test('handles object stats', async () => { + command.argv = [] + await command.init() + + const complexStats = { + collections: { + users: { count: 100 }, + products: { count: 50 } + }, + metadata: { + version: '1.0.0' + } + } + mockDbStats.mockResolvedValue(complexStats) + + const result = await command.run() + + expect(result.collections).toEqual(complexStats.collections) + expect(stdout.output).toContain('Collections:') + expect(stdout.output).toContain('Metadata:') + }) + }) + + describe('formatValue method', () => { + test('formats numbers with commas', async () => { + command.argv = [] + await command.init() + + const formatted = command.formatValue(1000) + expect(formatted).toBe('1,000') + }) + + test('formats objects as JSON', async () => { + command.argv = [] + await command.init() + + const obj = { key: 'value' } + const formatted = command.formatValue(obj) + expect(formatted).toContain('{\n "key": "value"\n}') + }) + + test('formats other types as strings', async () => { + command.argv = [] + await command.init() + + expect(command.formatValue('test')).toBe('test') + expect(command.formatValue(true)).toBe('true') + }) + }) + + describe('error handling', () => { + test('connection error', async () => { + command.argv = [] + await command.init() + + mockConnect.mockRejectedValue(new Error('Connection failed')) + + await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: Connection failed') + + expect(stdout.output).toContain('Failed to retrieve database statistics') + expect(stdout.output).toContain('Namespace: test-namespace') + expect(stdout.output).toContain('Error: Connection failed') + }) + + test('dbStats error', async () => { + command.argv = [] + await command.init() + + mockConnect.mockResolvedValue({ + dbStats: mockDbStats + }) + mockDbStats.mockRejectedValue(new Error('Query failed')) + + await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: Query failed') + + expect(stdout.output).toContain('Failed to retrieve database statistics') + expect(stdout.output).toContain('Error: Query failed') + }) + + test('authentication error', async () => { + command.argv = [] + await command.init() + + mockConnect.mockRejectedValue(new Error('401 Unauthorized')) + + await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: 401 Unauthorized') + }) + }) + + describe('json output', () => { + test('json flag works correctly', async () => { + command.argv = ['--json'] + await command.init() + + const statsData = { + totalCollections: 3, + totalDocuments: 300 + } + mockDbStats.mockResolvedValue(statsData) + + const result = await command.run() + + expect(result).toEqual({ + ...statsData, + namespace: 'test-namespace', + timestamp: expect.any(String) + }) + + // Should still show progress messages even with --json + expect(stdout.output).toContain('Fetching database statistics...') + }) + }) +}) From 78b52994ea810d44a251f08be02ce882aba58b9a Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Tue, 8 Jul 2025 13:32:09 +0530 Subject: [PATCH 03/11] feat:cext-4860 added tests for standalone db commands --- src/commands/app/db/getCollectionInfos.js | 20 +++++------ src/commands/app/db/getCollectionNames.js | 34 +++++++++---------- src/commands/app/db/showCollections.js | 2 +- src/commands/app/db/stats.js | 2 +- .../app/db/getCollectionInfos.test.js | 22 ++++++------ .../app/db/getCollectionNames.test.js | 21 ++++++------ test/commands/app/db/stats.test.js | 19 +++++------ 7 files changed, 58 insertions(+), 62 deletions(-) diff --git a/src/commands/app/db/getCollectionInfos.js b/src/commands/app/db/getCollectionInfos.js index 190b035..4c5eafc 100644 --- a/src/commands/app/db/getCollectionInfos.js +++ b/src/commands/app/db/getCollectionInfos.js @@ -18,17 +18,17 @@ export class GetCollectionInfos extends DBBaseCommand { this.debugLogger?.info?.('Fetching collection info') try { - this.log(chalk.blue('Fetching collection info...')) + this.log(chalk.blue('Fetching collection info...')) - const client = await this.db.connect() - const collectionInfo = await client.listCollections() + const client = await this.db.connect() + const collectionInfo = await client.listCollections() - this.log(collectionInfo) - } catch(error){ - this.debugLogger?.error?.('Error fetching collection info', error) - this.log(chalk.red('Error fetching collection info')) - this.log(error) - this.exit(1) + this.log(collectionInfo) + } catch (error) { + this.debugLogger?.error?.('Error fetching collection info', error) + this.log(chalk.red('Error fetching collection info')) + this.log(error) + this.exit(1) } } } @@ -44,4 +44,4 @@ GetCollectionInfos.flags = { ...DBBaseCommand.flags } -GetCollectionInfos.args = {} \ No newline at end of file +GetCollectionInfos.args = {} diff --git a/src/commands/app/db/getCollectionNames.js b/src/commands/app/db/getCollectionNames.js index a212f3c..4cf1a19 100644 --- a/src/commands/app/db/getCollectionNames.js +++ b/src/commands/app/db/getCollectionNames.js @@ -18,27 +18,27 @@ export class GetCollectionNames extends DBBaseCommand { this.debugLogger?.info?.('Fetching collection names') try { - this.log(chalk.blue('Fetching collection names...')) + this.log(chalk.blue('Fetching collection names...')) - const client = await this.db.connect() - const collectionInfo = await client.listCollections() + const client = await this.db.connect() + const collectionInfo = await client.listCollections() - // Extract only the names from the collection info - const collectionNames = collectionInfo.map(collection => collection.name) + // Extract only the names from the collection info + const collectionNames = collectionInfo.map(collection => collection.name) - if (this.flags.json) { - return collectionNames - } + if (this.flags.json) { + return collectionNames + } - this.log(chalk.green('Collection names:')) - this.log(JSON.stringify(collectionNames, null, 2)) + this.log(chalk.green('Collection names:')) + this.log(JSON.stringify(collectionNames, null, 2)) - return collectionNames - } catch(error){ - this.debugLogger?.error?.('Error fetching collection names', error) - this.log(chalk.red('Error fetching collection names')) - this.log(error) - this.exit(1) + return collectionNames + } catch (error) { + this.debugLogger?.error?.('Error fetching collection names', error) + this.log(chalk.red('Error fetching collection names')) + this.log(error) + this.exit(1) } } } @@ -54,4 +54,4 @@ GetCollectionNames.flags = { ...DBBaseCommand.flags } -GetCollectionNames.args = {} \ No newline at end of file +GetCollectionNames.args = {} diff --git a/src/commands/app/db/showCollections.js b/src/commands/app/db/showCollections.js index fb9c74f..b68c6df 100644 --- a/src/commands/app/db/showCollections.js +++ b/src/commands/app/db/showCollections.js @@ -17,4 +17,4 @@ export class ShowCollections extends DBBaseCommand { async run () { this.log(chalk.green('ShowCollections command not implemented yet')) } -} \ No newline at end of file +} diff --git a/src/commands/app/db/stats.js b/src/commands/app/db/stats.js index 84f051e..736a453 100644 --- a/src/commands/app/db/stats.js +++ b/src/commands/app/db/stats.js @@ -85,4 +85,4 @@ Stats.flags = { ...DBBaseCommand.flags } -Stats.args = {} \ No newline at end of file +Stats.args = {} diff --git a/test/commands/app/db/getCollectionInfos.test.js b/test/commands/app/db/getCollectionInfos.test.js index 70d47ea..a899cf5 100644 --- a/test/commands/app/db/getCollectionInfos.test.js +++ b/test/commands/app/db/getCollectionInfos.test.js @@ -16,7 +16,6 @@ import { stdout } from 'stdout-stderr' import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' // Use the global DB mock -const mockConnect = global.mockDBInstance.connect const mockListCollections = jest.fn() describe('prototype', () => { @@ -41,11 +40,10 @@ describe('run', () => { } // Reset mocks - mockConnect.mockReset() mockListCollections.mockReset() - // Mock the db client - mockConnect.mockResolvedValue({ + // Mock the db client connection + global.mockDBInstance.connect = jest.fn().mockResolvedValue({ listCollections: mockListCollections }) }) @@ -64,7 +62,7 @@ describe('run', () => { await command.run() - expect(mockConnect).toHaveBeenCalled() + expect(global.mockDBInstance.connect).toHaveBeenCalled() expect(mockListCollections).toHaveBeenCalled() expect(stdout.output).toContain('Fetching collection info...') @@ -93,10 +91,10 @@ describe('run', () => { command.argv = [] await command.init() - mockConnect.mockRejectedValue(new Error('Connection failed')) + global.mockDBInstance.connect.mockRejectedValue(new Error('Connection failed')) - // Mock process.exit to prevent test from actually exiting - const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + // Mock command.exit to prevent test from actually exiting + const mockExit = jest.spyOn(command, 'exit').mockImplementation(() => {}) await command.run() @@ -111,12 +109,12 @@ describe('run', () => { command.argv = [] await command.init() - mockConnect.mockResolvedValue({ + global.mockDBInstance.connect.mockResolvedValue({ listCollections: mockListCollections }) mockListCollections.mockRejectedValue(new Error('Query failed')) - const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + const mockExit = jest.spyOn(command, 'exit').mockImplementation(() => {}) await command.run() @@ -131,9 +129,9 @@ describe('run', () => { command.argv = [] await command.init() - mockConnect.mockRejectedValue(new Error('401 Unauthorized')) + global.mockDBInstance.connect.mockRejectedValue(new Error('401 Unauthorized')) - const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + const mockExit = jest.spyOn(command, 'exit').mockImplementation(() => {}) await command.run() diff --git a/test/commands/app/db/getCollectionNames.test.js b/test/commands/app/db/getCollectionNames.test.js index 0a1e101..94fcb3b 100644 --- a/test/commands/app/db/getCollectionNames.test.js +++ b/test/commands/app/db/getCollectionNames.test.js @@ -16,7 +16,6 @@ import { stdout } from 'stdout-stderr' import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' // Use the global DB mock -const mockConnect = global.mockDBInstance.connect const mockListCollections = jest.fn() describe('prototype', () => { @@ -41,11 +40,10 @@ describe('run', () => { } // Reset mocks - mockConnect.mockReset() mockListCollections.mockReset() - // Mock the db client - mockConnect.mockResolvedValue({ + // Mock the db client connection + global.mockDBInstance.connect = jest.fn().mockResolvedValue({ listCollections: mockListCollections }) }) @@ -64,7 +62,7 @@ describe('run', () => { const result = await command.run() - expect(mockConnect).toHaveBeenCalled() + expect(global.mockDBInstance.connect).toHaveBeenCalled() expect(mockListCollections).toHaveBeenCalled() expect(result).toEqual(['users', 'products', 'orders']) @@ -103,7 +101,8 @@ describe('run', () => { const result = await command.run() expect(result).toEqual(['test1', 'test2']) - expect(stdout.output).toContain('Fetching collection names...') + // Should not show console messages with --json + expect(stdout.output).not.toContain('Fetching collection names...') expect(stdout.output).not.toContain('Collection names:') }) }) @@ -113,10 +112,10 @@ describe('run', () => { command.argv = [] await command.init() - mockConnect.mockRejectedValue(new Error('Connection failed')) + global.mockDBInstance.connect.mockRejectedValue(new Error('Connection failed')) - // Mock process.exit to prevent test from actually exiting - const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + // Mock command.exit to prevent test from actually exiting + const mockExit = jest.spyOn(command, 'exit').mockImplementation(() => {}) await command.run() @@ -131,12 +130,12 @@ describe('run', () => { command.argv = [] await command.init() - mockConnect.mockResolvedValue({ + global.mockDBInstance.connect.mockResolvedValue({ listCollections: mockListCollections }) mockListCollections.mockRejectedValue(new Error('Query failed')) - const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}) + const mockExit = jest.spyOn(command, 'exit').mockImplementation(() => {}) await command.run() diff --git a/test/commands/app/db/stats.test.js b/test/commands/app/db/stats.test.js index 63f01a1..be3b511 100644 --- a/test/commands/app/db/stats.test.js +++ b/test/commands/app/db/stats.test.js @@ -16,7 +16,6 @@ import { stdout } from 'stdout-stderr' import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' // Use the global DB mock -const mockConnect = global.mockDBInstance.connect const mockDbStats = jest.fn() describe('prototype', () => { @@ -41,11 +40,10 @@ describe('run', () => { } // Reset mocks - mockConnect.mockReset() mockDbStats.mockReset() - // Mock the db client - mockConnect.mockResolvedValue({ + // Mock the db client connection + global.mockDBInstance.connect = jest.fn().mockResolvedValue({ dbStats: mockDbStats }) }) @@ -66,7 +64,7 @@ describe('run', () => { const result = await command.run() - expect(mockConnect).toHaveBeenCalled() + expect(global.mockDBInstance.connect).toHaveBeenCalled() expect(mockDbStats).toHaveBeenCalled() expect(result).toEqual({ ...statsData, @@ -155,7 +153,7 @@ describe('run', () => { command.argv = [] await command.init() - mockConnect.mockRejectedValue(new Error('Connection failed')) + global.mockDBInstance.connect.mockRejectedValue(new Error('Connection failed')) await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: Connection failed') @@ -168,7 +166,7 @@ describe('run', () => { command.argv = [] await command.init() - mockConnect.mockResolvedValue({ + global.mockDBInstance.connect.mockResolvedValue({ dbStats: mockDbStats }) mockDbStats.mockRejectedValue(new Error('Query failed')) @@ -183,7 +181,7 @@ describe('run', () => { command.argv = [] await command.init() - mockConnect.mockRejectedValue(new Error('401 Unauthorized')) + global.mockDBInstance.connect.mockRejectedValue(new Error('401 Unauthorized')) await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: 401 Unauthorized') }) @@ -208,8 +206,9 @@ describe('run', () => { timestamp: expect.any(String) }) - // Should still show progress messages even with --json - expect(stdout.output).toContain('Fetching database statistics...') + // Should not show console messages with --json + expect(stdout.output).not.toContain('Fetching database statistics...') + expect(stdout.output).not.toContain('Database Statistics:') }) }) }) From eda07842fe54792519643c903ccd23a5b8d690d7 Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Tue, 8 Jul 2025 14:36:38 +0530 Subject: [PATCH 04/11] feat:cext-4860 updated README --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/README.md b/README.md index afeceba..699ca73 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,11 @@ $ aio app db --help # Commands +* [`aio app db getCollectionInfos`](#aio-app-db-getcollectioninfos) +* [`aio app db getCollectionNames`](#aio-app-db-getcollectionnames) * [`aio app db ping`](#aio-app-db-ping) * [`aio app db provision`](#aio-app-db-provision) +* [`aio app db stats`](#aio-app-db-stats) * [`aio app db status`](#aio-app-db-status) * [`aio app state delete [KEYS]`](#aio-app-state-delete-keys) * [`aio app state get KEY`](#aio-app-state-get-key) @@ -37,6 +40,66 @@ $ aio app db --help * [`aio app state stats`](#aio-app-state-stats) * [`aio help [COMMAND]`](#aio-help-command) +## `aio app db getCollectionInfos` + +Get details about your App Builder database + +``` +USAGE + $ aio app db getCollectionInfos [--json] + +GLOBAL FLAGS + --json Format output as json. + +DESCRIPTION + Get details about your App Builder database + +EXAMPLES + $ aio app db getCollectionInfos + + $ aio app db getCollectionInfos --json +``` + +## `aio app db getCollectionNames` + +Get collection names from your App Builder database + +``` +USAGE + $ aio app db getCollectionNames [--json] + +GLOBAL FLAGS + --json Format output as json. + +DESCRIPTION + Get collection names from your App Builder database + +EXAMPLES + $ aio app db getCollectionNames + + $ aio app db getCollectionNames --json +``` + +## `aio app db stats` + +Get statistics about your App Builder database + +``` +USAGE + $ aio app db stats [--json] + +GLOBAL FLAGS + --json Format output as json. + +DESCRIPTION + Get statistics about your App Builder database + +EXAMPLES + $ aio app db stats + + $ aio app db stats --json +``` + ## `aio app state delete [KEYS]` Delete key-values From ea1efd56f487b33eaf3c94147baa11b3ce59d83f Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Tue, 8 Jul 2025 14:40:12 +0530 Subject: [PATCH 05/11] feat:cext-4860 updated description --- src/commands/app/db/getCollectionInfos.js | 2 +- src/commands/app/db/getCollectionNames.js | 2 +- src/commands/app/db/showCollections.js | 14 ++++++++++++++ src/commands/app/db/stats.js | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/commands/app/db/getCollectionInfos.js b/src/commands/app/db/getCollectionInfos.js index 4c5eafc..864efb3 100644 --- a/src/commands/app/db/getCollectionInfos.js +++ b/src/commands/app/db/getCollectionInfos.js @@ -33,7 +33,7 @@ export class GetCollectionInfos extends DBBaseCommand { } } -GetCollectionInfos.description = 'Get details about your App Builder database' +GetCollectionInfos.description = 'Get details of your App Builder database collections' GetCollectionInfos.examples = [ '$ aio app db getCollectionInfos', diff --git a/src/commands/app/db/getCollectionNames.js b/src/commands/app/db/getCollectionNames.js index 4cf1a19..50389ba 100644 --- a/src/commands/app/db/getCollectionNames.js +++ b/src/commands/app/db/getCollectionNames.js @@ -43,7 +43,7 @@ export class GetCollectionNames extends DBBaseCommand { } } -GetCollectionNames.description = 'Get collection names from your App Builder database' +GetCollectionNames.description = 'Get collection names of your App Builder database' GetCollectionNames.examples = [ '$ aio app db GetCollectionNames', diff --git a/src/commands/app/db/showCollections.js b/src/commands/app/db/showCollections.js index b68c6df..15cf03c 100644 --- a/src/commands/app/db/showCollections.js +++ b/src/commands/app/db/showCollections.js @@ -18,3 +18,17 @@ export class ShowCollections extends DBBaseCommand { this.log(chalk.green('ShowCollections command not implemented yet')) } } + +ShowCollections.description = 'Get details about your App Builder database' + +ShowCollections.examples = [ + '$ aio app db showCollections', + '$ aio app db showCollections --json' +] + +ShowCollections.flags = { + ...DBBaseCommand.flags +} + +ShowCollections.args = {} + diff --git a/src/commands/app/db/stats.js b/src/commands/app/db/stats.js index 736a453..31e1ee0 100644 --- a/src/commands/app/db/stats.js +++ b/src/commands/app/db/stats.js @@ -74,7 +74,7 @@ export class Stats extends DBBaseCommand { } } -Stats.description = 'Get statistics about your App Builder database' +Stats.description = 'Get statistics of your App Builder database' Stats.examples = [ '$ aio app db stats', From 08c2ec717a76f46fdd141d09c01695879a0e20fa Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Tue, 8 Jul 2025 14:49:30 +0530 Subject: [PATCH 06/11] feat:cext-4860 implemented show collections command --- .../collections.js} | 23 ++++--- test/commands/app/db/show/collections.test.js | 64 +++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) rename src/commands/app/db/{showCollections.js => show/collections.js} (56%) create mode 100644 test/commands/app/db/show/collections.test.js diff --git a/src/commands/app/db/showCollections.js b/src/commands/app/db/show/collections.js similarity index 56% rename from src/commands/app/db/showCollections.js rename to src/commands/app/db/show/collections.js index 15cf03c..003b6a3 100644 --- a/src/commands/app/db/showCollections.js +++ b/src/commands/app/db/show/collections.js @@ -10,25 +10,24 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import { DBBaseCommand } from '../../../DBBaseCommand.js' -import chalk from 'chalk' +import { GetCollectionNames } from '../getCollectionNames.js' -export class ShowCollections extends DBBaseCommand { +export class Collections extends GetCollectionNames { async run () { - this.log(chalk.green('ShowCollections command not implemented yet')) + // Delegate to the parent GetCollectionNames implementation + return super.run() } } -ShowCollections.description = 'Get details about your App Builder database' +Collections.description = 'Show collection names of your App Builder database (alias for getCollectionNames)' -ShowCollections.examples = [ - '$ aio app db showCollections', - '$ aio app db showCollections --json' +Collections.examples = [ + '$ aio app db show collections', + '$ aio app db show collections --json' ] -ShowCollections.flags = { - ...DBBaseCommand.flags +Collections.flags = { + ...GetCollectionNames.flags } -ShowCollections.args = {} - +Collections.args = {} diff --git a/test/commands/app/db/show/collections.test.js b/test/commands/app/db/show/collections.test.js new file mode 100644 index 0000000..9eb796c --- /dev/null +++ b/test/commands/app/db/show/collections.test.js @@ -0,0 +1,64 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { Collections } from '../../../../../src/commands/app/db/show/collections.js' +import { GetCollectionNames } from '../../../../../src/commands/app/db/getCollectionNames.js' +import { expect, jest } from '@jest/globals' +import { stdout } from 'stdout-stderr' +import { DBBaseCommand } from '../../../../../src/DBBaseCommand.js' + +describe('prototype', () => { + test('extends GetCollectionNames', () => { + expect(Collections.prototype instanceof GetCollectionNames).toBe(true) + }) + + test('extends DBBaseCommand', () => { + expect(Collections.prototype instanceof DBBaseCommand).toBe(true) + }) +}) + +describe('run', () => { + let command + beforeEach(async () => { + command = new Collections([]) + command.config = { + runHook: jest.fn().mockResolvedValue({}) + } + }) + + describe('alias behavior', () => { + test('delegates to GetCollectionNames implementation', async () => { + // Mock the database connection and collection info + const mockClient = { + listCollections: jest.fn().mockResolvedValue([ + { name: 'collection1' }, + { name: 'collection2' } + ]) + } + + command.db = { + connect: jest.fn().mockResolvedValue(mockClient) + } + + command.argv = [] + await command.init() + + const result = await command.run() + + expect(result).toEqual(['collection1', 'collection2']) + expect(stdout.output).toContain('Fetching collection names...') + expect(stdout.output).toContain('Collection names:') + expect(stdout.output).toContain('collection1') + expect(stdout.output).toContain('collection2') + }) + }) +}) From ded163ff505296945cefc56e4083e3d9bf734cdd Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Tue, 8 Jul 2025 14:52:10 +0530 Subject: [PATCH 07/11] feat:cext-4860 implemented show collections command --- test/commands/app/db/show/collections.test.js | 32 ++++++++------ test/commands/app/db/showCollections.test.js | 43 ------------------- 2 files changed, 20 insertions(+), 55 deletions(-) delete mode 100644 test/commands/app/db/showCollections.test.js diff --git a/test/commands/app/db/show/collections.test.js b/test/commands/app/db/show/collections.test.js index 9eb796c..51b7358 100644 --- a/test/commands/app/db/show/collections.test.js +++ b/test/commands/app/db/show/collections.test.js @@ -16,6 +16,9 @@ import { expect, jest } from '@jest/globals' import { stdout } from 'stdout-stderr' import { DBBaseCommand } from '../../../../../src/DBBaseCommand.js' +// Use the global DB mock +const mockListCollections = jest.fn() + describe('prototype', () => { test('extends GetCollectionNames', () => { expect(Collections.prototype instanceof GetCollectionNames).toBe(true) @@ -33,28 +36,33 @@ describe('run', () => { command.config = { runHook: jest.fn().mockResolvedValue({}) } + + // Reset mocks + mockListCollections.mockReset() + + // Mock the db client connection + global.mockDBInstance.connect = jest.fn().mockResolvedValue({ + listCollections: mockListCollections + }) }) describe('alias behavior', () => { test('delegates to GetCollectionNames implementation', async () => { - // Mock the database connection and collection info - const mockClient = { - listCollections: jest.fn().mockResolvedValue([ - { name: 'collection1' }, - { name: 'collection2' } - ]) - } - - command.db = { - connect: jest.fn().mockResolvedValue(mockClient) - } - command.argv = [] await command.init() + const collectionInfo = [ + { name: 'collection1', documentCount: 100 }, + { name: 'collection2', documentCount: 50 } + ] + mockListCollections.mockResolvedValue(collectionInfo) + const result = await command.run() + expect(global.mockDBInstance.connect).toHaveBeenCalled() + expect(mockListCollections).toHaveBeenCalled() expect(result).toEqual(['collection1', 'collection2']) + expect(stdout.output).toContain('Fetching collection names...') expect(stdout.output).toContain('Collection names:') expect(stdout.output).toContain('collection1') diff --git a/test/commands/app/db/showCollections.test.js b/test/commands/app/db/showCollections.test.js deleted file mode 100644 index 925e800..0000000 --- a/test/commands/app/db/showCollections.test.js +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright 2025 Adobe. All rights reserved. -This file is licensed to you under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. You may obtain a copy -of the License at http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under -the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS -OF ANY KIND, either express or implied. See the License for the specific language -governing permissions and limitations under the License. -*/ - -import { ShowCollections } from '../../../../src/commands/app/db/showCollections.js' -import { expect, jest } from '@jest/globals' -import { stdout } from 'stdout-stderr' -import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' - -describe('prototype', () => { - test('extends DBBaseCommand', () => { - expect(ShowCollections.prototype instanceof DBBaseCommand).toBe(true) - }) -}) - -describe('run', () => { - let command - beforeEach(async () => { - command = new ShowCollections([]) - command.config = { - runHook: jest.fn().mockResolvedValue({}) - } - }) - - describe('current implementation', () => { - test('shows not implemented message', async () => { - command.argv = [] - await command.init() - - await command.run() - - expect(stdout.output).toContain('ShowCollections command not implemented yet') - }) - }) -}) From e764f6a87b7e82828bffec197dbd3831027cae6c Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Wed, 9 Jul 2025 18:56:59 +0530 Subject: [PATCH 08/11] first commit --- src/commands/app/db/getCollectionInfos.js | 95 +++++++ src/commands/app/db/runMongoSh.js | 27 ++ src/commands/app/db/stats.js | 97 +++++++ .../app/db/getCollectionInfos.test.js | 207 +++++++++++++++ test/commands/app/db/stats.test.js | 246 ++++++++++++++++++ 5 files changed, 672 insertions(+) create mode 100644 src/commands/app/db/getCollectionInfos.js create mode 100644 src/commands/app/db/runMongoSh.js create mode 100644 src/commands/app/db/stats.js create mode 100644 test/commands/app/db/getCollectionInfos.test.js create mode 100644 test/commands/app/db/stats.test.js diff --git a/src/commands/app/db/getCollectionInfos.js b/src/commands/app/db/getCollectionInfos.js new file mode 100644 index 0000000..8814665 --- /dev/null +++ b/src/commands/app/db/getCollectionInfos.js @@ -0,0 +1,95 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { DBBaseCommand } from '../../../DBBaseCommand.js' +import chalk from 'chalk' + +export class GetCollectionInfos extends DBBaseCommand { + async run () { + this.debugLogger?.info?.('Fetching collection info') + + try { + if (!this.flags.json) { + this.log(chalk.blue('Fetching collection info...')) + } + + const client = await this.db.connect() + const collectionInfo = await client.listCollections() + + this.debugLogger?.info?.('Collection info retrieved:', collectionInfo) + + if (this.flags.json) { + return collectionInfo + } + + this.log(chalk.green('Collection Information:')) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + + if (collectionInfo && collectionInfo.length > 0) { + this.log(chalk.dim(` Total Collections: ${collectionInfo.length}`)) + this.log('') + + collectionInfo.forEach((collection, index) => { + this.log(chalk.cyan(` Collection ${index + 1}:`)) + Object.entries(collection).forEach(([key, value]) => { + const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()) + this.log(chalk.dim(` ${formattedKey}: ${this.formatValue(value)}`)) + }) + if (index < collectionInfo.length - 1) { + this.log('') + } + }) + } else { + this.log(chalk.dim(' No collections found')) + } + + this.log('') + this.log(chalk.dim(` Retrieved: ${new Date().toLocaleString()}`)) + + return collectionInfo + } catch (error) { + this.debugLogger?.error?.('Error fetching collection info', error) + + if (!this.flags.json) { + this.log(chalk.red('Failed to retrieve collection information')) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + this.log(chalk.dim(` Error: ${error.message}`)) + } + + this.error(`Failed to fetch collection information: ${error.message}`) + } + } + + formatValue (value) { + if (typeof value === 'number') { + // Format large numbers with commas + return value.toLocaleString() + } + if (typeof value === 'object' && value !== null) { + return JSON.stringify(value, null, 2) + } + return String(value) + } +} + +GetCollectionInfos.description = 'Get details of your App Builder database collections' + +GetCollectionInfos.examples = [ + '$ aio app db getCollectionInfos', + '$ aio app db getCollectionInfos --json' +] + +GetCollectionInfos.flags = { + ...DBBaseCommand.flags +} + +GetCollectionInfos.args = {} diff --git a/src/commands/app/db/runMongoSh.js b/src/commands/app/db/runMongoSh.js new file mode 100644 index 0000000..146e72f --- /dev/null +++ b/src/commands/app/db/runMongoSh.js @@ -0,0 +1,27 @@ +import { Args, Command } from '@oclif/core' +import { exec } from 'child_process' + +export default class RunMongosh extends Command { + static args = [ + { + name: 'mongoCommand', + required: true, + description: 'The MongoDB command to run in mongosh (e.g. db.stats(), db.users.find({}))', + }, + ] + + async run() { + const { args } = await this.parse(RunMongosh) + const { mongoCommand } = args + + const command = `mongosh --eval "${mongoCommand}"` + + exec(command, (error, stdout, stderr) => { + if (error) { + this.error(`❌ Execution failed: ${stderr}`) + return + } + this.log(`✅ Output:\n${stdout}`) + }) + } +} diff --git a/src/commands/app/db/stats.js b/src/commands/app/db/stats.js new file mode 100644 index 0000000..3a6e6b4 --- /dev/null +++ b/src/commands/app/db/stats.js @@ -0,0 +1,97 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { DBBaseCommand } from '../../../DBBaseCommand.js' +import chalk from 'chalk' + +export class Stats extends DBBaseCommand { + async run () { + this.debugLogger?.info?.('Fetching database statistics') + + try { + if (!this.flags.json) { + this.log(chalk.blue('Fetching database statistics...')) + } + + const client = await this.db.connect() + const stats = await client.dbStats() + + this.debugLogger?.info?.('Database stats retrieved:', stats) + + const result = { + ...stats, + namespace: this.rtNamespace, + timestamp: new Date().toISOString() + } + + if (this.flags.json) { + return result + } + + this.displayStats(stats) + + return result + } catch (error) { + this.debugLogger?.error?.('Stats command error:', error) + + if (!this.flags.json) { + this.log(chalk.red('Failed to retrieve database statistics')) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + this.log(chalk.dim(` Error: ${error.message}`)) + } + + this.error(`Failed to fetch database statistics: ${error.message}`) + } + } + + displayStats (stats) { + this.log(chalk.green('Database Statistics:')) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + + if (stats && typeof stats === 'object') { + // Format and display stats in a readable way + Object.entries(stats).forEach(([key, value]) => { + const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()) + this.log(chalk.dim(` ${formattedKey}: ${this.formatValue(value)}`)) + }) + } else { + this.log(chalk.dim(` Raw Stats: ${JSON.stringify(stats, null, 2)}`)) + } + + this.log('') + this.log(chalk.dim(` Retrieved: ${new Date().toLocaleString()}`)) + } + + formatValue (value) { + if (typeof value === 'number') { + // Format large numbers with commas + return value.toLocaleString() + } + if (typeof value === 'object' && value !== null) { + return JSON.stringify(value, null, 2) + } + return String(value) + } +} + +Stats.description = 'Get statistics of your App Builder database' + +Stats.examples = [ + '$ aio app db stats', + '$ aio app db stats --json' +] + +Stats.flags = { + ...DBBaseCommand.flags +} + +Stats.args = {} diff --git a/test/commands/app/db/getCollectionInfos.test.js b/test/commands/app/db/getCollectionInfos.test.js new file mode 100644 index 0000000..bb65b2a --- /dev/null +++ b/test/commands/app/db/getCollectionInfos.test.js @@ -0,0 +1,207 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { GetCollectionInfos } from '../../../../src/commands/app/db/getCollectionInfos.js' +import { expect, jest } from '@jest/globals' +import { stdout } from 'stdout-stderr' +import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' + +// Use the global DB mock +const mockListCollections = jest.fn() + +describe('prototype', () => { + test('extends DBBaseCommand', () => { + expect(GetCollectionInfos.prototype instanceof DBBaseCommand).toBe(true) + }) + test('args', () => { + expect(Object.keys(GetCollectionInfos.args)).toEqual([]) + }) + test('flags', () => { + expect(Object.keys(GetCollectionInfos.flags).sort()).toEqual(['region']) + expect(GetCollectionInfos.enableJsonFlag).toEqual(true) + }) +}) + +describe('run', () => { + let command + beforeEach(async () => { + command = new GetCollectionInfos([]) + command.config = { + runHook: jest.fn().mockResolvedValue({}) + } + + // Reset mocks + mockListCollections.mockReset() + + // Mock the db client connection + global.mockDBInstance.connect = jest.fn().mockResolvedValue({ + listCollections: mockListCollections + }) + }) + + describe('successful collection info retrieval', () => { + test('returns full collection information without --json flag', async () => { + command.argv = [] + await command.init() + + const collectionInfo = [ + { name: 'users', documentCount: 100, size: 1024 }, + { name: 'products', documentCount: 50, size: 512 }, + { name: 'orders', documentCount: 200, size: 2048 } + ] + mockListCollections.mockResolvedValue(collectionInfo) + + const result = await command.run() + + expect(global.mockDBInstance.connect).toHaveBeenCalled() + expect(mockListCollections).toHaveBeenCalled() + expect(result).toEqual(collectionInfo) + + expect(stdout.output).toContain('Fetching collection info...') + expect(stdout.output).toContain('Collection Information:') + expect(stdout.output).toContain('Namespace: test-namespace') + expect(stdout.output).toContain('Total Collections: 3') + expect(stdout.output).toContain('Collection 1:') + expect(stdout.output).toContain('Name: users') + expect(stdout.output).toContain('Document Count: 100') + expect(stdout.output).toContain('Size: 1,024') + expect(stdout.output).toContain('Retrieved:') + }) + + test('returns collection information with --json flag', async () => { + command.argv = ['--json'] + await command.init() + + const collectionInfo = [ + { name: 'users', documentCount: 100, size: 1024 }, + { name: 'products', documentCount: 50, size: 512 } + ] + mockListCollections.mockResolvedValue(collectionInfo) + + const result = await command.run() + + expect(result).toEqual(collectionInfo) + + // Should not show console messages with --json + expect(stdout.output).not.toContain('Fetching collection info...') + expect(stdout.output).not.toContain('Collection Information:') + expect(stdout.output).not.toContain('Namespace:') + }) + + test('handles empty collection list', async () => { + command.argv = [] + await command.init() + + mockListCollections.mockResolvedValue([]) + + const result = await command.run() + + expect(result).toEqual([]) + expect(stdout.output).toContain('Collection Information:') + expect(stdout.output).toContain('No collections found') + }) + + test('handles empty collection list with --json flag', async () => { + command.argv = ['--json'] + await command.init() + + mockListCollections.mockResolvedValue([]) + + const result = await command.run() + + expect(result).toEqual([]) + expect(stdout.output).not.toContain('No collections found') + }) + }) + + describe('formatValue method', () => { + test('formats numbers with commas', async () => { + command.argv = [] + await command.init() + + const formatted = command.formatValue(1000) + expect(formatted).toBe('1,000') + }) + + test('formats objects as JSON', async () => { + command.argv = [] + await command.init() + + const obj = { key: 'value' } + const formatted = command.formatValue(obj) + expect(formatted).toContain('{\n "key": "value"\n}') + }) + + test('formats other types as strings', async () => { + command.argv = [] + await command.init() + + expect(command.formatValue('test')).toBe('test') + expect(command.formatValue(true)).toBe('true') + }) + }) + + describe('error handling', () => { + test('connection error without --json flag', async () => { + command.argv = [] + await command.init() + + global.mockDBInstance.connect.mockRejectedValue(new Error('Connection failed')) + + await expect(command.run()).rejects.toThrow('Failed to fetch collection information: Connection failed') + + expect(stdout.output).toContain('Failed to retrieve collection information') + expect(stdout.output).toContain('Namespace: test-namespace') + expect(stdout.output).toContain('Error: Connection failed') + }) + + test('connection error with --json flag', async () => { + command.argv = ['--json'] + await command.init() + + global.mockDBInstance.connect.mockRejectedValue(new Error('Connection failed')) + + await expect(command.run()).rejects.toThrow('Failed to fetch collection information: Connection failed') + + // Should not show console messages with --json + expect(stdout.output).not.toContain('Failed to retrieve collection information') + expect(stdout.output).not.toContain('Namespace:') + }) + + test('listCollections error', async () => { + command.argv = [] + await command.init() + + global.mockDBInstance.connect.mockResolvedValue({ + listCollections: mockListCollections + }) + mockListCollections.mockRejectedValue(new Error('Query failed')) + + await expect(command.run()).rejects.toThrow('Failed to fetch collection information: Query failed') + + expect(stdout.output).toContain('Failed to retrieve collection information') + expect(stdout.output).toContain('Error: Query failed') + }) + + test('authentication error', async () => { + command.argv = [] + await command.init() + + global.mockDBInstance.connect.mockRejectedValue(new Error('401 Unauthorized')) + + await expect(command.run()).rejects.toThrow('Failed to fetch collection information: 401 Unauthorized') + + expect(stdout.output).toContain('Failed to retrieve collection information') + expect(stdout.output).toContain('401 Unauthorized') + }) + }) +}) diff --git a/test/commands/app/db/stats.test.js b/test/commands/app/db/stats.test.js new file mode 100644 index 0000000..acb2d8c --- /dev/null +++ b/test/commands/app/db/stats.test.js @@ -0,0 +1,246 @@ +/* +Copyright 2025 Adobe. All rights reserved. +This file is licensed to you under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. You may obtain a copy +of the License at http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under +the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS +OF ANY KIND, either express or implied. See the License for the specific language +governing permissions and limitations under the License. +*/ + +import { Stats } from '../../../../src/commands/app/db/stats.js' +import { expect, jest } from '@jest/globals' +import { stdout } from 'stdout-stderr' +import { DBBaseCommand } from '../../../../src/DBBaseCommand.js' + +// Use the global DB mock +const mockDbStats = jest.fn() + +describe('prototype', () => { + test('extends DBBaseCommand', () => { + expect(Stats.prototype instanceof DBBaseCommand).toBe(true) + }) + test('args', () => { + expect(Object.keys(Stats.args)).toEqual([]) + }) + test('flags', () => { + expect(Object.keys(Stats.flags).sort()).toEqual(['region']) + expect(Stats.enableJsonFlag).toEqual(true) + }) +}) + +describe('run', () => { + let command + beforeEach(async () => { + command = new Stats([]) + command.config = { + runHook: jest.fn().mockResolvedValue({}) + } + + // Reset mocks + mockDbStats.mockReset() + + // Mock the db client connection + global.mockDBInstance.connect = jest.fn().mockResolvedValue({ + dbStats: mockDbStats + }) + }) + + describe('successful stats retrieval', () => { + test('returns and displays database statistics without --json flag', async () => { + command.argv = [] + await command.init() + + const statsData = { + totalCollections: 5, + totalDocuments: 1000, + totalSize: 2048, + avgDocumentSize: 2.048, + lastUpdated: '2025-01-01T00:00:00Z' + } + mockDbStats.mockResolvedValue(statsData) + + const result = await command.run() + + expect(global.mockDBInstance.connect).toHaveBeenCalled() + expect(mockDbStats).toHaveBeenCalled() + expect(result).toEqual({ + ...statsData, + namespace: 'test-namespace', + timestamp: expect.any(String) + }) + + expect(stdout.output).toContain('Fetching database statistics...') + expect(stdout.output).toContain('Database Statistics:') + expect(stdout.output).toContain('Namespace: test-namespace') + expect(stdout.output).toContain('Total Collections: 5') + expect(stdout.output).toContain('Total Documents: 1,000') + expect(stdout.output).toContain('Total Size: 2,048') + expect(stdout.output).toContain('Avg Document Size: 2.048') + expect(stdout.output).toContain('Retrieved:') + }) + + test('returns database statistics with --json flag', async () => { + command.argv = ['--json'] + await command.init() + + const statsData = { + totalCollections: 3, + totalDocuments: 300, + totalSize: 1024 + } + mockDbStats.mockResolvedValue(statsData) + + const result = await command.run() + + expect(result).toEqual({ + ...statsData, + namespace: 'test-namespace', + timestamp: expect.any(String) + }) + + // Should not show console messages with --json + expect(stdout.output).not.toContain('Fetching database statistics...') + expect(stdout.output).not.toContain('Database Statistics:') + expect(stdout.output).not.toContain('Namespace:') + }) + + test('handles empty/null stats', async () => { + command.argv = [] + await command.init() + + mockDbStats.mockResolvedValue(null) + + const result = await command.run() + + expect(result).toEqual({ + namespace: 'test-namespace', + timestamp: expect.any(String) + }) + + expect(stdout.output).toContain('Raw Stats: null') + }) + + test('handles empty/null stats with --json flag', async () => { + command.argv = ['--json'] + await command.init() + + mockDbStats.mockResolvedValue(null) + + const result = await command.run() + + expect(result).toEqual({ + namespace: 'test-namespace', + timestamp: expect.any(String) + }) + + expect(stdout.output).not.toContain('Raw Stats: null') + }) + + test('handles object stats', async () => { + command.argv = [] + await command.init() + + const complexStats = { + collections: { + users: { count: 100 }, + products: { count: 50 } + }, + metadata: { + version: '1.0.0' + } + } + mockDbStats.mockResolvedValue(complexStats) + + const result = await command.run() + + expect(result.collections).toEqual(complexStats.collections) + expect(stdout.output).toContain('Collections:') + expect(stdout.output).toContain('Metadata:') + }) + }) + + describe('formatValue method', () => { + test('formats numbers with commas', async () => { + command.argv = [] + await command.init() + + const formatted = command.formatValue(1000) + expect(formatted).toBe('1,000') + }) + + test('formats objects as JSON', async () => { + command.argv = [] + await command.init() + + const obj = { key: 'value' } + const formatted = command.formatValue(obj) + expect(formatted).toContain('{\n "key": "value"\n}') + }) + + test('formats other types as strings', async () => { + command.argv = [] + await command.init() + + expect(command.formatValue('test')).toBe('test') + expect(command.formatValue(true)).toBe('true') + }) + }) + + describe('error handling', () => { + test('connection error without --json flag', async () => { + command.argv = [] + await command.init() + + global.mockDBInstance.connect.mockRejectedValue(new Error('Connection failed')) + + await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: Connection failed') + + expect(stdout.output).toContain('Failed to retrieve database statistics') + expect(stdout.output).toContain('Namespace: test-namespace') + expect(stdout.output).toContain('Error: Connection failed') + }) + + test('connection error with --json flag', async () => { + command.argv = ['--json'] + await command.init() + + global.mockDBInstance.connect.mockRejectedValue(new Error('Connection failed')) + + await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: Connection failed') + + // Should not show console messages with --json + expect(stdout.output).not.toContain('Failed to retrieve database statistics') + expect(stdout.output).not.toContain('Namespace:') + }) + + test('dbStats error', async () => { + command.argv = [] + await command.init() + + global.mockDBInstance.connect.mockResolvedValue({ + dbStats: mockDbStats + }) + mockDbStats.mockRejectedValue(new Error('Query failed')) + + await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: Query failed') + + expect(stdout.output).toContain('Failed to retrieve database statistics') + expect(stdout.output).toContain('Error: Query failed') + }) + + test('authentication error', async () => { + command.argv = [] + await command.init() + + global.mockDBInstance.connect.mockRejectedValue(new Error('401 Unauthorized')) + + await expect(command.run()).rejects.toThrow('Failed to fetch database statistics: 401 Unauthorized') + + expect(stdout.output).toContain('Failed to retrieve database statistics') + expect(stdout.output).toContain('401 Unauthorized') + }) + }) +}) From 1d309fb547f2867dde2b927d7b619164197391b6 Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Thu, 10 Jul 2025 15:01:25 +0530 Subject: [PATCH 09/11] feat:cext-4860 Removed the conditional logs based on json flag --- src/commands/app/db/getCollectionInfos.js | 15 +++++---------- src/commands/app/db/getCollectionNames.js | 7 +++++-- src/commands/app/db/ping.js | 4 +--- src/commands/app/db/stats.js | 15 +++++---------- 4 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/commands/app/db/getCollectionInfos.js b/src/commands/app/db/getCollectionInfos.js index 8814665..d8be160 100644 --- a/src/commands/app/db/getCollectionInfos.js +++ b/src/commands/app/db/getCollectionInfos.js @@ -18,9 +18,7 @@ export class GetCollectionInfos extends DBBaseCommand { this.debugLogger?.info?.('Fetching collection info') try { - if (!this.flags.json) { - this.log(chalk.blue('Fetching collection info...')) - } + this.log(chalk.blue('Fetching collection info...')) const client = await this.db.connect() const collectionInfo = await client.listCollections() @@ -41,8 +39,7 @@ export class GetCollectionInfos extends DBBaseCommand { collectionInfo.forEach((collection, index) => { this.log(chalk.cyan(` Collection ${index + 1}:`)) Object.entries(collection).forEach(([key, value]) => { - const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()) - this.log(chalk.dim(` ${formattedKey}: ${this.formatValue(value)}`)) + this.log(chalk.dim(` ${key}: ${this.formatValue(value)}`)) }) if (index < collectionInfo.length - 1) { this.log('') @@ -59,11 +56,9 @@ export class GetCollectionInfos extends DBBaseCommand { } catch (error) { this.debugLogger?.error?.('Error fetching collection info', error) - if (!this.flags.json) { - this.log(chalk.red('Failed to retrieve collection information')) - this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) - this.log(chalk.dim(` Error: ${error.message}`)) - } + this.log(chalk.red('Failed to retrieve collection information')) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + this.log(chalk.dim(` Error: ${error.message}`)) this.error(`Failed to fetch collection information: ${error.message}`) } diff --git a/src/commands/app/db/getCollectionNames.js b/src/commands/app/db/getCollectionNames.js index 50389ba..3d76c41 100644 --- a/src/commands/app/db/getCollectionNames.js +++ b/src/commands/app/db/getCollectionNames.js @@ -36,9 +36,12 @@ export class GetCollectionNames extends DBBaseCommand { return collectionNames } catch (error) { this.debugLogger?.error?.('Error fetching collection names', error) + this.log(chalk.red('Error fetching collection names')) - this.log(error) - this.exit(1) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + this.log(chalk.dim(` Error: ${error.message}`)) + + this.error(`Failed to fetch collection names: ${error.message}`) } } } diff --git a/src/commands/app/db/ping.js b/src/commands/app/db/ping.js index b4bb291..de2d2e6 100644 --- a/src/commands/app/db/ping.js +++ b/src/commands/app/db/ping.js @@ -43,9 +43,7 @@ export class Ping extends DBBaseCommand { timestamp: new Date().toISOString() } - if (!this.flags.json) { - this.log(chalk.dim('Database is ready for operations')) - } + this.log(chalk.dim('Database is ready for operations')) return result } catch (error) { diff --git a/src/commands/app/db/stats.js b/src/commands/app/db/stats.js index 3a6e6b4..bcccd7c 100644 --- a/src/commands/app/db/stats.js +++ b/src/commands/app/db/stats.js @@ -18,9 +18,7 @@ export class Stats extends DBBaseCommand { this.debugLogger?.info?.('Fetching database statistics') try { - if (!this.flags.json) { - this.log(chalk.blue('Fetching database statistics...')) - } + this.log(chalk.blue('Fetching database statistics...')) const client = await this.db.connect() const stats = await client.dbStats() @@ -43,11 +41,9 @@ export class Stats extends DBBaseCommand { } catch (error) { this.debugLogger?.error?.('Stats command error:', error) - if (!this.flags.json) { - this.log(chalk.red('Failed to retrieve database statistics')) - this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) - this.log(chalk.dim(` Error: ${error.message}`)) - } + this.log(chalk.red('Failed to retrieve database statistics')) + this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) + this.log(chalk.dim(` Error: ${error.message}`)) this.error(`Failed to fetch database statistics: ${error.message}`) } @@ -60,8 +56,7 @@ export class Stats extends DBBaseCommand { if (stats && typeof stats === 'object') { // Format and display stats in a readable way Object.entries(stats).forEach(([key, value]) => { - const formattedKey = key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase()) - this.log(chalk.dim(` ${formattedKey}: ${this.formatValue(value)}`)) + this.log(chalk.dim(` ${key}: ${this.formatValue(value)}`)) }) } else { this.log(chalk.dim(` Raw Stats: ${JSON.stringify(stats, null, 2)}`)) From 015d961bb4790390815df7661254f0cdacb822f8 Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Thu, 10 Jul 2025 15:03:52 +0530 Subject: [PATCH 10/11] feat:cext-4860 Removed the conditional logs based on json flag --- .../app/db/getCollectionInfos.test.js | 6 +++--- .../app/db/getCollectionNames.test.js | 21 ++++++------------- test/commands/app/db/stats.test.js | 12 +++++------ 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/test/commands/app/db/getCollectionInfos.test.js b/test/commands/app/db/getCollectionInfos.test.js index bb65b2a..47860c3 100644 --- a/test/commands/app/db/getCollectionInfos.test.js +++ b/test/commands/app/db/getCollectionInfos.test.js @@ -71,9 +71,9 @@ describe('run', () => { expect(stdout.output).toContain('Namespace: test-namespace') expect(stdout.output).toContain('Total Collections: 3') expect(stdout.output).toContain('Collection 1:') - expect(stdout.output).toContain('Name: users') - expect(stdout.output).toContain('Document Count: 100') - expect(stdout.output).toContain('Size: 1,024') + expect(stdout.output).toContain('name: users') + expect(stdout.output).toContain('documentCount: 100') + expect(stdout.output).toContain('size: 1,024') expect(stdout.output).toContain('Retrieved:') }) diff --git a/test/commands/app/db/getCollectionNames.test.js b/test/commands/app/db/getCollectionNames.test.js index 94fcb3b..b175350 100644 --- a/test/commands/app/db/getCollectionNames.test.js +++ b/test/commands/app/db/getCollectionNames.test.js @@ -114,16 +114,11 @@ describe('run', () => { global.mockDBInstance.connect.mockRejectedValue(new Error('Connection failed')) - // Mock command.exit to prevent test from actually exiting - const mockExit = jest.spyOn(command, 'exit').mockImplementation(() => {}) - - await command.run() + await expect(command.run()).rejects.toThrow('Failed to fetch collection names: Connection failed') expect(stdout.output).toContain('Error fetching collection names') - expect(stdout.output).toContain('Connection failed') - expect(mockExit).toHaveBeenCalledWith(1) - - mockExit.mockRestore() + expect(stdout.output).toContain('Namespace: test-namespace') + expect(stdout.output).toContain('Error: Connection failed') }) test('listCollections error', async () => { @@ -135,15 +130,11 @@ describe('run', () => { }) mockListCollections.mockRejectedValue(new Error('Query failed')) - const mockExit = jest.spyOn(command, 'exit').mockImplementation(() => {}) - - await command.run() + await expect(command.run()).rejects.toThrow('Failed to fetch collection names: Query failed') expect(stdout.output).toContain('Error fetching collection names') - expect(stdout.output).toContain('Query failed') - expect(mockExit).toHaveBeenCalledWith(1) - - mockExit.mockRestore() + expect(stdout.output).toContain('Namespace: test-namespace') + expect(stdout.output).toContain('Error: Query failed') }) }) }) diff --git a/test/commands/app/db/stats.test.js b/test/commands/app/db/stats.test.js index acb2d8c..b395eeb 100644 --- a/test/commands/app/db/stats.test.js +++ b/test/commands/app/db/stats.test.js @@ -75,10 +75,10 @@ describe('run', () => { expect(stdout.output).toContain('Fetching database statistics...') expect(stdout.output).toContain('Database Statistics:') expect(stdout.output).toContain('Namespace: test-namespace') - expect(stdout.output).toContain('Total Collections: 5') - expect(stdout.output).toContain('Total Documents: 1,000') - expect(stdout.output).toContain('Total Size: 2,048') - expect(stdout.output).toContain('Avg Document Size: 2.048') + expect(stdout.output).toContain('totalCollections: 5') + expect(stdout.output).toContain('totalDocuments: 1,000') + expect(stdout.output).toContain('totalSize: 2,048') + expect(stdout.output).toContain('avgDocumentSize: 2.048') expect(stdout.output).toContain('Retrieved:') }) @@ -157,8 +157,8 @@ describe('run', () => { const result = await command.run() expect(result.collections).toEqual(complexStats.collections) - expect(stdout.output).toContain('Collections:') - expect(stdout.output).toContain('Metadata:') + expect(stdout.output).toContain('collections:') + expect(stdout.output).toContain('metadata:') }) }) From 1ec05ab696b9a08590f8264dbd3a0c9e96e6dac7 Mon Sep 17 00:00:00 2001 From: Simran Gulati Date: Thu, 10 Jul 2025 18:09:00 +0530 Subject: [PATCH 11/11] feat:cext-4860 Removed redundant logs --- src/commands/app/db/getCollectionInfos.js | 4 +--- src/commands/app/db/getCollectionNames.js | 4 ++-- src/commands/app/db/ping.js | 4 +--- src/commands/app/db/provision.js | 2 -- src/commands/app/db/stats.js | 4 +--- 5 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/commands/app/db/getCollectionInfos.js b/src/commands/app/db/getCollectionInfos.js index d8be160..7fb2b7b 100644 --- a/src/commands/app/db/getCollectionInfos.js +++ b/src/commands/app/db/getCollectionInfos.js @@ -15,15 +15,13 @@ import chalk from 'chalk' export class GetCollectionInfos extends DBBaseCommand { async run () { - this.debugLogger?.info?.('Fetching collection info') - try { this.log(chalk.blue('Fetching collection info...')) const client = await this.db.connect() const collectionInfo = await client.listCollections() - this.debugLogger?.info?.('Collection info retrieved:', collectionInfo) + this.debugLogger?.info?.(`Retrieved ${collectionInfo.length} collections with full details:`, collectionInfo) if (this.flags.json) { return collectionInfo diff --git a/src/commands/app/db/getCollectionNames.js b/src/commands/app/db/getCollectionNames.js index 3d76c41..a1866b1 100644 --- a/src/commands/app/db/getCollectionNames.js +++ b/src/commands/app/db/getCollectionNames.js @@ -15,14 +15,14 @@ import chalk from 'chalk' export class GetCollectionNames extends DBBaseCommand { async run () { - this.debugLogger?.info?.('Fetching collection names') - try { this.log(chalk.blue('Fetching collection names...')) const client = await this.db.connect() const collectionInfo = await client.listCollections() + this.debugLogger?.info?.(`Retrieved ${collectionInfo.length} collections from database`) + // Extract only the names from the collection info const collectionNames = collectionInfo.map(collection => collection.name) diff --git a/src/commands/app/db/ping.js b/src/commands/app/db/ping.js index de2d2e6..436e69e 100644 --- a/src/commands/app/db/ping.js +++ b/src/commands/app/db/ping.js @@ -15,8 +15,6 @@ import chalk from 'chalk' export class Ping extends DBBaseCommand { async run () { - this.debugLogger?.info?.('Starting database ping test') - try { this.log(chalk.blue('Testing database connectivity...')) @@ -25,7 +23,7 @@ export class Ping extends DBBaseCommand { const endTime = Date.now() const responseTime = endTime - startTime - this.debugLogger?.info?.('Ping result:', pingResult) + this.debugLogger?.info?.(`Database ping completed in ${responseTime}ms:`, pingResult) this.log(chalk.green('Database connection successful')) this.log(chalk.dim(` Namespace: ${this.rtNamespace}`)) diff --git a/src/commands/app/db/provision.js b/src/commands/app/db/provision.js index e6f8322..34d86f0 100644 --- a/src/commands/app/db/provision.js +++ b/src/commands/app/db/provision.js @@ -19,8 +19,6 @@ export class Provision extends DBBaseCommand { async run () { const { region } = this.flags - this.debugLogger?.info?.('Starting database provisioning process') - try { // First check if database is already provisioned this.log(chalk.blue('Checking current provisioning status...')) diff --git a/src/commands/app/db/stats.js b/src/commands/app/db/stats.js index bcccd7c..150c199 100644 --- a/src/commands/app/db/stats.js +++ b/src/commands/app/db/stats.js @@ -15,15 +15,13 @@ import chalk from 'chalk' export class Stats extends DBBaseCommand { async run () { - this.debugLogger?.info?.('Fetching database statistics') - try { this.log(chalk.blue('Fetching database statistics...')) const client = await this.db.connect() const stats = await client.dbStats() - this.debugLogger?.info?.('Database stats retrieved:', stats) + this.debugLogger?.info?.('Database statistics retrieved:', stats) const result = { ...stats,