@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
933 lines (873 loc) • 29.7 kB
text/typescript
/**
* Projects Service for BackendSDK
*
* Provides comprehensive project management functionality including:
* - Project CRUD operations
* - Project statistics and analytics
* - API key management for projects
* - Project settings and configuration
*
* @class ProjectsService
* @example
* const projectsService = new ProjectsService(dbConnection, logger);
* const projects = await projectsService.getAllProjects({ limit: 10 });
*/
import crypto from "crypto";
import { DatabaseConnection, Logger } from "./core";
import { CountRow } from "./database-types";
import { normalizeError } from "./utils/error-handler";
export interface Project {
id: string;
name: string;
description?: string;
owner_id: string;
api_key: string;
allowed_origins: string[];
settings: ProjectSettings;
created_at: string;
updated_at: string;
is_active: boolean;
storage_used: number;
last_api_call?: string;
total_api_calls: number;
rate_limit: number;
rate_limit_window: string;
}
export interface ProjectSettings {
authentication_required: boolean;
cors_enabled: boolean;
rate_limiting_enabled: boolean;
logging_enabled: boolean;
encryption_enabled: boolean;
backup_enabled: boolean;
max_file_size: number;
allowed_file_types: string[];
webhook_url?: string;
custom_headers: Record<string, string>;
environment: "development" | "staging" | "production";
}
export interface ProjectStatistics {
totalCollections: number;
totalDocuments: number;
totalFiles: number;
storageUsed: number;
apiCallsToday: number;
apiCallsThisMonth: number;
lastActivity?: string;
topCollections: Array<{
name: string;
documentCount: number;
lastModified: string;
}>;
recentActivity: Array<{
action: string;
entity: string;
timestamp: string;
user: string;
}>;
}
export interface ProjectApiKey {
id: string;
key: string;
name: string;
project_id: string;
scopes: string[];
expires_at?: string;
last_used_at?: string;
created_at: string;
is_active: boolean;
usage_count: number;
}
export interface CreateProjectRequest {
name: string;
description?: string;
settings?: Partial<ProjectSettings>;
allowed_origins?: string[];
}
export interface UpdateProjectRequest {
name?: string;
description?: string;
settings?: Partial<ProjectSettings>;
allowed_origins?: string[];
is_active?: boolean;
}
export class ProjectsService {
private db: DatabaseConnection;
private logger: Logger;
/**
* Create a new ProjectsService instance
*
* @param {DatabaseConnection} databaseConnection - Database connection
* @param {Logger} logger - Logger instance
*/
constructor(databaseConnection: DatabaseConnection, logger: Logger) {
this.db = databaseConnection;
this.logger = logger;
}
/**
* Get all projects
*
* @param {Object} [options] - Query options
* @param {number} [options.limit] - Maximum number of projects
* @param {number} [options.offset] - Number of projects to skip
* @param {string} [options.search] - Search term for project name/description
* @param {boolean} [options.active] - Filter by active status
* @param {string} [options.owner_id] - Filter by owner ID
* @returns {Promise<Project[]>} Array of projects
* @throws {Error} If query fails
*
* @example
* const projects = await projectsService.getAllProjects({ limit: 10, active: true });
*/
async getAllProjects(options?: {
limit?: number;
offset?: number;
search?: string;
active?: boolean;
owner_id?: string;
}): Promise<Project[]> {
try {
let query = "SELECT * FROM projects WHERE 1=1";
const params: unknown[] = [];
let paramCount = 0;
if (options?.active !== undefined) {
paramCount++;
query += ` AND is_active = $${paramCount}`;
params.push(options.active);
}
if (options?.owner_id) {
paramCount++;
query += ` AND owner_id = $${paramCount}`;
params.push(options.owner_id);
}
if (options?.search) {
paramCount++;
// SQLite uses LIKE (case-insensitive by default with NOCASE collation)
query += ` AND (name LIKE $${paramCount} OR description LIKE $${paramCount})`;
params.push(`%${options.search}%`);
}
query += " ORDER BY created_at DESC";
if (options?.limit) {
paramCount++;
query += ` LIMIT $${paramCount}`;
params.push(options.limit);
}
if (options?.offset) {
paramCount++;
query += ` OFFSET $${paramCount}`;
params.push(options.offset);
}
const result = await this.db.query(query, params);
return result.rows as Project[];
} catch (error) {
this.logger.error("Failed to get projects:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getProjects",
});
}
}
/**
* Get project by ID
*
* Retrieves a single project by its ID.
*
* @param {string} projectId - Project ID
* @returns {Promise<Project | null>} Project or null if not found
* @throws {Error} If query fails
*
* @example
* const project = await projectsService.getProjectById('project-id');
*/
async getProjectById(projectId: string): Promise<Project | null> {
try {
const result = await this.db.query(
"SELECT * FROM projects WHERE id = $1",
[projectId]
);
return result.rows.length > 0 ? (result.rows[0] as Project) : null;
} catch (error) {
this.logger.error("Failed to get project by ID:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getProjectById",
});
}
}
/**
* Create a new project
*
* Creates a new project with the specified data and owner.
* Automatically generates an API key and applies default settings.
*
* @param {string} ownerId - Owner user ID
* @param {CreateProjectRequest} projectData - Project creation data
* @param {string} projectData.name - Project name (required)
* @param {string} [projectData.description] - Project description
* @param {Partial<ProjectSettings>} [projectData.settings] - Project settings
* @param {string[]} [projectData.allowed_origins] - Allowed CORS origins
* @returns {Promise<Project>} Created project
* @throws {Error} If creation fails or project name already exists
*
* @example
* const project = await projectsService.createProject('owner-id', {
* name: 'My Project',
* description: 'Project description',
* settings: { authentication_required: true }
* });
*/
async createProject(
ownerId: string,
projectData: CreateProjectRequest
): Promise<Project> {
try {
// Generate API key for the project
const apiKey = `pk_${Math.random()
.toString(36)
.substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
// Default settings
const defaultSettings: ProjectSettings = {
authentication_required: true,
cors_enabled: true,
rate_limiting_enabled: true,
logging_enabled: true,
encryption_enabled: false,
backup_enabled: false,
max_file_size: 10485760, // 10MB
allowed_file_types: ["jpg", "jpeg", "png", "pdf", "txt", "csv"],
custom_headers: {},
environment: "development",
...projectData.settings,
};
// Generate project ID using UUID format (matches database service and frontend expectations)
// Use crypto.randomUUID() if available (Node.js 14.17+), otherwise fall back to simple random ID
let projectId: string;
if (typeof crypto !== "undefined" && crypto.randomUUID) {
projectId = crypto.randomUUID();
} else {
// Fallback: generate a UUID-like string manually
const hex = () => Math.floor(Math.random() * 16).toString(16);
projectId = `${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}-${hex()}${hex()}${hex()}${hex()}-4${hex()}${hex()}${hex()}-${((Math.floor(Math.random() * 4) + 8).toString(16))}${hex()}${hex()}${hex()}-${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}${hex()}`;
}
// SQLite-compatible INSERT (no RETURNING *)
await this.db.query(
`INSERT INTO projects (id, name, description, project_url, owner_id, api_key, allowed_origins, settings, is_active, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[
projectId,
projectData.name,
projectData.description || null,
null, // project_url is optional, not in CreateProjectRequest type
ownerId,
apiKey,
JSON.stringify(projectData.allowed_origins || []), // SQLite stores arrays as JSON strings
JSON.stringify(defaultSettings),
1, // is_active (SQLite uses INTEGER 1 for true)
ownerId, // created_by defaults to owner_id
]
);
// Query back the inserted row (SQLite doesn't support RETURNING *)
const result = await this.db.query(
`SELECT * FROM projects WHERE id = $1`,
[projectId]
);
return result.rows[0] as Project;
} catch (error) {
this.logger.error("Failed to create project:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "createProject",
});
}
}
/**
* Update an existing project
*
* Updates project information with the provided data.
*
* @param {string} projectId - Project ID
* @param {UpdateProjectRequest} updates - Project update data
* @param {string} [updates.name] - New project name
* @param {string} [updates.description] - New project description
* @param {Partial<ProjectSettings>} [updates.settings] - Updated project settings
* @param {string[]} [updates.allowed_origins] - Updated allowed CORS origins
* @param {boolean} [updates.is_active] - Active status
* @returns {Promise<Project | null>} Updated project or null if not found
* @throws {Error} If update fails
*
* @example
* const updated = await projectsService.updateProject('project-id', {
* name: 'Updated Name',
* description: 'Updated description'
* });
*/
async updateProject(
projectId: string,
updates: UpdateProjectRequest
): Promise<Project | null> {
try {
const fields: string[] = [];
const values: unknown[] = [];
let paramCount = 1;
if (updates.name !== undefined) {
fields.push(`name = $${paramCount++}`);
values.push(updates.name);
}
if (updates.description !== undefined) {
fields.push(`description = $${paramCount++}`);
values.push(updates.description);
}
if (updates.allowed_origins !== undefined) {
fields.push(`allowed_origins = $${paramCount++}`);
values.push(JSON.stringify(updates.allowed_origins)); // SQLite stores arrays as JSON strings
}
if (updates.settings !== undefined) {
// Get current settings and merge
const currentProject = await this.getProjectById(projectId);
if (currentProject) {
const mergedSettings = {
...currentProject.settings,
...updates.settings,
};
fields.push(`settings = $${paramCount++}`);
values.push(JSON.stringify(mergedSettings));
}
}
if (updates.is_active !== undefined) {
fields.push(`is_active = $${paramCount++}`);
values.push(updates.is_active ? 1 : 0); // SQLite uses INTEGER 1/0 for booleans
}
if (fields.length === 0) {
return this.getProjectById(projectId);
}
fields.push(`updated_at = CURRENT_TIMESTAMP`);
values.push(projectId);
// SQLite doesn't support RETURNING *, so update and query back separately
await this.db.query(
`UPDATE projects SET ${fields.join(
", "
)} WHERE id = $${paramCount}`,
values
);
// Query back the updated row
return this.getProjectById(projectId);
} catch (error) {
this.logger.error("Failed to update project:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "updateProject",
});
}
}
/**
* Soft delete a project
*
* Marks a project as inactive (soft delete) rather than permanently removing it.
*
* @param {string} projectId - Project ID
* @returns {Promise<boolean>} True if deletion successful
* @throws {Error} If deletion fails or project not found
*
* @example
* const deleted = await projectsService.deleteProject('project-id');
*/
async deleteProject(projectId: string): Promise<boolean> {
try {
// Soft delete by setting is_active to false
const result = await this.db.query(
"UPDATE projects SET is_active = false, updated_at = CURRENT_TIMESTAMP WHERE id = $1",
[projectId]
);
return result.rowCount > 0;
} catch (error) {
this.logger.error("Failed to delete project:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "deleteProject",
});
}
}
/**
* Permanently delete a project
*
* Permanently removes a project and all associated data from the database.
* This action cannot be undone.
*
* @param {string} projectId - Project ID
* @returns {Promise<boolean>} True if deletion successful
* @throws {Error} If deletion fails or project not found
*
* @example
* const deleted = await projectsService.hardDeleteProject('project-id');
*/
async hardDeleteProject(projectId: string): Promise<boolean> {
try {
// Hard delete - removes all data
await this.db.query(
"DELETE FROM documents WHERE collection_id IN (SELECT id FROM collections WHERE project_id = $1)",
[projectId]
);
await this.db.query("DELETE FROM collections WHERE project_id = $1", [
projectId,
]);
await this.db.query("DELETE FROM files WHERE project_id = $1", [
projectId,
]);
await this.db.query("DELETE FROM api_keys WHERE project_id = $1", [
projectId,
]);
const result = await this.db.query("DELETE FROM projects WHERE id = $1", [
projectId,
]);
return result.rowCount > 0;
} catch (error) {
this.logger.error("Failed to hard delete project:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "hardDeleteProject",
});
}
}
// Project Statistics
/**
* Get project statistics
*
* Retrieves comprehensive statistics for a project including collections,
* documents, files, storage usage, API calls, and activity.
*
* @param {string} projectId - Project ID
* @returns {Promise<ProjectStatistics>} Project statistics
* @throws {Error} If query fails or project not found
*
* @example
* const stats = await projectsService.getProjectStatistics('project-id');
* console.log(`Total collections: ${stats.totalCollections}`);
* console.log(`Storage used: ${stats.storageUsed} bytes`);
*/
async getProjectStatistics(projectId: string): Promise<ProjectStatistics> {
try {
const [
collectionsResult,
documentsResult,
filesResult,
storageResult,
todayCallsResult,
monthCallsResult,
lastActivityResult,
topCollectionsResult,
recentActivityResult,
] = await Promise.all([
this.db.query(
"SELECT COUNT(*) FROM collections WHERE project_id = $1",
[projectId]
),
this.db.query(
"SELECT COUNT(*) FROM documents WHERE collection_id IN (SELECT id FROM collections WHERE project_id = $1)",
[projectId]
),
this.db.query("SELECT COUNT(*) FROM files WHERE project_id = $1", [
projectId,
]),
this.db.query(
"SELECT COALESCE(SUM(size), 0) as total_storage FROM files WHERE project_id = $1",
[projectId]
),
this.db.query(
"SELECT COUNT(*) FROM changelog WHERE project_id = $1 AND created_at >= CURRENT_DATE",
[projectId]
),
this.db.query(
// SQLite-compatible: use strftime to get first day of current month
"SELECT COUNT(*) FROM changelog WHERE project_id = $1 AND created_at >= date('now', 'start of month')",
[projectId]
),
this.db.query(
"SELECT MAX(created_at) FROM changelog WHERE project_id = $1",
[projectId]
),
this.db.query(
`
SELECT c.name, COUNT(d.id) as document_count, MAX(d.updated_at) as last_modified
FROM collections c
LEFT JOIN documents d ON c.id = d.collection_id
WHERE c.project_id = $1
GROUP BY c.id, c.name
ORDER BY document_count DESC
LIMIT 10
`,
[projectId]
),
this.db.query(
`
SELECT action, entity_type as entity, created_at as timestamp, performed_by as user
FROM changelog
WHERE project_id = $1
ORDER BY created_at DESC
LIMIT 20
`,
[projectId]
),
]);
const lastActivity = lastActivityResult.rows[0]
? (lastActivityResult.rows[0] as { max: string | null }).max
: null;
const baseResult = {
totalCollections: parseInt(
(collectionsResult.rows[0] as CountRow).count
),
totalDocuments: parseInt((documentsResult.rows[0] as CountRow).count),
totalFiles: parseInt((filesResult.rows[0] as CountRow).count),
storageUsed: parseInt(
(storageResult.rows[0] as { total_storage?: string }).total_storage ||
"0"
),
apiCallsToday: parseInt((todayCallsResult.rows[0] as CountRow).count),
apiCallsThisMonth: parseInt(
(monthCallsResult.rows[0] as CountRow).count
),
topCollections: topCollectionsResult.rows.map((row) => {
const typedRow = row as {
name: string;
document_count: string;
last_modified: string;
};
return {
name: typedRow.name,
documentCount: parseInt(typedRow.document_count),
lastModified: typedRow.last_modified,
};
}),
recentActivity: recentActivityResult.rows.map((row) => {
const typedRow = row as {
action: string;
entity: string;
timestamp: string;
user: string;
};
return {
action: typedRow.action,
entity: typedRow.entity,
timestamp: typedRow.timestamp,
user: typedRow.user,
};
}),
};
return lastActivity ? { ...baseResult, lastActivity } : baseResult;
} catch (error) {
this.logger.error("Failed to get project statistics:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getProjectStatistics",
});
}
}
// API Key Management
/**
* Get all API keys for a project
*
* Retrieves all API keys associated with a project.
*
* @param {string} projectId - Project ID
* @returns {Promise<ProjectApiKey[]>} Array of project API keys
* @throws {Error} If query fails
*
* @example
* const apiKeys = await projectsService.getProjectApiKeys('project-id');
*/
async getProjectApiKeys(projectId: string): Promise<ProjectApiKey[]> {
try {
const result = await this.db.query(
"SELECT * FROM api_keys WHERE project_id = $1 AND type = 'project' ORDER BY created_at DESC",
[projectId]
);
return result.rows as ProjectApiKey[];
} catch (error) {
this.logger.error("Failed to get project API keys:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getProjectApiKeys",
});
}
}
/**
* Create a new API key for a project
*
* Creates a new API key with specified scopes and optional expiration.
*
* @param {string} projectId - Project ID
* @param {Object} apiKeyData - API key data
* @param {string} apiKeyData.name - API key name/description
* @param {string[]} apiKeyData.scopes - Array of permission scopes
* @param {string} [apiKeyData.expires_at] - Optional expiration date (ISO string)
* @returns {Promise<ProjectApiKey>} Created API key (includes the key value)
* @throws {Error} If creation fails
*
* @example
* const apiKey = await projectsService.createProjectApiKey('project-id', {
* name: 'My API Key',
* scopes: ['collections:read', 'documents:write'],
* expires_at: '2024-12-31T23:59:59Z'
* });
* console.log(`API Key: ${apiKey.key}`); // Save this securely!
*/
async createProjectApiKey(
projectId: string,
apiKeyData: {
name: string;
scopes: string[];
expires_at?: string;
}
): Promise<ProjectApiKey> {
try {
const apiKey = `pk_${Math.random()
.toString(36)
.substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
// Generate key ID (SQLite doesn't support RETURNING *)
const keyId = crypto.randomUUID();
const now = new Date().toISOString();
// SQLite-compatible INSERT (no RETURNING *)
await this.db.query(
`INSERT INTO api_keys (id, key, name, type, project_id, scopes, expires_at, created_at, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
keyId,
apiKey,
apiKeyData.name,
"project",
projectId,
JSON.stringify(apiKeyData.scopes), // SQLite stores arrays as JSON strings
apiKeyData.expires_at,
now,
1, // is_active = true
]
);
// Query back the inserted row
const result = await this.db.query(
"SELECT * FROM api_keys WHERE id = $1",
[keyId]
);
return result.rows[0] as ProjectApiKey;
} catch (error) {
this.logger.error("Failed to create project API key:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "createProjectApiKey",
});
}
}
/**
* Regenerate a project's main API key
*
* Generates a new main API key for a project, invalidating the old one.
*
* @param {string} projectId - Project ID
* @returns {Promise<{newApiKey: string}>} New API key value
* @throws {Error} If regeneration fails or project not found
*
* @example
* const { newApiKey } = await projectsService.regenerateProjectApiKey('project-id');
* console.log(`New API Key: ${newApiKey}`); // Save this securely!
*/
async regenerateProjectApiKey(
projectId: string
): Promise<{ newApiKey: string }> {
try {
const newApiKey = `pk_${Math.random()
.toString(36)
.substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
await this.db.query(
"UPDATE projects SET api_key = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2",
[newApiKey, projectId]
);
return { newApiKey };
} catch (error) {
this.logger.error("Failed to regenerate project API key:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "regenerateProjectApiKey",
});
}
}
/**
* Get project activity log
*
* Retrieves activity log entries for a project with optional filtering.
*
* @param {string} projectId - Project ID
* @param {Object} [options={}] - Query options
* @param {number} [options.limit=50] - Maximum number of entries
* @param {number} [options.days] - Filter by number of days back
* @returns {Promise<Array>} Array of activity log entries
* @throws {Error} If query fails
*
* @example
* const activity = await projectsService.getProjectActivity('project-id', {
* limit: 50,
* days: 7
* });
*/
async getProjectActivity(
projectId: string,
options: {
limit?: number;
days?: number;
} = {}
): Promise<Array<{
id: string;
type: string;
timestamp: string;
details: Record<string, unknown>;
}>> {
try {
const { limit = 50, days } = options;
let query = `
SELECT
c.id,
c.action as type,
c.created_at as timestamp,
c.changes as details
FROM changelog c
WHERE c.project_id = $1
`;
const queryParams: unknown[] = [projectId];
let paramIndex = 2;
if (days) {
// Calculate cutoff date in JavaScript (SQLite doesn't support INTERVAL)
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
query += ` AND c.created_at >= $${paramIndex++}`;
queryParams.push(cutoffDate.toISOString());
}
query += ` ORDER BY c.created_at DESC LIMIT $${paramIndex}`;
queryParams.push(limit);
const result = await this.db.query(query, queryParams);
return result.rows.map((row) => {
const typedRow = row as {
id: string;
type: string;
timestamp: string;
details: Record<string, unknown>;
};
return {
id: typedRow.id,
type: typedRow.type,
timestamp: typedRow.timestamp,
details: typedRow.details || {},
};
});
} catch (error) {
this.logger.error("Failed to get project activity:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getProjectActivity",
});
}
}
/**
* Delete a project API key
*
* Permanently deletes an API key, revoking all access.
*
* @param {string} keyId - API key ID
* @returns {Promise<boolean>} True if deletion successful
* @throws {Error} If deletion fails or key not found
*
* @example
* const deleted = await projectsService.deleteProjectApiKey('key-id');
*/
async deleteProjectApiKey(keyId: string): Promise<boolean> {
try {
const result = await this.db.query(
"UPDATE api_keys SET is_active = false WHERE id = $1",
[keyId]
);
return result.rowCount > 0;
} catch (error) {
this.logger.error("Failed to delete project API key:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "deleteProjectApiKey",
});
}
}
// Project Settings
/**
* Update project settings
*
* Updates project configuration settings, merging with existing settings.
*
* @param {string} projectId - Project ID
* @param {Partial<ProjectSettings>} settings - Settings to update
* @returns {Promise<Project | null>} Updated project with new settings or null if not found
* @throws {Error} If update fails
*
* @example
* const updated = await projectsService.updateProjectSettings('project-id', {
* authentication_required: true,
* rate_limiting_enabled: true,
* max_file_size: 10485760 // 10MB
* });
*/
async updateProjectSettings(
projectId: string,
settings: Partial<ProjectSettings>
): Promise<Project | null> {
try {
const currentProject = await this.getProjectById(projectId);
if (!currentProject) {
return null;
}
const mergedSettings = { ...currentProject.settings, ...settings };
// SQLite doesn't support RETURNING *, so update and query back separately
await this.db.query(
"UPDATE projects SET settings = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2",
[JSON.stringify(mergedSettings), projectId]
);
// Query back the updated row
return this.getProjectById(projectId);
} catch (error) {
this.logger.error("Failed to update project settings:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "updateProjectSettings",
});
}
}
// Project Activity Tracking
/**
* Record an API call for a project
*
* Records that an API call was made for analytics and rate limiting.
*
* @param {string} projectId - Project ID
* @returns {Promise<void>}
* @throws {Error} If recording fails
*
* @example
* await projectsService.recordApiCall('project-id');
*/
async recordApiCall(projectId: string): Promise<void> {
try {
await this.db.query(
"UPDATE projects SET total_api_calls = total_api_calls + 1, last_api_call = CURRENT_TIMESTAMP WHERE id = $1",
[projectId]
);
} catch (error) {
this.logger.error("Failed to record API call:", error);
// Don't throw here as this shouldn't break the main operation
}
}
/**
* Get project by API key
*
* Retrieves the project associated with a given API key.
*
* @param {string} apiKey - API key value
* @returns {Promise<Project | null>} Project or null if key not found/invalid
* @throws {Error} If query fails
*
* @example
* const project = await projectsService.getProjectByApiKey('pk_...');
*/
async getProjectByApiKey(apiKey: string): Promise<Project | null> {
try {
const result = await this.db.query(
"SELECT * FROM projects WHERE api_key = $1 AND is_active = 1",
[apiKey]
);
return result.rows.length > 0 ? (result.rows[0] as Project) : null;
} catch (error) {
this.logger.error("Failed to get project by API key:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getProjectByApiKey",
});
}
}
}