@smartsamurai/krapi-sdk
Version:
KRAPI TypeScript SDK - Easy-to-use client SDK for connecting to self-hosted KRAPI servers (like Appwrite SDK)
1,526 lines (1,521 loc) • 850 kB
JavaScript
"use strict";
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/core/krapi-error.ts
function maskSensitiveValue(value) {
if (typeof value === "string") {
if (value.length > 10 && (value.toLowerCase().includes("password") || value.toLowerCase().includes("token") || value.toLowerCase().includes("secret") || value.toLowerCase().includes("key"))) {
return `${value.substring(0, 4)}****`;
}
if (value.length > 50) {
return `${value.substring(0, 20)}...[${value.length - 40} chars]...${value.substring(value.length - 20)}`;
}
}
return value;
}
function getErrorCodeFromMessage(message) {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes("unauthorized") || lowerMessage.includes("not authorized")) {
return "UNAUTHORIZED";
}
if (lowerMessage.includes("forbidden") || lowerMessage.includes("permission denied")) {
return "FORBIDDEN";
}
if (lowerMessage.includes("invalid credentials") || lowerMessage.includes("authentication failed")) {
return "UNAUTHORIZED";
}
if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) {
return "NOT_FOUND";
}
if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) {
return "CONFLICT";
}
if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) {
return "VALIDATION_ERROR";
}
if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) {
return "NETWORK_ERROR";
}
if (lowerMessage.includes("rate limit")) {
return "RATE_LIMIT_EXCEEDED";
}
if (lowerMessage.includes("internal server") || lowerMessage.includes("database error") || lowerMessage.includes("query failed")) {
return "INTERNAL_ERROR";
}
if (lowerMessage.includes("service unavailable") || lowerMessage.includes("temporarily unavailable")) {
return "SERVICE_UNAVAILABLE";
}
if (lowerMessage.includes("bad request")) {
return "BAD_REQUEST";
}
return void 0;
}
var KrapiError;
var init_krapi_error = __esm({
"src/core/krapi-error.ts"() {
"use strict";
KrapiError = class _KrapiError extends Error {
/**
* Create a new KrapiError instance
*
* @param {string} message - Error message
* @param {ErrorCode} [code] - Error code
* @param {number} [status] - HTTP status code
* @param {Record<string, unknown>} [details] - Additional error details
* @param {string} [requestId] - Request ID for tracking
* @param {unknown} [cause] - Original error that caused this error
*/
constructor(message, code = "INTERNAL_ERROR", status, details, requestId, cause) {
super(message);
this.name = "KrapiError";
this.code = code;
if (status !== void 0) this.status = status;
if (details !== void 0) this.details = details;
if (requestId !== void 0) this.requestId = requestId;
if (cause !== void 0) this.cause = cause;
this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
if (Error.captureStackTrace) {
Error.captureStackTrace(this, _KrapiError);
}
}
// Static factory methods
/**
* Create KrapiError from HttpError
*
* @param httpError - HttpError instance to convert
* @param context - Additional context to include
* @returns New KrapiError instance
*/
static fromHttpError(httpError, context) {
let errorCode = "INTERNAL_ERROR";
if (httpError.status) {
switch (httpError.status) {
case 400:
errorCode = "BAD_REQUEST";
break;
case 401:
errorCode = "UNAUTHORIZED";
break;
case 403:
errorCode = "FORBIDDEN";
break;
case 404:
errorCode = "NOT_FOUND";
break;
case 409:
errorCode = "CONFLICT";
break;
case 422:
errorCode = "UNPROCESSABLE_ENTITY";
break;
case 429:
errorCode = "RATE_LIMIT_EXCEEDED";
break;
case 500:
errorCode = "SERVER_ERROR";
break;
case 502:
case 503:
errorCode = "SERVICE_UNAVAILABLE";
break;
case 408:
errorCode = "TIMEOUT";
break;
}
}
return new _KrapiError(
httpError.message,
errorCode,
httpError.status,
{
httpError: {
code: httpError.code,
responseData: httpError.responseData
},
...context
}
);
}
/**
* Create KrapiError from generic Error
*
* @param error - Generic error to convert
* @param defaultCode - Default error code if cannot be inferred
* @param context - Additional context to include
* @returns New KrapiError instance
*/
static fromError(error, defaultCode = "INTERNAL_ERROR", context) {
const code = getErrorCodeFromMessage(error.message) || defaultCode;
return new _KrapiError(
error.message,
code,
void 0,
{
originalStack: error.stack,
...context
},
void 0,
error
);
}
/**
* Create validation error
*
* @param message - Error message
* @param field - Field that failed validation
* @param value - Invalid value (will be masked for security)
* @param context - Additional context
* @returns New validation error
*/
static validationError(message, field, value, context) {
return new _KrapiError(
message,
"VALIDATION_ERROR",
400,
{
field,
value: maskSensitiveValue(value),
...context
}
);
}
/**
* Create not found error
*
* @param message - Error message or resource type
* @param context - Additional context (optional)
* @returns New not found error
*/
static notFound(message, context) {
return new _KrapiError(
message,
"NOT_FOUND",
404,
context
);
}
/**
* Create authentication error
*
* @param message - Error message
* @param context - Additional context
* @returns New authentication error
*/
static authError(message = "Authentication required", context) {
return new _KrapiError(
message,
"UNAUTHORIZED",
401,
context
);
}
/**
* Create authorization error
*
* @param message - Error message
* @param context - Additional context
* @returns New authorization error
*/
static forbidden(message = "Access forbidden", context) {
return new _KrapiError(
message,
"FORBIDDEN",
403,
context
);
}
/**
* Create conflict error
*
* @param message - Error message
* @param context - Additional context
* @returns New conflict error
*/
static conflict(message, context) {
return new _KrapiError(
message,
"CONFLICT",
409,
context
);
}
/**
* Create internal server error
*
* @param message - Error message
* @param context - Additional context
* @returns New internal server error
*/
static internalError(message = "Internal server error", context) {
return new _KrapiError(
message,
"INTERNAL_ERROR",
500,
context
);
}
/**
* Create bad request error
*
* @param message - Error message
* @param context - Additional context
* @returns New bad request error
*/
static badRequest(message, context) {
return new _KrapiError(
message,
"BAD_REQUEST",
400,
context
);
}
/**
* Create service unavailable error
*
* @param message - Error message
* @param context - Additional context
* @returns New service unavailable error
*/
static serviceUnavailable(message = "Service unavailable", context) {
return new _KrapiError(
message,
"SERVICE_UNAVAILABLE",
503,
context
);
}
// Instance methods
/**
* Create a new error with additional context
*
* @param context - Additional context to merge
* @returns New KrapiError with merged context
*/
withContext(context) {
return new _KrapiError(
this.message,
this.code,
this.status,
{
...this.details || {},
...context
},
this.requestId,
this.cause
);
}
/**
* Check if this is a validation error
*
* @returns True if this is a validation error
*/
isValidationError() {
return this.code === "VALIDATION_ERROR";
}
/**
* Check if this is a not found error
*
* @returns True if this is a not found error
*/
isNotFound() {
return this.code === "NOT_FOUND";
}
/**
* Check if this is an authentication error
*
* @returns True if this is an authentication error
*/
isAuthError() {
return this.code === "UNAUTHORIZED" || this.code === "FORBIDDEN";
}
/**
* Check if this is a client error (4xx)
*
* @returns True if this is a client error
*/
isClientError() {
return this.status !== void 0 && this.status >= 400 && this.status < 500;
}
/**
* Check if this is a server error (5xx)
*
* @returns True if this is a server error
*/
isServerError() {
return this.status !== void 0 && this.status >= 500;
}
/**
* Convert error to JSON format
*
* @returns {Object} Error as JSON object
*/
toJSON() {
const result = {
code: this.code,
message: this.message,
timestamp: this.timestamp
};
if (this.status !== void 0) result.status = this.status;
if (this.details !== void 0) result.details = this.details;
if (this.requestId !== void 0) result.request_id = this.requestId;
if (this.cause !== void 0) {
result.cause = this.cause instanceof Error ? this.cause.message : String(this.cause);
}
return result;
}
/**
* Get detailed error message
*
* @returns {string} Detailed error message
*/
getDetailedMessage() {
let message = this.message;
if (this.code) {
message = `[${this.code}] ${message}`;
}
if (this.status) {
message = `${message} (HTTP ${this.status})`;
}
return message;
}
};
}
});
// src/http-clients/http-error.ts
var HttpError;
var init_http_error = __esm({
"src/http-clients/http-error.ts"() {
"use strict";
HttpError = class _HttpError extends Error {
/**
* Create a new HttpError instance
*
* @param {string} message - Error message
* @param {Object} options - Error options
* @param {number} [options.status] - HTTP status code
* @param {string} [options.method] - HTTP method
* @param {string} [options.url] - Request URL
* @param {Record<string, string>} [options.requestHeaders] - Request headers
* @param {unknown} [options.requestBody] - Request body/data sent
* @param {Record<string, unknown>} [options.requestQuery] - Request query parameters
* @param {unknown} [options.responseData] - Response data
* @param {Record<string, string>} [options.responseHeaders] - Response headers
* @param {string} [options.code] - Error code
* @param {unknown} [options.originalError] - Original error
*/
constructor(message, options = {}) {
super(message);
this.name = "HttpError";
if (options.status !== void 0) {
this.status = options.status;
}
if (options.method !== void 0) {
this.method = options.method;
}
if (options.url !== void 0) {
this.url = options.url;
}
if (options.requestHeaders !== void 0) {
this.requestHeaders = options.requestHeaders;
}
if (options.requestBody !== void 0) {
this.requestBody = options.requestBody;
}
if (options.requestQuery !== void 0) {
this.requestQuery = options.requestQuery;
}
if (options.responseData !== void 0) {
this.responseData = options.responseData;
}
if (options.responseHeaders !== void 0) {
this.responseHeaders = options.responseHeaders;
}
if (options.code !== void 0) {
this.code = options.code;
}
if (options.originalError !== void 0) {
this.originalError = options.originalError;
}
this.isApiError = options.status !== void 0 && options.status >= 400;
this.isNetworkError = options.status === void 0;
this.isAuthError = options.status === 401 || options.status === 403;
this.isClientError = options.status !== void 0 && options.status >= 400 && options.status < 500;
this.isServerError = options.status !== void 0 && options.status >= 500;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, _HttpError);
}
}
/**
* Get a detailed error message with all available information
*
* @returns {string} Detailed error message
*/
getDetailedMessage() {
const parts = [this.message];
if (this.status) {
parts.push(`(HTTP ${this.status})`);
}
if (this.method && this.url) {
parts.push(`[${this.method} ${this.url}]`);
}
if (this.code) {
parts.push(`[Code: ${this.code}]`);
}
if (this.responseData && typeof this.responseData === "object") {
const response = this.responseData;
if (response.error && typeof response.error === "string") {
parts.push(`Backend error: ${response.error}`);
} else if (response.message && typeof response.message === "string") {
parts.push(`Backend message: ${response.message}`);
}
}
return parts.join(" ");
}
/**
* Convert error to JSON for logging
*
* @returns {Record<string, unknown>} JSON representation
*/
toJSON() {
return {
name: this.name,
message: this.message,
status: this.status,
method: this.method,
url: this.url,
code: this.code,
isApiError: this.isApiError,
isNetworkError: this.isNetworkError,
isAuthError: this.isAuthError,
isClientError: this.isClientError,
isServerError: this.isServerError,
requestHeaders: this.requestHeaders,
requestBody: this.requestBody,
requestQuery: this.requestQuery,
responseData: this.responseData,
responseHeaders: this.responseHeaders,
originalError: this.originalError,
stack: this.stack
};
}
};
}
});
// src/utils/error-handler.ts
var error_handler_exports = {};
__export(error_handler_exports, {
createRequestId: () => createRequestId,
enrichError: () => enrichError,
getErrorCodeFromMessage: () => getErrorCodeFromMessage2,
getErrorCodeFromStatus: () => getErrorCodeFromStatus,
normalizeError: () => normalizeError,
preserveErrorContext: () => preserveErrorContext,
transformHttpError: () => transformHttpError
});
function normalizeError(error, defaultCode = "INTERNAL_ERROR", context) {
if (error instanceof KrapiError) {
return context ? enrichError(error, context) : error;
}
if (error instanceof HttpError) {
return transformHttpError(error, context);
}
if (error instanceof Error) {
const code = getErrorCodeFromMessage2(error.message);
return new KrapiError(
error.message,
code,
void 0,
{
...context,
originalError: error,
stack: error.stack
}
);
}
if (typeof error === "string") {
const code = getErrorCodeFromMessage2(error);
return new KrapiError(
error,
code,
void 0,
context
);
}
return new KrapiError(
"An unexpected error occurred",
defaultCode,
void 0,
{
...context,
originalError: error
}
);
}
function transformHttpError(httpError, context) {
const errorCode = getErrorCodeFromStatus(httpError.status);
let message = httpError.message;
if (httpError.status && !message.includes(`HTTP ${httpError.status}`)) {
message = `${message} (HTTP ${httpError.status})`;
}
const krapiError = new KrapiError(
message,
errorCode,
httpError.status,
{
...context,
httpError: {
method: httpError.method,
url: httpError.url,
status: httpError.status,
code: httpError.code,
isApiError: httpError.isApiError,
isNetworkError: httpError.isNetworkError,
isAuthError: httpError.isAuthError,
isClientError: httpError.isClientError,
isServerError: httpError.isServerError,
// Request context - what was sent
requestHeaders: httpError.requestHeaders,
requestBody: httpError.requestBody,
requestQuery: httpError.requestQuery,
// Response context - what was received
responseData: httpError.responseData,
responseHeaders: httpError.responseHeaders
},
originalError: httpError
}
);
return krapiError;
}
function enrichError(error, context) {
const mergedDetails = {
...error.details || {},
...context
};
if (error.details?.originalError) {
mergedDetails.originalError = error.details.originalError;
}
return new KrapiError(
error.message,
error.code,
error.status,
mergedDetails,
error.requestId,
error.cause
);
}
function getErrorCodeFromStatus(status) {
if (!status) {
return "NETWORK_ERROR";
}
switch (status) {
case 400:
return "BAD_REQUEST";
case 401:
return "UNAUTHORIZED";
case 403:
return "FORBIDDEN";
case 404:
return "NOT_FOUND";
case 409:
return "CONFLICT";
case 422:
return "UNPROCESSABLE_ENTITY";
case 429:
return "RATE_LIMIT_EXCEEDED";
case 500:
return "SERVER_ERROR";
case 502:
case 503:
case 504:
return "SERVICE_UNAVAILABLE";
case 408:
return "TIMEOUT";
default:
if (status >= 400 && status < 500) {
return "BAD_REQUEST";
}
if (status >= 500) {
return "SERVER_ERROR";
}
return "INTERNAL_ERROR";
}
}
function getErrorCodeFromMessage2(message) {
const lowerMessage = message.toLowerCase();
if (lowerMessage.includes("unauthorized") || lowerMessage.includes("not authorized")) {
return "UNAUTHORIZED";
}
if (lowerMessage.includes("forbidden") || lowerMessage.includes("permission denied")) {
return "FORBIDDEN";
}
if (lowerMessage.includes("invalid credentials") || lowerMessage.includes("authentication failed")) {
return "UNAUTHORIZED";
}
if (lowerMessage.includes("not found") || lowerMessage.includes("does not exist")) {
return "NOT_FOUND";
}
if (lowerMessage.includes("already exists") || lowerMessage.includes("duplicate")) {
return "CONFLICT";
}
if (lowerMessage.includes("validation") || lowerMessage.includes("invalid") || lowerMessage.includes("required") || lowerMessage.includes("missing")) {
return "VALIDATION_ERROR";
}
if (lowerMessage.includes("network") || lowerMessage.includes("connection") || lowerMessage.includes("timeout")) {
return "NETWORK_ERROR";
}
if (lowerMessage.includes("rate limit")) {
return "RATE_LIMIT_EXCEEDED";
}
if (lowerMessage.includes("internal server") || lowerMessage.includes("database error") || lowerMessage.includes("query failed")) {
return "INTERNAL_ERROR";
}
if (lowerMessage.includes("service unavailable") || lowerMessage.includes("temporarily unavailable")) {
return "SERVICE_UNAVAILABLE";
}
if (lowerMessage.includes("bad request")) {
return "BAD_REQUEST";
}
return "INTERNAL_ERROR";
}
function preserveErrorContext(error, additionalContext) {
if (error instanceof KrapiError) {
return additionalContext ? enrichError(error, additionalContext) : error;
}
return normalizeError(error, void 0, additionalContext);
}
function createRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
var init_error_handler = __esm({
"src/utils/error-handler.ts"() {
"use strict";
init_krapi_error();
init_http_error();
}
});
// src/utils/error-logger.ts
function logError(logger, error, context, level = "error") {
const logEntry = createErrorLogEntry(error, context);
switch (level) {
case "error":
logger.error(formatErrorMessage(logEntry), logEntry);
break;
case "warn":
logger.warn(formatErrorMessage(logEntry), logEntry);
break;
case "info":
logger.info(formatErrorMessage(logEntry), logEntry);
break;
case "debug":
logger.debug(formatErrorMessage(logEntry), logEntry);
break;
}
}
function createErrorLogEntry(error, context) {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
if (error instanceof HttpError) {
const httpContext = {
...context || {},
...error.method ? { httpMethod: error.method } : {},
...error.url ? { httpUrl: error.url } : {},
...error.status !== void 0 ? { httpStatus: error.status } : {},
...error.requestHeaders ? { requestHeaders: error.requestHeaders } : {},
...error.requestBody !== void 0 ? { requestBody: error.requestBody } : {},
...error.requestQuery ? { requestQuery: error.requestQuery } : {},
...error.responseData !== void 0 ? { responseData: error.responseData } : {},
...error.responseHeaders ? { responseHeaders: error.responseHeaders } : {}
};
const logEntry2 = {
timestamp,
level: "error",
message: error.message,
error: {
code: error.code || "HTTP_ERROR",
message: error.message,
...error.status !== void 0 ? { status: error.status } : {},
details: {
isApiError: error.isApiError,
isNetworkError: error.isNetworkError,
isAuthError: error.isAuthError,
isClientError: error.isClientError,
isServerError: error.isServerError,
originalError: error.originalError
}
},
stack: error.stack,
context: httpContext
};
return logEntry2;
}
if (error instanceof KrapiError) {
const httpErrorDetails = error.details?.httpError;
const enhancedContext = {
...error.requestId ? { requestId: error.requestId } : {},
...context,
// Extract HTTP context from error details
...httpErrorDetails ? {
httpMethod: httpErrorDetails.method,
httpUrl: httpErrorDetails.url,
httpStatus: httpErrorDetails.status,
requestBody: httpErrorDetails.requestBody,
requestQuery: httpErrorDetails.requestQuery,
requestHeaders: httpErrorDetails.requestHeaders,
responseData: httpErrorDetails.responseData,
responseHeaders: httpErrorDetails.responseHeaders
} : {},
// Include all error details
...error.details ? { errorDetails: error.details } : {}
};
const logEntry2 = {
timestamp,
level: getLogLevelFromError(error),
message: error.message,
error: {
code: error.code,
message: error.message,
...error.status !== void 0 ? { status: error.status } : {},
...error.details ? { details: error.details } : {}
},
stack: error.stack,
context: enhancedContext
};
return logEntry2;
}
if (error instanceof Error) {
const logEntry2 = {
timestamp,
level: "error",
message: error.message,
error: {
code: "UNKNOWN_ERROR",
message: error.message,
details: {
errorName: error.name,
originalError: error
}
},
stack: error.stack,
...context ? { context } : {}
};
return logEntry2;
}
const errorString = String(error);
const logEntry = {
timestamp,
level: "error",
message: errorString,
error: {
code: "UNKNOWN_ERROR",
message: errorString,
details: {
errorType: typeof error,
errorValue: error
}
},
...context ? { context } : {}
};
return logEntry;
}
function formatErrorMessage(entry) {
const parts = [
`[${entry.error.code}]`,
entry.message
];
if (entry.error.status) {
parts.push(`(HTTP ${entry.error.status})`);
}
if (entry.context?.operation) {
parts.push(`[${entry.context.operation}]`);
}
if (entry.context?.requestId) {
parts.push(`[Request: ${entry.context.requestId}]`);
}
return parts.join(" ");
}
function getLogLevelFromError(error) {
if (error.status && error.status >= 500) {
return "error";
}
if (error.status && error.status >= 400 && error.status < 500) {
if (error.code === "VALIDATION_ERROR" || error.code === "BAD_REQUEST") {
return "warn";
}
return "error";
}
if (error.code === "UNAUTHORIZED" || error.code === "FORBIDDEN") {
return "warn";
}
if (error.code === "NOT_FOUND") {
return "info";
}
return "error";
}
function logServiceOperationError(logger, error, service, operation, inputData, additionalContext) {
const enhancedContext = {
service,
operation,
inputData: inputData || {},
...additionalContext || {}
};
logError(logger, error, enhancedContext);
}
var init_error_logger = __esm({
"src/utils/error-logger.ts"() {
"use strict";
init_krapi_error();
init_http_error();
}
});
// src/admin-service.ts
var admin_service_exports = {};
__export(admin_service_exports, {
AdminService: () => AdminService
});
var import_crypto7, AdminService;
var init_admin_service = __esm({
"src/admin-service.ts"() {
"use strict";
import_crypto7 = __toESM(require("crypto"));
init_krapi_error();
init_error_handler();
init_error_logger();
AdminService = class {
/**
* Create a new AdminService instance
*
* @param {DatabaseConnection} databaseConnection - Database connection
* @param {Logger} logger - Logger instance
*/
constructor(databaseConnection, logger) {
this.db = databaseConnection;
this.logger = logger;
}
/**
* Set backup service (called from BackendSDK constructor)
*
* @param {BackupService} backupService - Backup service instance
* @returns {void}
*/
setBackupService(backupService) {
this._backupService = backupService;
}
/**
* Get all admin users
*
* @param {Object} [options] - Query options
* @param {number} [options.limit] - Maximum number of users
* @param {number} [options.offset] - Number of users to skip
* @param {string} [options.search] - Search term for username/email
* @param {boolean} [options.active] - Filter by active status
* @returns {Promise<AdminUser[]>} Array of admin users
* @throws {Error} If query fails
*
* @example
* const users = await adminService.getUsers({ limit: 10, active: true });
*/
async getUsers(options) {
try {
let query = "SELECT * FROM admin_users WHERE 1=1";
const params = [];
let paramCount = 0;
if (options?.active !== void 0) {
paramCount++;
query += ` AND is_active = $${paramCount}`;
params.push(options.active);
}
if (options?.search) {
paramCount++;
query += ` AND (username LIKE $${paramCount} OR email 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;
} catch (error) {
logServiceOperationError(
this.logger,
error,
"AdminService",
"getUsers",
{ options },
{}
);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getUsers",
options
});
}
}
/**
* Get admin user by ID
*
* @param {string} userId - User ID
* @returns {Promise<AdminUser | null>} Admin user or null if not found
* @throws {Error} If query fails
*
* @example
* const user = await adminService.getUserById('user-id');
*/
async getUserById(userId) {
try {
const result = await this.db.query(
"SELECT * FROM admin_users WHERE id = $1",
[userId]
);
return result.rows.length > 0 ? result.rows[0] : null;
} catch (error) {
logServiceOperationError(
this.logger,
error,
"AdminService",
"getUserById",
{ userId },
{}
);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getUserById",
userId
});
}
}
/**
* Create a new admin user
*
* Creates a new admin user account with the provided data.
*
* @param {Omit<AdminUser, "id" | "created_at" | "updated_at">} userData - User creation data
* @param {string} userData.username - Username (required)
* @param {string} userData.email - Email address (required)
* @param {string} userData.password_hash - Hashed password (required)
* @param {string} userData.role - User role
* @param {string} userData.access_level - Access level
* @param {string[]} userData.permissions - Permission scopes
* @param {boolean} userData.active - Whether user is active
* @param {string} [userData.api_key] - Optional API key
* @returns {Promise<AdminUser>} Created admin user
* @throws {Error} If creation fails or user already exists
*
* @example
* const user = await adminService.createUser({
* username: 'newadmin',
* email: 'admin@example.com',
* password_hash: 'hashed_password',
* role: 'admin',
* access_level: 'full',
* permissions: ['admin:read', 'admin:write'],
* active: true
* });
*/
async createUser(userData) {
try {
const userId = import_crypto7.default.randomUUID();
const now = (/* @__PURE__ */ new Date()).toISOString();
await this.db.query(
`INSERT INTO admin_users (id, username, email, password_hash, role, access_level, permissions, is_active, api_key, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
[
userId,
userData.username,
userData.email,
userData.password_hash,
userData.role,
userData.access_level,
JSON.stringify(userData.permissions),
// SQLite stores arrays as JSON strings
userData.active ? 1 : 0,
// SQLite uses INTEGER 1/0 for booleans
userData.api_key,
now,
now
]
);
const result = await this.db.query(
"SELECT * FROM admin_users WHERE id = $1",
[userId]
);
return result.rows[0];
} catch (error) {
const inputData = {
username: userData.username,
email: userData.email,
role: userData.role,
access_level: userData.access_level,
permissions: userData.permissions
// Note: password is intentionally excluded for security
};
logServiceOperationError(
this.logger,
error,
"AdminService",
"createUser",
inputData,
{}
);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "createUser",
username: userData.username,
email: userData.email
});
}
}
/**
* Update an admin user
*
* Updates admin user information with the provided data.
*
* @param {string} userId - User ID
* @param {Partial<AdminUser>} updates - User update data
* @returns {Promise<AdminUser | null>} Updated user or null if not found
* @throws {Error} If update fails
*
* @example
* const updated = await adminService.updateUser('user-id', {
* role: 'master_admin',
* permissions: ['master']
* });
*/
async updateUser(userId, updates) {
try {
const fields = [];
const values = [];
let paramCount = 1;
if (updates.username !== void 0) {
fields.push(`username = $${paramCount++}`);
values.push(updates.username);
}
if (updates.email !== void 0) {
fields.push(`email = $${paramCount++}`);
values.push(updates.email);
}
if (updates.role !== void 0) {
fields.push(`role = $${paramCount++}`);
values.push(updates.role);
}
if (updates.access_level !== void 0) {
fields.push(`access_level = $${paramCount++}`);
values.push(updates.access_level);
}
if (updates.permissions !== void 0) {
fields.push(`permissions = $${paramCount++}`);
values.push(updates.permissions);
}
if (updates.active !== void 0) {
fields.push(`is_active = $${paramCount++}`);
values.push(updates.active);
}
if (updates.api_key !== void 0) {
fields.push(`api_key = $${paramCount++}`);
values.push(updates.api_key);
}
if (fields.length === 0) {
return this.getUserById(userId);
}
fields.push(`updated_at = $${paramCount++}`);
values.push((/* @__PURE__ */ new Date()).toISOString());
values.push(userId);
await this.db.query(
`UPDATE admin_users SET ${fields.join(
", "
)} WHERE id = $${paramCount}`,
values
);
const result = await this.db.query(
"SELECT * FROM admin_users WHERE id = $1",
[userId]
);
return result.rows.length > 0 ? result.rows[0] : null;
} catch (error) {
this.logger.error("Failed to update admin user:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "updateUser",
userId
});
}
}
/**
* Delete an admin user
*
* Permanently deletes an admin user from the database.
*
* @param {string} userId - User ID
* @returns {Promise<boolean>} True if deletion successful
* @throws {Error} If deletion fails
*
* @example
* const deleted = await adminService.deleteUser('user-id');
*/
async deleteUser(userId) {
try {
const result = await this.db.query(
"DELETE FROM admin_users WHERE id = $1",
[userId]
);
return result.rowCount > 0;
} catch (error) {
this.logger.error("Failed to delete admin user:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "deleteUser",
userId
});
}
}
/**
* Toggle user active status
*
* Toggles a user's active/inactive status.
*
* @param {string} userId - User ID
* @returns {Promise<AdminUser | null>} Updated user or null if not found
* @throws {Error} If toggle fails
*
* @example
* const user = await adminService.toggleUserStatus('user-id');
*/
async toggleUserStatus(userId) {
try {
const now = (/* @__PURE__ */ new Date()).toISOString();
await this.db.query(
`UPDATE admin_users
SET is_active = CASE WHEN is_active = 1 THEN 0 ELSE 1 END, updated_at = $1
WHERE id = $2`,
[now, userId]
);
const result = await this.db.query(
"SELECT * FROM admin_users WHERE id = $1",
[userId]
);
return result.rows.length > 0 ? result.rows[0] : null;
} catch (error) {
this.logger.error("Failed to toggle user status:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "toggleUserStatus",
userId
});
}
}
/**
* Get all API keys for a user
*
* Retrieves all API keys owned by a specific admin user.
*
* @param {string} userId - User ID
* @returns {Promise<ApiKey[]>} Array of API keys
* @throws {Error} If query fails
*
* @example
* const apiKeys = await adminService.getUserApiKeys('user-id');
*/
async getUserApiKeys(userId) {
try {
const result = await this.db.query(
"SELECT * FROM api_keys WHERE owner_id = $1 AND type = 'admin' ORDER BY created_at DESC",
[userId]
);
return result.rows;
} catch (error) {
this.logger.error("Failed to get user API keys:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "getUserApiKeys",
userId
});
}
}
/**
* Create an API key for a user
*
* Creates a new admin API key for a specific user.
*
* @param {string} userId - User ID
* @param {Object} apiKeyData - API key data
* @param {string} apiKeyData.name - API key name
* @param {string} apiKeyData.key - API key value
* @param {string[]} apiKeyData.scopes - Permission scopes
* @param {string[]} [apiKeyData.project_ids] - Project IDs (for project-scoped keys)
* @param {string} [apiKeyData.expires_at] - Expiration date
* @returns {Promise<ApiKey>} Created API key
* @throws {Error} If creation fails
*
* @example
* const apiKey = await adminService.createUserApiKey('user-id', {
* name: 'My API Key',
* key: 'ak_...',
* scopes: ['admin:read', 'admin:write']
* });
*/
async createUserApiKey(userId, apiKeyData) {
try {
const keyId = import_crypto7.default.randomUUID();
const now = (/* @__PURE__ */ new Date()).toISOString();
await this.db.query(
`INSERT INTO api_keys (id, key, name, type, owner_id, scopes, project_ids, expires_at, created_at, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
[
keyId,
apiKeyData.key,
apiKeyData.name,
"admin",
userId,
JSON.stringify(apiKeyData.scopes),
// SQLite stores arrays as JSON strings
JSON.stringify(apiKeyData.project_ids || []),
// SQLite stores arrays as JSON strings
apiKeyData.expires_at,
now,
1
// is_active = true
]
);
const result = await this.db.query(
"SELECT * FROM api_keys WHERE id = $1",
[keyId]
);
return result.rows[0];
} catch (error) {
this.logger.error("Failed to create user API key:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "createUserApiKey",
userId
});
}
}
/**
* Create an API key (with auto-generation)
*
* Creates a new admin API key with auto-generated key value.
*
* @param {string} userId - User ID
* @param {Object} keyData - API key data
* @param {string} keyData.name - API key name
* @param {string[]} keyData.permissions - Permission scopes
* @param {string} [keyData.expires_at] - Expiration date
* @returns {Promise<{key: string, data: ApiKey}>} Generated key value and API key data
* @throws {Error} If creation fails
*
* @example
* const { key, data } = await adminService.createApiKey('user-id', {
* name: 'My API Key',
* permissions: ['admin:read']
* });
* console.log(`API Key: ${key}`); // Save this securely!
*/
async createApiKey(userId, keyData) {
try {
const apiKey = `ak_${Math.random().toString(36).substring(2, 15)}${Math.random().toString(36).substring(2, 15)}`;
const keyId = import_crypto7.default.randomUUID();
const now = (/* @__PURE__ */ new Date()).toISOString();
await this.db.query(
`INSERT INTO api_keys (id, key, name, type, owner_id, scopes, expires_at, created_at, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
[
keyId,
apiKey,
keyData.name,
"admin",
userId,
JSON.stringify(keyData.permissions),
// SQLite stores arrays as JSON strings
keyData.expires_at,
now,
1
// is_active = true
]
);
const result = await this.db.query(
"SELECT * FROM api_keys WHERE id = $1",
[keyId]
);
const createdKey = result.rows[0];
return {
key: apiKey,
data: createdKey
};
} catch (error) {
this.logger.error("Failed to create API key:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "createApiKey"
});
}
}
/**
* Create a master API key
*
* Creates a master API key with full system access.
*
* @returns {Promise<ApiKey>} Created master API key
* @throws {Error} If creation fails
*
* @example
* const masterKey = await adminService.createMasterApiKey();
* console.log(`Master API Key: ${masterKey.key}`); // Save this securely!
*/
async createMasterApiKey() {
try {
const masterKey = `mak_${Math.random().toString(36).substring(2, 15)}`;
const keyId = import_crypto7.default.randomUUID();
const now = (/* @__PURE__ */ new Date()).toISOString();
await this.db.query(
`INSERT INTO api_keys (id, key, name, type, owner_id, scopes, created_at, is_active)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[
keyId,
masterKey,
"Master API Key",
"master",
"system",
// System owner for master key
JSON.stringify(["master"]),
// SQLite stores arrays as JSON strings
now,
1
// is_active = true
]
);
const result = await this.db.query(
"SELECT * FROM api_keys WHERE id = $1",
[keyId]
);
return result.rows[0];
} catch (error) {
this.logger.error("Failed to create master API key:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "createMasterApiKey"
});
}
}
/**
* Delete an API key
*
* Soft deletes an API key by marking it as inactive.
*
* @param {string} keyId - API key ID
* @returns {Promise<boolean>} True if deletion successful
* @throws {Error} If deletion fails
*
* @example
* const deleted = await adminService.deleteApiKey('key-id');
*/
async deleteApiKey(keyId) {
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 API key:", error);
throw normalizeError(error, "INTERNAL_ERROR", {
operation: "deleteApiKey"
});
}
}
/**
* Get all API keys for a project
*
* Retrieves all API keys associated with a project.
*
* @param {string} projectId - Project ID
* @returns {Promise<ApiKey[]>} Array of project API keys
* @throws {Error} If query fails
*
* @example
* const apiKeys = await adminService.getProjectApiKeys('project-id');
*/
async getProjectApiKeys(projectId) {
try {
const result = await this.db.query(
`SELECT * FROM api_keys
WHERE (
project_ids LIKE $1
OR project_ids LIKE $2
OR project_ids LIKE $3
OR JSON_EXTRACT(project_ids, '$') IS NOT NULL
AND EXISTS (
SELECT 1 FROM json_each(project_ids)
WHERE json_each.value = $4
)
) AND is_active = true
ORDER BY created_at DESC`,
[
`%"${projectId}"%`,
`%"${projectId}",%`,
`%,"${projectId}"%`,
projectId
]
);
if (result.rows.length === 0) {
const fallbackResult = await this.db.query(
`SELECT * FROM api_keys
WHERE project_ids LIKE $1 AND is_active = true
ORDER BY created_at DESC`,
[`%${projectId}%`]
);
return fallbackResult.rows;
}
return result.rows;
} catch {
try {
const fallbackResult = await this.db.query(
`SELECT * FROM api_keys
WHERE project_ids LIKE $1 AND is_active = true
ORDER BY created_at DESC`,
[`%${projectId}%`]
);
return fallbackResult.rows;
} catch (fallbackError) {
this.logger.error("Failed to get project API keys:", fallbackError);
throw normalizeError(fallbackError, "INTERNAL_ERROR", {
operation: "getProjectApiKeys"
});
}
}
}
/**
* Create a project API key
*
* Creates a new API key for a project with auto-generated key value.
*
* @param {string} projectId - Project ID
* @param {Object} keyData - API key data
* @param {string} keyData.name - API key name
* @param {string} [keyData.description] - API key description
* @param {string[]} keyData.scopes - Permission scopes
* @param {string} [keyData.expires_at] - Expiration date
* @pa