@ddl-tech/wappler-pocketbase
Version:
PocketBase CRUD module for Wappler Server Connect with complete database operations, authentication, and file management
522 lines (426 loc) • 19.9 kB
JavaScript
const PocketBase = require('pocketbase/cjs');
const { toSystemPath } = require('../../../lib/core/path');
const fs = require('fs');
// Global PocketBase client instances cache
const clients = new Map();
// Helper function to get or create PocketBase client
function getClient(url, authToken = null) {
const key = `${url}_${authToken || 'anonymous'}`;
if (!clients.has(key)) {
const pb = new PocketBase(url);
// Set auth token if provided
if (authToken) {
pb.authStore.save(authToken, null);
}
clients.set(key, pb);
}
return clients.get(key);
}
// Helper function to handle file uploads
function processFiles(data) {
if (!data || typeof data !== 'object') return data;
const processedData = { ...data };
for (const [key, value] of Object.entries(processedData)) {
if (value && typeof value === 'string' && value.startsWith('/')) {
// Convert file path to system path
try {
const systemPath = toSystemPath(value);
if (fs.existsSync(systemPath)) {
const fileBuffer = fs.readFileSync(systemPath);
const fileName = value.split('/').pop();
processedData[key] = new File([fileBuffer], fileName);
}
} catch (error) {
// If file processing fails, keep original value
console.warn(`Failed to process file ${value}:`, error.message);
}
}
}
return processedData;
}
// Connect to PocketBase
exports.connect = async function(options) {
try {
const url = this.parseRequired(options.url, 'string', 'PocketBase URL is required');
const authToken = this.parseOptional(options.authToken, 'string', '');
const pb = getClient(url, authToken);
// Test connection
await pb.health.check();
// Store URL in session for other actions to use (but not auth token - you handle that yourself)
this.setSession('pb_url', url);
return {
success: true,
url: url,
isAuthenticated: pb.authStore.isValid,
user: pb.authStore.record,
authToken: authToken // Return the auth token so it can be used by other actions
};
} catch (error) {
throw new Error(`PocketBase connection failed: ${error.message}`);
}
};
// Authenticate user
exports.authenticate = async function(options) {
try {
console.log('PocketBase authenticate options:', options);
let url = this.parseOptional(options.url, 'string', '');
const email = this.parseRequired(options.email, 'string', 'Email is required');
// Handle password as either string or number, convert to string
let password = options.password;
if (password === undefined || password === null || password === '') {
throw new Error('Password is required');
}
password = String(password);
const collection = this.parseOptional(options.collection, 'string', 'users');
// Extract base URL if full endpoint URL is provided
if (url.includes('/api/collections/')) {
url = url.split('/api/collections/')[0];
console.log('Extracted base URL from full endpoint:', url);
}
console.log('Parsed values - Base URL:', url, 'Email:', email, 'Password type:', typeof password, 'Collection:', collection);
if (!url) {
throw new Error('PocketBase URL is required for authentication.');
}
const pb = getClient(url);
console.log('Attempting authentication with PocketBase...');
const authData = await pb.collection(collection).authWithPassword(email, password);
console.log('Authentication successful:', {
hasToken: !!authData.token,
hasUser: !!authData.record,
userId: authData.record?.id
});
return {
success: true,
token: authData.token,
user: authData.record,
collection: collection
};
} catch (error) {
console.error('PocketBase authenticate error:', error);
console.error('Error details:', {
message: error.message,
status: error.status,
data: error.data,
isAbort: error.isAbort
});
// Provide more specific error messages
if (error.status === 400) {
throw new Error(`Authentication failed: Invalid credentials. Please check your email and password.`);
} else if (error.status === 404) {
throw new Error(`Authentication failed: User collection '${collection}' not found.`);
} else {
throw new Error(`Authentication failed: ${error.message}`);
}
}
};
// Create record
exports.create = async function(options) {
try {
console.log('PocketBase create options:', options);
// Handle empty options object
if (!options || Object.keys(options).length === 0) {
throw new Error('No options provided. Please check your Wappler action configuration.');
}
const url = this.parseOptional(options.url, 'string', this.getSession('pb_url'));
const collection = this.parseRequired(options.collection, 'string', 'Collection name is required');
let data = this.parseRequired(options.data, 'string', 'Record data is required');
const authToken = this.parseOptional(options.authToken, 'string', this.getSession('pb_auth_token'));
console.log('Parsed values - URL:', url, 'Collection:', collection, 'Data:', data);
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
// Parse data if it's a string (JSON)
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (parseError) {
throw new Error(`Invalid JSON in data parameter: ${parseError.message}`);
}
}
// Ensure data is an object
if (!data || typeof data !== 'object') {
throw new Error('Record data must be a valid JSON object');
}
const pb = getClient(url, authToken);
const processedData = processFiles(data);
const record = await pb.collection(collection).create(processedData);
return {
success: true,
record: record,
id: record.id
};
} catch (error) {
console.error('PocketBase create error:', error);
throw new Error(`Create record failed: ${error.message}`);
}
};
// Get single record
exports.getOne = async function(options) {
try {
const url = this.parseOptional(options.url, 'string', this.getSession('pb_url'));
const collection = this.parseRequired(options.collection, 'string', 'Collection name is required');
const id = this.parseRequired(options.id, 'string', 'Record ID is required');
const expand = this.parseOptional(options.expand, 'string', '');
const fields = this.parseOptional(options.fields, 'string', '');
const authToken = this.parseOptional(options.authToken, 'string', this.getSession('pb_auth_token'));
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
const pb = getClient(url, authToken);
const queryOptions = {};
if (expand) queryOptions.expand = expand;
if (fields) queryOptions.fields = fields;
const record = await pb.collection(collection).getOne(id, queryOptions);
return {
success: true,
record: record
};
} catch (error) {
throw new Error(`Get record failed: ${error.message}`);
}
};
// Get list of records
exports.getList = async function(options) {
try {
const url = this.parseOptional(options.url, 'string', this.getSession('pb_url'));
const collection = this.parseRequired(options.collection, 'string', 'Collection name is required');
const page = this.parseOptional(options.page, 'number', 1);
const perPage = this.parseOptional(options.perPage, 'number', 30);
const filter = this.parseOptional(options.filter, 'string', '');
const sort = this.parseOptional(options.sort, 'string', '');
const expand = this.parseOptional(options.expand, 'string', '');
const fields = this.parseOptional(options.fields, 'string', '');
const authToken = this.parseOptional(options.authToken, 'string', this.getSession('pb_auth_token'));
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
const pb = getClient(url, authToken);
const queryOptions = {};
if (filter) queryOptions.filter = filter;
if (sort) queryOptions.sort = sort;
if (expand) queryOptions.expand = expand;
if (fields) queryOptions.fields = fields;
const result = await pb.collection(collection).getList(page, perPage, queryOptions);
return {
success: true,
items: result.items,
page: result.page,
perPage: result.perPage,
totalItems: result.totalItems,
totalPages: result.totalPages
};
} catch (error) {
throw new Error(`Get list failed: ${error.message}`);
}
};
// Get full list of records
exports.getFullList = async function(options) {
try {
console.log('PocketBase getFullList options:', options);
// Clean the URL - remove any invisible characters
let url = this.parseOptional(options.url, 'string', '');
if (url) {
url = url.trim().replace(/[\u200B-\u200D\uFEFF]/g, ''); // Remove zero-width characters
}
if (!url || url === '') {
url = this.getSession('pb_url');
}
const collection = this.parseRequired(options.collection, 'string', 'Collection name is required');
const filter = this.parseOptional(options.filter, 'string', '');
const sort = this.parseOptional(options.sort, 'string', '');
const expand = this.parseOptional(options.expand, 'string', '');
const fields = this.parseOptional(options.fields, 'string', '');
const batch = this.parseOptional(options.batch, 'number', 200);
const authToken = this.parseOptional(options.authToken, 'string', '');
console.log('Session pb_url:', this.getSession('pb_url'));
console.log('Session pb_auth_token present:', !!this.getSession('pb_auth_token'));
console.log('Parsed values - URL:', JSON.stringify(url), 'Collection:', collection, 'AuthToken present:', !!authToken);
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
const pb = getClient(url, authToken);
const queryOptions = { batch };
if (filter) queryOptions.filter = filter;
if (sort) queryOptions.sort = sort;
if (expand) queryOptions.expand = expand;
if (fields) queryOptions.fields = fields;
console.log('Attempting to get full list from PocketBase...');
const records = await pb.collection(collection).getFullList(queryOptions);
console.log('Get full list successful:', { count: records.length });
return {
success: true,
records: records,
count: records.length
};
} catch (error) {
console.error('PocketBase getFullList error:', error);
console.error('Error details:', {
message: error.message,
status: error.status,
data: error.data,
isAbort: error.isAbort
});
// Provide more specific error messages
if (error.status === 400) {
throw new Error(`Get full list failed: Invalid request. Check your collection name and parameters.`);
} else if (error.status === 404) {
throw new Error(`Get full list failed: Collection '${collection}' not found.`);
} else {
throw new Error(`Get full list failed: ${error.message}`);
}
}
};
// Update record
exports.update = async function(options) {
try {
console.log('PocketBase update options:', options);
const url = this.parseOptional(options.url, 'string', this.getSession('pb_url'));
const collection = this.parseRequired(options.collection, 'string', 'Collection name is required');
const id = this.parseRequired(options.id, 'string', 'Record ID is required');
let data = this.parseRequired(options.data, 'string', 'Update data is required');
const authToken = this.parseOptional(options.authToken, 'string', this.getSession('pb_auth_token'));
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
// Parse data if it's a string (JSON)
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (parseError) {
throw new Error(`Invalid JSON in data parameter: ${parseError.message}`);
}
}
// Ensure data is an object
if (!data || typeof data !== 'object') {
throw new Error('Update data must be a valid JSON object');
}
const pb = getClient(url, authToken);
const processedData = processFiles(data);
const record = await pb.collection(collection).update(id, processedData);
return {
success: true,
record: record
};
} catch (error) {
throw new Error(`Update record failed: ${error.message}`);
}
};
// Delete record
exports.delete = async function(options) {
try {
const url = this.parseOptional(options.url, 'string', this.getSession('pb_url'));
const collection = this.parseRequired(options.collection, 'string', 'Collection name is required');
const id = this.parseRequired(options.id, 'string', 'Record ID is required');
const authToken = this.parseOptional(options.authToken, 'string', this.getSession('pb_auth_token'));
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
const pb = getClient(url, authToken);
await pb.collection(collection).delete(id);
return {
success: true,
message: 'Record deleted successfully',
id: id
};
} catch (error) {
throw new Error(`Delete record failed: ${error.message}`);
}
};
// Get file URL
exports.getFileUrl = async function(options) {
try {
const url = this.parseOptional(options.url, 'string', this.getSession('pb_url'));
const collection = this.parseRequired(options.collection, 'string', 'Collection name is required');
const recordId = this.parseRequired(options.recordId, 'string', 'Record ID is required');
const filename = this.parseRequired(options.filename, 'string', 'Filename is required');
const thumb = this.parseOptional(options.thumb, 'string', '');
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
const pb = getClient(url);
// Create a mock record object for the file URL generation
const record = { id: recordId, collectionId: collection, collectionName: collection };
const queryParams = {};
if (thumb) queryParams.thumb = thumb;
const fileUrl = pb.files.getUrl(record, filename, queryParams);
return {
success: true,
url: fileUrl,
filename: filename
};
} catch (error) {
throw new Error(`Get file URL failed: ${error.message}`);
}
};
// Batch operations
exports.batch = async function(options) {
try {
console.log('PocketBase batch options:', options);
const url = this.parseOptional(options.url, 'string', this.getSession('pb_url'));
let operations = this.parseRequired(options.operations, 'string', 'Operations array is required');
const authToken = this.parseOptional(options.authToken, 'string', this.getSession('pb_auth_token'));
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
// Parse operations if it's a string (JSON)
if (typeof operations === 'string') {
try {
operations = JSON.parse(operations);
} catch (parseError) {
throw new Error(`Invalid JSON in operations parameter: ${parseError.message}`);
}
}
// Ensure operations is an array
if (!Array.isArray(operations)) {
throw new Error('Operations must be a valid JSON array');
}
const pb = getClient(url, authToken);
const batch = pb.createBatch();
// Process each operation
for (const operation of operations) {
const { type, collection, id, data } = operation;
switch (type) {
case 'create':
batch.collection(collection).create(processFiles(data));
break;
case 'update':
batch.collection(collection).update(id, processFiles(data));
break;
case 'delete':
batch.collection(collection).delete(id);
break;
case 'upsert':
batch.collection(collection).upsert(processFiles(data));
break;
default:
throw new Error(`Unknown batch operation type: ${type}`);
}
}
const result = await batch.send();
return {
success: true,
results: result,
operationsCount: operations.length
};
} catch (error) {
throw new Error(`Batch operations failed: ${error.message}`);
}
};
// Health check
exports.healthCheck = async function(options) {
try {
const url = this.parseOptional(options.url, 'string', this.getSession('pb_url'));
if (!url) {
throw new Error('PocketBase URL not found. Please connect first.');
}
const pb = getClient(url);
const health = await pb.health.check();
return {
success: true,
status: 'healthy',
data: health
};
} catch (error) {
throw new Error(`Health check failed: ${error.message}`);
}
};