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,327 lines (1,322 loc) • 43 kB
JavaScript
'use strict';
// 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;
}
exports.BaseFlowClient = BaseFlowClient;
exports.createClient = createClient;
exports.createSchema = createSchema;
exports.defineTable = defineTable;
exports.validateFieldType = validateFieldType;
//# sourceMappingURL=index.js.map