Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions packages/atxp/src/commands/paas/analytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock the call-tool module
vi.mock('../../call-tool.js', () => ({
callTool: vi.fn(),
}));

import { callTool } from '../../call-tool.js';
import {
analyticsQueryCommand,
analyticsEventsCommand,
analyticsStatsCommand,
} from './analytics.js';

describe('Analytics Commands', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
});

describe('analyticsQueryCommand', () => {
it('should execute an analytics query', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "data": []}');

await analyticsQueryCommand({ sql: 'SELECT COUNT(*) FROM analytics_data' });

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'query_analytics', {
sql: 'SELECT COUNT(*) FROM analytics_data',
});
});

it('should pass time range option', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "data": []}');

await analyticsQueryCommand({
sql: 'SELECT COUNT(*) FROM analytics_data',
range: '7d',
});

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'query_analytics', {
sql: 'SELECT COUNT(*) FROM analytics_data',
time_range: '7d',
});
});

it('should exit with error when sql is missing', async () => {
await expect(analyticsQueryCommand({})).rejects.toThrow('process.exit called');
expect(console.error).toHaveBeenCalled();
});

it('should exit with error for invalid time range', async () => {
await expect(
analyticsQueryCommand({
sql: 'SELECT * FROM analytics_data',
range: 'invalid',
})
).rejects.toThrow('process.exit called');
expect(console.error).toHaveBeenCalled();
});
});

describe('analyticsEventsCommand', () => {
it('should list analytics events', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "events": []}');

await analyticsEventsCommand({});

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'list_analytics_events', {});
});

it('should filter by event name', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "events": []}');

await analyticsEventsCommand({ event: 'page_view' });

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'list_analytics_events', {
event_name: 'page_view',
});
});

it('should pass limit and time range options', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "events": []}');

await analyticsEventsCommand({
limit: 50,
range: '24h',
});

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'list_analytics_events', {
limit: 50,
time_range: '24h',
});
});

it('should exit with error for invalid time range', async () => {
await expect(analyticsEventsCommand({ range: 'invalid' })).rejects.toThrow(
'process.exit called'
);
expect(console.error).toHaveBeenCalled();
});
});

describe('analyticsStatsCommand', () => {
it('should get analytics stats', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "stats": []}');

await analyticsStatsCommand({});

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_analytics_stats', {});
});

it('should pass group by option', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "stats": []}');

await analyticsStatsCommand({ groupBy: 'hour' });

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_analytics_stats', {
group_by: 'hour',
});
});

it('should pass time range option', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "stats": []}');

await analyticsStatsCommand({ range: '30d' });

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'get_analytics_stats', {
time_range: '30d',
});
});

it('should exit with error for invalid group by value', async () => {
await expect(analyticsStatsCommand({ groupBy: 'invalid' })).rejects.toThrow(
'process.exit called'
);
expect(console.error).toHaveBeenCalled();
});

it('should exit with error for invalid time range', async () => {
await expect(analyticsStatsCommand({ range: 'invalid' })).rejects.toThrow(
'process.exit called'
);
expect(console.error).toHaveBeenCalled();
});
});
});
92 changes: 92 additions & 0 deletions packages/atxp/src/commands/paas/analytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { callTool } from '../../call-tool.js';
import chalk from 'chalk';

const SERVER = 'paas.mcp.atxp.ai';

interface AnalyticsQueryOptions {
sql?: string;
range?: string;
}

interface AnalyticsEventsOptions {
event?: string;
limit?: number;
range?: string;
}

interface AnalyticsStatsOptions {
groupBy?: string;
range?: string;
}

export async function analyticsQueryCommand(options: AnalyticsQueryOptions): Promise<void> {
if (!options.sql) {
console.error(chalk.red('Error: --sql flag is required'));
console.log(`Usage: ${chalk.cyan('npx atxp paas analytics query --sql <query>')}`);
console.log();
console.log('Example queries:');
console.log(' --sql "SELECT blob1 as event, COUNT(*) as count FROM analytics_data GROUP BY blob1"');
console.log(' --sql "SELECT SUM(double1) as total FROM analytics_data"');
process.exit(1);
}

const args: Record<string, unknown> = { sql: options.sql };

if (options.range) {
const validRanges = ['1h', '6h', '24h', '7d', '30d'];
if (!validRanges.includes(options.range)) {
console.error(chalk.red(`Error: Invalid time range. Must be one of: ${validRanges.join(', ')}`));
process.exit(1);
}
args.time_range = options.range;
}

const result = await callTool(SERVER, 'query_analytics', args);
console.log(result);
}

export async function analyticsEventsCommand(options: AnalyticsEventsOptions): Promise<void> {
const args: Record<string, unknown> = {};

if (options.event) {
args.event_name = options.event;
}
if (options.limit !== undefined) {
args.limit = options.limit;
}
if (options.range) {
const validRanges = ['1h', '6h', '24h', '7d', '30d'];
if (!validRanges.includes(options.range)) {
console.error(chalk.red(`Error: Invalid time range. Must be one of: ${validRanges.join(', ')}`));
process.exit(1);
}
args.time_range = options.range;
}

const result = await callTool(SERVER, 'list_analytics_events', args);
console.log(result);
}

export async function analyticsStatsCommand(options: AnalyticsStatsOptions): Promise<void> {
const args: Record<string, unknown> = {};

if (options.groupBy) {
const validGroupBy = ['event_name', 'hour', 'day'];
if (!validGroupBy.includes(options.groupBy)) {
console.error(chalk.red(`Error: Invalid group-by value. Must be one of: ${validGroupBy.join(', ')}`));
process.exit(1);
}
args.group_by = options.groupBy;
}
if (options.range) {
const validRanges = ['1h', '6h', '24h', '7d', '30d'];
if (!validRanges.includes(options.range)) {
console.error(chalk.red(`Error: Invalid time range. Must be one of: ${validRanges.join(', ')}`));
process.exit(1);
}
args.time_range = options.range;
}

const result = await callTool(SERVER, 'get_analytics_stats', args);
console.log(result);
}
102 changes: 102 additions & 0 deletions packages/atxp/src/commands/paas/db.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';

// Mock the call-tool module
vi.mock('../../call-tool.js', () => ({
callTool: vi.fn(),
}));

import { callTool } from '../../call-tool.js';
import {
dbCreateCommand,
dbListCommand,
dbQueryCommand,
dbDeleteCommand,
} from './db.js';

describe('Database Commands', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
});

describe('dbCreateCommand', () => {
it('should create a database', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true}');

await dbCreateCommand('my-database');

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'create_database', {
name: 'my-database',
});
});
});

describe('dbListCommand', () => {
it('should list all databases', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "databases": []}');

await dbListCommand();

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'list_databases', {});
});
});

describe('dbQueryCommand', () => {
it('should execute a SQL query', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "results": []}');

await dbQueryCommand('my-database', { sql: 'SELECT * FROM users' });

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'query', {
database: 'my-database',
sql: 'SELECT * FROM users',
});
});

it('should pass params when provided as JSON', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true, "results": []}');

await dbQueryCommand('my-database', {
sql: 'SELECT * FROM users WHERE id = ?',
params: '["123"]',
});

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'query', {
database: 'my-database',
sql: 'SELECT * FROM users WHERE id = ?',
params: ['123'],
});
});

it('should exit with error when sql flag is missing', async () => {
await expect(dbQueryCommand('my-database', {})).rejects.toThrow('process.exit called');
expect(console.error).toHaveBeenCalled();
});

it('should exit with error when params is invalid JSON', async () => {
await expect(
dbQueryCommand('my-database', {
sql: 'SELECT * FROM users',
params: 'invalid-json',
})
).rejects.toThrow('process.exit called');
expect(console.error).toHaveBeenCalled();
});
});

describe('dbDeleteCommand', () => {
it('should delete a database', async () => {
vi.mocked(callTool).mockResolvedValue('{"success": true}');

await dbDeleteCommand('my-database');

expect(callTool).toHaveBeenCalledWith('paas.mcp.atxp.ai', 'delete_database', {
name: 'my-database',
});
});
});
});
Loading
Loading