From 7d6e6424116a96979e3ee1efe28f03bc22d380e9 Mon Sep 17 00:00:00 2001 From: Stuart Rowlands Date: Mon, 9 Mar 2026 23:38:05 +1000 Subject: [PATCH] Upgrade quant-client to v4.12.0, fix env logs, and extend test suite - Upgrade @quantcdn/quant-client from 4.3.0 to 4.12.0 - Fix env logs returning no data (response uses logEvents, not logs) - Simplify API client config to use Configuration.accessToken - Use machine_name consistently for project/crawler selection - Add autocomplete prompts for crawler project/crawler selection - Add DOM lib to tsconfig for VRT type annotations - Expand explicit params for backup and metrics API calls - Replace ASCII banner with cleaner logo in cyan - Extend API client test suite from 14 to 52 tests with mocked SDK --- __tests__/utils/api.test.ts | 561 ++++++++++++++++++++++++++++++------ package-lock.json | 68 ++--- package.json | 2 +- src/commands/backup.ts | 26 +- src/commands/crawler.ts | 101 +++++-- src/commands/env.ts | 75 +---- src/commands/project.ts | 18 +- src/commands/vrt.ts | 4 +- src/index.ts | 23 +- src/utils/api.ts | 27 +- tsconfig.json | 2 +- 11 files changed, 663 insertions(+), 244 deletions(-) diff --git a/__tests__/utils/api.test.ts b/__tests__/utils/api.test.ts index 50ba4ea..3450542 100644 --- a/__tests__/utils/api.test.ts +++ b/__tests__/utils/api.test.ts @@ -1,129 +1,524 @@ -import { describe, it, expect } from '@jest/globals'; -import { ApiClient } from '../../src/utils/api.js'; +import { describe, it, expect, jest, beforeEach, beforeAll, afterEach } from '@jest/globals'; +import type { ApiClient as ApiClientType } from '../../src/utils/api.js'; + +// Create typed mock functions +const mockListApplications = jest.fn<(...args: any[]) => any>(); +const mockListEnvironments = jest.fn<(...args: any[]) => any>(); +const mockGetEnvironmentMetrics = jest.fn<(...args: any[]) => any>(); +const mockGetEnvironmentLogs = jest.fn<(...args: any[]) => any>(); +const mockGetSshAccessCredentials = jest.fn<(...args: any[]) => any>(); +const mockListBackups = jest.fn<(...args: any[]) => any>(); +const mockProjectsList = jest.fn<(...args: any[]) => any>(); +const mockProjectsRead = jest.fn<(...args: any[]) => any>(); +const mockCrawlersList = jest.fn<(...args: any[]) => any>(); +const mockCrawlersRun = jest.fn<(...args: any[]) => any>(); + +// Mock the SDK module before importing +jest.unstable_mockModule('@quantcdn/quant-client', () => ({ + Configuration: jest.fn(), + ApplicationsApi: jest.fn().mockImplementation(() => ({ + listApplications: mockListApplications, + })), + EnvironmentsApi: jest.fn().mockImplementation(() => ({ + listEnvironments: mockListEnvironments, + getEnvironmentMetrics: mockGetEnvironmentMetrics, + getEnvironmentLogs: mockGetEnvironmentLogs, + })), + SSHAccessApi: jest.fn().mockImplementation(() => ({ + getSshAccessCredentials: mockGetSshAccessCredentials, + })), + BackupManagementApi: jest.fn().mockImplementation(() => ({ + listBackups: mockListBackups, + })), + ProjectsApi: jest.fn().mockImplementation(() => ({ + projectsList: mockProjectsList, + projectsRead: mockProjectsRead, + })), + CrawlersApi: jest.fn().mockImplementation(() => ({ + crawlersList: mockCrawlersList, + crawlersRun: mockCrawlersRun, + })), +})); + +let ApiClient: typeof ApiClientType; + +beforeAll(async () => { + const mod = await import('../../src/utils/api.js'); + ApiClient = mod.ApiClient; +}); describe('ApiClient', () => { - // Note: Full integration tests for ApiClient require mocking the SDK's complex types - // and would be better suited as E2E tests against a test server. - // - // These tests document the expected behavior: + beforeEach(() => { + jest.clearAllMocks(); + }); describe('constructor', () => { - it('should create an ApiClient with configuration', () => { - const client = new ApiClient( - 'https://test.quantcdn.io', - 'test-token', - 'org-123' - ); - + it('should create an ApiClient with required parameters', () => { + const client = new ApiClient('https://api.quantcdn.io', 'test-token'); expect(client).toBeInstanceOf(ApiClient); - expect(client.baseUrl).toBe('https://test.quantcdn.io'); + expect(client.baseUrl).toBe('https://api.quantcdn.io'); + }); + + it('should expose public API instances', () => { + const client = new ApiClient('https://api.quantcdn.io', 'test-token', 'org-1'); expect(client.environmentsApi).toBeDefined(); expect(client.sshAccessApi).toBeDefined(); expect(client.backupManagementApi).toBeDefined(); + expect(client.crawlersApi).toBeDefined(); }); + }); - it('should initialize with all optional parameters', () => { - const client = new ApiClient( - 'https://test.quantcdn.io', - 'test-token', - 'org-123', - 'app-456', - 'env-789' - ); + describe('getApplications', () => { + it('should return applications using default org ID', async () => { + const apps = [{ name: 'app-1' }, { name: 'app-2' }]; + mockListApplications.mockResolvedValue({ data: apps }); - expect(client).toBeInstanceOf(ApiClient); - expect(client.baseUrl).toBe('https://test.quantcdn.io'); + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + const result = await client.getApplications(); + + expect(result).toEqual(apps); + expect(mockListApplications).toHaveBeenCalledWith('org-1'); }); - }); - describe('API configuration', () => { - it('should configure APIs with proper authentication headers', () => { - const client = new ApiClient( - 'https://test.quantcdn.io', - 'test-token', - 'org-123' - ); + it('should use override org ID from apiOptions', async () => { + const apps = [{ name: 'app-1' }]; + mockListApplications.mockResolvedValue({ data: apps }); - // Verifies APIs are instantiated properly - expect(typeof client.environmentsApi.listEnvironments).toBe('function'); - expect(typeof client.sshAccessApi.getSshAccessCredentials).toBe('function'); - expect(typeof client.backupManagementApi.listBackups).toBe('function'); + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + const result = await client.getApplications({ organizationId: 'org-override' }); + + expect(result).toEqual(apps); + expect(mockListApplications).toHaveBeenCalledWith('org-override'); }); - }); - describe('Method signatures', () => { - it('getApplications should accept optional ApiOptions', () => { - const client = new ApiClient( - 'https://test.quantcdn.io', - 'test-token', - 'org-123' - ); + it('should throw when no organization ID available', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getApplications()).rejects.toThrow(/Organization not found/); + }); - // Documents that method exists and accepts options - expect(typeof client.getApplications).toBe('function'); + it('should throw friendly error on 404', async () => { + mockListApplications.mockRejectedValue({ statusCode: 404 }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getApplications()).rejects.toThrow(/not found/); }); - it('getEnvironments should accept optional ApiOptions', () => { - const client = new ApiClient( - 'https://test.quantcdn.io', - 'test-token', - 'org-123', - 'app-456' - ); + it('should throw friendly error on 403', async () => { + mockListApplications.mockRejectedValue({ statusCode: 403 }); - expect(typeof client.getEnvironments).toBe('function'); + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getApplications()).rejects.toThrow(/Access denied/); }); - it('getUserInfo should be available', () => { - const client = new ApiClient( - 'https://test.quantcdn.io', - 'test-token' - ); + it('should throw friendly error on 401', async () => { + mockListApplications.mockRejectedValue({ statusCode: 401 }); - expect(typeof client.getUserInfo).toBe('function'); + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getApplications()).rejects.toThrow(/Authentication expired/); }); - it('getEnvironmentMetrics should accept environment options', () => { - const client = new ApiClient( - 'https://test.quantcdn.io', - 'test-token', - 'org-123', - 'app-456' - ); + it('should throw generic error for other failures', async () => { + mockListApplications.mockRejectedValue(new Error('Network timeout')); - expect(typeof client.getEnvironmentMetrics).toBe('function'); + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getApplications()).rejects.toThrow(/Failed to fetch applications/); }); }); - describe('Error handling', () => { - it('getApplications should throw error when no organization ID', async () => { - const client = new ApiClient('https://test.quantcdn.io', 'test-token'); + describe('getEnvironments', () => { + it('should return environments using default org and app IDs', async () => { + const envs = [{ envName: 'staging' }, { envName: 'production' }]; + mockListEnvironments.mockResolvedValue({ data: envs }); - await expect(client.getApplications()).rejects.toThrow(/Organization not found/); + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + const result = await client.getEnvironments(); + + expect(result).toEqual(envs); + expect(mockListEnvironments).toHaveBeenCalledWith('org-1', 'app-1'); }); - it('getEnvironments should throw error when no organization ID', async () => { - const client = new ApiClient('https://test.quantcdn.io', 'test-token'); + it('should use override IDs from apiOptions', async () => { + const envs = [{ envName: 'staging' }]; + mockListEnvironments.mockResolvedValue({ data: envs }); + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + const result = await client.getEnvironments({ + organizationId: 'org-2', + applicationId: 'app-2', + }); + + expect(result).toEqual(envs); + expect(mockListEnvironments).toHaveBeenCalledWith('org-2', 'app-2'); + }); + + it('should throw when no organization ID available', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token'); await expect(client.getEnvironments()).rejects.toThrow(/Organization not found/); }); - it('getEnvironments should throw error when no application ID', async () => { - const client = new ApiClient( - 'https://test.quantcdn.io', - 'test-token', - 'org-123' + it('should throw when no application ID available', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getEnvironments()).rejects.toThrow(/Application not found/); + }); + + it('should throw friendly error on 404 with application not found', async () => { + mockListEnvironments.mockRejectedValue({ + statusCode: 404, + body: { message: 'Application app-1 not found' }, + }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + await expect(client.getEnvironments()).rejects.toThrow(/Application.*not found/); + }); + + it('should throw friendly error on 404 without application message', async () => { + mockListEnvironments.mockRejectedValue({ statusCode: 404 }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + await expect(client.getEnvironments()).rejects.toThrow(/Organization.*not found/); + }); + + it('should throw friendly error on 403', async () => { + mockListEnvironments.mockRejectedValue({ statusCode: 403 }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + await expect(client.getEnvironments()).rejects.toThrow(/Access denied/); + }); + + it('should throw friendly error on 401', async () => { + mockListEnvironments.mockRejectedValue({ statusCode: 401 }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + await expect(client.getEnvironments()).rejects.toThrow(/Authentication expired/); + }); + }); + + describe('getUserInfo', () => { + let originalFetch: typeof globalThis.fetch; + + beforeEach(() => { + originalFetch = globalThis.fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + it('should fetch user info from OAuth endpoint', async () => { + const userInfo = { name: 'Test User', email: 'test@example.com', organizations: ['org-1'] }; + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(userInfo), + } as Response); + + const client = new ApiClient('https://api.quantcdn.io', 'my-token'); + const result = await client.getUserInfo(); + + expect(result).toEqual(userInfo); + expect(globalThis.fetch).toHaveBeenCalledWith( + 'https://api.quantcdn.io/api/oauth/user', + expect.objectContaining({ + headers: expect.objectContaining({ + 'Authorization': 'Bearer my-token', + }), + }), ); + }); - await expect(client.getEnvironments()).rejects.toThrow(/Application not found/); + it('should throw on 401 response', async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 401, + } as Response); + + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getUserInfo()).rejects.toThrow(/Authentication expired/); + }); + + it('should throw on 403 response', async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 403, + } as Response); + + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getUserInfo()).rejects.toThrow(/Access denied/); }); - it('getEnvironmentMetrics should throw error when missing required IDs', async () => { - const client = new ApiClient('https://test.quantcdn.io', 'test-token'); + it('should throw on other HTTP errors', async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as Response); + + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getUserInfo()).rejects.toThrow(/Failed to get user info.*500/); + }); + it('should throw on network failure', async () => { + globalThis.fetch = jest.fn().mockRejectedValue(new Error('fetch failed')); + + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getUserInfo()).rejects.toThrow(/Failed to get user info.*fetch failed/); + }); + }); + + describe('getEnvironmentMetrics', () => { + it('should fetch metrics with default org and app IDs', async () => { + const metrics = { cpu: 50, memory: 1024 }; + mockGetEnvironmentMetrics.mockResolvedValue({ data: metrics }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + const result = await client.getEnvironmentMetrics({ environmentId: 'env-1' }); + + expect(result).toEqual(metrics); + expect(mockGetEnvironmentMetrics).toHaveBeenCalledWith('org-1', 'app-1', 'env-1'); + }); + + it('should use override IDs', async () => { + const metrics = { cpu: 75 }; + mockGetEnvironmentMetrics.mockResolvedValue({ data: metrics }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + const result = await client.getEnvironmentMetrics({ + organizationId: 'org-2', + applicationId: 'app-2', + environmentId: 'env-2', + }); + + expect(result).toEqual(metrics); + expect(mockGetEnvironmentMetrics).toHaveBeenCalledWith('org-2', 'app-2', 'env-2'); + }); + + it('should throw when missing org or app ID', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token'); await expect( - client.getEnvironmentMetrics({ environmentId: 'env-789' }) + client.getEnvironmentMetrics({ environmentId: 'env-1' }) ).rejects.toThrow(/Organization ID and Application ID are required/); }); + + it('should throw friendly error on 404', async () => { + mockGetEnvironmentMetrics.mockRejectedValue({ statusCode: 404 }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + await expect( + client.getEnvironmentMetrics({ environmentId: 'env-1' }) + ).rejects.toThrow(/not found.*metrics unavailable/); + }); + + it('should throw friendly error on 401', async () => { + mockGetEnvironmentMetrics.mockRejectedValue({ statusCode: 401 }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + await expect( + client.getEnvironmentMetrics({ environmentId: 'env-1' }) + ).rejects.toThrow(/Authentication expired/); + }); + + it('should throw friendly error on 403', async () => { + mockGetEnvironmentMetrics.mockRejectedValue({ statusCode: 403 }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1', 'app-1'); + await expect( + client.getEnvironmentMetrics({ environmentId: 'env-1' }) + ).rejects.toThrow(/Access denied/); + }); + }); + + describe('getProjects', () => { + it('should return projects using default org ID', async () => { + const projects = [{ machine_name: 'proj-1' }, { machine_name: 'proj-2' }]; + mockProjectsList.mockResolvedValue({ data: projects }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + const result = await client.getProjects(); + + expect(result).toEqual(projects); + expect(mockProjectsList).toHaveBeenCalledWith('org-1'); + }); + + it('should use override org ID', async () => { + const projects = [{ machine_name: 'proj-1' }]; + mockProjectsList.mockResolvedValue({ data: projects }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + const result = await client.getProjects({ organizationId: 'org-override' }); + + expect(result).toEqual(projects); + expect(mockProjectsList).toHaveBeenCalledWith('org-override'); + }); + + it('should throw when no organization ID', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getProjects()).rejects.toThrow(/Organization not found/); + }); + + it('should throw friendly error on 404', async () => { + mockProjectsList.mockRejectedValue({ statusCode: 404, response: { status: 404 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getProjects()).rejects.toThrow(/not found/); + }); + + it('should throw friendly error on 403', async () => { + mockProjectsList.mockRejectedValue({ statusCode: 403, response: { status: 403 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getProjects()).rejects.toThrow(/Access denied/); + }); + + it('should throw friendly error on 401', async () => { + mockProjectsList.mockRejectedValue({ statusCode: 401, response: { status: 401 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getProjects()).rejects.toThrow(/Authentication expired/); + }); + }); + + describe('getProjectDetails', () => { + it('should fetch project details with withToken=false', async () => { + const project = { machine_name: 'proj-1', domain: 'example.com' }; + mockProjectsRead.mockResolvedValue({ data: project }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + const result = await client.getProjectDetails('proj-1'); + + expect(result).toEqual(project); + expect(mockProjectsRead).toHaveBeenCalledWith('org-1', 'proj-1', false); + }); + + it('should throw when no organization ID', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getProjectDetails('proj-1')).rejects.toThrow(/Organization not found/); + }); + + it('should throw friendly error on 404', async () => { + mockProjectsRead.mockRejectedValue({ statusCode: 404, response: { status: 404 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getProjectDetails('proj-1')).rejects.toThrow(/not found/); + }); + + it('should throw friendly error on 403', async () => { + mockProjectsRead.mockRejectedValue({ statusCode: 403, response: { status: 403 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getProjectDetails('proj-1')).rejects.toThrow(/Access denied/); + }); + + it('should throw friendly error on 401', async () => { + mockProjectsRead.mockRejectedValue({ statusCode: 401, response: { status: 401 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getProjectDetails('proj-1')).rejects.toThrow(/Authentication expired/); + }); + }); + + describe('getCrawlers', () => { + it('should return crawlers for a project', async () => { + const crawlers = [{ uuid: 'c-1', name: 'Crawler 1' }]; + mockCrawlersList.mockResolvedValue({ data: crawlers }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + const result = await client.getCrawlers('proj-1'); + + expect(result).toEqual(crawlers); + expect(mockCrawlersList).toHaveBeenCalledWith('org-1', 'proj-1'); + }); + + it('should throw when no organization ID', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getCrawlers('proj-1')).rejects.toThrow(/Organization not found/); + }); + + it('should throw friendly error on 404', async () => { + mockCrawlersList.mockRejectedValue({ statusCode: 404, response: { status: 404 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getCrawlers('proj-1')).rejects.toThrow(/not found/); + }); + + it('should throw friendly error on 403', async () => { + mockCrawlersList.mockRejectedValue({ statusCode: 403, response: { status: 403 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getCrawlers('proj-1')).rejects.toThrow(/Access denied/); + }); + + it('should throw friendly error on 401', async () => { + mockCrawlersList.mockRejectedValue({ statusCode: 401, response: { status: 401 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.getCrawlers('proj-1')).rejects.toThrow(/Authentication expired/); + }); + }); + + describe('runCrawler', () => { + it('should run crawler with URLs', async () => { + const response = { status: 'started', jobId: 'job-1' }; + mockCrawlersRun.mockResolvedValue({ data: response }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + const result = await client.runCrawler('proj-1', 'crawler-1', ['https://example.com']); + + expect(result).toEqual(response); + expect(mockCrawlersRun).toHaveBeenCalledWith( + 'org-1', 'proj-1', 'crawler-1', { urls: ['https://example.com'] } + ); + }); + + it('should run crawler without URLs (empty payload)', async () => { + const response = { status: 'started' }; + mockCrawlersRun.mockResolvedValue({ data: response }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + const result = await client.runCrawler('proj-1', 'crawler-1'); + + expect(result).toEqual(response); + expect(mockCrawlersRun).toHaveBeenCalledWith('org-1', 'proj-1', 'crawler-1', {}); + }); + + it('should send empty payload for empty URLs array', async () => { + mockCrawlersRun.mockResolvedValue({ data: {} }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await client.runCrawler('proj-1', 'crawler-1', []); + + expect(mockCrawlersRun).toHaveBeenCalledWith('org-1', 'proj-1', 'crawler-1', {}); + }); + + it('should throw when no organization ID', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.runCrawler('proj-1', 'crawler-1')).rejects.toThrow(/Organization not found/); + }); + + it('should throw friendly error on 404', async () => { + mockCrawlersRun.mockRejectedValue({ statusCode: 404, response: { status: 404 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.runCrawler('proj-1', 'crawler-1')).rejects.toThrow(/not found/); + }); + + it('should throw friendly error on 403', async () => { + mockCrawlersRun.mockRejectedValue({ statusCode: 403, response: { status: 403 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.runCrawler('proj-1', 'crawler-1')).rejects.toThrow(/Access denied/); + }); + + it('should throw friendly error on 401', async () => { + mockCrawlersRun.mockRejectedValue({ statusCode: 401, response: { status: 401 } }); + + const client = new ApiClient('https://api.quantcdn.io', 'token', 'org-1'); + await expect(client.runCrawler('proj-1', 'crawler-1')).rejects.toThrow(/Authentication expired/); + }); + }); + + describe('getOrganizations', () => { + it('should throw not implemented error', async () => { + const client = new ApiClient('https://api.quantcdn.io', 'token'); + await expect(client.getOrganizations()).rejects.toThrow(/not yet implemented/); + }); }); }); diff --git a/package-lock.json b/package-lock.json index 1be57b4..dd7a757 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.4.0", "license": "MIT", "dependencies": { - "@quantcdn/quant-client": "^4.3.0", + "@quantcdn/quant-client": "^4.12.0", "axios": "^1.6.0", "chalk": "^5.3.0", "commander": "^11.1.0", @@ -595,9 +595,9 @@ } }, "node_modules/@emnapi/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz", - "integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", + "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", "dev": true, "license": "MIT", "optional": true, @@ -607,9 +607,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz", - "integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "dev": true, "license": "MIT", "optional": true, @@ -968,9 +968,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -1709,9 +1709,9 @@ } }, "node_modules/@quantcdn/quant-client": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@quantcdn/quant-client/-/quant-client-4.3.0.tgz", - "integrity": "sha512-czSXbL7koO4q/w/7ILnSqauVd4MU7hSXhFzd5HYgHx5riH6NdJftfU1vta/CcKgSeG+P3ixdg0hsba1ULeVKCg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@quantcdn/quant-client/-/quant-client-4.12.0.tgz", + "integrity": "sha512-ttMOFUshWjrsEqEr5nnUKL8obaiVR12aEjtfmCW2JOAafZOgEe0GjDlDwfxzsVq45kRe3l8DBsCg6BGZam1pvQ==", "license": "MIT", "dependencies": { "axios": "^1.6.1" @@ -2087,9 +2087,9 @@ "license": "MIT" }, "node_modules/@types/yargs": { - "version": "17.0.34", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz", - "integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, "license": "MIT", "dependencies": { @@ -2912,9 +2912,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.28", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", - "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", + "version": "2.8.30", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz", + "integrity": "sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3177,9 +3177,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001754", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", - "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", + "version": "1.0.30001756", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", + "integrity": "sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A==", "dev": true, "funding": [ { @@ -3814,9 +3814,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.251", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.251.tgz", - "integrity": "sha512-lmyEOp4G0XT3qrYswNB4np1kx90k6QCXpnSHYv2xEsUuEu8JCobpDVYO6vMseirQyyCC6GCIGGxd5szMBa0tRA==", + "version": "1.5.259", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz", + "integrity": "sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ==", "dev": true, "license": "ISC" }, @@ -4376,9 +4376,9 @@ } }, "node_modules/figlet": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.3.tgz", - "integrity": "sha512-majPgOpVtrZN1iyNGbsUP6bOtZ6eaJgg5HHh0vFvm5DJhh8dc+FJpOC4GABvMZ/A7XHAJUuJujhgUY/2jPWgMA==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.4.tgz", + "integrity": "sha512-uN6QE+TrzTAHC1IWTyrc4FfGo2KH/82J8Jl1tyKB7+z5DBit/m3D++Iu5lg91qJMnQQ3vpJrj5gxcK/pk4R9tQ==", "license": "MIT", "dependencies": { "commander": "^14.0.0" @@ -4626,9 +4626,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4782,9 +4782,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index 1bba672..f53b4e7 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "url": "https://github.com/quantcdn/quant-cloud-cli/issues" }, "dependencies": { - "@quantcdn/quant-client": "^4.3.0", + "@quantcdn/quant-client": "^4.12.0", "axios": "^1.6.0", "chalk": "^5.3.0", "commander": "^11.1.0", diff --git a/src/commands/backup.ts b/src/commands/backup.ts index 2d2a36f..672f3ee 100644 --- a/src/commands/backup.ts +++ b/src/commands/backup.ts @@ -116,7 +116,18 @@ async function handleBackupList(options: BackupListOptions): Promise { try { const backupType = (options.type || 'database') as 'database' | 'filesystem'; - const response = await client.backupManagementApi.listBackups(orgId, appId, envId, backupType); + const response = await client.backupManagementApi.listBackups( + orgId, + appId, + envId, + backupType, + undefined, // order + undefined, // limit + undefined, // createdBefore + undefined, // createdAfter + undefined, // status + undefined, // nextToken + ); const backups = response.data?.backups || []; spinner.succeed(`Found ${backups.length} backups`); @@ -293,7 +304,18 @@ async function handleBackupDownload(backupId: string | undefined, options: Backu try { // First, get the list of backups const backupType = (options.type || 'database') as 'database' | 'filesystem'; - const listResponse = await client.backupManagementApi.listBackups(orgId, appId, envId, backupType); + const listResponse = await client.backupManagementApi.listBackups( + orgId, + appId, + envId, + backupType, + undefined, // order + undefined, // limit + undefined, // createdBefore + undefined, // createdAfter + undefined, // status + undefined, // nextToken + ); const backups = listResponse.data?.backups || []; if (backups.length === 0) { diff --git a/src/commands/crawler.ts b/src/commands/crawler.ts index 959ac81..3fc6ea6 100644 --- a/src/commands/crawler.ts +++ b/src/commands/crawler.ts @@ -1,11 +1,15 @@ import { Command } from 'commander'; import chalk from 'chalk'; import inquirer from 'inquirer'; +import inquirerAutocompletePrompt from 'inquirer-autocomplete-prompt'; import { createSpinner } from '../utils/spinner.js'; import { ApiClient } from '../utils/api.js'; import { Logger } from '../utils/logger.js'; import { getActivePlatformConfig } from '../utils/config.js'; +// Register the autocomplete prompt +inquirer.registerPrompt('autocomplete', inquirerAutocompletePrompt); + const logger = new Logger('Crawler'); export function crawlerCommand(program: Command) { @@ -69,18 +73,37 @@ async function handleCrawlerRun(crawlerId?: string, options?: CrawlerOptions) { } if (projects.length === 1) { - projectName = projects[0].name || projects[0].machine_name; + projectName = projects[0].machine_name; logger.info(`Using project: ${chalk.cyan(projectName)}`); } else { + const projectChoices = projects.map((p: any) => ({ + name: `${p.name || p.machine_name} ${p.url ? chalk.gray(`(${p.url})`) : ''}`, + value: p.machine_name // Always use machine_name for API calls + })); + const { selectedProject } = await inquirer.prompt([ { - type: 'list', + type: 'autocomplete', name: 'selectedProject', - message: 'Select a project:', - choices: projects.map((p: any) => ({ - name: `${p.name || p.machine_name} ${p.url ? chalk.gray(`(${p.url})`) : ''}`, - value: p.name || p.machine_name - })) + message: 'Select a project (type to filter):', + source: async (answersSoFar: any, input: string) => { + if (!input) { + return projectChoices; + } + + // Filter projects based on user input + const filtered = projectChoices.filter((choice: any) => + choice.name.toLowerCase().includes(input.toLowerCase()) || + choice.value.toLowerCase().includes(input.toLowerCase()) + ); + + return filtered.length > 0 ? filtered : [ + { name: chalk.red(`No projects matching "${input}"`), value: null, disabled: true } + ]; + }, + pageSize: 10, + searchText: 'Searching...', + emptyText: 'No projects found' } ]); projectName = selectedProject; @@ -108,15 +131,34 @@ async function handleCrawlerRun(crawlerId?: string, options?: CrawlerOptions) { crawlerId = crawlers[0].uuid || crawlers[0].machine_name; logger.info(`Using crawler: ${chalk.cyan(crawlerId)}`); } else { + const crawlerChoices = crawlers.map((c: any) => ({ + name: `${c.name || c.machine_name} ${c.description ? chalk.gray(`(${c.description})`) : ''}`, + value: c.uuid || c.machine_name + })); + const { selectedCrawler } = await inquirer.prompt([ { - type: 'list', + type: 'autocomplete', name: 'selectedCrawler', - message: 'Select a crawler:', - choices: crawlers.map((c: any) => ({ - name: `${c.name || c.machine_name} ${c.description ? chalk.gray(`(${c.description})`) : ''}`, - value: c.uuid || c.machine_name - })) + message: 'Select a crawler (type to filter):', + source: async (answersSoFar: any, input: string) => { + if (!input) { + return crawlerChoices; + } + + // Filter crawlers based on user input + const filtered = crawlerChoices.filter((choice: any) => + choice.name.toLowerCase().includes(input.toLowerCase()) || + choice.value.toLowerCase().includes(input.toLowerCase()) + ); + + return filtered.length > 0 ? filtered : [ + { name: chalk.red(`No crawlers matching "${input}"`), value: null, disabled: true } + ]; + }, + pageSize: 10, + searchText: 'Searching...', + emptyText: 'No crawlers found' } ]); crawlerId = selectedCrawler; @@ -181,18 +223,37 @@ async function handleCrawlerList(options?: CrawlerOptions) { } if (projects.length === 1) { - projectName = projects[0].name || projects[0].machine_name; + projectName = projects[0].machine_name; logger.info(`Using project: ${chalk.cyan(projectName)}`); } else { + const projectChoices = projects.map((p: any) => ({ + name: `${p.name || p.machine_name} ${p.url ? chalk.gray(`(${p.url})`) : ''}`, + value: p.machine_name // Always use machine_name for API calls + })); + const { selectedProject } = await inquirer.prompt([ { - type: 'list', + type: 'autocomplete', name: 'selectedProject', - message: 'Select a project:', - choices: projects.map((p: any) => ({ - name: `${p.name || p.machine_name} ${p.url ? chalk.gray(`(${p.url})`) : ''}`, - value: p.name || p.machine_name - })) + message: 'Select a project (type to filter):', + source: async (answersSoFar: any, input: string) => { + if (!input) { + return projectChoices; + } + + // Filter projects based on user input + const filtered = projectChoices.filter((choice: any) => + choice.name.toLowerCase().includes(input.toLowerCase()) || + choice.value.toLowerCase().includes(input.toLowerCase()) + ); + + return filtered.length > 0 ? filtered : [ + { name: chalk.red(`No projects matching "${input}"`), value: null, disabled: true } + ]; + }, + pageSize: 10, + searchText: 'Searching...', + emptyText: 'No projects found' } ]); projectName = selectedProject; diff --git a/src/commands/env.ts b/src/commands/env.ts index a6d2976..926be61 100644 --- a/src/commands/env.ts +++ b/src/commands/env.ts @@ -694,46 +694,13 @@ async function fetchLogs(client: ApiClient, organizationId: string, applicationI applicationId, environmentId ); - - // SDK incorrectly types this as void, but it actually returns data - // Cast to any to work around the SDK type issue - const data = response.data as any; - - // The logs might be in different formats, let's handle various possibilities - if (data) { - // If body is already parsed JSON - if (Array.isArray(data)) { - return data; - } - - // If body has a logs property - if (data.logs && Array.isArray(data.logs)) { - return data.logs; - } - - // If body is a single log entry - if (data.message || data.timestamp) { - return [data]; - } - - // Try to parse as JSON string if it's a string - if (typeof data === 'string') { - try { - const parsed = JSON.parse(data); - if (Array.isArray(parsed)) { - return parsed; - } - if (parsed.logs) { - return parsed.logs; - } - return [parsed]; - } catch { - // If parsing fails, treat as plain text logs - return [{ message: data, timestamp: new Date().toISOString() }]; - } - } + + const data = response.data; + + if (data && data.logEvents && Array.isArray(data.logEvents)) { + return data.logEvents; } - + return []; } catch (error: any) { logger.error('API call failed:', error); @@ -742,32 +709,14 @@ async function fetchLogs(client: ApiClient, organizationId: string, applicationI } function displayLog(log: any) { - const timestamp = log.timestamp || log.time || log.created_at || new Date().toISOString(); - const message = log.message || log.msg || log.log || String(log); - const level = log.level || log.severity || 'info'; - - // Format timestamp + const timestamp = log.timestamp || new Date().getTime(); + const message = log.message || String(log); + + // timestamp is Unix ms from CloudWatch const date = new Date(timestamp); const formattedTime = date.toLocaleTimeString(); - - // Color code by log level - let levelColor = chalk.gray; - if (level.toLowerCase().includes('error') || level.toLowerCase().includes('err')) { - levelColor = chalk.red; - } else if (level.toLowerCase().includes('warn')) { - levelColor = chalk.yellow; - } else if (level.toLowerCase().includes('info')) { - levelColor = chalk.blue; - } else if (level.toLowerCase().includes('debug')) { - levelColor = chalk.gray; - } - - // Display formatted log entry - console.log( - chalk.gray(`[${formattedTime}]`) + ' ' + - levelColor(`${level.toUpperCase()}`) + ' ' + - message - ); + + console.log(chalk.gray(`[${formattedTime}]`) + ' ' + message); } async function handleEnvMetrics(envId?: string, options?: EnvMetricsOptions) { diff --git a/src/commands/project.ts b/src/commands/project.ts index 5118005..f73aa59 100644 --- a/src/commands/project.ts +++ b/src/commands/project.ts @@ -133,7 +133,7 @@ async function handleProjectSelect(projectName?: string, options?: ProjectOption // If no project name provided, show interactive selection if (!targetProjectName) { if (projects.length === 1) { - targetProjectName = projects[0].machine_name || projects[0].name; + targetProjectName = projects[0].machine_name; logger.info(`Only one project available: ${chalk.cyan(targetProjectName)}`); } else { const { selectedProjectName } = await inquirer.prompt([ @@ -142,8 +142,8 @@ async function handleProjectSelect(projectName?: string, options?: ProjectOption name: 'selectedProjectName', message: 'Select project:', choices: projects.map((proj: any) => ({ - name: `${chalk.cyan(proj.machine_name || proj.name)} ${proj.name && proj.name !== proj.machine_name ? chalk.gray(`(${proj.name})`) : ''} ${proj.domain ? `- ${chalk.blue(proj.domain)}` : ''}`, - value: proj.machine_name || proj.name + name: `${chalk.cyan(proj.machine_name)} ${proj.name && proj.name !== proj.machine_name ? chalk.gray(`(${proj.name})`) : ''} ${proj.domain ? `- ${chalk.blue(proj.domain)}` : ''}`, + value: proj.machine_name // Always use machine_name for API calls })) } ]); @@ -151,23 +151,23 @@ async function handleProjectSelect(projectName?: string, options?: ProjectOption } } - // Validate the project exists + // Validate the project exists (match by machine_name only) const targetProject = projects.find((p: any) => - p.machine_name === targetProjectName || p.name === targetProjectName + p.machine_name === targetProjectName ); if (!targetProject) { logger.error(`Project '${targetProjectName}' not found.`); logger.info('Available projects:'); projects.forEach((proj: any) => { - logger.info(` ${chalk.cyan(proj.machine_name || proj.name)}`); + logger.info(` ${chalk.cyan(proj.machine_name)}`); }); return; } - // Save the selected project + // Save the selected project (always use machine_name) await saveActivePlatformConfig({ - activeProject: targetProject.machine_name || targetProject.name + activeProject: targetProject.machine_name }); const displayName = targetProject.name || targetProject.machine_name; @@ -206,7 +206,7 @@ async function handleProjectCurrent() { spinner.succeed('Loaded project data'); const activeProject = projects.find((p: any) => - p.machine_name === auth.activeProject || p.name === auth.activeProject + p.machine_name === auth.activeProject ); if (!activeProject) { diff --git a/src/commands/vrt.ts b/src/commands/vrt.ts index 3157e0a..3d13b40 100644 --- a/src/commands/vrt.ts +++ b/src/commands/vrt.ts @@ -335,9 +335,9 @@ async function crawlPages( // Only crawl deeper if we haven't reached max depth if (depth < maxDepth && pages.length < maxPages) { // Find all links on the page - const links = await page.$$eval('a[href]', (anchors, base) => { + const links = await page.$$eval('a[href]', (anchors: HTMLAnchorElement[], base: string) => { return anchors - .map(a => { + .map((a: HTMLAnchorElement) => { try { const href = a.getAttribute('href'); if (!href) return null; diff --git a/src/index.ts b/src/index.ts index 2f42ad7..0681f9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,6 @@ import { program } from 'commander'; import chalk from 'chalk'; -import figlet from 'figlet'; -import gradient from 'gradient-string'; import { loginCommand } from './commands/login.js'; import { logoutCommand } from './commands/logout.js'; import { whoamiCommand } from './commands/whoami.js'; @@ -29,20 +27,21 @@ const version = packageJson.version; // Display full banner with ASCII art function displayBanner() { console.clear(); - const title = figlet.textSync('QUANT CLI', { - font: 'ANSI Shadow', - horizontalLayout: 'default', - verticalLayout: 'default' - }); - - console.log(gradient(['#00ff88', '#0088ff', '#8800ff'])(title)); - console.log(chalk.cyan('Quant Cloud Platform CLI\n')); + const title = +" \n" + +" _ _ _ _ _ \n" + +" ___ _ _ ___ ___| |_ ___| |___ _ _ _| | ___| |_|\n" + +"| . | | | .'| | _| _| | . | | | . | | _| | |\n" + +"|_ |___|__,|_|_|_| |___|_|___|___|___| |___|_|_|\n" + +" |_| "; + + console.log(chalk.cyan(title)); + console.log(); } // Display slim banner for subcommands function displaySlimBanner() { - const title = gradient(['#00ff88', '#0088ff', '#8800ff'])('█ QUANT CLI'); - console.log(title); + console.log(chalk.cyan('quantcloud cli')); } async function displayContext() { diff --git a/src/utils/api.ts b/src/utils/api.ts index d788975..46906fe 100644 --- a/src/utils/api.ts +++ b/src/utils/api.ts @@ -25,23 +25,11 @@ export class ApiClient { private token: string; constructor(baseUrl: string, token: string, defaultOrganizationId?: string, defaultApplicationId?: string, defaultEnvironmentId?: string) { - // Set authentication headers - const defaultHeaders = { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json', - 'Content-Type': 'application/json', - ...(defaultOrganizationId && { 'X-Organization': defaultOrganizationId }), - ...(defaultApplicationId && { 'X-Application': defaultApplicationId }), - ...(defaultEnvironmentId && { 'X-Environment': defaultEnvironmentId }), - }; - - // Configure the APIs with Configuration object + // Configure the APIs using Configuration object (v4.3.0) + // The client automatically handles Authorization header via accessToken const config = new Configuration({ basePath: baseUrl, - accessToken: token, - baseOptions: { - headers: defaultHeaders - } + accessToken: token }); this.applicationsApi = new ApplicationsApi(config); @@ -178,7 +166,12 @@ export class ApiClient { } try { - const response = await this.environmentsApi.getEnvironmentMetrics(organizationId, applicationId, options.environmentId); + // getEnvironmentMetrics has many optional parameters before options + const response = await this.environmentsApi.getEnvironmentMetrics( + organizationId, + applicationId, + options.environmentId + ); return response.data; } catch (error: any) { // Provide friendly error messages instead of debug dumps @@ -253,7 +246,7 @@ export class ApiClient { try { const response = await this.crawlersApi.crawlersList(organizationId, projectName); - return response.data; + return response.data as any; } catch (error: any) { if (error.statusCode === 404 || error.response?.status === 404) { throw new Error(`Project '${projectName}' not found or has no crawlers.`); diff --git a/tsconfig.json b/tsconfig.json index cd5bb84..b1acab5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "ES2020", "module": "ES2020", - "lib": ["ES2020"], + "lib": ["ES2020", "DOM"], "outDir": "./dist", "rootDir": "./src", "strict": true,