fix all tests and stuffc

This commit is contained in:
dal 2025-08-26 12:22:31 -06:00
parent 93e80f5698
commit 40a612f10a
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
28 changed files with 494 additions and 421 deletions

View File

@ -1,27 +1,30 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createProxiedResponse } from './electricHandler';
// Mock @buster/secrets
vi.mock('@buster/secrets', () => ({
getSecret: vi.fn(),
ELECTRIC_KEYS: {
ELECTRIC_PROXY_URL: 'ELECTRIC_PROXY_URL',
ELECTRIC_SECRET: 'ELECTRIC_SECRET',
ELECTRIC_SOURCE_ID: 'ELECTRIC_SOURCE_ID',
},
}));
import { getSecret } from '@buster/secrets';
describe('createProxiedResponse', () => {
const mockFetch = vi.fn<typeof fetch>();
let originalEnv: string | undefined;
beforeEach(() => {
vi.stubGlobal('fetch', mockFetch);
vi.clearAllMocks();
// Store original environment variable
originalEnv = process.env.ELECTRIC_SECRET;
// Set default secret for tests
process.env.ELECTRIC_SECRET = 'test-secret';
// Default mock for getSecret - can be overridden in individual tests
vi.mocked(getSecret).mockResolvedValue('test-secret');
});
afterEach(() => {
vi.restoreAllMocks();
// Restore original environment variable
if (originalEnv !== undefined) {
process.env.ELECTRIC_SECRET = originalEnv;
} else {
process.env.ELECTRIC_SECRET = undefined;
}
});
it('should proxy a successful response and remove content-encoding and content-length headers', async () => {
@ -213,8 +216,8 @@ describe('createProxiedResponse', () => {
});
it('should throw error when ELECTRIC_SECRET environment variable is not set', async () => {
// Remove the environment variable
process.env.ELECTRIC_SECRET = '';
// Mock getSecret to return empty string for ELECTRIC_SECRET
vi.mocked(getSecret).mockResolvedValue('');
const testUrl = new URL('https://example.com/test');
@ -226,7 +229,7 @@ describe('createProxiedResponse', () => {
it('should add secret key to URL params when ELECTRIC_SECRET is set', async () => {
const secretKey = 'test-secret-key-123';
process.env.ELECTRIC_SECRET = secretKey;
vi.mocked(getSecret).mockResolvedValue(secretKey);
const testUrl = new URL('https://example.com/test');
const mockResponseBody = 'test response';

View File

@ -1,33 +1,31 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { extractParamFromWhere, getElectricShapeUrl } from '.';
// Mock @buster/secrets
vi.mock('@buster/secrets', () => ({
getSecret: vi.fn(),
ELECTRIC_KEYS: {
ELECTRIC_PROXY_URL: 'ELECTRIC_PROXY_URL',
ELECTRIC_SECRET: 'ELECTRIC_SECRET',
ELECTRIC_SOURCE_ID: 'ELECTRIC_SOURCE_ID',
},
}));
import { getSecret } from '@buster/secrets';
describe('getElectricShapeUrl', () => {
let originalElectricUrl: string | undefined;
let originalSourceId: string | undefined;
beforeEach(() => {
// Save original environment variables
originalElectricUrl = process.env.ELECTRIC_PROXY_URL;
originalSourceId = process.env.ELECTRIC_SOURCE_ID;
// Set default test values
process.env.ELECTRIC_PROXY_URL = 'http://localhost:3000';
process.env.ELECTRIC_SOURCE_ID = '';
vi.clearAllMocks();
// Default mock for getSecret
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'ELECTRIC_PROXY_URL') return 'http://localhost:3000';
if (key === 'ELECTRIC_SOURCE_ID') return '';
throw new Error(`Secret not found: ${key}`);
});
});
afterEach(() => {
// Restore original environment variables
if (originalElectricUrl !== undefined) {
process.env.ELECTRIC_PROXY_URL = originalElectricUrl;
} else {
delete process.env.ELECTRIC_PROXY_URL;
}
if (originalSourceId !== undefined) {
process.env.ELECTRIC_SOURCE_ID = originalSourceId;
} else {
delete process.env.ELECTRIC_SOURCE_ID;
}
vi.restoreAllMocks();
});
it('should return default URL with /v1/shape path when no ELECTRIC_PROXY_URL is set', async () => {
@ -38,7 +36,11 @@ describe('getElectricShapeUrl', () => {
});
it('should use ELECTRIC_PROXY_URL environment variable when set', async () => {
process.env.ELECTRIC_PROXY_URL = 'https://electric.example.com';
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'ELECTRIC_PROXY_URL') return 'https://electric.example.com';
if (key === 'ELECTRIC_SOURCE_ID') return '';
throw new Error(`Secret not found: ${key}`);
});
const requestUrl = 'http://example.com/test?table=users&live=true';
const result = await getElectricShapeUrl(requestUrl);
@ -169,7 +171,11 @@ describe('getElectricShapeUrl', () => {
});
it('should handle ELECTRIC_PROXY_URL with trailing slash', async () => {
process.env.ELECTRIC_PROXY_URL = 'https://electric.example.com/';
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'ELECTRIC_PROXY_URL') return 'https://electric.example.com/';
if (key === 'ELECTRIC_SOURCE_ID') return '';
throw new Error(`Secret not found: ${key}`);
});
const requestUrl = 'http://example.com/test?table=users';
const result = await getElectricShapeUrl(requestUrl);
@ -177,7 +183,11 @@ describe('getElectricShapeUrl', () => {
});
it('should handle ELECTRIC_PROXY_URL with path', async () => {
process.env.ELECTRIC_PROXY_URL = 'https://api.example.com/electric';
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'ELECTRIC_PROXY_URL') return 'https://api.example.com/electric';
if (key === 'ELECTRIC_SOURCE_ID') return '';
throw new Error(`Secret not found: ${key}`);
});
const requestUrl = 'http://example.com/test?table=users';
const result = await getElectricShapeUrl(requestUrl);

View File

@ -2,144 +2,180 @@ import { App } from 'octokit';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { createGitHubApp, getGitHubAppCredentials } from './github-app';
// Mock @buster/secrets
vi.mock('@buster/secrets', () => ({
getSecret: vi.fn(),
GITHUB_KEYS: {
GITHUB_APP_ID: 'GITHUB_APP_ID',
GITHUB_APP_NAME: 'GITHUB_APP_NAME',
GITHUB_APP_PRIVATE_KEY_BASE64: 'GITHUB_APP_PRIVATE_KEY_BASE64',
GITHUB_APP_PRIVATE_KEY_BASE: 'GITHUB_APP_PRIVATE_KEY_BASE',
GITHUB_WEBHOOK_SECRET: 'GITHUB_WEBHOOK_SECRET',
GITHUB_TOKEN: 'GITHUB_TOKEN',
},
}));
// Mock the octokit module
vi.mock('octokit', () => ({
App: vi.fn(),
}));
describe('github-app', () => {
const originalEnv = process.env;
import { getSecret } from '@buster/secrets';
describe('github-app', () => {
beforeEach(() => {
// Reset environment variables
process.env = { ...originalEnv };
vi.clearAllMocks();
});
afterEach(() => {
// Restore original environment
process.env = originalEnv;
vi.restoreAllMocks();
});
describe('getGitHubAppCredentials', () => {
it('should return credentials when all environment variables are set', () => {
it('should return credentials when all environment variables are set', async () => {
// Arrange
process.env.GITHUB_APP_ID = '123456';
process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = Buffer.from(
'-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----'
).toString('base64');
process.env.GITHUB_WEBHOOK_SECRET = 'webhook-secret';
const privateKey = '-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----';
const privateKeyBase64 = Buffer.from(privateKey).toString('base64');
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '123456';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return privateKeyBase64;
if (key === 'GITHUB_WEBHOOK_SECRET') return 'webhook-secret';
throw new Error(`Unexpected key: ${key}`);
});
// Act
const credentials = getGitHubAppCredentials();
const credentials = await getGitHubAppCredentials();
// Assert
expect(credentials).toEqual({
appId: 123456,
privateKey: '-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----',
privateKey: privateKey,
webhookSecret: 'webhook-secret',
});
});
it('should throw error when GITHUB_APP_ID is missing', () => {
it('should throw error when GITHUB_APP_ID is missing', async () => {
// Arrange
delete process.env.GITHUB_APP_ID;
process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = 'test';
process.env.GITHUB_WEBHOOK_SECRET = 'test';
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return 'test';
if (key === 'GITHUB_WEBHOOK_SECRET') return 'test';
throw new Error(`Unexpected key: ${key}`);
});
// Act & Assert
expect(() => getGitHubAppCredentials()).toThrow(
await expect(getGitHubAppCredentials()).rejects.toThrow(
'GITHUB_APP_ID environment variable is not set'
);
});
it('should throw error when GITHUB_APP_PRIVATE_KEY_BASE64 is missing', () => {
it('should throw error when GITHUB_APP_PRIVATE_KEY_BASE64 is missing', async () => {
// Arrange
process.env.GITHUB_APP_ID = '123456';
delete process.env.GITHUB_APP_PRIVATE_KEY_BASE64;
process.env.GITHUB_WEBHOOK_SECRET = 'test';
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '123456';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return '';
if (key === 'GITHUB_WEBHOOK_SECRET') return 'test';
throw new Error(`Unexpected key: ${key}`);
});
// Act & Assert
expect(() => getGitHubAppCredentials()).toThrow(
await expect(getGitHubAppCredentials()).rejects.toThrow(
'GITHUB_APP_PRIVATE_KEY_BASE64 environment variable is not set'
);
});
it('should throw error when GITHUB_WEBHOOK_SECRET is missing', () => {
it('should throw error when GITHUB_WEBHOOK_SECRET is missing', async () => {
// Arrange
process.env.GITHUB_APP_ID = '123456';
process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = 'test';
delete process.env.GITHUB_WEBHOOK_SECRET;
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '123456';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return 'test';
if (key === 'GITHUB_WEBHOOK_SECRET') return '';
throw new Error(`Unexpected key: ${key}`);
});
// Act & Assert
expect(() => getGitHubAppCredentials()).toThrow(
await expect(getGitHubAppCredentials()).rejects.toThrow(
'GITHUB_WEBHOOK_SECRET environment variable is not set'
);
});
it('should throw error when private key base64 is invalid', () => {
it('should throw error when private key base64 is invalid', async () => {
// Arrange
process.env.GITHUB_APP_ID = '123456';
process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = 'not-valid-base64!@#$%';
process.env.GITHUB_WEBHOOK_SECRET = 'test';
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '123456';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return 'not-valid-base64!@#$%';
if (key === 'GITHUB_WEBHOOK_SECRET') return 'test';
throw new Error(`Unexpected key: ${key}`);
});
// Act & Assert
expect(() => getGitHubAppCredentials()).toThrow(
await expect(getGitHubAppCredentials()).rejects.toThrow(
'Failed to decode GITHUB_APP_PRIVATE_KEY_BASE64: Invalid base64 encoding'
);
});
it('should throw error when private key format is invalid', () => {
it('should throw error when private key format is invalid', async () => {
// Arrange
process.env.GITHUB_APP_ID = '123456';
process.env.GITHUB_APP_PRIVATE_KEY_BASE64 =
Buffer.from('not-a-private-key').toString('base64');
process.env.GITHUB_WEBHOOK_SECRET = 'test';
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '123456';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return Buffer.from('not-a-private-key').toString('base64');
if (key === 'GITHUB_WEBHOOK_SECRET') return 'test';
throw new Error(`Unexpected key: ${key}`);
});
// Act & Assert
expect(() => getGitHubAppCredentials()).toThrow(
await expect(getGitHubAppCredentials()).rejects.toThrow(
'Invalid GitHub App private key format. Expected PEM-encoded RSA private key or PKCS#8 private key'
);
});
});
describe('createGitHubApp', () => {
it('should create GitHub App with valid credentials', () => {
it('should create GitHub App with valid credentials', async () => {
// Arrange
process.env.GITHUB_APP_ID = '123456';
process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = Buffer.from(
'-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----'
).toString('base64');
process.env.GITHUB_WEBHOOK_SECRET = 'webhook-secret';
const privateKey = '-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----';
const privateKeyBase64 = Buffer.from(privateKey).toString('base64');
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '123456';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return privateKeyBase64;
if (key === 'GITHUB_WEBHOOK_SECRET') return 'webhook-secret';
throw new Error(`Unexpected key: ${key}`);
});
const mockApp = { octokit: {} };
(App as any).mockImplementation(() => mockApp);
// Act
const app = createGitHubApp();
const app = await createGitHubApp();
// Assert
expect(App).toHaveBeenCalledWith({
appId: 123456,
privateKey: '-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----',
privateKey: privateKey,
});
expect(app).toBe(mockApp);
});
it('should throw error when App creation fails', () => {
it('should throw error when App creation fails', async () => {
// Arrange
process.env.GITHUB_APP_ID = '123456';
process.env.GITHUB_APP_PRIVATE_KEY_BASE64 = Buffer.from(
'-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----'
).toString('base64');
process.env.GITHUB_WEBHOOK_SECRET = 'webhook-secret';
const privateKey = '-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----';
const privateKeyBase64 = Buffer.from(privateKey).toString('base64');
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '123456';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return privateKeyBase64;
if (key === 'GITHUB_WEBHOOK_SECRET') return 'webhook-secret';
throw new Error(`Unexpected key: ${key}`);
});
(App as any).mockImplementation(() => {
throw new Error('Failed to create app');
});
// Act & Assert
expect(() => createGitHubApp()).toThrow('Failed to create GitHub App: Failed to create app');
await expect(createGitHubApp()).rejects.toThrow('Failed to create GitHub App: Failed to create app');
});
});
});

View File

@ -7,93 +7,106 @@ import {
verifyGitHubWebhookSignature,
} from './verify-webhook-signature';
describe('verify-webhook-signature', () => {
const originalEnv = process.env;
// Mock @buster/secrets
vi.mock('@buster/secrets', () => ({
getSecret: vi.fn(),
GITHUB_KEYS: {
GITHUB_APP_ID: 'GITHUB_APP_ID',
GITHUB_APP_NAME: 'GITHUB_APP_NAME',
GITHUB_APP_PRIVATE_KEY_BASE64: 'GITHUB_APP_PRIVATE_KEY_BASE64',
GITHUB_APP_PRIVATE_KEY_BASE: 'GITHUB_APP_PRIVATE_KEY_BASE',
GITHUB_WEBHOOK_SECRET: 'GITHUB_WEBHOOK_SECRET',
GITHUB_TOKEN: 'GITHUB_TOKEN',
},
}));
import { getSecret } from '@buster/secrets';
describe('verify-webhook-signature', () => {
beforeEach(() => {
// Set up test environment
process.env = {
...originalEnv,
GITHUB_APP_ID: '123456',
GITHUB_APP_PRIVATE_KEY_BASE64: Buffer.from(
'-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----'
).toString('base64'),
GITHUB_WEBHOOK_SECRET: 'test-webhook-secret',
};
vi.clearAllMocks();
// Default mock for getSecret
vi.mocked(getSecret).mockImplementation(async (key: string) => {
if (key === 'GITHUB_APP_ID') return '123456';
if (key === 'GITHUB_APP_PRIVATE_KEY_BASE64') return Buffer.from(
'-----BEGIN RSA PRIVATE KEY-----\ntest-key\n-----END RSA PRIVATE KEY-----'
).toString('base64');
if (key === 'GITHUB_WEBHOOK_SECRET') return 'test-webhook-secret';
throw new Error(`Unexpected key: ${key}`);
});
});
afterEach(() => {
process.env = originalEnv;
vi.restoreAllMocks();
});
describe('verifyGitHubWebhookSignature', () => {
it('should return true for valid signature', () => {
it('should return true for valid signature', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
const signature = `sha256=${createHmac('sha256', 'test-webhook-secret').update(payload).digest('hex')}`;
// Act
const result = verifyGitHubWebhookSignature(payload, signature);
const result = await verifyGitHubWebhookSignature(payload, signature);
// Assert
expect(result).toBe(true);
});
it('should return false for invalid signature', () => {
it('should return false for invalid signature', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
const signature = 'sha256=invalid-signature';
// Act
const result = verifyGitHubWebhookSignature(payload, signature);
const result = await verifyGitHubWebhookSignature(payload, signature);
// Assert
expect(result).toBe(false);
});
it('should return false when signature is missing', () => {
it('should return false when signature is missing', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
// Act
const result = verifyGitHubWebhookSignature(payload, undefined);
const result = await verifyGitHubWebhookSignature(payload, undefined);
// Assert
expect(result).toBe(false);
});
it('should return false when signature format is invalid', () => {
it('should return false when signature format is invalid', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
const signature = 'invalid-format-signature';
// Act
const result = verifyGitHubWebhookSignature(payload, signature);
const result = await verifyGitHubWebhookSignature(payload, signature);
// Assert
expect(result).toBe(false);
});
it('should return false when signature has wrong algorithm', () => {
it('should return false when signature has wrong algorithm', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
const signature = `sha1=${createHmac('sha1', 'test-webhook-secret').update(payload).digest('hex')}`;
// Act
const result = verifyGitHubWebhookSignature(payload, signature);
const result = await verifyGitHubWebhookSignature(payload, signature);
// Assert
expect(result).toBe(false);
});
it('should handle different payload types', () => {
it('should handle different payload types', async () => {
// Arrange
const payload = 'plain-text-payload';
const signature = `sha256=${createHmac('sha256', 'test-webhook-secret').update(payload).digest('hex')}`;
// Act
const result = verifyGitHubWebhookSignature(payload, signature);
const result = await verifyGitHubWebhookSignature(payload, signature);
// Assert
expect(result).toBe(true);
@ -156,7 +169,7 @@ describe('verify-webhook-signature', () => {
});
describe('verifyGitHubWebhook', () => {
it('should not throw for valid webhook', () => {
it('should not throw for valid webhook', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
const signature = `sha256=${createHmac('sha256', 'test-webhook-secret').update(payload).digest('hex')}`;
@ -165,21 +178,21 @@ describe('verify-webhook-signature', () => {
};
// Act & Assert
expect(() => verifyGitHubWebhook(payload, headers)).not.toThrow();
await expect(verifyGitHubWebhook(payload, headers)).resolves.not.toThrow();
});
it('should throw when signature is missing', () => {
it('should throw when signature is missing', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
const headers = {};
// Act & Assert
expect(() => verifyGitHubWebhook(payload, headers)).toThrow(
await expect(verifyGitHubWebhook(payload, headers)).rejects.toThrow(
'Missing X-Hub-Signature-256 header'
);
});
it('should throw when signature is invalid', () => {
it('should throw when signature is invalid', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
const headers = {
@ -187,10 +200,10 @@ describe('verify-webhook-signature', () => {
};
// Act & Assert
expect(() => verifyGitHubWebhook(payload, headers)).toThrow('Invalid webhook signature');
await expect(verifyGitHubWebhook(payload, headers)).rejects.toThrow('Invalid webhook signature');
});
it('should include error code in thrown error', () => {
it('should include error code in thrown error', async () => {
// Arrange
const payload = JSON.stringify({ test: 'data' });
const headers = {
@ -200,7 +213,7 @@ describe('verify-webhook-signature', () => {
// Act
let error: any;
try {
verifyGitHubWebhook(payload, headers);
await verifyGitHubWebhook(payload, headers);
} catch (e) {
error = e;
}

View File

@ -4,6 +4,52 @@ import { HTTPException } from 'hono/http-exception';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SlackHandler } from './handler';
// Mock @buster/secrets
vi.mock('@buster/secrets', () => ({
getSecret: vi.fn().mockImplementation(async (key: string) => {
const secrets: Record<string, string> = {
BUSTER_URL: 'https://test.com',
SLACK_CLIENT_ID: 'test-client-id',
SLACK_CLIENT_SECRET: 'test-client-secret',
SERVER_URL: 'https://test.com',
};
return secrets[key] || '';
}),
SERVER_KEYS: {
DATABASE_URL: 'DATABASE_URL',
SUPABASE_URL: 'SUPABASE_URL',
SUPABASE_SERVICE_ROLE_KEY: 'SUPABASE_SERVICE_ROLE_KEY',
ELECTRIC_PROXY_URL: 'ELECTRIC_PROXY_URL',
ELECTRIC_SOURCE_ID: 'ELECTRIC_SOURCE_ID',
ELECTRIC_SECRET: 'ELECTRIC_SECRET',
TRIGGER_SECRET_KEY: 'TRIGGER_SECRET_KEY',
SLACK_CLIENT_ID: 'SLACK_CLIENT_ID',
SLACK_CLIENT_SECRET: 'SLACK_CLIENT_SECRET',
SLACK_SIGNING_SECRET: 'SLACK_SIGNING_SECRET',
SLACK_APP_SUPPORT_URL: 'SLACK_APP_SUPPORT_URL',
SERVER_PORT: 'SERVER_PORT',
SERVER_URL: 'SERVER_URL',
BUSTER_URL: 'BUSTER_URL',
ENVIRONMENT: 'ENVIRONMENT',
},
SLACK_KEYS: {
SLACK_CLIENT_ID: 'SLACK_CLIENT_ID',
SLACK_CLIENT_SECRET: 'SLACK_CLIENT_SECRET',
SLACK_SIGNING_SECRET: 'SLACK_SIGNING_SECRET',
SLACK_REDIRECT_URI: 'SLACK_REDIRECT_URI',
SLACK_BOT_TOKEN: 'SLACK_BOT_TOKEN',
SLACK_CHANNEL_ID: 'SLACK_CHANNEL_ID',
SLACK_TEST_JOIN_CHANNEL_ID: 'SLACK_TEST_JOIN_CHANNEL_ID',
SLACK_TEST_LEAVE_CHANNEL_ID: 'SLACK_TEST_LEAVE_CHANNEL_ID',
SLACK_TEST_ACCESS_TOKEN: 'SLACK_TEST_ACCESS_TOKEN',
SLACK_SKIP_DELETE_TESTS: 'SLACK_SKIP_DELETE_TESTS',
SLACK_SKIP_LEAVE_TESTS: 'SLACK_SKIP_LEAVE_TESTS',
SLACK_APP_SUPPORT_URL: 'SLACK_APP_SUPPORT_URL',
BUSTER_ALERT_CHANNEL_TOKEN: 'BUSTER_ALERT_CHANNEL_TOKEN',
BUSTER_ALERT_CHANNEL_ID: 'BUSTER_ALERT_CHANNEL_ID',
},
}));
// Mock dependencies
const mockSlackOAuthService = {
isEnabled: vi.fn(),
@ -136,7 +182,7 @@ describe('SlackHandler', () => {
await handler.handleOAuthCallback(mockContext);
expect(mockContext.redirect).toHaveBeenCalledWith(
'/app/settings/integrations?status=cancelled'
'https://test.com/app/settings/integrations?status=cancelled'
);
});
@ -146,7 +192,7 @@ describe('SlackHandler', () => {
await handler.handleOAuthCallback(mockContext);
expect(mockContext.redirect).toHaveBeenCalledWith(
'/app/settings/integrations?status=error&error=invalid_parameters'
'https://test.com/app/settings/integrations?status=error&error=invalid_parameters'
);
});
@ -171,7 +217,7 @@ describe('SlackHandler', () => {
state: 'test-state',
});
expect(mockContext.redirect).toHaveBeenCalledWith(
'/dashboard?status=success&workspace=Test%20Workspace'
'https://test.com/dashboard?status=success&workspace=Test%20Workspace'
);
});
@ -191,7 +237,7 @@ describe('SlackHandler', () => {
await handler.handleOAuthCallback(mockContext);
expect(mockContext.redirect).toHaveBeenCalledWith(
'/app/settings/integrations?status=error&error=invalid_state'
'https://test.com/app/settings/integrations?status=error&error=invalid_state'
);
});
});

View File

@ -3,6 +3,19 @@ import { HTTPException } from 'hono/http-exception';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { githubWebhookValidator } from './github-webhook-validator';
// Mock @buster/secrets
vi.mock('@buster/secrets', () => ({
getSecret: vi.fn(),
GITHUB_KEYS: {
GITHUB_APP_ID: 'GITHUB_APP_ID',
GITHUB_APP_NAME: 'GITHUB_APP_NAME',
GITHUB_APP_PRIVATE_KEY_BASE64: 'GITHUB_APP_PRIVATE_KEY_BASE64',
GITHUB_APP_PRIVATE_KEY_BASE: 'GITHUB_APP_PRIVATE_KEY_BASE',
GITHUB_WEBHOOK_SECRET: 'GITHUB_WEBHOOK_SECRET',
GITHUB_TOKEN: 'GITHUB_TOKEN',
},
}));
// Mock the verify webhook signature from github package
vi.mock('@buster/github', () => ({
verifyGitHubWebhookSignature: vi.fn(),
@ -12,10 +25,13 @@ vi.mock('@buster/github', () => ({
}));
import { InstallationCallbackSchema, verifyGitHubWebhookSignature } from '@buster/github';
import { getSecret } from '@buster/secrets';
describe('githubWebhookValidator', () => {
beforeEach(() => {
vi.clearAllMocks();
// Default mock for getSecret - can be overridden in individual tests
vi.mocked(getSecret).mockResolvedValue('test-webhook-secret');
});
const mockPayload = {
action: 'created' as const,
@ -51,8 +67,6 @@ describe('githubWebhookValidator', () => {
it('should validate a valid webhook request', async () => {
const { app, headers, body } = createMockContext('sha256=test-signature');
// Mock environment variable
process.env.GITHUB_WEBHOOK_SECRET = 'test-secret';
// Mock signature verification to return true
vi.mocked(verifyGitHubWebhookSignature).mockReturnValue(true);
@ -109,8 +123,8 @@ describe('githubWebhookValidator', () => {
it('should reject request when webhook secret is not configured', async () => {
const { app, headers, body } = createMockContext('sha256=test-signature');
// Remove environment variable
delete process.env.GITHUB_WEBHOOK_SECRET;
// Mock getSecret to reject for webhook secret
vi.mocked(getSecret).mockRejectedValue(new Error('Secret not found'));
const res = await app.request('/', {
method: 'POST',

View File

@ -1,6 +1,24 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Mock } from 'vitest';
// Mock @buster/secrets
vi.mock('@buster/secrets', () => ({
getSecret: vi.fn().mockImplementation(async (key: string) => {
// Return process.env values to allow tests to control them
return process.env[key];
}),
SHARED_KEYS: {
NODE_ENV: 'NODE_ENV',
ENVIRONMENT: 'ENVIRONMENT',
DATABASE_URL: 'DATABASE_URL',
SUPABASE_URL: 'SUPABASE_URL',
SUPABASE_SERVICE_ROLE_KEY: 'SUPABASE_SERVICE_ROLE_KEY',
SUPABASE_ANON_KEY: 'SUPABASE_ANON_KEY',
LOG_LEVEL: 'LOG_LEVEL',
CI: 'CI',
},
}));
describe('logger middleware', () => {
let originalEnv: NodeJS.ProcessEnv;
let originalConsole: {

View File

@ -5,7 +5,6 @@ import { AnalystAgentTaskInputSchema, type AnalystAgentTaskOutput } from './type
// Task 2 & 4: Database helpers (IMPLEMENTED)
import {
dbInitialized,
getBraintrustMetadata,
getChatConversationHistory,
getMessageContext,
@ -240,9 +239,6 @@ export const analystAgentTask: ReturnType<
const taskStartTime = Date.now();
const resourceTracker = new ResourceTracker();
// Ensure database is initialized before any queries
await dbInitialized;
// Log initial performance metrics
logPerformanceMetrics('task-start', payload.message_id, taskStartTime, resourceTracker);

View File

@ -2,7 +2,7 @@ import { randomBytes } from 'node:crypto';
import { type AssetPermissionCheck, checkPermission } from '@buster/access-controls';
import { createAdapter, getProviderForOrganization } from '@buster/data-source';
import type { Credentials } from '@buster/data-source';
import { dbInitialized, getDataSourceCredentials, getMetricForExport } from '@buster/database';
import { getDataSourceCredentials, getMetricForExport } from '@buster/database';
import { logger, schemaTask } from '@trigger.dev/sdk';
import { convertToCSV, estimateCSVSize } from './csv-helpers';
import {
@ -48,9 +48,6 @@ export const exportMetricData: ReturnType<
run: async (payload): Promise<ExportMetricDataOutput> => {
const startTime = Date.now();
// Ensure database is initialized before any queries
await dbInitialized;
try {
logger.log('Starting metric export', {
metricId: payload.metricId,

View File

@ -10,7 +10,15 @@ import {
// Mock the database module
vi.mock('@buster/database', () => ({
getDb: vi.fn(),
db: {
select: vi.fn(),
from: vi.fn(),
innerJoin: vi.fn(),
leftJoin: vi.fn(),
where: vi.fn(),
orderBy: vi.fn(),
limit: vi.fn(),
},
and: vi.fn((...args) => ({ type: 'and', args })),
eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
lt: vi.fn((a, b) => ({ type: 'lt', a, b })),
@ -27,7 +35,8 @@ vi.mock('@buster/database', () => ({
rawLlmMessages: 'messages.rawLlmMessages',
},
chats: { id: 'chats.id', organizationId: 'chats.organizationId' },
users: { id: 'users.id', name: 'users.name' },
users: { id: 'users.id', name: 'users.name', email: 'users.email' },
getChatConversationHistory: vi.fn(),
}));
// Mock access controls
@ -40,24 +49,20 @@ describe('message-fetchers', () => {
beforeEach(() => {
vi.clearAllMocks();
mockDb = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
innerJoin: vi.fn().mockReturnThis(),
leftJoin: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn(),
limit: vi.fn(),
};
// Get the mocked db object from the module mock
mockDb = vi.mocked(database.db);
// Set up the mock chain to return itself for most methods
mockDb.select.mockReturnValue(mockDb);
mockDb.from.mockReturnValue(mockDb);
mockDb.innerJoin.mockReturnValue(mockDb);
mockDb.leftJoin.mockReturnValue(mockDb);
mockDb.where.mockReturnValue(mockDb);
vi.mocked(database.getDb).mockReturnValue(mockDb);
mockDb.orderBy.mockReturnValue(mockDb);
// Also mock getChatConversationHistory
vi.mocked(database.getChatConversationHistory).mockResolvedValue([]);
});
describe('fetchMessageWithContext', () => {
@ -69,10 +74,16 @@ describe('message-fetchers', () => {
createdAt: '2024-01-01T00:00:00Z',
rawLlmMessages: [{ role: 'user', content: 'Test message' }],
userName: 'John Doe',
userEmail: 'john@example.com',
organizationId: '423e4567-e89b-12d3-a456-426614174000',
};
mockDb.limit.mockResolvedValue([messageData]);
// Mock getChatConversationHistory to return the expected messages
vi.mocked(database.getChatConversationHistory).mockResolvedValue(
messageData.rawLlmMessages as any
);
const result = await fetchMessageWithContext(messageData.id);
@ -107,10 +118,16 @@ describe('message-fetchers', () => {
createdAt: '2024-01-01T00:00:00Z',
rawLlmMessages: [{ role: 'user', content: 'Test' }],
userName: null,
userEmail: null,
organizationId: '423e4567-e89b-12d3-a456-426614174000',
};
mockDb.limit.mockResolvedValue([messageData]);
// Mock getChatConversationHistory to return the expected messages
vi.mocked(database.getChatConversationHistory).mockResolvedValue(
messageData.rawLlmMessages as any
);
const result = await fetchMessageWithContext(messageData.id);
expect(result.userName).toBe('Unknown');

View File

@ -2,7 +2,7 @@ import { getPermissionedDatasets } from '@buster/access-controls';
import {
and,
chats,
dbInitialized,
db,
eq,
getChatConversationHistory,
isNotNull,
@ -23,8 +23,6 @@ import {
* Fetch current message with user and chat info
*/
export async function fetchMessageWithContext(messageId: string): Promise<MessageContext> {
const db = await dbInitialized;
try {
const result = await db
.select({
@ -88,8 +86,6 @@ export async function fetchPreviousPostProcessingMessages(
chatId: string,
beforeTimestamp: Date
): Promise<PostProcessingResult[]> {
const db = await dbInitialized;
try {
const result = await db
.select({

View File

@ -12,20 +12,20 @@ vi.mock('@buster/slack', () => ({
// Mock the database
vi.mock('@buster/database', () => ({
getDb: vi.fn(() => ({
db: {
select: vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({
limit: vi.fn(() =>
Promise.resolve([
{ id: 'integration-1', defaultChannel: { id: 'C123' }, tokenVaultKey: 'vault-key-1' },
{ id: 'integration-1', defaultChannel: { id: 'C123' }, tokenVaultKey: 'vault-key-1', status: 'active' },
])
),
})),
})),
})),
transaction: vi.fn(),
})),
},
getSecretByName: vi.fn(() => Promise.resolve({ secret: 'xoxb-test-token' })),
eq: vi.fn(),
and: vi.fn(),
@ -36,6 +36,24 @@ vi.mock('@buster/database', () => ({
slackMessageTracking: {},
}));
// Mock the @buster/secrets module
vi.mock('@buster/secrets', () => ({
SERVER_KEYS: {
BUSTER_URL: 'BUSTER_URL',
},
SLACK_KEYS: {
BUSTER_ALERT_CHANNEL_TOKEN: 'BUSTER_ALERT_CHANNEL_TOKEN',
BUSTER_ALERT_CHANNEL_ID: 'BUSTER_ALERT_CHANNEL_ID',
},
getSecret: vi.fn((key) => {
if (key === 'BUSTER_URL') {
return Promise.resolve('https://platform.buster.so');
}
// Return null for alert channel keys to skip that step in tests
return Promise.resolve(null);
}),
}));
// Mock fetch
global.fetch = vi.fn();

View File

@ -1,6 +1,6 @@
import {
and,
dbInitialized,
db,
eq,
getSecretByName,
isNull,
@ -77,8 +77,6 @@ export async function getExistingSlackMessageForChat(chatId: string): Promise<{
integrationId?: string;
} | null> {
try {
const db = await dbInitialized;
// Find messages from the same chat that have been sent to Slack
const existingSlackMessages = await db
.select({
@ -151,7 +149,6 @@ export async function sendSlackNotification(
): Promise<SlackNotificationResult> {
try {
// Step 1: Check if organization has active Slack integration
const db = await dbInitialized;
const [integration] = await db
.select()
.from(slackIntegrations)
@ -610,8 +607,6 @@ export async function trackSlackNotification(params: {
summaryMessage?: string;
slackBlocks?: SlackBlock[];
}): Promise<void> {
const db = await dbInitialized;
try {
await db.transaction(async (tx) => {
// Insert into slack_message_tracking

View File

@ -1,12 +1,4 @@
import {
chats,
db as dbImport,
dbInitialized,
eq,
messages,
organizations,
users,
} from '@buster/database';
import { chats, db as dbImport, eq, messages, organizations, users } from '@buster/database';
import { runs, tasks } from '@trigger.dev/sdk';
import { v4 as uuidv4 } from 'uuid';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@ -230,7 +222,7 @@ describe.skipIf(skipIntegrationTests)('messagePostProcessingTask integration', (
expect(result.output?.result?.workflowCompleted).toBe(true);
// Verify database was updated
const db = await dbInitialized;
const db = dbImport;
const updatedMessage = await db
.select({ postProcessingMessage: messages.postProcessingMessage })
.from(messages)
@ -257,7 +249,7 @@ describe.skipIf(skipIntegrationTests)('messagePostProcessingTask integration', (
});
// Manually add post-processing result to first message
const db = await dbInitialized;
const db = dbImport;
await db
.update(messages)
.set({
@ -308,7 +300,7 @@ describe.skipIf(skipIntegrationTests)('messagePostProcessingTask integration', (
expect(result.output?.messageId).toBe(messageId);
// Cleanup - reset postProcessingMessage to null
const db = await dbInitialized;
const db = dbImport;
await db
.update(messages)
.set({ postProcessingMessage: null })
@ -339,7 +331,7 @@ describe.skipIf(skipIntegrationTests)('messagePostProcessingTask integration', (
expect(duration).toBeLessThan(60000);
// Cleanup - reset postProcessingMessage to null
const db = await dbInitialized;
const db = dbImport;
await db
.update(messages)
.set({ postProcessingMessage: null })

View File

@ -23,6 +23,11 @@ vi.mock('./helpers', () => ({
}));
vi.mock('@buster/database', () => ({
db: {
update: vi.fn(),
set: vi.fn(),
where: vi.fn(),
},
getDb: vi.fn(),
eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
messages: { id: 'messages.id', postProcessingMessage: 'messages.postProcessingMessage' },
@ -79,7 +84,17 @@ describe('messagePostProcessingTask', () => {
vi.clearAllMocks();
// Mock BRAINTRUST_KEY for unit tests
vi.stubEnv('BRAINTRUST_KEY', 'test-braintrust-key');
mockDb = {
// Get the mocked db object from the module mock
mockDb = vi.mocked(database.db);
// Set up the mock chain to return itself for most methods
mockDb.update.mockReturnValue(mockDb);
mockDb.set.mockReturnValue(mockDb);
mockDb.where.mockReturnValue(mockDb);
// Also keep the old getDb mock for backward compatibility if still used
const mockGetDb = {
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
@ -88,13 +103,10 @@ describe('messagePostProcessingTask', () => {
limit: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
};
// Default mock chain behavior
mockDb.where.mockReturnValue(mockDb);
mockDb.limit.mockResolvedValue([{ tokenVaultKey: 'vault-key-123' }]);
mockDb.orderBy.mockResolvedValue([]);
vi.mocked(database.getDb).mockReturnValue(mockDb);
mockGetDb.where.mockReturnValue(mockGetDb);
mockGetDb.limit.mockResolvedValue([{ tokenVaultKey: 'vault-key-123' }]);
mockGetDb.orderBy.mockResolvedValue([]);
vi.mocked(database.getDb).mockReturnValue(mockGetDb);
});
it('should process message successfully for initial message', async () => {

View File

@ -1,13 +1,7 @@
import postProcessingWorkflow, {
type PostProcessingWorkflowOutput,
} from '@buster/ai/workflows/message-post-processing-workflow/message-post-processing-workflow';
import {
dbInitialized,
eq,
getBraintrustMetadata,
messages,
slackIntegrations,
} from '@buster/database';
import { db, eq, getBraintrustMetadata, messages, slackIntegrations } from '@buster/database';
import { BRAINTRUST_KEYS, SERVER_KEYS, getSecret } from '@buster/secrets';
import type {
AssumptionClassification,
@ -214,7 +208,6 @@ export const messagePostProcessingTask: ReturnType<
const dbData = extractDbFields(validatedOutput, messageContext.userName);
try {
const db = await dbInitialized;
await db
.update(messages)
.set({
@ -285,7 +278,6 @@ export const messagePostProcessingTask: ReturnType<
});
// Need to get integration details to get the token vault key
const db = await dbInitialized;
const [integration] = await db
.select({ tokenVaultKey: slackIntegrations.tokenVaultKey })
.from(slackIntegrations)

View File

@ -1,5 +1,6 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { db } from '@buster/database';
import { esbuildPlugin } from '@trigger.dev/build/extensions';
import { defineConfig } from '@trigger.dev/sdk';
import * as dotenv from 'dotenv';

View File

@ -42,8 +42,7 @@ vi.mock('@buster/database', () => {
const mockDbInstance = createChainableMock();
return {
getDb: vi.fn(() => Promise.resolve(mockDbInstance)),
dbInitialized: Promise.resolve(mockDbInstance),
db: mockDbInstance,
and: vi.fn((...args) => ({ type: 'and', args })),
eq: vi.fn((a, b) => ({ type: 'eq', a, b })),
isNull: vi.fn((field) => ({ type: 'isNull', field })),
@ -88,9 +87,9 @@ describe('Access Controls Unit Tests - Organization Default Permission Group', (
});
it('should return empty array when user has no organization', async () => {
// Get the dbInitialized mock
// Get the db mock
const dbModule = await import('@buster/database');
const mockDb = await dbModule.dbInitialized;
const mockDb = dbModule.db;
// Mock for user organization query (returns empty)
mockDb.select.mockReturnValueOnce(mockDb);
@ -105,9 +104,9 @@ describe('Access Controls Unit Tests - Organization Default Permission Group', (
});
it('should return datasets for admin users', async () => {
// Get the dbInitialized mock
// Get the db mock
const dbModule = await import('@buster/database');
const mockDb = await dbModule.dbInitialized;
const mockDb = dbModule.db;
// Mock for user organization query - returns admin user
mockDb.select.mockReturnValueOnce(mockDb);
@ -155,9 +154,9 @@ describe('Access Controls Unit Tests - Organization Default Permission Group', (
});
it('should return false when dataset is deleted', async () => {
// Get the dbInitialized mock
// Get the db mock
const dbModule = await import('@buster/database');
const mockDb = await dbModule.dbInitialized;
const mockDb = dbModule.db;
// Clear previous mock calls
vi.clearAllMocks();
@ -198,9 +197,9 @@ describe('Access Controls Unit Tests - Organization Default Permission Group', (
});
it('should return false when not all datasets exist', async () => {
// Get the dbInitialized mock
// Get the db mock
const dbModule = await import('@buster/database');
const mockDb = await dbModule.dbInitialized;
const mockDb = dbModule.db;
// Clear previous mock calls
vi.clearAllMocks();
@ -226,9 +225,9 @@ describe('Access Controls Unit Tests - Organization Default Permission Group', (
describe('Edge Cases', () => {
it('should handle database connection errors gracefully', async () => {
// Get the dbInitialized mock
// Get the db mock
const dbModule = await import('@buster/database');
const mockDb = await dbModule.dbInitialized;
const mockDb = dbModule.db;
// Clear previous mock calls
vi.clearAllMocks();

View File

@ -4,7 +4,7 @@ import {
datasetPermissions,
datasets,
datasetsToPermissionGroups,
dbInitialized,
db,
eq,
inArray,
isNull,
@ -60,8 +60,6 @@ const HasAllDatasetsAccessSchema = z.object({
// Path 1: Direct User -> Dataset
async function fetchDirectUserDatasetIds(userId: string): Promise<string[]> {
const db = await dbInitialized;
const results = await db
.select({ datasetId: datasetPermissions.datasetId })
.from(datasetPermissions)
@ -78,8 +76,6 @@ async function fetchDirectUserDatasetIds(userId: string): Promise<string[]> {
// Path 3: User -> Team -> Dataset (Direct team assignment)
async function fetchTeamDirectDatasetIds(userId: string): Promise<string[]> {
const db = await dbInitialized;
const results = await db
.selectDistinct({ datasetId: datasetPermissions.datasetId })
.from(datasetPermissions)
@ -99,8 +95,6 @@ async function fetchTeamDirectDatasetIds(userId: string): Promise<string[]> {
// Path 2: User -> Group -> Dataset
async function fetchUserGroupDatasetIds(userId: string): Promise<string[]> {
const db = await dbInitialized;
const results = await db
.selectDistinct({ datasetId: datasetsToPermissionGroups.datasetId })
.from(datasetsToPermissionGroups)
@ -127,8 +121,6 @@ async function fetchUserGroupDatasetIds(userId: string): Promise<string[]> {
// Path 4: User -> Team -> Group -> Dataset
async function fetchTeamGroupDatasetIds(userId: string): Promise<string[]> {
const db = await dbInitialized;
const results = await db
.selectDistinct({ datasetId: datasetsToPermissionGroups.datasetId })
.from(datasetsToPermissionGroups)
@ -162,8 +154,6 @@ async function fetchTeamGroupDatasetIds(userId: string): Promise<string[]> {
// Path 5: User -> Organization -> Default Permission Group -> Dataset
async function fetchOrgDefaultDatasetIds(userId: string): Promise<string[]> {
const db = await dbInitialized;
// Get all the user's organizations
const userOrgs = await db
.select({ organizationId: usersToOrganizations.organizationId })
@ -229,8 +219,6 @@ export async function getPermissionedDatasets(
// Validate inputs
const input = GetPermissionedDatasetsSchema.parse({ userId, page, pageSize });
const db = await dbInitialized;
// Fetch all user's organizations and roles
const userOrgs = await db
.select({
@ -334,8 +322,6 @@ export async function hasDatasetAccess(userId: string, datasetId: string): Promi
// Validate inputs
const input = HasDatasetAccessSchema.parse({ userId, datasetId });
const db = await dbInitialized;
// --- Check if Dataset exists and get Organization ID and deleted status ---
const datasetInfo = await db
.select({
@ -408,8 +394,6 @@ export async function hasAllDatasetsAccess(userId: string, datasetIds: string[])
return false; // No datasets means no access granted
}
const db = await dbInitialized;
// --- Step 1: Verify all datasets exist, are not deleted, and get their org IDs ---
const datasetInfos = await db
.select({
@ -498,8 +482,6 @@ export async function hasAllDatasetsAccess(userId: string, datasetIds: string[])
// --- Helper Functions for Individual Permission Checks ---
async function checkDirectUserPermission(userId: string, datasetId: string): Promise<boolean> {
const db = await dbInitialized;
const result = await db
.select({ count: count() })
.from(datasetPermissions)
@ -518,8 +500,6 @@ async function checkDirectUserPermission(userId: string, datasetId: string): Pro
}
async function checkTeamDirectPermission(userId: string, datasetId: string): Promise<boolean> {
const db = await dbInitialized;
const result = await db
.select({ count: count() })
.from(datasetPermissions)
@ -540,8 +520,6 @@ async function checkTeamDirectPermission(userId: string, datasetId: string): Pro
}
async function checkUserGroupPermission(userId: string, datasetId: string): Promise<boolean> {
const db = await dbInitialized;
const result = await db
.select({ count: count() })
.from(datasetsToPermissionGroups)
@ -574,8 +552,6 @@ async function checkUserGroupPermission(userId: string, datasetId: string): Prom
}
async function checkTeamGroupPermission(userId: string, datasetId: string): Promise<boolean> {
const db = await dbInitialized;
const result = await db
.select({ count: count() })
.from(datasetsToPermissionGroups)
@ -615,8 +591,6 @@ async function checkTeamGroupPermission(userId: string, datasetId: string): Prom
}
async function checkOrgDefaultPermission(userId: string, datasetId: string): Promise<boolean> {
const db = await dbInitialized;
// Get all the user's organizations
const userOrgs = await db
.select({ organizationId: usersToOrganizations.organizationId })

View File

@ -14,8 +14,7 @@ vi.mock('@buster/database', () => {
};
return {
getDb: vi.fn(() => Promise.resolve(mockDb)),
dbInitialized: Promise.resolve(mockDb),
db: mockDb,
and: vi.fn((...args) => ({ _and: args })),
eq: vi.fn((a, b) => ({ _eq: [a, b] })),
isNull: vi.fn((a) => ({ _isNull: a })),
@ -58,7 +57,7 @@ describe('canUserAccessChat', () => {
vi.clearAllMocks();
// Get the mock database object
const dbModule = await import('@buster/database');
mockDb = await dbModule.dbInitialized;
mockDb = dbModule.db;
// Reset all mocks before each test
mockDb.select.mockClear();

View File

@ -4,7 +4,7 @@ import {
assetTypeEnum,
chats,
collectionsToAssets,
dbInitialized,
db as database,
eq,
identityTypeEnum,
isNull,
@ -28,21 +28,19 @@ export const canUserAccessChat = async ({
// Validate inputs
const input = CanUserAccessChatSchema.parse({ userId, chatId });
const db = await dbInitialized;
// Run all permission checks concurrently for optimal performance
const [directPermission, collectionPermission, chatInfo, userOrgs] = await Promise.all([
// Check 1: Direct user permission on chat
checkDirectChatPermission(db, input.userId, input.chatId),
checkDirectChatPermission(database, input.userId, input.chatId),
// Check 2: User permission through collections
checkCollectionChatPermission(db, input.userId, input.chatId),
checkCollectionChatPermission(database, input.userId, input.chatId),
// Check 3: Get chat info (creator & organization)
getChatInfo(db, input.chatId),
getChatInfo(database, input.chatId),
// Check 4: Get user's organizations and roles
getUserOrganizations(db, input.userId),
getUserOrganizations(database, input.userId),
]);
// If chat doesn't exist or is deleted, deny access
@ -77,7 +75,7 @@ export const canUserAccessChat = async ({
// Helper function to check direct chat permission
async function checkDirectChatPermission(
db: Awaited<typeof dbInitialized>,
db: typeof database,
userId: string,
chatId: string
): Promise<boolean> {
@ -100,7 +98,7 @@ async function checkDirectChatPermission(
// Helper function to check collection-based chat permission
async function checkCollectionChatPermission(
db: Awaited<typeof dbInitialized>,
db: typeof database,
userId: string,
chatId: string
): Promise<boolean> {
@ -131,7 +129,7 @@ async function checkCollectionChatPermission(
// Helper function to get chat info (creator and organization)
async function getChatInfo(
db: Awaited<typeof dbInitialized>,
db: typeof database,
chatId: string
): Promise<{ createdBy: string; organizationId: string } | null> {
const result = await db
@ -148,7 +146,7 @@ async function getChatInfo(
// Helper function to get user's organizations and roles
async function getUserOrganizations(
db: Awaited<typeof dbInitialized>,
db: typeof database,
userId: string
): Promise<Array<{ organizationId: string; role: string }>> {
const result = await db

View File

@ -24,7 +24,7 @@ vi.mock('@buster/database', () => {
return {
getDb: vi.fn(() => Promise.resolve(mockDb)),
dbInitialized: Promise.resolve(mockDb),
db: mockDb,
and: vi.fn((...args) => ({ _and: args })),
eq: vi.fn((a, b) => ({ _eq: [a, b] })),
isNull: vi.fn((a) => ({ _isNull: a })),
@ -61,7 +61,7 @@ describe('user-organizations', () => {
vi.clearAllMocks();
// Get the mock database object
const dbModule = await import('@buster/database');
mockDb = await dbModule.dbInitialized;
mockDb = dbModule.db;
// Reset mock methods to return 'this' for chaining
mockDb.select.mockReturnThis();
@ -85,8 +85,6 @@ describe('user-organizations', () => {
},
]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await checkUserInOrganization(TEST_USER_ID, TEST_ORG_ID);
expect(result).toEqual({
@ -103,8 +101,6 @@ describe('user-organizations', () => {
it('should return null when user does not exist in org', async () => {
mockDb.limit.mockResolvedValue([]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await checkUserInOrganization(TEST_USER_ID, TEST_ORG_ID);
expect(result).toBeNull();
@ -136,8 +132,6 @@ describe('user-organizations', () => {
mockDb.where.mockResolvedValue(mockOrgs);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await getUserOrganizations(TEST_USER_ID);
expect(result).toEqual(mockOrgs);
@ -147,8 +141,6 @@ describe('user-organizations', () => {
it('should return empty array when user has no organizations', async () => {
mockDb.where.mockResolvedValue([]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await getUserOrganizations(TEST_USER_ID);
expect(result).toEqual([]);
@ -159,8 +151,6 @@ describe('user-organizations', () => {
it('should return true when email domain matches org domain', async () => {
mockDb.limit.mockResolvedValue([{ domains: ['example.com', 'test.com'] }]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(true);
@ -169,8 +159,6 @@ describe('user-organizations', () => {
it('should return true when email domain matches with @ prefix', async () => {
mockDb.limit.mockResolvedValue([{ domains: ['@example.com', 'test.com'] }]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(true);
@ -179,8 +167,6 @@ describe('user-organizations', () => {
it('should return false when email domain does not match', async () => {
mockDb.limit.mockResolvedValue([{ domains: ['other.com'] }]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(false);
@ -189,8 +175,6 @@ describe('user-organizations', () => {
it('should return false when org has no domains', async () => {
mockDb.limit.mockResolvedValue([{ domains: null }]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(false);
@ -199,8 +183,6 @@ describe('user-organizations', () => {
it('should return false when org does not exist', async () => {
mockDb.limit.mockResolvedValue([]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(false);
@ -209,8 +191,6 @@ describe('user-organizations', () => {
it('should be case insensitive for domain matching', async () => {
mockDb.limit.mockResolvedValue([{ domains: ['EXAMPLE.COM'] }]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await checkEmailDomainForOrganization('user@example.com', TEST_ORG_ID);
expect(result).toBe(true);
@ -230,8 +210,6 @@ describe('user-organizations', () => {
mockDb.limit.mockResolvedValue([mockOrg]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await getOrganizationWithDefaults(TEST_ORG_ID);
expect(result).toEqual(mockOrg);
@ -240,8 +218,6 @@ describe('user-organizations', () => {
it('should return null when organization does not exist', async () => {
mockDb.limit.mockResolvedValue([]);
vi.mocked(db.getDb).mockResolvedValue(mockDb as any);
const result = await getOrganizationWithDefaults(TEST_ORG_ID);
expect(result).toBeNull();

View File

@ -1,7 +1,7 @@
import {
type User,
and,
dbInitialized,
db,
eq,
isNull,
organizations,
@ -58,7 +58,6 @@ export async function checkUserInOrganization(
organizationId: string
): Promise<UserOrganizationInfo | null> {
const input = CheckUserInOrganizationSchema.parse({ userId, organizationId });
const db = await dbInitialized;
const result = await db
.select({
@ -91,7 +90,6 @@ export async function checkUserInOrganization(
*/
export async function getUserOrganizations(userId: string): Promise<UserOrganizationInfo[]> {
const input = GetUserOrganizationsSchema.parse({ userId });
const db = await dbInitialized;
const results = await db
.select({
@ -119,7 +117,6 @@ export async function checkEmailDomainForOrganization(
organizationId: string
): Promise<boolean> {
const input = CheckEmailDomainSchema.parse({ email, organizationId });
const db = await dbInitialized;
// Get organization domains
const orgResult = await db
@ -161,8 +158,6 @@ export async function checkEmailDomainForOrganization(
export async function getOrganizationWithDefaults(
organizationId: string
): Promise<OrganizationWithDefaults | null> {
const db = await dbInitialized;
const result = await db
.select()
.from(organizations)
@ -191,7 +186,6 @@ export async function createUserInOrganization(
createdById: string
): Promise<{ user: User; membership: UserOrganizationInfo }> {
const input = CreateUserInOrganizationSchema.parse({ email, name, organizationId, createdById });
const db = await dbInitialized;
// Get organization defaults
const org = await getOrganizationWithDefaults(input.organizationId);

View File

@ -1,12 +1,12 @@
import { InvalidToolInputError, NoSuchToolError } from 'ai';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ANALYST_AGENT_NAME } from '../../agents';
import { repairToolCall } from './repair-tool-call';
import type { RepairContext } from './types';
vi.mock('braintrust', () => ({
wrapTraced: (fn: any) => fn,
}));
// Set up environment variables before any imports to prevent initialization errors
process.env.ANTHROPIC_API_KEY = 'test-anthropic-key';
process.env.OPENAI_API_KEY = 'test-openai-key';
process.env.VERTEX_CLIENT_EMAIL = 'test-vertex-email';
process.env.VERTEX_PRIVATE_KEY = 'test-vertex-key';
process.env.VERTEX_PROJECT = 'test-vertex-project';
// Mock the strategy functions
vi.mock('./strategies/structured-output-strategy', () => ({
@ -19,6 +19,11 @@ vi.mock('./strategies/re-ask-strategy', () => ({
repairWrongToolName: vi.fn(),
}));
// Import after all mocks are set up
import { ANALYST_AGENT_NAME } from '../../agents';
import { repairToolCall } from './repair-tool-call';
import type { RepairContext } from './types';
describe('repairToolCall', () => {
beforeEach(() => {
vi.clearAllMocks();

View File

@ -16,8 +16,20 @@ vi.mock('@buster/database', () => ({
getSecretByName: vi.fn(),
}));
// Mock secrets package
vi.mock('@buster/secrets', () => ({
getSecret: vi.fn(),
DATA_SOURCE_KEYS: {
R2_ACCOUNT_ID: 'R2_ACCOUNT_ID',
R2_ACCESS_KEY_ID: 'R2_ACCESS_KEY_ID',
R2_SECRET_ACCESS_KEY: 'R2_SECRET_ACCESS_KEY',
R2_BUCKET: 'R2_BUCKET',
},
}));
// Import after mocking
import { getS3IntegrationByOrganizationId, getSecretByName } from '@buster/database';
import { getSecret } from '@buster/secrets';
vi.mock('./providers/s3-provider');
vi.mock('./providers/r2-provider');
vi.mock('./providers/gcs-provider');
@ -115,23 +127,18 @@ describe('Storage Factory', () => {
});
describe('getDefaultProvider', () => {
const originalEnv = process.env;
it('should create default R2 provider with environment variables', async () => {
(getSecret as Mock).mockImplementation(async (key: string) => {
const secrets: Record<string, string> = {
R2_ACCOUNT_ID: 'test-account',
R2_ACCESS_KEY_ID: 'test-key',
R2_SECRET_ACCESS_KEY: 'test-secret',
R2_BUCKET: 'custom-bucket',
};
return secrets[key];
});
beforeEach(() => {
process.env = { ...originalEnv };
});
afterEach(() => {
process.env = originalEnv;
});
it('should create default R2 provider with environment variables', () => {
process.env.R2_ACCOUNT_ID = 'test-account';
process.env.R2_ACCESS_KEY_ID = 'test-key';
process.env.R2_SECRET_ACCESS_KEY = 'test-secret';
process.env.R2_BUCKET = 'custom-bucket';
const provider = getDefaultProvider();
const provider = await getDefaultProvider();
expect(createR2Provider).toHaveBeenCalledWith({
provider: 'r2',
@ -143,55 +150,59 @@ describe('Storage Factory', () => {
expect(provider).toBeDefined();
});
it('should use default bucket name if not specified', () => {
process.env.R2_ACCOUNT_ID = 'test-account';
process.env.R2_ACCESS_KEY_ID = 'test-key';
process.env.R2_SECRET_ACCESS_KEY = 'test-secret';
delete process.env.R2_BUCKET;
const provider = getDefaultProvider();
expect(createR2Provider).toHaveBeenCalledWith({
provider: 'r2',
accountId: 'test-account',
bucket: 'metric-exports',
accessKeyId: 'test-key',
secretAccessKey: 'test-secret',
it('should use default bucket name if not specified', async () => {
(getSecret as Mock).mockImplementation(async (key: string) => {
const secrets: Record<string, string | undefined> = {
R2_ACCOUNT_ID: 'test-account',
R2_ACCESS_KEY_ID: 'test-key',
R2_SECRET_ACCESS_KEY: 'test-secret',
R2_BUCKET: undefined,
};
return secrets[key];
});
expect(provider).toBeDefined();
await expect(getDefaultProvider()).rejects.toThrow(
'Default R2 storage credentials not configured'
);
});
it('should throw error if R2 credentials are missing', () => {
delete process.env.R2_ACCOUNT_ID;
delete process.env.R2_ACCESS_KEY_ID;
delete process.env.R2_SECRET_ACCESS_KEY;
it('should throw error if R2 credentials are missing', async () => {
(getSecret as Mock).mockResolvedValue(undefined);
expect(() => getDefaultProvider()).toThrow('Default R2 storage credentials not configured');
await expect(getDefaultProvider()).rejects.toThrow(
'Default R2 storage credentials not configured'
);
});
it('should throw error if partial R2 credentials are missing', () => {
process.env.R2_ACCOUNT_ID = 'test-account';
delete process.env.R2_ACCESS_KEY_ID;
process.env.R2_SECRET_ACCESS_KEY = 'test-secret';
it('should throw error if partial R2 credentials are missing', async () => {
(getSecret as Mock).mockImplementation(async (key: string) => {
const secrets: Record<string, string | undefined> = {
R2_ACCOUNT_ID: 'test-account',
R2_ACCESS_KEY_ID: undefined,
R2_SECRET_ACCESS_KEY: 'test-secret',
R2_BUCKET: 'metric-exports',
};
return secrets[key];
});
expect(() => getDefaultProvider()).toThrow('Default R2 storage credentials not configured');
await expect(getDefaultProvider()).rejects.toThrow(
'Default R2 storage credentials not configured'
);
});
});
describe('getProviderForOrganization', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = {
...originalEnv,
R2_ACCOUNT_ID: 'default-account',
R2_ACCESS_KEY_ID: 'default-key',
R2_SECRET_ACCESS_KEY: 'default-secret',
};
});
afterEach(() => {
process.env = originalEnv;
// Mock getSecret for default provider
(getSecret as Mock).mockImplementation(async (key: string) => {
const secrets: Record<string, string> = {
R2_ACCOUNT_ID: 'default-account',
R2_ACCESS_KEY_ID: 'default-key',
R2_SECRET_ACCESS_KEY: 'default-secret',
R2_BUCKET: 'metric-exports',
};
return secrets[key];
});
});
it('should return S3 provider for organization with S3 integration', async () => {

View File

@ -1,4 +1,4 @@
import { DATABASE_KEYS, SHARED_KEYS, getSecret } from '@buster/secrets';
import { DATABASE_KEYS, SHARED_KEYS, getSecretSync } from '@buster/secrets';
import { drizzle } from 'drizzle-orm/postgres-js';
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
@ -7,20 +7,20 @@ import postgres from 'postgres';
let globalPool: postgres.Sql | null = null;
let globalDb: PostgresJsDatabase | null = null;
// Helper to safely get secret
async function getEnvValue(key: string, defaultValue?: string): Promise<string | undefined> {
// Helper to safely get secret synchronously
function getEnvValue(key: string, defaultValue?: string): string | undefined {
try {
return await getSecret(key);
return getSecretSync(key);
} catch {
return defaultValue;
}
}
// Environment validation
async function validateEnvironment(): Promise<string> {
const isTest = (await getEnvValue(SHARED_KEYS.NODE_ENV)) === 'test';
const isProduction = (await getEnvValue(SHARED_KEYS.NODE_ENV)) === 'production';
const dbUrl = await getEnvValue(DATABASE_KEYS.DATABASE_URL);
// Environment validation (now synchronous)
function validateEnvironment(): string {
const isTest = getEnvValue(SHARED_KEYS.NODE_ENV) === 'test';
const isProduction = getEnvValue(SHARED_KEYS.NODE_ENV) === 'production';
const dbUrl = getEnvValue(DATABASE_KEYS.DATABASE_URL);
// Use default local database URL if none provided
if (!dbUrl) {
@ -30,7 +30,7 @@ async function validateEnvironment(): Promise<string> {
}
// Prevent accidental production database usage in tests
const allowProdInTests = await getEnvValue('ALLOW_PROD_DB_IN_TESTS'); // Not in constants - rarely used
const allowProdInTests = getEnvValue('ALLOW_PROD_DB_IN_TESTS'); // Not in constants - rarely used
if (isTest && dbUrl.includes('prod') && !allowProdInTests) {
throw new Error(
'Production database detected in test environment. Set ALLOW_PROD_DB_IN_TESTS=true to override.'
@ -38,7 +38,7 @@ async function validateEnvironment(): Promise<string> {
}
// Warn about non-pooled connections in production
const poolSize = await getEnvValue('DATABASE_POOL_SIZE'); // Not in constants - optional config
const poolSize = getEnvValue('DATABASE_POOL_SIZE'); // Not in constants - optional config
if (isProduction && !poolSize) {
console.warn('DATABASE_POOL_SIZE not set - using default pool size of 100');
}
@ -46,19 +46,18 @@ async function validateEnvironment(): Promise<string> {
return dbUrl;
}
// Initialize the database pool
export async function initializePool<T extends Record<string, postgres.PostgresType>>(
// Initialize the database pool (now synchronous)
export function initializePool<T extends Record<string, postgres.PostgresType>>(
config: postgres.Options<T> | undefined = {}
): Promise<PostgresJsDatabase> {
const connectionString = await validateEnvironment();
const poolSizeStr = await getEnvValue('DATABASE_POOL_SIZE'); // Not in constants - optional config
const poolSize = poolSizeStr ? Number.parseInt(poolSizeStr) : 100;
): PostgresJsDatabase {
if (globalPool && globalDb) {
return globalDb;
}
const connectionString = validateEnvironment();
const poolSizeStr = getEnvValue('DATABASE_POOL_SIZE'); // Not in constants - optional config
const poolSize = poolSizeStr ? Number.parseInt(poolSizeStr) : 100;
// Create postgres client with pool configuration
globalPool = postgres(connectionString, {
max: poolSize,
@ -74,18 +73,18 @@ export async function initializePool<T extends Record<string, postgres.PostgresT
return globalDb;
}
// Get the database instance (initializes if not already done)
export async function getDb(): Promise<PostgresJsDatabase> {
// Get the database instance (initializes if not already done) - now synchronous
export function getDb(): PostgresJsDatabase {
if (!globalDb) {
return await initializePool();
return initializePool();
}
return globalDb;
}
// Get the raw postgres client
export async function getClient(): Promise<postgres.Sql> {
// Get the raw postgres client (now synchronous)
export function getClient(): postgres.Sql {
if (!globalPool) {
await initializePool();
initializePool();
}
if (!globalPool) {
throw new Error('Failed to initialize database pool');
@ -105,7 +104,7 @@ export async function closePool(): Promise<void> {
// Ping the database to check if connection is possible
export async function dbPing(): Promise<boolean> {
try {
const client = await getClient();
const client = getClient();
await client`SELECT 1`;
return true;
} catch (error) {
@ -122,43 +121,5 @@ export function getSyncDb(): PostgresJsDatabase {
return globalDb;
}
// Export the database initialization promise
export const dbInitialized = getDb();
// Auto-initialize database on module import for Trigger.dev tasks
// This ensures the database is ready when the task runs
getDb().catch((error) => {
console.error('Failed to initialize database connection on module load:', error);
// Don't throw - let individual queries handle the error
});
// Export a synchronous database instance (will throw if not initialized)
// This maintains backwards compatibility for existing code
export const db = new Proxy({} as PostgresJsDatabase, {
get(_target, prop) {
if (!globalDb) {
throw new Error(
'Database not initialized. Import and await dbInitialized first, or use getSyncDb() after initialization.'
);
}
return Reflect.get(globalDb, prop);
},
has(_target, prop) {
if (!globalDb) {
throw new Error('Database not initialized. Import and await dbInitialized first.');
}
return prop in globalDb;
},
ownKeys(_target) {
if (!globalDb) {
throw new Error('Database not initialized.');
}
return Reflect.ownKeys(globalDb);
},
getOwnPropertyDescriptor(_target, prop) {
if (!globalDb) {
throw new Error('Database not initialized.');
}
return Reflect.getOwnPropertyDescriptor(globalDb, prop);
},
});
// Export the database instance - initializes synchronously on first use
export const db: PostgresJsDatabase = getDb();

View File

@ -38,7 +38,7 @@ export type UpdateSecretInput = z.infer<typeof UpdateSecretInputSchema>;
*/
export async function createSecret(input: CreateSecretInput): Promise<string> {
const validatedInput = CreateSecretInputSchema.parse(input);
const client = await getClient();
const client = getClient();
try {
const result = await client`
@ -66,7 +66,7 @@ export async function createSecret(input: CreateSecretInput): Promise<string> {
*/
export async function updateSecret(input: UpdateSecretInput): Promise<string> {
const validatedInput = UpdateSecretInputSchema.parse(input);
const client = await getClient();
const client = getClient();
try {
// Note: vault.update_secret returns void, not an ID
@ -93,7 +93,7 @@ export async function updateSecret(input: UpdateSecretInput): Promise<string> {
*/
export async function deleteSecret(id: string): Promise<void> {
const validatedId = z.string().uuid().parse(id);
const client = await getClient();
const client = getClient();
try {
await client`
@ -112,7 +112,7 @@ export async function deleteSecret(id: string): Promise<void> {
*/
export async function getSecret(id: string): Promise<VaultSecret | null> {
const validatedId = z.string().uuid().parse(id);
const client = await getClient();
const client = getClient();
try {
const result = await client`
@ -147,7 +147,7 @@ export async function getSecret(id: string): Promise<VaultSecret | null> {
*/
export async function getSecretByName(name: string): Promise<VaultSecret | null> {
const validatedName = z.string().parse(name);
const client = await getClient();
const client = getClient();
try {
const result = await client`
@ -182,7 +182,7 @@ export async function getSecretByName(name: string): Promise<VaultSecret | null>
*/
export async function listSecrets(limit = 100): Promise<VaultSecret[]> {
const validatedLimit = z.number().positive().max(1000).parse(limit);
const client = await getClient();
const client = getClient();
try {
const result = await client`

View File

@ -63,7 +63,7 @@ export async function searchValuesByEmbedding(
const validOptions = SearchOptionsSchema.parse(options);
const schemaName = formatSchemaName(validDataSourceId);
const client = await getClient();
const client = getClient();
// Build similarity expression with optional threshold
const similarityExpr = validOptions.similarityThreshold
@ -158,7 +158,7 @@ export async function searchValuesByEmbeddingWithFilters(
const validOptions = SearchOptionsSchema.parse(options);
const pgSchemaName = formatSchemaName(validDataSourceId);
const client = await getClient();
const client = getClient();
// Build filter conditions and parameters
const filters: string[] = [];
@ -445,7 +445,7 @@ export async function healthCheck(dataSourceId: string): Promise<boolean> {
try {
const validDataSourceId = UuidSchema.parse(dataSourceId);
const schemaName = formatSchemaName(validDataSourceId);
const client = await getClient();
const client = getClient();
// Check if the table exists and is accessible
const result = await client.unsafe(