import PocketBase from 'pocketbase';
import { appConfig } from './utils/config.js';
import { logger } from './utils/logger.js';
export class PocketBaseError extends Error {
status;
data;
constructor(message, status, data) {
super(message);
this.status = status;
this.data = data;
this.name = 'PocketBaseError';
}
}
export class PocketBaseService {
userClient;
adminClient;
isAdminAuthenticated = false;
constructor() {
this.userClient = new PocketBase(appConfig.pocketbase.url);
this.adminClient = new PocketBase(appConfig.pocketbase.url);
// Disable auto cancellation for concurrent requests
this.adminClient.autoCancellation(false);
this.userClient.autoCancellation(false);
// Setup auth change listeners
this.setupAuthListeners();
}
setupAuthListeners() {
this.userClient.authStore.onChange(() => {
logger.debug('User auth state changed', {
isValid: this.userClient.authStore.isValid,
model: this.userClient.authStore.model?.id,
});
});
this.adminClient.authStore.onChange(() => {
this.isAdminAuthenticated = this.adminClient.authStore.isValid;
logger.debug('Admin auth state changed', {
isValid: this.adminClient.authStore.isValid,
model: this.adminClient.authStore.model?.id,
});
});
}
handlePocketBaseError(error) {
if (error && typeof error === 'object' && 'response' in error) {
const pbError = error;
throw new PocketBaseError(pbError.response?.message ?? 'Unknown PocketBase error', pbError.response?.code, pbError.response?.data);
}
if (error instanceof Error) {
throw new PocketBaseError(error.message);
}
throw new PocketBaseError('Unknown error occurred');
}
// Admin authentication and management
async authenticateAdmin() {
try {
logger.info('Authenticating as superuser');
const authData = await this.adminClient
.collection('_superusers')
.authWithPassword(appConfig.pocketbase.superuser.email, appConfig.pocketbase.superuser.password, { autoRefreshThreshold: 30 * 60 });
this.isAdminAuthenticated = true;
logger.info('Superuser authentication successful');
return authData;
}
catch (error) {
logger.error('Superuser authentication failed', error);
this.handlePocketBaseError(error);
}
}
async ensureAdminAuth() {
if (!this.isAdminAuthenticated) {
await this.authenticateAdmin();
}
}
// User authentication methods
async loginUser(email, password, options) {
try {
logger.info('User login attempt', { email });
const authData = await this.userClient
.collection('users')
.authWithPassword(email, password, options);
logger.info('User login successful', { userId: authData.record.id });
return authData;
}
catch (error) {
logger.error('User login failed', { email, error });
this.handlePocketBaseError(error);
}
}
async registerUser(email, password, passwordConfirm, additionalData) {
try {
logger.info('User registration attempt', { email });
const authData = await this.userClient
.collection('users')
.create({
email,
password,
passwordConfirm,
...additionalData,
});
logger.info('User registration successful', { userId: authData.id });
// Auto-login after registration
return await this.loginUser(email, password);
}
catch (error) {
logger.error('User registration failed', { email, error });
this.handlePocketBaseError(error);
}
}
async refreshUserAuth(options) {
try {
if (!this.userClient.authStore.isValid) {
throw new PocketBaseError('No valid user session to refresh');
}
const authData = await this.userClient
.collection('users')
.authRefresh(options);
logger.info('User auth refresh successful');
return authData;
}
catch (error) {
logger.error('User auth refresh failed', error);
this.handlePocketBaseError(error);
}
}
logoutUser() {
this.userClient.authStore.clear();
logger.info('User logged out');
}
getCurrentUser() {
return this.userClient.authStore.model;
}
isUserAuthenticated() {
return this.userClient.authStore.isValid;
}
// Password management
async requestPasswordReset(email) {
try {
await this.userClient.collection('users').requestPasswordReset(email);
logger.info('Password reset requested', { email });
return true;
}
catch (error) {
logger.error('Password reset request failed', { email, error });
this.handlePocketBaseError(error);
}
}
async confirmPasswordReset(token, password, passwordConfirm) {
try {
await this.userClient
.collection('users')
.confirmPasswordReset(token, password, passwordConfirm);
logger.info('Password reset confirmed');
return true;
}
catch (error) {
logger.error('Password reset confirmation failed', error);
this.handlePocketBaseError(error);
}
}
// Collection management (requires admin)
async listCollections(options) {
await this.ensureAdminAuth();
try {
const listOptions = {};
if (options?.sort)
listOptions.sort = options.sort;
if (options?.filter)
listOptions.filter = options.filter;
const result = await this.adminClient.collections.getList(options?.page, options?.perPage, listOptions);
return result;
}
catch (error) {
logger.error('Failed to list collections', error);
this.handlePocketBaseError(error);
}
}
async getCollection(idOrName) {
await this.ensureAdminAuth();
try {
const collection = await this.adminClient.collections.getOne(idOrName);
return collection;
}
catch (error) {
logger.error('Failed to get collection', { idOrName, error });
this.handlePocketBaseError(error);
}
}
async createCollection(data) {
await this.ensureAdminAuth();
try {
const collection = await this.adminClient.collections.create(data);
logger.info('Collection created', { name: collection.name });
return collection;
}
catch (error) {
logger.error('Failed to create collection', { data, error });
this.handlePocketBaseError(error);
}
}
async updateCollection(idOrName, data) {
await this.ensureAdminAuth();
try {
const collection = await this.adminClient.collections.update(idOrName, data);
logger.info('Collection updated', { idOrName });
return collection;
}
catch (error) {
logger.error('Failed to update collection', { idOrName, data, error });
this.handlePocketBaseError(error);
}
}
async deleteCollection(idOrName) {
await this.ensureAdminAuth();
try {
await this.adminClient.collections.delete(idOrName);
logger.info('Collection deleted', { idOrName });
return true;
}
catch (error) {
logger.error('Failed to delete collection', { idOrName, error });
this.handlePocketBaseError(error);
}
}
// Record management
async listRecords(collection, options) {
try {
const client = this.isUserAuthenticated() ? this.userClient : this.adminClient;
if (!this.isUserAuthenticated()) {
await this.ensureAdminAuth();
}
const listOptions = {};
if (options?.sort)
listOptions.sort = options.sort;
if (options?.filter)
listOptions.filter = options.filter;
if (options?.expand)
listOptions.expand = options.expand;
if (options?.fields)
listOptions.fields = options.fields;
const result = await client.collection(collection).getList(options?.page, options?.perPage, listOptions);
return result;
}
catch (error) {
logger.error('Failed to list records', { collection, options, error });
this.handlePocketBaseError(error);
}
}
async getRecord(collection, id, options) {
try {
const client = this.isUserAuthenticated() ? this.userClient : this.adminClient;
if (!this.isUserAuthenticated()) {
await this.ensureAdminAuth();
}
const record = await client.collection(collection).getOne(id, options);
return record;
}
catch (error) {
logger.error('Failed to get record', { collection, id, error });
this.handlePocketBaseError(error);
}
}
async createRecord(collection, data, options) {
try {
const client = this.isUserAuthenticated() ? this.userClient : this.adminClient;
if (!this.isUserAuthenticated()) {
await this.ensureAdminAuth();
}
const record = await client.collection(collection).create(data, options);
logger.info('Record created', { collection, recordId: record.id });
return record;
}
catch (error) {
logger.error('Failed to create record', { collection, data, error });
this.handlePocketBaseError(error);
}
}
async updateRecord(collection, id, data, options) {
try {
const client = this.isUserAuthenticated() ? this.userClient : this.adminClient;
if (!this.isUserAuthenticated()) {
await this.ensureAdminAuth();
}
const record = await client.collection(collection).update(id, data, options);
logger.info('Record updated', { collection, recordId: id });
return record;
}
catch (error) {
logger.error('Failed to update record', { collection, id, data, error });
this.handlePocketBaseError(error);
}
}
async deleteRecord(collection, id) {
try {
const client = this.isUserAuthenticated() ? this.userClient : this.adminClient;
if (!this.isUserAuthenticated()) {
await this.ensureAdminAuth();
}
await client.collection(collection).delete(id);
logger.info('Record deleted', { collection, recordId: id });
return true;
}
catch (error) {
logger.error('Failed to delete record', { collection, id, error });
this.handlePocketBaseError(error);
}
}
// File management
getFileUrl(record, filename, options) {
const client = this.isUserAuthenticated() ? this.userClient : this.adminClient;
return client.files.getUrl(record, filename, options);
}
async getFileToken() {
try {
const client = this.isUserAuthenticated() ? this.userClient : this.adminClient;
if (!this.isUserAuthenticated()) {
await this.ensureAdminAuth();
}
const tokenResponse = await client.files.getToken();
return typeof tokenResponse === 'string' ? tokenResponse : tokenResponse.token;
}
catch (error) {
logger.error('Failed to get file token', error);
this.handlePocketBaseError(error);
}
}
// System operations (require admin)
async healthCheck() {
try {
const health = await this.adminClient.health.check();
return health;
}
catch (error) {
logger.error('Health check failed', error);
this.handlePocketBaseError(error);
}
}
async getLogs(options) {
await this.ensureAdminAuth();
try {
const listOptions = {};
if (options?.sort)
listOptions.sort = options.sort;
if (options?.filter)
listOptions.filter = options.filter;
const result = await this.adminClient.logs.getList(options?.page, options?.perPage, listOptions);
return result;
}
catch (error) {
logger.error('Failed to get logs', error);
this.handlePocketBaseError(error);
}
}
async createBackup(name) {
await this.ensureAdminAuth();
try {
await this.adminClient.backups.create(name || '');
logger.info('Backup created', { name });
}
catch (error) {
logger.error('Failed to create backup', { name, error });
this.handlePocketBaseError(error);
}
}
async listBackups() {
await this.ensureAdminAuth();
try {
const backups = await this.adminClient.backups.getFullList();
return backups;
}
catch (error) {
logger.error('Failed to list backups', error);
this.handlePocketBaseError(error);
}
}
// User impersonation (admin only)
async impersonateUser(recordId, duration) {
await this.ensureAdminAuth();
try {
// Note: Impersonation may not be available in all PocketBase versions
// This is a placeholder implementation
const impersonateClient = new (await import('pocketbase')).default(this.adminClient.baseUrl);
// For now, we'll create a new client and manually set auth
// In a real implementation, this would use the actual impersonate API
logger.warn('User impersonation is not fully implemented - using placeholder');
logger.info('User impersonation requested', { recordId, duration });
return impersonateClient;
}
catch (error) {
logger.error('Failed to impersonate user', { recordId, error });
this.handlePocketBaseError(error);
}
}
// Utility methods
getUserClient() {
return this.userClient;
}
getAdminClient() {
return this.adminClient;
}
}
// Export singleton instance
export const pocketBaseService = new PocketBaseService();
//# sourceMappingURL=pocketbase-service.js.map