UNPKG

@e-techsolutions/e-baas-sdk

Version:

Official E-BaaS TypeScript SDK - Backend as a Service client library

1,440 lines (1,430 loc) 80 kB
import axios from 'axios'; class EBaaSError extends Error { constructor(message, status = 0, details) { super(message); this.name = 'EBaaSError'; this.status = status; this.details = details; if (Error.captureStackTrace) { Error.captureStackTrace(this, EBaaSError); } } toJSON() { return { name: this.name, message: this.message, status: this.status, details: this.details, stack: this.stack }; } toString() { return `${this.name}: ${this.message} (Status: ${this.status})`; } } class HttpClient { constructor(config) { this.baseURL = config.url; this.apiKey = config.apiKey; this.client = axios.create({ baseURL: this.baseURL, timeout: config.timeout || 10000, headers: { apikey: this.apiKey, ...config.headers, }, }); this.setupInterceptors(); } setupInterceptors() { // Request interceptor this.client.interceptors.request.use((config) => { const isFormData = typeof FormData !== 'undefined' && config.data instanceof FormData; const headers = config.headers ?? {}; if (isFormData) { delete headers['Content-Type']; delete headers['content-type']; } else if (config.data && typeof config.data === 'object' && !(config.data instanceof ArrayBuffer)) { if (!('Content-Type' in headers) && !('content-type' in headers)) { headers['Content-Type'] = 'application/json'; } } config.headers = headers; return config; }, (error) => { return Promise.reject(new EBaaSError('Request failed', 0, error)); }); // Response interceptor this.client.interceptors.response.use((response) => { return response; }, (error) => { const status = error.response?.status || 0; const message = error.response?.data?.message || error.message || 'Unknown error'; const details = error.response?.data || error; return Promise.reject(new EBaaSError(message, status, details)); }); } async get(url, config) { try { const response = await this.client.get(url, config); return { data: response.data, status: response.status, statusText: response.statusText, headers: response.headers, }; } catch (error) { throw this.handleError(error); } } async post(url, data, config) { try { const response = await this.client.post(url, data, config); return { data: response.data, status: response.status, statusText: response.statusText, headers: response.headers, }; } catch (error) { throw this.handleError(error); } } async put(url, data, config) { try { const response = await this.client.put(url, data, config); return { data: response.data, status: response.status, statusText: response.statusText, headers: response.headers, }; } catch (error) { throw this.handleError(error); } } async patch(url, data, config) { try { const response = await this.client.patch(url, data, config); return { data: response.data, status: response.status, statusText: response.statusText, headers: response.headers, }; } catch (error) { throw this.handleError(error); } } async delete(url, config) { try { const response = await this.client.delete(url, config); return { data: response.data, status: response.status, statusText: response.statusText, headers: response.headers, }; } catch (error) { throw this.handleError(error); } } getBaseUrl() { return this.baseURL; } setAuthToken(token) { this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`; } removeAuthToken() { delete this.client.defaults.headers.common['Authorization']; this.client.defaults.headers.common['Authorization'] = `Bearer ${this.apiKey}`; } handleError(error) { if (error instanceof EBaaSError) { return error; } return new EBaaSError(error.message || 'HTTP request failed', error.status || 0, error); } } class AuthClient { constructor(httpClient, workspaceId) { this.currentSession = null; this.httpClient = httpClient; this.workspaceId = workspaceId; this.loadSession(); } async signUp(data) { try { const headers = {}; if (this.workspaceId) { headers['x-workspace-id'] = this.workspaceId; } const payload = this.buildSignUpPayload(data); const response = await this.httpClient.post('/auth/v1/signup', payload, { headers }); if (response.data.session) { this.setSessionInternal(response.data.session); } // autoSignIn: if requested and no session returned by signUp, automatically sign in if (data.autoSignIn && !response.data.session) { try { const signInResponse = await this.signIn({ email: data.email, password: data.password }); return signInResponse; } catch (_) { // If auto sign-in fails, still return the original signUp response } } return response.data; } catch (error) { throw this.handleAuthError(error); } } async signIn(data) { try { const headers = {}; // Incluir x-workspace-id se workspaceId foi fornecido if (this.workspaceId) { headers['x-workspace-id'] = this.workspaceId; } const response = await this.httpClient.post('/auth/v1/sdk/signin', data, { headers }); if (response.data.session) { this.setSessionInternal(response.data.session); } return response.data; } catch (error) { throw this.handleAuthError(error); } } async signOut() { try { await this.httpClient.post('/auth/v1/signout'); this.clearSession(); } catch (error) { this.clearSession(); throw this.handleAuthError(error); } } async getUser() { if (!this.currentSession?.access_token) { return null; } try { const response = await this.httpClient.get('/auth/v1/user'); return response.data.user; } catch (error) { if (error instanceof EBaaSError && error.status === 401) { this.clearSession(); return null; } throw this.handleAuthError(error); } } async refreshSession() { if (!this.currentSession?.refresh_token) { return null; } try { const headers = {}; if (this.workspaceId) { headers['x-workspace-id'] = this.workspaceId; } const response = await this.httpClient.post('/auth/v1/refresh', { refresh_token: this.currentSession.refresh_token, }, { headers, }); if (response.data.session) { this.setSession(response.data.session); return response.data.session; } return null; } catch (error) { this.clearSession(); throw this.handleAuthError(error); } } async resetPassword(email) { try { await this.httpClient.post('/auth/v1/reset-password', { email }); } catch (error) { throw this.handleAuthError(error); } } async updateUser(data) { try { const response = await this.httpClient.put('/auth/v1/user', data); return response.data.user; } catch (error) { throw this.handleAuthError(error); } } getSession() { return this.currentSession; } setSession(session) { this.currentSession = session; this.httpClient.setAuthToken(session.access_token); // Persist session if (typeof localStorage !== 'undefined') { localStorage.setItem('ebaas.auth.session', JSON.stringify(session)); } } onAuthStateChange(callback) { // Implementation for auth state change listener // This would integrate with the actual auth state management system return () => { }; } /** * Atualiza o workspace ID (útil quando o usuário troca de workspace) */ setWorkspaceId(workspaceId) { this.workspaceId = workspaceId; } buildSignUpPayload(data) { const { metadata, ...rest } = data; const payload = { ...rest }; if (metadata) { payload.metadata = metadata; } const { firstName, lastName } = this.resolveNamesFromMetadata(rest.firstName, rest.lastName, metadata); if (firstName && !payload.firstName) { payload.firstName = firstName; } if (lastName && !payload.lastName) { payload.lastName = lastName; } return payload; } resolveNamesFromMetadata(baseFirstName, baseLastName, metadata) { const result = {}; if (baseFirstName) { result.firstName = this.getStringValue(baseFirstName); } if (baseLastName) { result.lastName = this.getStringValue(baseLastName); } const metadataFirstName = this.getStringValue(metadata?.['first_name'] ?? metadata?.firstName); const metadataLastName = this.getStringValue(metadata?.['last_name'] ?? metadata?.lastName); const metadataName = this.getStringValue(metadata?.name); if (!result.firstName) { result.firstName = metadataFirstName ?? (metadataName ? metadataName.split(/\s+/)[0] : undefined); } if (!result.lastName) { if (metadataLastName) { result.lastName = metadataLastName; } else if (metadataName) { const parts = metadataName.split(/\s+/); if (parts.length > 1) { result.lastName = parts.slice(1).join(' '); } } } return result; } getStringValue(value) { if (typeof value !== 'string') { return undefined; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : undefined; } setSessionInternal(session) { this.currentSession = session; this.httpClient.setAuthToken(session.access_token); // Persist session if (typeof localStorage !== 'undefined') { localStorage.setItem('ebaas.auth.session', JSON.stringify(session)); } } clearSession() { this.currentSession = null; this.httpClient.removeAuthToken(); // Clear persisted session if (typeof localStorage !== 'undefined') { localStorage.removeItem('ebaas.auth.session'); } } loadSession() { if (typeof localStorage !== 'undefined') { const storedSession = localStorage.getItem('ebaas.auth.session'); if (storedSession) { try { const session = JSON.parse(storedSession); this.setSessionInternal(session); } catch (error) { this.clearSession(); } } } } handleAuthError(error) { if (error instanceof EBaaSError) { return error; } return new EBaaSError(error.message || 'Authentication failed', error.status || 0, error); } } /** * Returns session expiration as milliseconds timestamp. * Handles both seconds and milliseconds formats from the API. */ function getExpiresAtMs(session) { if (typeof session.expires_at === 'number' && session.expires_at < 1e12) { return session.expires_at * 1000; // seconds -> ms } return Number(session.expires_at); // already ms } class QueryBuilder { constructor(httpClient, table, schema, workspaceId, restUrl) { this.httpClient = httpClient; this.table = table; this.schema = schema; this.workspaceId = workspaceId; this.restUrl = restUrl || '/sdk/v1'; this.query = { filters: [] }; } select(columns = '*', options) { this.query.select = columns; return this; } insert(data, options) { this.query.insertData = data; this.query.insertOptions = options; return this; } update(data, options) { this.query.updateData = data; this.query.updateOptions = options; return this; } upsert(data, options) { return this.executeQuery('POST', `${this.restUrl}/${this.table}?operation=upsert`, { data, options: { ...options, upsert: true } }); } delete(options) { this.query.deleteOptions = options; this.query.isDelete = true; return this; } // Filter methods eq(column, value) { this.query.filters.push({ column, operator: 'eq', value }); return this; } neq(column, value) { this.query.filters.push({ column, operator: 'neq', value }); return this; } gt(column, value) { this.query.filters.push({ column, operator: 'gt', value }); return this; } gte(column, value) { this.query.filters.push({ column, operator: 'gte', value }); return this; } lt(column, value) { this.query.filters.push({ column, operator: 'lt', value }); return this; } lte(column, value) { this.query.filters.push({ column, operator: 'lte', value }); return this; } like(column, pattern) { this.query.filters.push({ column, operator: 'like', value: pattern }); return this; } ilike(column, pattern) { this.query.filters.push({ column, operator: 'ilike', value: pattern }); return this; } is(column, value) { this.query.filters.push({ column, operator: 'is', value }); return this; } in(column, values) { this.query.filters.push({ column, operator: 'in', value: values }); return this; } contains(column, value) { this.query.filters.push({ column, operator: 'contains', value }); return this; } match(query) { Object.entries(query).forEach(([column, value]) => { this.eq(column, value); }); return this; } // Ordering methods order(column, options) { if (!this.query.order) { this.query.order = []; } this.query.order.push({ column, ascending: options?.ascending !== false }); return this; } // Limiting methods limit(count) { this.query.limit = count; return this; } range(from, to) { this.query.range = { from, to }; return this; } // Execution method for SELECT, UPDATE, or DELETE async execute() { // Detectar tipo de operação baseado nos dados do query if (this.query.insertData) { // INSERT operation return this.executeQuery('POST', `${this.restUrl}/${this.table}?operation=insert`, { data: this.query.insertData, options: this.query.insertOptions }); } else if (this.query.updateData) { // UPDATE operation return this.executeQuery('PATCH', `${this.restUrl}/${this.table}`, { data: this.query.updateData, options: this.query.updateOptions, filters: this.query.filters }); } else if (this.query.isDelete) { // DELETE operation return this.executeQuery('DELETE', `${this.restUrl}/${this.table}?operation=delete`, { options: this.query.deleteOptions, filters: this.query.filters }); } else { // SELECT operation (default) return this.executeQuery('GET', `${this.restUrl}/${this.table}?operation=select`, { select: this.query.select, filters: this.query.filters, order: this.query.order, limit: this.query.limit, range: this.query.range }); } } then(onfulfilled, onrejected) { return this.execute().then(onfulfilled, onrejected); } // Single row execution async single() { this.limit(1); const result = await this.execute(); return { data: result.data?.[0] || null, error: result.error, }; } async executeQuery(method, url, payload) { try { let response; // Configurar headers obrigatórios const headers = {}; if (this.workspaceId) { headers['x-workspace-id'] = this.workspaceId; } if (method === 'GET') { // Extrair parâmetros existentes da URL (como operation=select) const urlParts = url.split('?'); const baseUrl = urlParts[0]; const existingParams = new URLSearchParams(urlParts[1] || ''); // Adicionar novos parâmetros de query if (payload.select) existingParams.append('select', payload.select); // Converter filtros para formato PostgREST if (payload.filters?.length) { payload.filters.forEach((filter) => { existingParams.append(filter.column, `${filter.operator}.${filter.value}`); }); } // Converter ordenação para formato PostgREST if (payload.order?.length) { const orderStr = payload.order.map((ord) => `${ord.column}${ord.ascending ? '.asc' : '.desc'}`).join(','); existingParams.append('order', orderStr); } if (payload.limit) existingParams.append('limit', payload.limit.toString()); if (payload.range) { existingParams.append('offset', payload.range.from.toString()); existingParams.append('limit', (payload.range.to - payload.range.from + 1).toString()); } response = await this.httpClient.get(`${baseUrl}?${existingParams.toString()}`, { headers }); } else if (method === 'DELETE') { // Para DELETE, os filtros devem estar na URL (query string), não no body // Extrair parâmetros existentes da URL (como operation=delete) const urlParts = url.split('?'); const baseUrl = urlParts[0]; const existingParams = new URLSearchParams(urlParts[1] || ''); // Adicionar parâmetro operation=delete se não estiver presente if (!existingParams.has('operation')) { existingParams.append('operation', 'delete'); } if (payload.filters?.length) { // Converter filtros para formato PostgREST payload.filters.forEach((filter) => { existingParams.append(filter.column, `${filter.operator}.${filter.value}`); }); } const deleteUrl = `${baseUrl}?${existingParams.toString()}`; response = await this.httpClient.delete(deleteUrl, { headers }); } else if (method === 'PATCH') { // Para UPDATE (PATCH), os filtros também devem estar na URL // Extrair parâmetros existentes da URL (como operation=update) const urlParts = url.split('?'); const baseUrl = urlParts[0]; const existingParams = new URLSearchParams(urlParts[1] || ''); // Adicionar parâmetro operation=update se não estiver presente if (!existingParams.has('operation')) { existingParams.append('operation', 'update'); } if (payload.filters?.length) { // Converter filtros para formato PostgREST payload.filters.forEach((filter) => { existingParams.append(filter.column, `${filter.operator}.${filter.value}`); }); } // Adicionar headers Prefer se necessário if (payload.options?.prefer) { headers['Prefer'] = payload.options.prefer; } const updateUrl = `${baseUrl}?${existingParams.toString()}`; response = await this.httpClient.patch(updateUrl, payload.data, { headers }); } else { // POST mantém o payload no body (para INSERT) let postData = payload.data; // Sempre retornar dados inseridos por padrão if (!headers['Prefer']) { headers['Prefer'] = 'return=representation'; } // Se for upsert, adicionar header Prefer if (payload.options?.upsert) { headers['Prefer'] = 'resolution=merge-duplicates,return=representation'; } // Se for inserção múltipla, usar dataArray if (payload.dataArray) { postData = payload.dataArray; } response = await this.httpClient.post(url, postData, { headers }); } return { data: response.data, error: null, count: response.headers?.['x-total-count'] ? parseInt(response.headers['x-total-count']) : undefined }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Database operation failed', 0, error) }; } } } class CustomDatabaseManager { constructor(httpClient, workspaceId) { this.httpClient = httpClient; this.workspaceId = workspaceId; } /** * Detecta automaticamente se o workspace usa banco customizado */ async detectCustomDatabase() { if (!this.workspaceId) { return { hasCustomDatabase: false }; } try { // Busca a configuração do workspace const response = await this.httpClient.get('/admin/v1/workspace/custom-database', { headers: { 'x-workspace-id': this.workspaceId, }, }); if (response.status === 200 && response.data?.config) { const config = response.data.config; // Converte para formato de credenciais do SDK const credentials = { host: config.postgresHost, port: config.postgresPort, database: config.postgresDatabase, username: config.postgresUsername, password: '', // Senha não é retornada pela API por segurança schema: config.postgresSchema, sslEnabled: config.sslEnabled, }; this.detectedConfig = { hasCustomDatabase: true, config, credentials, }; return this.detectedConfig; } } catch (error) { // Se erro 404, significa que não há configuração customizada if (error?.status === 404) { this.detectedConfig = { hasCustomDatabase: false }; return this.detectedConfig; } console.warn('Failed to detect custom database configuration:', error); } this.detectedConfig = { hasCustomDatabase: false }; return this.detectedConfig; } /** * Testa conexão com banco customizado */ async testConnection(credentials) { try { const response = await this.httpClient.post('/admin/v1/workspace/custom-database/test-connection', credentials, { headers: this.workspaceId ? { 'X-Workspace-ID': this.workspaceId } : {}, }); return response.data; } catch (error) { return { success: false, message: error?.message || 'Failed to test connection', }; } } /** * Configura credenciais customizadas para o workspace */ async configureCustomDatabase(credentials) { if (!this.workspaceId) { throw new Error('Workspace ID is required to configure custom database'); } const response = await this.httpClient.post('/admin/v1/workspace/custom-database', { workspaceId: this.workspaceId, ...credentials, }, { headers: { 'X-Workspace-ID': this.workspaceId, }, }); const config = response.data.config; // Atualiza a configuração detectada this.detectedConfig = { hasCustomDatabase: true, config, credentials, }; return config; } /** * Remove configuração de banco customizado */ async removeCustomDatabase() { if (!this.workspaceId) { throw new Error('Workspace ID is required to remove custom database'); } await this.httpClient.delete('/admin/v1/workspace/custom-database', { headers: { 'X-Workspace-ID': this.workspaceId, }, }); // Limpa a configuração detectada this.detectedConfig = { hasCustomDatabase: false }; } /** * Retorna a configuração detectada em cache */ getDetectedConfig() { return this.detectedConfig; } /** * Verifica se tem configuração customizada em cache */ hasCustomDatabase() { return this.detectedConfig?.hasCustomDatabase ?? false; } /** * Retorna as credenciais customizadas se disponíveis */ getCustomCredentials() { return this.detectedConfig?.credentials; } /** * Atualiza o workspace ID */ setWorkspaceId(workspaceId) { this.workspaceId = workspaceId; // Limpa configuração anterior quando workspace muda this.detectedConfig = undefined; } /** * Retorna o workspace ID atual */ getWorkspaceId() { return this.workspaceId; } /** * Retorna informações sobre o status do banco customizado */ getStatus() { return { workspaceId: this.workspaceId, hasConfig: this.hasCustomDatabase(), configDetails: this.detectedConfig?.credentials ? { host: this.detectedConfig.credentials.host, port: this.detectedConfig.credentials.port, database: this.detectedConfig.credentials.database, schema: this.detectedConfig.credentials.schema || 'public', sslEnabled: this.detectedConfig.credentials.sslEnabled || false, } : undefined, }; } } class DatabaseClient { constructor(httpClient, workspaceId, restUrl) { this.httpClient = httpClient; this.customDatabaseManager = new CustomDatabaseManager(httpClient, workspaceId); this.restUrl = restUrl || '/sdk/v1'; } from(table) { return new QueryBuilder(this.httpClient, table, this.currentSchema, this.customDatabaseManager.getWorkspaceId(), this.restUrl); } rpc(fn, args) { return this.httpClient.post(`/rpc/${fn}`, args); } schema(schema) { const client = new DatabaseClient(this.httpClient, this.customDatabaseManager.getWorkspaceId(), this.restUrl); client.currentSchema = schema; client.customDatabaseManager = this.customDatabaseManager; return client; } // Métodos para gerenciar banco customizado /** * Detecta automaticamente se o workspace usa banco customizado */ async detectCustomDatabase() { return this.customDatabaseManager.detectCustomDatabase(); } /** * Testa conexão com credenciais customizadas */ async testCustomConnection(credentials) { return this.customDatabaseManager.testConnection(credentials); } /** * Configura banco customizado para o workspace */ async configureCustomDatabase(credentials) { return this.customDatabaseManager.configureCustomDatabase(credentials); } /** * Remove configuração de banco customizado */ async removeCustomDatabase() { return this.customDatabaseManager.removeCustomDatabase(); } /** * Verifica se o workspace está usando banco customizado */ isUsingCustomDatabase() { return this.customDatabaseManager.hasCustomDatabase(); } /** * Retorna informações sobre o banco customizado */ getCustomDatabaseStatus() { return this.customDatabaseManager.getStatus(); } /** * Atualiza o workspace ID (útil quando o usuário troca de workspace) */ setWorkspaceId(workspaceId) { this.customDatabaseManager.setWorkspaceId(workspaceId); } /** * Inicializa a detecção automática do banco customizado * Deve ser chamado após a autenticação se autoDetectCustomDatabase estiver habilitado */ async initialize() { try { await this.customDatabaseManager.detectCustomDatabase(); } catch (error) { console.warn('Failed to initialize custom database detection:', error); } } } const STORAGE_BASE_PATH$1 = '/storage/v1'; class StorageBucket { constructor(httpClient, bucketId, workspaceId) { this.httpClient = httpClient; this.bucketId = bucketId; this.workspaceId = workspaceId; } setWorkspaceId(workspaceId) { this.workspaceId = workspaceId; } getWorkspaceHeaders() { if (!this.workspaceId) { throw new EBaaSError('Workspace ID is required for storage operations', 400); } return { 'x-workspace-id': this.workspaceId, }; } normalizePath(path) { return path .trim() .replace(/^\/+/, '') .replace(/\/+$/, '') .replace(/\/+/g, '/'); } encodePath(path) { const normalized = this.normalizePath(path); return normalized .split('/') .filter(Boolean) .map((segment) => encodeURIComponent(segment)) .join('/'); } extractPathParts(path) { const normalized = this.normalizePath(path); if (!normalized) { throw new EBaaSError('Path must include the file name', 400); } const segments = normalized.split('/').filter(Boolean); if (segments.length === 0) { throw new EBaaSError('Path must include the file name', 400); } const fileName = segments.pop(); const directory = segments.length ? segments.join('/') : undefined; return { fileName, directory }; } serializeTransform(transform) { if (!transform) { return undefined; } const parts = Object.entries(transform) .filter(([, value]) => value !== undefined && value !== null) .map(([key, value]) => `${key}=${value}`); return parts.length ? parts.join(',') : undefined; } appendFileToFormData(formData, file, fileName, contentType) { if (file instanceof File) { if (file.name !== fileName) { formData.append('file', file, fileName); } else { formData.append('file', file); } return; } if (file instanceof Blob) { formData.append('file', file, fileName); return; } if (file instanceof ArrayBuffer) { formData.append('file', new Blob([file], { type: contentType ?? 'application/octet-stream' }), fileName); return; } if (typeof file === 'string') { formData.append('file', new Blob([file], { type: contentType ?? 'text/plain' }), fileName); return; } throw new EBaaSError('Unsupported file type for upload', 400); } async upload(path, file, options) { try { const headers = this.getWorkspaceHeaders(); const normalizedPath = this.normalizePath(path); const { fileName, directory } = this.extractPathParts(normalizedPath); const formData = new FormData(); this.appendFileToFormData(formData, file, fileName, options?.contentType); if (directory) { formData.append('path', directory); } if (options?.cacheControl) { formData.append('cacheControl', options.cacheControl); } if (options?.metadata) { formData.append('metadata', JSON.stringify(options.metadata)); } if (options?.contentType) { formData.append('contentType', options.contentType); } if (options?.expiresIn) { formData.append('expiresIn', String(options.expiresIn)); } if (options?.upsert) { formData.append('overwrite', 'true'); } const encodedPath = this.encodePath(normalizedPath); const response = await this.httpClient.post(`${STORAGE_BASE_PATH$1}/object/${this.bucketId}/${encodedPath}`, formData, { headers, }); return { data: response.data, error: null, }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to upload file', 0, error), }; } } async download(path, options) { try { const headers = this.getWorkspaceHeaders(); const normalizedPath = this.normalizePath(path); const encodedPath = this.encodePath(normalizedPath); const transformString = this.serializeTransform(options?.transform); const query = transformString ? `?transform=${encodeURIComponent(transformString)}` : ''; const response = await this.httpClient.get(`${STORAGE_BASE_PATH$1}/object/authenticated/${this.bucketId}/${encodedPath}${query}`, { responseType: 'blob', headers, }); return { data: response.data, error: null, }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to download file', 0, error), }; } } async list(prefix, options) { try { const headers = this.getWorkspaceHeaders(); const params = new URLSearchParams(); if (prefix) { const normalizedPrefix = prefix.replace(/^\/+/, ''); if (normalizedPrefix) { params.append('prefix', normalizedPrefix); } } if (options?.limit) { params.append('limit', String(options.limit)); } if (options?.offset) { params.append('offset', options.offset); } if (options?.search) { params.append('search', options.search); } if (options?.sortBy && options.sortBy.length > 0) { const sortValue = options.sortBy .map((item) => { const order = item.order ?? 'asc'; return `${item.column}:${order}`; }) .join(','); params.append('sortBy', sortValue); } const query = params.toString(); const response = await this.httpClient.get(`${STORAGE_BASE_PATH$1}/object/list/${this.bucketId}${query ? `?${query}` : ''}`, { headers, }); return { data: response.data, error: null, }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to list files', 0, error), }; } } async remove(paths) { try { const headers = this.getWorkspaceHeaders(); for (const path of paths) { const normalizedPath = this.normalizePath(path); const encodedPath = this.encodePath(normalizedPath); await this.httpClient.delete(`${STORAGE_BASE_PATH$1}/object/${this.bucketId}/${encodedPath}`, { headers, }); } return { data: { message: 'Files removed successfully' }, error: null, }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to remove files', 0, error), }; } } async move(_fromPath, _toPath) { return { data: null, error: new EBaaSError('Move operation is not supported by the current E-BaaS backend', 501), }; } async copy(_fromPath, _toPath) { return { data: null, error: new EBaaSError('Copy operation is not supported by the current E-BaaS backend', 501), }; } async createSignedUrl(path, expiresIn, options) { try { const headers = this.getWorkspaceHeaders(); const normalizedPath = this.normalizePath(path); const response = await this.httpClient.post(`${STORAGE_BASE_PATH$1}/object/sign`, { bucketName: this.bucketId, filePath: normalizedPath, operation: options?.operation ?? 'download', expiresIn: options?.expiresIn ?? expiresIn, }, { headers, }); return { data: response.data, error: null, }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to create signed URL', 0, error), }; } } getPublicUrl(path, options) { const baseUrl = this.httpClient.baseURL; if (!baseUrl) { throw new EBaaSError('HTTP client base URL is not configured', 400); } if (!this.workspaceId) { throw new EBaaSError('Workspace ID is required to generate public URLs', 400); } const normalizedPath = this.normalizePath(path); const encodedPath = this.encodePath(normalizedPath); const params = new URLSearchParams(); params.append('workspaceId', this.workspaceId); const transformString = this.serializeTransform(options?.transform); if (transformString) { params.append('transform', transformString); } const publicUrl = `${baseUrl.replace(/\/+$/, '')}${STORAGE_BASE_PATH$1}/object/public/${this.bucketId}/${encodedPath}`; const query = params.toString(); return { data: { publicUrl: query ? `${publicUrl}?${query}` : publicUrl, }, }; } async getFileInfo(_path) { return { data: null, error: new EBaaSError('File info retrieval is not supported by the current E-BaaS backend', 501), }; } async updateFileMetadata(_path, _metadata) { return { data: null, error: new EBaaSError('Updating file metadata is not supported by the current E-BaaS backend', 501), }; } } const STORAGE_BASE_PATH = '/storage/v1'; class StorageClient { constructor(httpClient, workspaceId) { this.httpClient = httpClient; this.workspaceId = workspaceId; } from(bucketId) { return new StorageBucket(this.httpClient, bucketId, this.workspaceId); } setWorkspaceId(workspaceId) { this.workspaceId = workspaceId; } getWorkspaceHeaders() { if (!this.workspaceId) { throw new EBaaSError('Workspace ID is required for storage operations', 400); } return { 'x-workspace-id': this.workspaceId, }; } mapBucketOptions(options) { if (!options) { return {}; } const visibility = options.visibility ?? (options.public ? 'public' : undefined); const maxFileSize = options.maxFileSize ?? options.fileSizeLimit; const enableVersioning = options.enableVersioning ?? options.versioningEnabled; return { visibility, description: options.description, allowedMimeTypes: options.allowedMimeTypes, maxFileSize, enableVersioning, enableCors: options.enableCors, corsOrigins: options.corsOrigins, }; } async createBucket(id, options) { try { const payload = { name: id, ...this.mapBucketOptions(options), }; const response = await this.httpClient.post(`${STORAGE_BASE_PATH}/buckets`, payload, { headers: this.getWorkspaceHeaders(), }); return { data: response.data, error: null }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to create bucket', 0, error) }; } } async getBucket(id) { try { const response = await this.httpClient.get(`${STORAGE_BASE_PATH}/buckets/${id}`, { headers: this.getWorkspaceHeaders(), }); return { data: response.data, error: null }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to get bucket', 0, error) }; } } async listBuckets() { try { const response = await this.httpClient.get(`${STORAGE_BASE_PATH}/buckets`, { headers: this.getWorkspaceHeaders(), }); return { data: response.data, error: null }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to list buckets', 0, error) }; } } async updateBucket(id, options) { try { const payload = this.mapBucketOptions(options); const response = await this.httpClient.put(`${STORAGE_BASE_PATH}/buckets/${id}`, payload, { headers: this.getWorkspaceHeaders(), }); return { data: response.data, error: null }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to update bucket', 0, error) }; } } async deleteBucket(id) { try { const response = await this.httpClient.delete(`${STORAGE_BASE_PATH}/buckets/${id}`, { headers: this.getWorkspaceHeaders(), }); return { data: response.data, error: null }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to delete bucket', 0, error) }; } } async getUsage() { try { const response = await this.httpClient.get(`${STORAGE_BASE_PATH}/usage`, { headers: this.getWorkspaceHeaders(), }); return { data: response.data, error: null }; } catch (error) { return { data: null, error: error instanceof EBaaSError ? error : new EBaaSError(error instanceof Error ? error.message : 'Failed to get storage usage', 0, error) }; } } } class EventManager { constructor() { this.events = new Map(); this.maxListeners = 10; } on(event, callback) { if (!this.events.has(event)) { this.events.set(event, new Set()); } this.events.get(event).add(callback); } off(event, callback) { if (!this.events.has(event)) return; const eventCallbacks = this.events.get(event); if (callback) { eventCallbacks.delete(callback); } else { eventCallbacks.clear(); } if (eventCallbacks.size === 0) { this.events.delete(event); } } emit(event, ...args) { if (!this.events.has(event)) return false; const eventCallbacks = this.events.get(event); for (const callback of eventCallbacks) { try { callback(...args); } catch (error) { console.error(`Error in event listener for "${event}":`, error); } } return eventCallbacks.size > 0; } removeAllListeners(event) { if (event) { this.events.delete(event); } else { this.events.clear(); } } listenerCount(event) { return this.events.get(event)?.size ?? 0; } eventNames() { return Array.from(this.events.keys()); } listeners(event) { return Array.from(this.events.get(event) ?? []); } addListener(event, callback) { this.on(event, callback); } removeListener(event, callback) { this.off(event, callback); } once(event, callback) { const onceWrapper = (...args) => { this.off(event, onceWrapper); callback(...args); }; this.on(event, onceWrapper); } prependListener(event, callback) { this.on(event, callback); } prependOnceListener(event, callback) { const onceWrapper = (...args) => { this.off(event, onceWrapper); callback(...args); }; this.on(event, onceWrapper); } setMaxListeners(max) { this.maxListeners = max; return this; } getMaxListeners() { return this.maxListeners; } rawListeners(event) { return this.listeners(event); } } class RealtimeChannel extends EventManager { constructor(topic, socket) { super(); this.joinedOnce = false; this.state = 'closed'; this.subscriptions = new Map(); this.topic = topic; this.socket = socket; } subscribe(callback) { if (!this.joinedOnce) { this.joinedOnce = true; this._join(); } if (callback) { this.on('status', cal