UNPKG

@dataql/react-native

Version:

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

500 lines (499 loc) 19.4 kB
import { BaseDataQLClient, } from "@dataql/core"; import { DatabaseClient } from "./db/client"; import { OfflineCacheManager } from "./cache/OfflineCacheManager"; import { SyncManager } from "./sync/SyncManager"; import { setGlobalInstances } from "./hooks/useDataQL"; import { getWorkerUrl } from "@dataql/core"; // React Native specific internal connection MongoDB client class ReactNativeInternalMongoClient { constructor(config) { this.config = config; this.connectionUrl = `${config.mongodbUrl}/${config.databaseName}`; } async create(collection, data) { 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.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(collection, filter) { 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(collection, id, data) { 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, 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, id) { 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 { constructor(config) { // Transform React Native config to base config // Map React Native environments to core environments const mapEnvToCore = (env) => { 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 = { appToken: config.appToken, schemas: config.schemas, debug: config.debug, env: mapEnvToCore(config.env), devPrefix: config.devPrefix, internalConnection: config.internalConnection, database: config.database, }; const 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 = { async validateToken(token) { // Simple validation - in practice, you'd verify against your auth service return token.length > 0 && token !== "invalid"; }, async getConnectionDetails(_appName) { return { databaseName: internalConnectionConfig.databaseName, mongodbUrl: internalConnectionConfig.mongodbUrl, permissions: ["read", "write", "delete"], // Example permissions }; }, async generateSessionToken(appName, privateToken) { // 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 = { 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 async createViaInternalConnection(tableName, data) { if (!this.internalMongoClient) { return { success: false, error: "Internal MongoDB client not initialized", }; } return await this.internalMongoClient.create(tableName, data); } async readViaInternalConnection(tableName, filter) { if (!this.internalMongoClient) { return []; } return await this.internalMongoClient.read(tableName, filter); } async updateViaInternalConnection(tableName, id, data) { if (!this.internalMongoClient) { return { success: false, error: "Internal MongoDB client not initialized", }; } return await this.internalMongoClient.update(tableName, id, data); } async deleteViaInternalConnection(tableName, id) { 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() { 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(tableName, data) { 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.id, }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : "Failed to store data locally", }; } } async retrieveDataLocally(tableName, filter) { try { const result = await this.cacheManager.queryOffline(tableName, filter); // Handle both QueryResult format and direct array format return Array.isArray(result) ? result : result?.data || []; } catch (error) { console.error(`[DataQL React Native] Failed to retrieve data from ${tableName}:`, error); return []; } } async updateDataLocally(tableName, id, data) { try { await this.cacheManager.updateOffline(tableName, id, data); // Simple conversion for now return { success: true, data: data, // 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, id) { 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() { try { return await this.syncManager.syncNow(); } catch (error) { console.error("[DataQL React Native] Sync failed:", error); return false; } } async getStoredSyncStatus() { try { const status = await this.cacheManager.getSyncStatus(); // Convert React Native SyncStatus to Core SyncStatus return { lastSyncTime: status.lastSyncTime || undefined, pendingOperations: status.pendingOperations, failedOperations: status.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(tableName, data) { return this.create(tableName, data); } /** @deprecated Use update() instead */ async updateOffline(tableName, id, data) { return this.update(tableName, id, data); } /** @deprecated Use delete() instead */ async deleteOffline(tableName, id) { return this.delete(tableName, id); } /** @deprecated Use query() instead */ async queryOffline(tableName, filter) { return this.query(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) { // 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); } // Override custom connection methods to also update React Native config setCustomConnection(customConnection) { super.setCustomConnection(customConnection); this.reactNativeConfig.syncConfig = this.reactNativeConfig.syncConfig || {}; this.reactNativeConfig.syncConfig.customConnection = customConnection; this.syncManager.updateConfig({ customConnection }); } setWorkerBinding(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) { 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() { 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; } } }