@dataql/react-native
Version:
DataQL React Native SDK with offline-first capabilities and clean API
660 lines (589 loc) • 19.1 kB
text/typescript
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;
}
}
}