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
652 lines (649 loc) • 19.3 kB
JavaScript
// 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);
}
export { BaseFlowLocalClient, LocalQueryBuilderImpl, createClient, createClient as default };
//# sourceMappingURL=index.esm.js.map