UNPKG

baseflow-client

Version:

Official TypeScript/JavaScript client for BaseFlow - a powerful BaaS with OAuth authentication, RPC functions, database indexes, real-time features, and Supabase-compatible API

1,321 lines (1,317 loc) 42.8 kB
// BaseFlow Query Builder - Supabase-like query interface /** * A thenable class that represents a PostgREST request. */ class PostgrestBuilder { constructor(client, table) { this.method = 'GET'; this.params = {}; this.body = null; this.headers = {}; this.client = client; this.table = table; this.path = `/rest/v1/${table}`; } /** * Executes the request and returns a promise that resolves with the response. * * @param onfulfilled A function to be called when the promise is fulfilled. * @param onrejected A function to be called when the promise is rejected. * @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 }); if (onfulfilled) { return onfulfilled(response); } return response; } catch (error) { if (onrejected) { return onrejected(error); } throw error; } } } /** * A builder for creating PostgREST filters. */ class PostgrestFilterBuilder extends PostgrestBuilder { /** * Adds an 'equals' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ eq(column, value) { this.params.where = this.addFilter(this.params.where, `${String(column)}=${this.escapeValue(value)}`); return this; } /** * Adds a 'not equals' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ neq(column, value) { this.params.where = this.addFilter(this.params.where, `${String(column)}<>${this.escapeValue(value)}`); return this; } /** * Adds a 'greater than' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ gt(column, value) { this.params.where = this.addFilter(this.params.where, `${String(column)}>${this.escapeValue(value)}`); return this; } /** * Adds a 'greater than or equal to' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ gte(column, value) { this.params.where = this.addFilter(this.params.where, `${String(column)}>=${this.escapeValue(value)}`); return this; } /** * Adds a 'less than' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ lt(column, value) { this.params.where = this.addFilter(this.params.where, `${String(column)}<${this.escapeValue(value)}`); return this; } /** * Adds a 'less than or equal to' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ lte(column, value) { this.params.where = this.addFilter(this.params.where, `${String(column)}<=${this.escapeValue(value)}`); return this; } /** * Adds a 'like' filter to the query. * * @param column The column to filter on. * @param pattern The pattern to match. * @returns The filter builder. */ like(column, pattern) { this.params.where = this.addFilter(this.params.where, `${String(column)} LIKE ${this.escapeValue(pattern)}`); return this; } /** * Adds a 'case-insensitive like' filter to the query. * * @param column The column to filter on. * @param pattern The pattern to match. * @returns The filter builder. */ ilike(column, pattern) { // SQLite doesn't have ILIKE, use LIKE with LOWER this.params.where = this.addFilter(this.params.where, `LOWER(${String(column)}) LIKE LOWER(${this.escapeValue(pattern)})`); return this; } /** * Adds an 'is' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ is(column, value) { if (value === null) { this.params.where = this.addFilter(this.params.where, `${String(column)} IS NULL`); } else { this.params.where = this.addFilter(this.params.where, `${String(column)}=${this.escapeValue(value)}`); } return this; } /** * Adds an 'in' filter to the query. * * @param column The column to filter on. * @param values The values to filter by. * @returns The filter builder. */ in(column, values) { const escapedValues = values.map(v => this.escapeValue(v)).join(','); this.params.where = this.addFilter(this.params.where, `${String(column)} IN (${escapedValues})`); return this; } /** * Adds a 'contains' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ contains(column, value) { // For JSON contains - simplified for SQLite this.params.where = this.addFilter(this.params.where, `${String(column)} LIKE ${this.escapeValue(`%${value}%`)}`); return this; } /** * Adds a 'contained by' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ containedBy(column, value) { // Simplified implementation return this.contains(column, value); } /** * Adds a 'range greater than' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ rangeGt(column, value) { return this.gt(column, value); } /** * Adds a 'range greater than or equal to' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ rangeGte(column, value) { return this.gte(column, value); } /** * Adds a 'range less than' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ rangeLt(column, value) { return this.lt(column, value); } /** * Adds a 'range less than or equal to' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ rangeLte(column, value) { return this.lte(column, value); } /** * Adds a 'range adjacent' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ rangeAdjacent(column, value) { // Simplified - not directly supported in SQLite return this.eq(column, value); } /** * Adds an 'overlaps' filter to the query. * * @param column The column to filter on. * @param value The value to filter by. * @returns The filter builder. */ overlaps(column, value) { // Simplified implementation return this.contains(column, value); } /** * Adds a 'text search' filter to the query. * * @param column The column to filter on. * @param query The query to search for. * @returns The filter builder. */ textSearch(column, query) { // Use LIKE for text search in SQLite this.params.where = this.addFilter(this.params.where, `${String(column)} LIKE ${this.escapeValue(`%${query}%`)}`); return this; } /** * Adds a 'match' filter to the query. * * @param query The query to match. * @returns The filter builder. */ match(query) { Object.entries(query).forEach(([key, value]) => { this.eq(key, value); }); return this; } /** * Adds a 'not' filter to the query. * * @param column The column to filter on. * @param operator The operator to use. * @param value The value to filter by. * @returns The filter builder. */ not(column, operator, value) { this.params.where = this.addFilter(this.params.where, `NOT (${String(column)} ${operator} ${this.escapeValue(value)})`); return this; } /** * Adds an 'or' filter to the query. * * @param filters The filters to apply. * @returns The filter builder. */ or(filters) { this.params.where = this.addFilter(this.params.where, `(${filters})`, 'OR'); return this; } /** * Adds a filter to the query. * * @param column The column to filter on. * @param operator The operator to use. * @param value The value to filter by. * @returns The filter builder. */ filter(column, operator, value) { this.params.where = this.addFilter(this.params.where, `${String(column)} ${operator} ${this.escapeValue(value)}`); return this; } addFilter(existing, newFilter, operator = 'AND') { if (!existing) { return newFilter; } return `${existing} ${operator} ${newFilter}`; } escapeValue(value) { if (value === null) return 'NULL'; if (typeof value === 'string') return `'${value.replace(/'/g, "''")}'`; if (typeof value === 'boolean') return value ? '1' : '0'; return String(value); } } /** * A builder for creating PostgREST queries. */ class PostgrestQueryBuilder extends PostgrestFilterBuilder { /** * Specifies the columns to select. * Supports Supabase-like syntax with JOINs and nested selections. * * Examples: * - select('*') - Select all columns * - select('name, email') - Select specific columns * - select('title, author:users(name, email)') - JOIN with users table * - select('title, comments(count)') - Aggregate count of comments * * @param columns The columns to select. * @returns The query builder. */ select(columns = '*') { this.params.select = columns; return this; } /** * Inserts data into the table. * * @param data The data to insert. * @returns A promise that resolves with the response. */ insert(data) { this.method = 'POST'; this.body = data; return this; } /** * Updates data in the table. * * @param data The data to update. * @returns A filter builder for specifying which rows to update. */ update(data) { this.method = 'PATCH'; this.body = data; return this; } /** * Deletes data from the table. * * @returns A filter builder for specifying which rows to delete. */ delete() { this.method = 'DELETE'; return this; } /** * Adds an 'order by' clause to the query. * * @param column The column to order by. * @param options The ordering options. * @returns The query builder. */ order(column, options = {}) { const direction = options.ascending !== false ? 'ASC' : 'DESC'; const nullsOrder = options.nullsFirst ? 'NULLS FIRST' : 'NULLS LAST'; this.params.order = `${String(column)} ${direction} ${nullsOrder}`; return this; } /** * Limits the number of rows returned. * * @param count The maximum number of rows to return. * @returns The query builder. */ limit(count) { this.params.limit = count; return this; } /** * Specifies a range of rows to return. * * @param from The starting row index. * @param to The ending row index. * @returns The query builder. */ range(from, to) { this.params.limit = to - from + 1; this.params.offset = from; return this; } /** * Returns a single row from the query. * * @returns The filter builder. */ single() { this.params.limit = 1; return this; } /** * Returns a single row from the query, or null if no rows are found. * * @returns The filter builder. */ maybeSingle() { this.params.limit = 1; return this; } /** * Subscribe to real-time changes on this table * * @param event The event type to listen for * @param callback The callback function to execute when changes occur * @returns The subscription object */ on(event, callback) { return new RealtimeSubscriptionBuilder(this.client, this.table, event, callback, this.params); } } /** * Real-time subscription builder */ class RealtimeSubscriptionBuilder { constructor(client, table, event, callback, filters = {}) { this.client = client; this.table = table; this.event = event; this.callback = callback; this.filters = filters; } } // BaseFlow Client - Main client class /** * The main client for interacting with a BaseFlow backend. * * @example * ```ts * const client = createClient({ * url: 'http://localhost:4000', * apiKey: 'your-api-key' * }); * * const { data, error } = await client.from('users').select('*'); * ``` */ class BaseFlowClient { /** * Creates a new BaseFlowClient instance. * * @param options The options for the client. */ constructor(options) { this.url = options.url.replace(/\/$/, ''); // Remove trailing slash this.apiKey = options.apiKey; this.headers = { 'Content-Type': 'application/json', 'Authorization': `Bearer ${options.apiKey}`, 'x-api-key': options.apiKey, ...options.headers }; // Use provided fetch or global fetch or cross-fetch // 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); this.realtime = new RealtimeAPI(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 PostgrestQueryBuilder(this, table); } /** * Makes a request to the BaseFlow 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 = {}) { // Use custom base URL if provided, otherwise use the client's URL const baseUrl = options.baseUrl || this.url; const url = new URL(`${baseUrl}${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 === null || data === void 0 ? void 0 : data.error) || (data === null || data === void 0 ? void 0 : data.message) || `HTTP ${response.status}`, code: (data === null || data === void 0 ? void 0 : 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' }; } } /** * Get project information */ async getProjectInfo() { return this.request('GET', '/rest/v1/'); } /** * Set authentication token */ setAuth(token) { if (token) { this.headers['Authorization'] = `Bearer ${token}`; } else { delete this.headers['Authorization']; } } /** * Execute a raw SQL query (if supported) */ async sql(query, params = []) { return this.request('POST', '/rest/v1/sql', { body: { query, params } }); } /** * Call a remote procedure/function */ async rpc(functionName, params = {}) { return this.request('POST', `/rest/v1/rpc/${functionName}`, { body: params }); } /** * Create a new function */ async createFunction(name, definition, options = {}) { return this.request('POST', '/rest/v1/functions', { body: { name, definition, ...options } }); } /** * List all functions */ async listFunctions() { return this.request('GET', '/rest/v1/functions'); } /** * Get function details */ async getFunction(name) { return this.request('GET', `/rest/v1/functions/${name}`); } /** * Delete a function */ async deleteFunction(name) { return this.request('DELETE', `/rest/v1/functions/${name}`); } /** * Create an index */ async createIndex(table, columns, options = {}) { return this.request('POST', '/rest/v1/indexes', { body: { table, columns, ...options } }); } /** * List indexes */ async listIndexes(table) { return this.request('GET', '/rest/v1/indexes', { params: table ? { table } : {} }); } /** * Drop an index */ async dropIndex(name) { return this.request('DELETE', `/rest/v1/indexes/${name}`); } /** * Analyze query performance */ async analyzeQuery(query) { return this.request('POST', '/rest/v1/analyze', { body: { query } }); } /** * Define and create tables from schema (BaseFlow-style) */ async defineSchema(schema) { return this.request('POST', '/rest/v1/schema', { body: { tables: schema } }); } /** * Get current project schema */ async getSchema() { return this.request('GET', '/rest/v1/schema'); } /** * Create a single table from definition */ async createTable(tableName, definition) { return this.request('POST', '/rest/v1/rpc/create_table', { body: { table_name: tableName, schema: definition } }); } } /** * Storage API for file operations */ class StorageAPI { constructor(client) { this.client = client; } async upload(path, file, options) { try { // Create FormData for file upload const formData = new FormData(); // Convert different file types to Blob let blob; if (file instanceof Buffer) { blob = new Blob([new Uint8Array(file)], { type: (options === null || options === void 0 ? void 0 : options.mimetype) || 'application/octet-stream' }); } else if (file instanceof Uint8Array) { blob = new Blob([new Uint8Array(file)], { type: (options === null || options === void 0 ? void 0 : options.mimetype) || 'application/octet-stream' }); } else if (typeof file === 'string') { blob = new Blob([file], { type: (options === null || options === void 0 ? void 0 : options.mimetype) || 'text/plain' }); } else { blob = file; } formData.append('file', blob, path.split('/').pop() || 'file'); formData.append('path', path); if (options === null || options === void 0 ? void 0 : options.mimetype) { formData.append('mimetype', options.mimetype); } // Make request with FormData const url = new URL(`${this.client['url']}/storage/upload`); const requestOptions = { method: 'POST', headers: { 'Authorization': this.client['headers']['Authorization'], 'x-api-key': this.client['headers']['x-api-key'] }, 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(); } else { data = await response.text(); } if (!response.ok) { return { data: null, error: { message: (data === null || data === void 0 ? void 0 : data.error) || (data === null || data === void 0 ? void 0 : data.message) || `HTTP ${response.status}`, code: (data === null || data === void 0 ? void 0 : data.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 } }; } } async getUrl(path) { return this.client.request('GET', '/storage/url', { params: { path } }); } async download(path) { return this.client.request('GET', '/storage/download', { params: { path } }); } async delete(path) { return this.client.request('DELETE', '/storage/files', { params: { path } }); } async list(path = '/') { return this.client.request('GET', '/storage/files', { params: { path } }); } async createFolder(path) { return this.client.request('POST', '/storage/folders', { body: { path } }); } } /** * Auth API for authentication operations */ class AuthAPI { constructor(client) { this.client = client; this.authToken = null; this.user = null; this.listeners = []; } /** * Sign up a new user */ async signUp(email, password, options) { const response = await this.client.request('POST', '/rest/v1/auth/signup', { body: { email, password, name: options === null || options === void 0 ? void 0 : options.name, data: options === null || options === void 0 ? void 0 : options.data, redirect_to: options === null || options === void 0 ? void 0 : options.redirectTo } }); if (response.data && response.data.user) { this.user = response.data.user; if (response.data.session && response.data.session.access_token) { this.authToken = response.data.session.access_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', '/rest/v1/auth/signin', { body: { email: credentials.email, password: credentials.password } }); if (response.data && response.data.user) { this.user = response.data.user; if (response.data.session && response.data.session.access_token) { this.authToken = response.data.session.access_token; this.client.setAuth(this.authToken); this.notifyListeners({ type: 'SIGNED_IN', user: this.user }); } } return response; } /** * Sign in with OAuth provider (GitHub, Google, etc.) */ async signInWithOAuth(provider, options) { const response = await this.client.request('POST', '/rest/v1/auth/oauth/signin', { body: { provider, redirect_to: options === null || options === void 0 ? void 0 : options.redirectTo, scopes: options === null || options === void 0 ? void 0 : options.scopes } }); if (response.data && response.data.url) { // Return the OAuth URL for the client to redirect to return { data: { url: response.data.url }, error: null }; } return response; } /** * Handle OAuth callback and exchange code for session */ async handleOAuthCallback(code, provider) { const response = await this.client.request('POST', '/rest/v1/auth/oauth/callback', { body: { code, provider } }); if (response.data && response.data.user) { this.user = response.data.user; if (response.data.session && response.data.session.access_token) { this.authToken = response.data.session.access_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', '/rest/v1/auth/signout'); 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 { user: null, error: null }; } const response = await this.client.request('GET', '/rest/v1/auth/user'); if (response.data && response.data.user) { this.user = response.data.user; } return response; } /** * Get the current session */ getSession() { return { access_token: this.authToken, user: this.user, expires_at: null, // TODO: Add token expiry refresh_token: null // TODO: Add refresh token support }; } // Note: updateUser, resetPasswordForEmail, and verifyOtp are not yet implemented // in the current BaseFlow version. These methods are placeholders for future features. /** * Set the auth token manually */ setSession(session) { this.authToken = session.access_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 signup(email, password, name) { return this.signUp(email, password, { name }); } async login(email, password) { return this.signInWithPassword({ email, password }); } async logout() { return this.signOut(); } } /** * Realtime API for WebSocket subscriptions */ class RealtimeAPI { constructor(client) { this.client = client; this.ws = null; this.subscriptions = new Map(); this.authenticated = false; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; } /** * Connect to realtime WebSocket */ async connect() { return new Promise((resolve, reject) => { try { // Extract base URL and create WebSocket URL const baseUrl = this.client['url']; const wsUrl = baseUrl.replace(/^http/, 'ws') + '/realtime'; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('🔌 Connected to BaseFlow Realtime'); this.reconnectAttempts = 0; this.authenticate().then(() => { resolve(); }).catch(reject); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.handleMessage(message); } catch (error) { console.error('Realtime message parse error:', error); } }; this.ws.onclose = () => { console.log('🔌 Disconnected from BaseFlow Realtime'); this.authenticated = false; this.attemptReconnect(); }; this.ws.onerror = (error) => { console.error('Realtime WebSocket error:', error); reject(error); }; } catch (error) { reject(error); } }); } /** * Authenticate with the realtime server */ async authenticate() { return new Promise((resolve, reject) => { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { reject(new Error('WebSocket not connected')); return; } // Get project info first this.client.getProjectInfo().then((projectInfo) => { var _a, _b; if (projectInfo.error || !((_b = (_a = projectInfo.data) === null || _a === void 0 ? void 0 : _a.project) === null || _b === void 0 ? void 0 : _b.id)) { reject(new Error('Failed to get project info')); return; } const authMessage = { type: 'auth', payload: { api_key: this.client['apiKey'], project_id: projectInfo.data.project.id } }; // Set up one-time listener for auth response const authHandler = (message) => { if (message.type === 'auth') { if (message.event === 'authenticated') { this.authenticated = true; console.log('🔐 Authenticated with BaseFlow Realtime'); resolve(); } else if (message.event === 'error') { reject(new Error(message.payload.message || 'Authentication failed')); } } }; // Temporarily add auth handler this.ws.addEventListener('message', (event) => { const message = JSON.parse(event.data); authHandler(message); }); this.ws.send(JSON.stringify(authMessage)); }).catch(reject); }); } /** * Subscribe to table changes */ subscribe(table, callback) { if (!this.subscriptions.has(table)) { this.subscriptions.set(table, new Set()); // Send subscription message if connected if (this.ws && this.ws.readyState === WebSocket.OPEN && this.authenticated) { this.ws.send(JSON.stringify({ type: 'subscribe', payload: { table, event: '*' } })); } } this.subscriptions.get(table).add(callback); // Return unsubscribe function return () => { const callbacks = this.subscriptions.get(table); if (callbacks) { callbacks.delete(callback); // If no more callbacks, unsubscribe from table if (callbacks.size === 0) { this.subscriptions.delete(table); if (this.ws && this.ws.readyState === WebSocket.OPEN && this.authenticated) { this.ws.send(JSON.stringify({ type: 'unsubscribe', payload: { table } })); } } } }; } /** * Handle incoming messages */ handleMessage(message) { switch (message.type) { case 'broadcast': if (message.event === 'change') { const { table } = message.payload; const callbacks = this.subscriptions.get(table); if (callbacks) { callbacks.forEach(callback => { try { callback(message.payload); } catch (error) { console.error('Realtime callback error:', error); } }); } } break; case 'subscription': if (message.event === 'subscribed') { console.log(`📡 Subscribed to ${message.payload.table}`); } else if (message.event === 'error') { console.error('Subscription error:', message.payload.message); } break; case 'error': console.error('Realtime error:', message.payload.message); break; case 'pong': // Heartbeat response break; default: console.log('Unknown realtime message:', message); } } /** * Attempt to reconnect */ attemptReconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('Max reconnection attempts reached'); return; } this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); console.log(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => { this.connect().catch((error) => { console.error('Reconnection failed:', error); }); }, delay); } /** * Disconnect from realtime */ disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } this.authenticated = false; this.subscriptions.clear(); this.reconnectAttempts = 0; } /** * Send ping to keep connection alive */ ping() { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping' })); } } /** * Get connection status */ getStatus() { var _a; return { connected: ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN || false, authenticated: this.authenticated, subscriptions: this.subscriptions.size }; } } /** * Create a new BaseFlow client instance */ function createClient(options) { return new BaseFlowClient(options); } /** * @module Schema * @description * This module provides a set of functions for defining the schema of a BaseFlow database. * It allows you to define tables, fields, and constraints using a simple and intuitive syntax. */ /** * Defines a table with the given name and fields. * * @param tableName The name of the table. * @param fields A record of field names and their definitions. * @returns A table definition object. */ function defineTable(tableName, fields) { const parsedFields = {}; let hasPrimaryKey = false; // Parse each field definition for (const [fieldName, fieldDefinition] of Object.entries(fields)) { const parsed = parseFieldDefinition(fieldName, fieldDefinition); if (parsed.constraints.primary) { if (hasPrimaryKey) { throw new Error(`Table "${tableName}" cannot have multiple primary keys`); } hasPrimaryKey = true; } parsedFields[fieldName] = parsed; } // Add auto-increment primary key if none specified if (!hasPrimaryKey) { parsedFields.id = { type: 'integer', constraints: { primary: true, autoIncrement: true, required: true } }; } return { name: tableName, fields: parsedFields, type: 'table' }; } /** * Parses a field definition string into a `FieldDefinition` object. * * @param fieldName The name of the field. * @param definition The field definition string. * @returns A `FieldDefinition` object. */ function parseFieldDefinition(fieldName, definition) { const parts = definition.split('@'); const type = parts[0].trim(); const constraintParts = parts.slice(1); const constraints = { primary: false, required: false, unique: false, autoIncrement: false, default: null }; // Parse constraints for (const constraint of constraintParts) { const trimmed = constraint.trim(); if (trimmed === 'primary') { constraints.primary = true; constraints.required = true; } else if (trimmed === 'required') { constraints.required = true; } else if (trimmed === 'unique') { constraints.unique = true; } else if (trimmed === 'autoincrement' || trimmed === 'auto_increment') { constraints.autoIncrement = true; } else if (trimmed.startsWith('default(') && trimmed.endsWith(')')) { const defaultValue = trimmed.slice(8, -1); if (defaultValue === 'now') { constraints.default = 'now'; } else if (defaultValue === 'true') { constraints.default = true; } else if (defaultValue === 'false') { constraints.default = false; } else if (!isNaN(Number(defaultValue))) { constraints.default = Number(defaultValue); } else { constraints.default = defaultValue.replace(/['"]/g, ''); } } else if (trimmed.startsWith('foreign(') && trimmed.endsWith(')')) { const foreignRef = trimmed.slice(8, -1); const [table, field] = foreignRef.split('.'); constraints.foreign = { table, field }; } } return { type, constraints }; } /** * Validates a field type. * * @param type The field type to validate. * @returns `true` if the field type is valid, `false` otherwise. */ function validateFieldType(type) { const validTypes = ['text', 'integer', 'real', 'boolean', 'datetime', 'json']; return validTypes.includes(type); } /** * Creates a schema from a record of table definitions. * * @param tables A record of table definitions. * @returns A record of table definitions. */ function createSchema(tables) { return tables; } export { BaseFlowClient, createClient, createSchema, defineTable, validateFieldType }; //# sourceMappingURL=index.esm.js.map