UNPKG

baseflow-local-client

Version:

Official TypeScript/JavaScript client for BaseFlow Local - a local-first BaaS with SQLite database, authentication, file storage, and real-time features

659 lines (654 loc) 19.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); // BaseFlow Local Query Builder /** * A thenable class that represents a BaseFlow Local request. */ class LocalRequestBuilder { constructor(client, table) { this.method = 'GET'; this.params = {}; this.body = null; this.headers = {}; this.idFilter = null; this.isSingle = false; this.client = client; this.table = table; this.path = `/api/tables/${table}`; } /** * Executes the request and returns a promise that resolves with the response. */ async then(onfulfilled, onrejected) { try { const response = await this.client.request(this.method, this.path, { params: this.params, body: this.body, headers: this.headers }); // Unwrap the server response structure // Server returns { success: true, data: ..., count, total } but client expects { data: ..., error: ... } let finalResponse = response; if (response.data && typeof response.data === 'object') { if ('success' in response.data && 'data' in response.data) { // Server wrapped the data with success flag, unwrap it let unwrappedData = response.data.data; // If single() was called and data is an array, return first element if (this.isSingle && Array.isArray(unwrappedData)) { unwrappedData = unwrappedData.length > 0 ? unwrappedData[0] : null; } finalResponse = { data: unwrappedData, error: response.error, status: response.status, statusText: response.statusText }; } else if ('data' in response.data) { // Server wrapped the data without success flag, unwrap it let unwrappedData = response.data.data; // If single() was called and data is an array, return first element if (this.isSingle && Array.isArray(unwrappedData)) { unwrappedData = unwrappedData.length > 0 ? unwrappedData[0] : null; } finalResponse = { data: unwrappedData, error: response.error, status: response.status, statusText: response.statusText }; } } if (onfulfilled) { return onfulfilled(finalResponse); } return finalResponse; } catch (error) { if (onrejected) { return onrejected(error); } throw error; } } } /** * A builder for creating filters. */ class LocalFilterBuilderImpl extends LocalRequestBuilder { /** * Adds an 'equals' filter to the query. */ eq(column, value) { // Track ID filter for UPDATE operations only // DELETE can use filters on the base path if (column === 'id' && this.method === 'PUT') { this.idFilter = value; this.path = `/api/tables/${this.table}/${value}`; } else { this.params[String(column)] = value; } return this; } /** * Adds a 'not equals' filter to the query. */ neq(column, value) { this.params[`${String(column)}[neq]`] = value; return this; } /** * Adds a 'greater than' filter to the query. */ gt(column, value) { this.params[`${String(column)}[gt]`] = value; return this; } /** * Adds a 'greater than or equal to' filter to the query. */ gte(column, value) { this.params[`${String(column)}[gte]`] = value; return this; } /** * Adds a 'less than' filter to the query. */ lt(column, value) { this.params[`${String(column)}[lt]`] = value; return this; } /** * Adds a 'less than or equal to' filter to the query. */ lte(column, value) { this.params[`${String(column)}[lte]`] = value; return this; } /** * Adds a 'like' filter to the query. */ like(column, pattern) { this.params[`${String(column)}[like]`] = pattern; return this; } /** * Adds a 'case-insensitive like' filter to the query. */ ilike(column, pattern) { this.params[`${String(column)}[ilike]`] = pattern; return this; } /** * Adds an 'is' filter to the query. */ is(column, value) { this.params[String(column)] = value; return this; } /** * Adds an 'in' filter to the query. */ in(column, values) { this.params[`${String(column)}[in]`] = values.join(','); return this; } /** * Adds a 'match' filter to the query. */ match(query) { Object.entries(query).forEach(([key, value]) => { this.eq(key, value); }); return this; } /** * Adds a filter to the query. */ filter(column, operator, value) { this.params[`${String(column)}[${operator}]`] = value; return this; } } /** * A builder for creating queries. */ class LocalQueryBuilderImpl extends LocalFilterBuilderImpl { /** * Specifies the columns to select. */ select(columns = '*') { this.params.select = columns; return this; } /** * Inserts data into the table. */ insert(data) { this.method = 'POST'; this.body = data; return this; } /** * Updates data in the table. */ update(data) { this.method = 'PUT'; this.body = data; return this; } /** * Deletes data from the table. */ delete() { this.method = 'DELETE'; return this; } /** * Adds an 'order by' clause to the query. */ order(column, options = {}) { this.params.sort = String(column); this.params.order = options.ascending !== false ? 'asc' : 'desc'; return this; } /** * Limits the number of rows returned. */ limit(count) { this.params.limit = count; return this; } /** * Skips a number of rows. */ offset(count) { this.params.offset = count; return this; } /** * Specifies a range of rows to return. */ range(from, to) { this.params.limit = to - from + 1; this.params.offset = from; return this; } /** * Returns a single row from the query. */ single() { this.params.limit = 1; this.isSingle = true; return this; } } // BaseFlow Local Client - Main client class /** * The main client for interacting with a BaseFlow Local backend. * * @example * ```ts * const client = createClient({ * url: 'http://localhost:5555' * }); * * const { data, error } = await client.from('users').select('*'); * ``` */ class BaseFlowLocalClient { /** * Creates a new BaseFlowLocalClient instance. * * @param options The options for the client. */ constructor(options = {}) { this.url = (options.url || 'http://localhost:5555').replace(/\/$/, ''); // Remove trailing slash this.headers = { 'Content-Type': 'application/json', ...options.headers }; if (options.token) { this.headers['Authorization'] = `Bearer ${options.token}`; } if (options.apiKey) { this.headers['x-api-key'] = options.apiKey; } // Use provided fetch or globalThis.fetch or cross-fetch if (options.fetch) { this.fetch = options.fetch; } else if (typeof globalThis.fetch === 'function') { this.fetch = (...args) => globalThis.fetch(...args); } else { this.fetch = require('cross-fetch'); } // Initialize API modules this.storage = new StorageAPI(this); this.auth = new AuthAPI(this); } /** * Creates a query builder for a specific table. * * @param table The name of the table to query. * @returns A query builder for the specified table. */ from(table) { return new LocalQueryBuilderImpl(this, table); } /** * Makes a request to the BaseFlow Local backend. * * @param method The HTTP method to use. * @param path The path to request. * @param options Additional options for the request. * @returns The response from the backend. */ async request(method, path, options = {}) { const url = new URL(`${this.url}${path}`); // Add query parameters if (options.params) { Object.entries(options.params).forEach(([key, value]) => { if (value !== undefined && value !== null) { url.searchParams.append(key, String(value)); } }); } const requestHeaders = { ...this.headers, ...options.headers }; const requestOptions = { method, headers: requestHeaders }; if (options.body && method !== 'GET' && method !== 'HEAD') { requestOptions.body = typeof options.body === 'string' ? options.body : JSON.stringify(options.body); } try { const response = await this.fetch(url.toString(), requestOptions); let data = null; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); } else { data = await response.text(); } if (!response.ok) { return { data: null, error: { message: data?.error || data?.message || `HTTP ${response.status}`, code: data?.code || 'HTTP_ERROR', details: data }, status: response.status, statusText: response.statusText }; } return { data, error: null, status: response.status, statusText: response.statusText }; } catch (error) { return { data: null, error: { message: error instanceof Error ? error.message : 'Network error', code: 'NETWORK_ERROR', details: error }, status: 0, statusText: 'Network Error' }; } } /** * Set authentication token */ setAuth(token) { if (token) { this.headers['Authorization'] = `Bearer ${token}`; } else { delete this.headers['Authorization']; } } /** * Call a remote procedure/function */ async rpc(functionName, params = {}) { return this.request('POST', `/api/rpc/${functionName}`, { body: params }); } /** * Get current schema */ async getSchema() { return this.request('GET', '/api/schema'); } /** * List all tables */ async listTables() { const response = await this.request('GET', '/api/tables'); // Server returns { success: true, data: [...], count: ... } // Unwrap to { data: { tables: [...] }, error: null } if (response.data && typeof response.data === 'object' && 'success' in response.data) { return { data: { tables: response.data.data || [] }, error: response.error, status: response.status, statusText: response.statusText }; } return response; } } /** * Storage API for file operations */ class StorageAPI { constructor(client) { this.client = client; } /** * Upload a file */ async upload(file, options = {}) { try { const formData = new FormData(); // Handle different file types if (file instanceof File) { formData.append('file', file); } else if (Buffer.isBuffer(file)) { // Convert Buffer to Blob for FormData const blob = new Blob([file]); formData.append('file', blob, options.filename || 'file'); } else { formData.append('file', file); } if (options.folder) { formData.append('folder', options.folder); } if (options.isPublic !== undefined) { formData.append('isPublic', String(options.isPublic)); } const url = new URL(`${this.client['url']}/api/storage/upload`); const requestOptions = { method: 'POST', headers: { ...(this.client['headers']['Authorization'] && { 'Authorization': this.client['headers']['Authorization'] }) }, body: formData }; const response = await this.client['fetch'](url.toString(), requestOptions); let data = null; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); } if (!response.ok) { return { data: null, error: { message: data?.error || `Upload failed: HTTP ${response.status}`, code: 'UPLOAD_ERROR', details: data } }; } return { data: data, error: null }; } catch (error) { return { data: null, error: { message: error instanceof Error ? error.message : 'Upload failed', code: 'NETWORK_ERROR', details: error } }; } } /** * List files */ async list(folder) { return this.client.request('GET', '/api/storage', { params: folder ? { folder } : {} }); } /** * Get file URL */ getUrl(filePath) { return `${this.client['url']}/api/storage/${filePath}`; } /** * Download file */ async download(filePath) { return this.client.request('GET', `/api/storage/${filePath}`); } /** * Delete file */ async delete(fileId) { return this.client.request('DELETE', `/api/storage/${fileId}`); } } /** * Auth API for authentication operations */ class AuthAPI { constructor(client) { this.client = client; this.authToken = null; this.user = null; this.listeners = []; } /** * Register a new user */ async signUp(credentials) { const response = await this.client.request('POST', '/api/auth/register', { body: credentials }); if (response.data && response.data.user) { this.user = response.data.user; if (response.data.token) { this.authToken = response.data.token; this.client.setAuth(this.authToken); this.notifyListeners({ type: 'SIGNED_IN', user: this.user }); } } return response; } /** * Sign in with email and password */ async signInWithPassword(credentials) { const response = await this.client.request('POST', '/api/auth/login', { body: credentials }); if (response.data && response.data.user) { this.user = response.data.user; if (response.data.token) { this.authToken = response.data.token; this.client.setAuth(this.authToken); this.notifyListeners({ type: 'SIGNED_IN', user: this.user }); } } return response; } /** * Sign out the current user */ async signOut() { const response = await this.client.request('POST', '/api/auth/logout'); this.authToken = null; this.user = null; this.client.setAuth(null); this.notifyListeners({ type: 'SIGNED_OUT' }); return response; } /** * Get the current user */ async getUser() { if (!this.authToken) { return { data: { user: null }, error: null }; } const response = await this.client.request('GET', '/api/auth/me'); if (response.data && response.data.user) { this.user = response.data.user; } return response; } /** * Get the current session */ getSession() { return { token: this.authToken, user: this.user }; } /** * Set the auth token manually */ setSession(session) { this.authToken = session.token; this.user = session.user || null; this.client.setAuth(this.authToken); if (this.user) { this.notifyListeners({ type: 'SIGNED_IN', user: this.user }); } } /** * Listen to auth state changes */ onAuthStateChange(callback) { this.listeners.push(callback); // Return unsubscribe function return () => { const index = this.listeners.indexOf(callback); if (index > -1) { this.listeners.splice(index, 1); } }; } /** * Notify all listeners of auth events */ notifyListeners(event) { this.listeners.forEach(listener => { try { listener(event); } catch (error) { console.error('Auth listener error:', error); } }); } // Legacy methods for backward compatibility async register(email, password, name) { return this.signUp({ email, password, name }); } async login(email, password) { return this.signInWithPassword({ email, password }); } async logout() { return this.signOut(); } } /** * Create a new BaseFlow Local client instance */ function createClient(options = {}) { return new BaseFlowLocalClient(options); } exports.BaseFlowLocalClient = BaseFlowLocalClient; exports.LocalQueryBuilderImpl = LocalQueryBuilderImpl; exports.createClient = createClient; exports.default = createClient; //# sourceMappingURL=index.js.map