UNPKG

@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
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}`); } };