UNPKG

@dataql/react-native

Version:

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

529 lines (401 loc) 13.4 kB
# @dataql/react-native An offline-first React Native SDK for DataQL with automatic synchronization using Drizzle ORM and Expo SQLite. ## Features - **Offline-First**: All data operations work offline and sync automatically when online - **Drizzle ORM**: Built on top of Drizzle ORM for type-safe database operations - **Expo SQLite**: Uses Expo SQLite for local storage with change listeners - **Live Queries**: Real-time data updates using Drizzle's live query capabilities - **Automatic Sync**: Background synchronization with configurable intervals - **Conflict Resolution**: Built-in conflict detection and resolution strategies - **React Hooks**: Easy-to-use React hooks for data operations - **TypeScript**: Full TypeScript support with type inference ## Installation ```bash npm install @dataql/react-native drizzle-orm expo-sqlite@next npm install -D drizzle-kit babel-plugin-inline-import ``` ## Setup ### 1. Configure Babel and Metro Update your `babel.config.js`: ```javascript module.exports = function (api) { api.cache(true); return { presets: ["babel-preset-expo"], plugins: [["inline-import", { extensions: [".sql"] }]], }; }; ``` Update your `metro.config.js`: ```javascript const { getDefaultConfig } = require("expo/metro-config"); const config = getDefaultConfig(__dirname); config.resolver.sourceExts.push("sql"); module.exports = config; ``` ### 2. Generate Migrations Create a `drizzle.config.ts` file: ```typescript import type { Config } from "drizzle-kit"; export default { schema: "./src/db/schema.ts", out: "./drizzle", dialect: "sqlite", driver: "expo", } satisfies Config; ``` Generate migrations: ```bash npx drizzle-kit generate ``` ## Quick Start ### 1. Initialize DataQL Client ```typescript import { DataQLClient, createDefaultConfig } from "@dataql/react-native"; const config = createDefaultConfig("https://your-worker-url.com/api"); const dataQLClient = new DataQLClient(config); // Initialize the client await dataQLClient.initialize(); ``` ### Using Custom Network Transport The SDK supports custom network transport, just like the Core SDK: ```typescript import { DataQLClient, createDefaultConfig, CustomRequestConnection, } from "@dataql/react-native"; // Create a custom connection const customConnection: CustomRequestConnection = { async request(url: string, options: RequestInit): Promise<Response> { // Your custom network implementation // Could route through other SDKs, add authentication, etc. console.log("Custom request to:", url); return fetch(url, options); }, }; // Configure with custom connection const config = createDefaultConfig( "https://your-worker-url.com/api", "myapp.db", { customConnection, } ); const dataQLClient = new DataQLClient(config); // Or set it later dataQLClient.setCustomConnection(customConnection); ``` ### 2. Use in React Components ```typescript import React from 'react'; import { View, Text, Button } from 'react-native'; import { useQuery, useMutation, useSync } from '@dataql/react-native'; function UserList() { const { data: users, loading, error, refetch } = useQuery('users'); const { create, update, delete: deleteUser } = useMutation(); const { sync, syncStatus, isOnline } = useSync(); const handleCreateUser = async () => { await create('users', { name: 'John Doe', email: 'john@example.com', }); refetch(); }; return ( <View> <Text>Users ({isOnline ? 'Online' : 'Offline'})</Text> <Text>Pending: {syncStatus.pendingOperations}</Text> {loading && <Text>Loading...</Text>} {error && <Text>Error: {error}</Text>} {users.map(user => ( <View key={user.id}> <Text>{user.name} - {user.email}</Text> <Button title="Delete" onPress={() => deleteUser('users', user.id)} /> </View> ))} <Button title="Add User" onPress={handleCreateUser} /> <Button title="Sync Now" onPress={sync} /> </View> ); } ``` ### 3. Handle Migrations ```typescript import { useDatabaseMigrations } from '@dataql/react-native'; import migrations from './drizzle/migrations'; function App() { const { success, error } = useDatabaseMigrations( dataQLClient.getDatabase(), migrations ); if (error) { return <Text>Migration error: {error.message}</Text>; } if (!success) { return <Text>Migration in progress...</Text>; } return <UserList />; } ``` ## API Reference ### DataQLClient The main client class for managing offline data and synchronization. ```typescript const client = new DataQLClient(config); await client.initialize(); // Data operations await client.createOffline("users", userData); await client.updateOffline("users", userId, updateData); await client.deleteOffline("users", userId); const results = await client.queryOffline("users"); // Sync operations await client.sync(); client.startAutoSync(); client.stopAutoSync(); // Status const status = await client.getSyncStatus(); const isOnline = client.isOnline(); // Custom network transport client.setCustomConnection(customConnection); client.setWorkerBinding(workerBinding); client.clearCustomConnection(); ``` ### Hooks #### useQuery Query data with offline-first approach: ```typescript const { data, loading, error, refetch, isFromCache, lastUpdated } = useQuery( "tableName", filter ); ``` #### useMutation Perform create, update, delete operations: ```typescript const { create, update, delete, loading, error } = useMutation(); await create('users', userData); await update('users', userId, updateData); await delete('users', userId); ``` ### Unique Document Creation DataQL React Native supports `createUnique()` method through the underlying BaseDataQLClient, preventing duplicate documents based on comparable fields (excluding ID and subdocument fields). ```typescript import { useCallback } from 'react'; import { useMutation } from '@dataql/react-native'; function UserRegistration() { const { create, loading, error } = useMutation(); const createUniqueUser = useCallback(async (userData) => { // Note: createUnique is accessed through the base client // You can implement this via custom hooks or direct client access const dataQLClient = /* get your client instance */; // Use the core SDK's createUnique functionality const userCollection = dataQLClient.collection('users', userSchema); const result = await userCollection.createUnique({ name: userData.name, email: userData.email, age: userData.age, preferences: { theme: 'dark', notifications: true, }, }); if (result.isExisting) { console.log('User already exists:', result.insertedId); } else { console.log('Created new user:', result.insertedId); } return result; }, []); return ( <View> <Button title="Register User" onPress={() => createUniqueUser({ name: 'John Doe', email: 'john@example.com', age: 30 })} disabled={loading} /> {error && <Text>Error: {error}</Text>} </View> ); } ``` **Excluded from comparison:** - ID fields: `id`, `_id`, and any field containing 'id' - Auto-generated timestamps: `createdAt`, `updatedAt` - Subdocument objects: nested objects like `preferences`, `profile` - Subdocument arrays: arrays of objects like `addresses`, `reviews` **Compared fields:** - Primitive fields: strings, numbers, booleans - Enum fields: category selections - Simple arrays: arrays of primitive values #### useSync Manage synchronization: ```typescript const { syncStatus, sync, startAutoSync, stopAutoSync, loading, error, isOnline, isSyncing, } = useSync(); ``` #### useNetworkStatus Monitor network connectivity: ```typescript const { isOnline } = useNetworkStatus(); ``` ### Live Queries DataQL React Native supports live queries with two approaches: 1. **DataQL-style** - Consistent with other DataQL operations (`tableName`, `filter`) 2. **Raw Drizzle** - Direct database queries for advanced use cases **Requirements:** - Database opened with `enableChangeListener: true` (DataQL does this by default) - Latest versions: `drizzle-orm@^0.44.2` and `expo-sqlite@^15.2.12` **DataQL-Style Live Queries (Recommended):** ```typescript import { useLiveQuery } from '@dataql/react-native'; function LiveUserList() { // Same signature as useQuery - automatically updates when data changes const { data: users, error, updatedAt } = useLiveQuery('users', { isActive: true }); return ( <View> <Text>Active Users: {users?.length || 0}</Text> {updatedAt && ( <Text>Last Updated: {updatedAt.toLocaleTimeString()}</Text> )} {error && <Text>Error: {error}</Text>} {users?.map(user => ( <Text key={user.id}>{user.name}</Text> ))} </View> ); } // Consistent with other DataQL operations: const { data: allUsers } = useLiveQuery('users'); // All users const { data: activeUsers } = useLiveQuery('users', { isActive: true }); // Filtered users const { data: adminUsers } = useLiveQuery('users', { role: 'admin' }); // Admin users ``` **Raw Drizzle Live Queries (Advanced):** ```typescript import { useRawLiveQuery } from '@dataql/react-native'; function AdvancedLiveQuery() { // Get database instance from DataQL client const db = dataQLClient.getDatabase(); // Complex live queries with joins, aggregations, etc. const { data: userStats, error, updatedAt } = useRawLiveQuery( db.select({ totalUsers: count(users.id), activeUsers: count(users.id).where(eq(users.isActive, true)), }).from(users) ); const { data: userProjects } = useRawLiveQuery( db.select() .from(projects) .innerJoin(users, eq(projects.ownerId, users.id)) .where(eq(users.id, currentUserId)) .orderBy(desc(projects.createdAt)) ); return ( <View> <Text>Total Users: {userStats?.totalUsers}</Text> <Text>Active Users: {userStats?.activeUsers}</Text> </View> ); } ``` **API Comparison:** | Hook | Signature | Use Case | | ----------------- | ---------------------- | ------------------------------------- | | `useQuery` | `(tableName, filter?)` | Standard offline-first queries | | `useLiveQuery` | `(tableName, filter?)` | Real-time updates with same API | | `useRawLiveQuery` | `(drizzleQuery)` | Advanced queries, joins, aggregations | **Features:** - ✅ Consistent API with other DataQL operations - ✅ Automatic re-rendering on data changes - ✅ Built-in error handling and timestamps - ✅ Works offline with local SQLite data - ✅ Seamless integration with DataQL's CRUD operations - ✅ Optimized performance for complex queries For complete Drizzle documentation, see: [Drizzle ORM Expo SQLite Guide](https://orm.drizzle.team/docs/connect-expo-sqlite) ## Configuration ### DataQLReactNativeConfig ```typescript interface DataQLReactNativeConfig { databaseName: string; syncConfig: SyncConfig; enableChangeListener?: boolean; debug?: boolean; } interface SyncConfig { workerUrl: string; syncInterval: number; // milliseconds retryCount: number; batchSize: number; autoSync: boolean; // Network transport options customConnection?: CustomRequestConnection; workerBinding?: WorkerBinding; } interface CustomRequestConnection { request(url: string, options: RequestInit): Promise<Response>; } interface WorkerBinding { fetch(request: Request): Promise<Response>; } ``` ### Default Configuration ```typescript const config = createDefaultConfig("https://your-api.com", "myapp.db"); // Returns: // { // databaseName: 'myapp.db', // syncConfig: { // workerUrl: 'https://your-api.com', // syncInterval: 30000, // 30 seconds // retryCount: 3, // batchSize: 50, // autoSync: true, // }, // enableChangeListener: true, // debug: false, // } ``` ## Architecture The SDK follows an offline-first architecture: 1. **Local SQLite Database**: All data is stored locally using Expo SQLite 2. **Operation Queue**: Changes are queued for synchronization 3. **Background Sync**: Automatic synchronization when online 4. **Conflict Resolution**: Handles conflicts between local and server data 5. **Event System**: Real-time updates via event listeners ## Best Practices 1. **Initialize Early**: Initialize the DataQL client early in your app lifecycle 2. **Handle Offline States**: Always show appropriate UI for offline states 3. **Monitor Sync Status**: Display sync status and pending operations to users 4. **Error Handling**: Implement proper error handling for sync failures 5. **Performance**: Use live queries sparingly for better performance ## Troubleshooting ### Migration Issues If you encounter migration errors: 1. Ensure `babel-plugin-inline-import` is properly configured 2. Check that `.sql` files are included in Metro resolver 3. Verify Drizzle configuration uses `driver: 'expo'` ### Sync Issues If synchronization fails: 1. Check network connectivity 2. Verify worker URL configuration 3. Monitor pending operations 4. Check server API compatibility ## License MIT ``` ```