UNPKG

@dataql/react-native

Version:

DataQL React Native SDK with offline-first capabilities and clean API

660 lines (589 loc) 19.1 kB
import { BaseDataQLClient, DataQLConfig, SyncConfig, SyncStatus, CRUDResult, InternalConnectionAuth, } from "@dataql/core"; import { DatabaseClient } from "./db/client"; import { OfflineCacheManager } from "./cache/OfflineCacheManager"; import { SyncManager } from "./sync/SyncManager"; import { setGlobalInstances } from "./hooks/useDataQL"; import type { DataQLReactNativeConfig, CustomRequestConnection, WorkerBinding, InternalConnectionConfig, SyncConfig as ReactNativeSyncConfig, } from "./types"; import { getWorkerUrl } from "@dataql/core"; // React Native specific internal connection MongoDB client class ReactNativeInternalMongoClient { private config: InternalConnectionConfig; private connectionUrl: string; constructor(config: InternalConnectionConfig) { this.config = config; this.connectionUrl = `${config.mongodbUrl}/${config.databaseName}`; } async create<T>(collection: string, data: T): Promise<CRUDResult<T>> { try { // In a real implementation, this would make HTTP requests to a MongoDB API // For now, we'll simulate the operation const response = await fetch( `${this.connectionUrl}/collections/${collection}`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.config.privateToken}`, "X-App-Name": this.config.appName, "X-Database-Name": this.config.databaseName, }, body: JSON.stringify(data), } ); if (!response.ok) { throw new Error( `MongoDB create failed: ${response.status} ${response.statusText}` ); } const result = await response.json(); return { success: true, data: result.data || data, insertedId: result.insertedId || (data as any).id, }; } catch (error) { console.error( "[DataQL React Native] Internal MongoDB create error:", error ); return { success: false, error: error instanceof Error ? error.message : "Internal connection create failed", }; } } async read<T>(collection: string, filter?: any): Promise<T[]> { try { const queryParams = filter ? `?filter=${encodeURIComponent(JSON.stringify(filter))}` : ""; const response = await fetch( `${this.connectionUrl}/collections/${collection}${queryParams}`, { method: "GET", headers: { Authorization: `Bearer ${this.config.privateToken}`, "X-App-Name": this.config.appName, "X-Database-Name": this.config.databaseName, }, } ); if (!response.ok) { if (response.status === 404) { return []; } throw new Error( `MongoDB read failed: ${response.status} ${response.statusText}` ); } const result = await response.json(); return result.data || []; } catch (error) { console.error( "[DataQL React Native] Internal MongoDB read error:", error ); return []; } } async update<T>( collection: string, id: string, data: Partial<T> ): Promise<CRUDResult<T>> { try { const response = await fetch( `${this.connectionUrl}/collections/${collection}/${id}`, { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${this.config.privateToken}`, "X-App-Name": this.config.appName, "X-Database-Name": this.config.databaseName, }, body: JSON.stringify(data), } ); if (!response.ok) { throw new Error( `MongoDB update failed: ${response.status} ${response.statusText}` ); } const result = await response.json(); return { success: true, data: result.data || (data as T), modifiedCount: result.modifiedCount || 1, }; } catch (error) { console.error( "[DataQL React Native] Internal MongoDB update error:", error ); return { success: false, error: error instanceof Error ? error.message : "Internal connection update failed", }; } } async delete(collection: string, id: string): Promise<CRUDResult> { try { const response = await fetch( `${this.connectionUrl}/collections/${collection}/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${this.config.privateToken}`, "X-App-Name": this.config.appName, "X-Database-Name": this.config.databaseName, }, } ); if (!response.ok) { throw new Error( `MongoDB delete failed: ${response.status} ${response.statusText}` ); } return { success: true, deletedCount: 1, }; } catch (error) { console.error( "[DataQL React Native] Internal MongoDB delete error:", error ); return { success: false, error: error instanceof Error ? error.message : "Internal connection delete failed", }; } } } export class DataQLClient extends BaseDataQLClient { private dbClient: DatabaseClient; private cacheManager: OfflineCacheManager; private syncManager: SyncManager; private reactNativeConfig: DataQLReactNativeConfig; private internalMongoClient?: ReactNativeInternalMongoClient; constructor(config: DataQLReactNativeConfig) { // Transform React Native config to base config // Map React Native environments to core environments const mapEnvToCore = (env?: string): "dev" | "prod" | undefined => { if (!env) return undefined; switch (env) { case "development": case "dev": return "dev"; case "staging": case "production": case "prod": return "prod"; default: return "prod"; // Default to prod for unknown environments } }; const baseConfig: DataQLConfig = { appToken: config.appToken, schemas: config.schemas, debug: config.debug, env: mapEnvToCore(config.env), devPrefix: config.devPrefix, internalConnection: config.internalConnection, database: config.database, }; const syncConfig: SyncConfig = { serverUrl: config.syncConfig?.serverUrl || getWorkerUrl(config.env), autoSync: config.syncConfig?.autoSync, syncInterval: config.syncConfig?.syncInterval, customConnection: config.syncConfig?.customConnection, workerBinding: config.syncConfig?.workerBinding, internalConnection: config.syncConfig?.internalConnection || config.internalConnection, }; super(baseConfig, syncConfig); this.reactNativeConfig = config; // Initialize internal MongoDB client if configured const internalConnectionConfig = config.internalConnection || config.syncConfig?.internalConnection; if (internalConnectionConfig) { this.internalMongoClient = new ReactNativeInternalMongoClient( internalConnectionConfig ); // Set up a simple internal connection auth (in practice, you'd implement proper auth) const internalAuth: InternalConnectionAuth = { async validateToken(token: string): Promise<boolean> { // Simple validation - in practice, you'd verify against your auth service return token.length > 0 && token !== "invalid"; }, async getConnectionDetails(_appName: string) { return { databaseName: internalConnectionConfig.databaseName, mongodbUrl: internalConnectionConfig.mongodbUrl, permissions: ["read", "write", "delete"], // Example permissions }; }, async generateSessionToken( appName: string, privateToken: string ): Promise<string> { // Generate a session token based on app name and private token // In practice, this would be a secure JWT or similar const timestamp = Date.now(); return `session_${appName}_${timestamp}_${privateToken.substring(0, 8)}`; }, }; this.setInternalConnectionAuth(internalAuth); } // Initialize React Native specific components this.dbClient = new DatabaseClient(config); this.cacheManager = new OfflineCacheManager(this.dbClient); // Handle potentially undefined syncConfig const reactNativeSyncConfig: ReactNativeSyncConfig = { serverUrl: config.syncConfig?.serverUrl || getWorkerUrl(config.env), syncInterval: config.syncConfig?.syncInterval || 30000, retryCount: config.syncConfig?.retryCount || 3, batchSize: config.syncConfig?.batchSize || 10, autoSync: config.syncConfig?.autoSync ?? true, customConnection: config.syncConfig?.customConnection, workerBinding: config.syncConfig?.workerBinding, }; this.syncManager = new SyncManager( this.cacheManager, reactNativeSyncConfig ); // Set global instances for hooks setGlobalInstances(this.cacheManager, this.syncManager, this.dbClient); } // Implement internal connection CRUD operations protected async createViaInternalConnection<T>( tableName: string, data: T ): Promise<CRUDResult<T>> { if (!this.internalMongoClient) { return { success: false, error: "Internal MongoDB client not initialized", }; } return await this.internalMongoClient.create(tableName, data); } protected async readViaInternalConnection<T>( tableName: string, filter?: any ): Promise<T[]> { if (!this.internalMongoClient) { return []; } return await this.internalMongoClient.read<T>(tableName, filter); } protected async updateViaInternalConnection<T>( tableName: string, id: string, data: Partial<T> ): Promise<CRUDResult<T>> { if (!this.internalMongoClient) { return { success: false, error: "Internal MongoDB client not initialized", }; } return await this.internalMongoClient.update(tableName, id, data); } protected async deleteViaInternalConnection( tableName: string, id: string ): Promise<CRUDResult> { if (!this.internalMongoClient) { return { success: false, error: "Internal MongoDB client not initialized", }; } return await this.internalMongoClient.delete(tableName, id); } // Implement abstract methods from BaseDataQLClient async initializeStorage(): Promise<boolean> { try { const dbInitialized = await this.dbClient.initializeDatabase(); if (!dbInitialized) { return false; } // Start auto sync if enabled if (this.syncConfig?.autoSync) { this.syncManager.startAutoSync(); } return true; } catch (error) { console.error( "[DataQL React Native] Failed to initialize storage:", error ); return false; } } async storeDataLocally<T>( tableName: string, data: T ): Promise<CRUDResult<T>> { try { await this.cacheManager.createOffline(tableName, data); // Simple conversion for now - this can be refined later return { success: true, data: data, // Return the original data since it was successfully stored insertedId: (data as any).id, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Failed to store data locally", }; } } async retrieveDataLocally<T>(tableName: string, filter?: any): Promise<T[]> { try { const result = await this.cacheManager.queryOffline<T>(tableName, filter); // Handle both QueryResult format and direct array format return Array.isArray(result) ? result : (result as any)?.data || []; } catch (error) { console.error( `[DataQL React Native] Failed to retrieve data from ${tableName}:`, error ); return []; } } async updateDataLocally<T>( tableName: string, id: string, data: Partial<T> ): Promise<CRUDResult<T>> { try { await this.cacheManager.updateOffline(tableName, id, data); // Simple conversion for now return { success: true, data: data as T, // Return the updated data modifiedCount: 1, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Failed to update data locally", }; } } async deleteDataLocally(tableName: string, id: string): Promise<CRUDResult> { try { await this.cacheManager.deleteOffline(tableName, id); return { success: true, deletedCount: 1, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Failed to delete data locally", }; } } async syncWithServer(): Promise<boolean> { try { return await this.syncManager.syncNow(); } catch (error) { console.error("[DataQL React Native] Sync failed:", error); return false; } } async getStoredSyncStatus(): Promise<SyncStatus> { try { const status = await this.cacheManager.getSyncStatus(); // Convert React Native SyncStatus to Core SyncStatus return { lastSyncTime: status.lastSyncTime || undefined, pendingOperations: status.pendingOperations, failedOperations: (status as any).failedOperations || 0, isOnline: status.isOnline, }; } catch (error) { console.error("[DataQL React Native] Failed to get sync status:", error); return { pendingOperations: 0, failedOperations: 0, isOnline: false, }; } } // React Native specific methods (keeping backward compatibility) getDatabase() { return this.dbClient.getDatabase(); } // Deprecated methods - keeping for backward compatibility but delegating to base class /** @deprecated Use create() instead */ async createOffline<T>(tableName: string, data: T) { return this.create(tableName, data); } /** @deprecated Use update() instead */ async updateOffline<T>(tableName: string, id: string, data: Partial<T>) { return this.update(tableName, id, data); } /** @deprecated Use delete() instead */ async deleteOffline(tableName: string, id: string) { return this.delete(tableName, id); } /** @deprecated Use query() instead */ async queryOffline<T>(tableName: string, filter?: any) { return this.query<T>(tableName, filter); } // React Native specific sync methods startAutoSync() { this.syncManager.startAutoSync(); } stopAutoSync() { this.syncManager.stopAutoSync(); } // Advanced: Access to React Native specific managers getSyncManager() { return this.syncManager; } getCacheManager() { return this.cacheManager; } // Check online status isOnline() { return this.syncManager.getOnlineStatus(); } // Configuration updates updateSyncConfig(newConfig: Partial<SyncConfig>) { // Update base config if (this.syncConfig) { Object.assign(this.syncConfig, newConfig); } // Update React Native config this.reactNativeConfig.syncConfig = { ...this.reactNativeConfig.syncConfig, ...newConfig, }; // Update sync manager this.syncManager.updateConfig(newConfig as any); } // Override custom connection methods to also update React Native config setCustomConnection(customConnection: CustomRequestConnection) { super.setCustomConnection(customConnection); this.reactNativeConfig.syncConfig = this.reactNativeConfig.syncConfig || {}; this.reactNativeConfig.syncConfig.customConnection = customConnection; this.syncManager.updateConfig({ customConnection }); } setWorkerBinding(workerBinding: WorkerBinding) { super.setWorkerBinding(workerBinding); this.reactNativeConfig.syncConfig = this.reactNativeConfig.syncConfig || {}; this.reactNativeConfig.syncConfig.workerBinding = workerBinding; this.syncManager.updateConfig({ workerBinding }); } // Internal connection configuration methods setInternalConnection(internalConnection: InternalConnectionConfig) { super.setInternalConnection(internalConnection); this.reactNativeConfig.internalConnection = internalConnection; // Reinitialize internal MongoDB client this.internalMongoClient = new ReactNativeInternalMongoClient( internalConnection ); // Update sync config if (this.reactNativeConfig.syncConfig) { this.reactNativeConfig.syncConfig.internalConnection = internalConnection; } } clearCustomConnection() { super.clearCustomConnection(); if (this.reactNativeConfig.syncConfig) { this.reactNativeConfig.syncConfig.customConnection = undefined; this.reactNativeConfig.syncConfig.workerBinding = undefined; } this.syncManager.updateConfig({ customConnection: undefined, workerBinding: undefined, }); } clearInternalConnection() { super.clearInternalConnection(); this.reactNativeConfig.internalConnection = undefined; this.internalMongoClient = undefined; if (this.reactNativeConfig.syncConfig) { this.reactNativeConfig.syncConfig.internalConnection = undefined; } } // Override destroy to clean up React Native specific resources async destroy() { try { this.syncManager.destroy(); await this.dbClient.close(); this.internalMongoClient = undefined; await super.destroy(); if (this.config.debug) { console.log("[DataQL React Native] Client destroyed"); } } catch (error) { console.error("[DataQL React Native] Error destroying client:", error); } } // React Native specific getters getReactNativeConfig() { return { ...this.reactNativeConfig }; } // Internal connection specific methods getInternalMongoClient() { return this.internalMongoClient; } // Check if internal connection is enabled and working async testInternalConnection(): Promise<boolean> { if (!this.isInternalConnectionActive() || !this.internalMongoClient) { return false; } try { // Try a simple read operation to test the connection await this.internalMongoClient.read("_test_connection", {}); return true; } catch (error) { if (this.config.debug) { console.error( "[DataQL React Native] Internal connection test failed:", error ); } return false; } } }