diff --git a/TODOS_FEATURE.md b/TODOS_FEATURE.md new file mode 100644 index 0000000..f484167 --- /dev/null +++ b/TODOS_FEATURE.md @@ -0,0 +1,239 @@ +# Reactive SQLite Todos Feature + +## Overview + +This project now includes a complete reactive SQLite implementation with a todos demo app. The reactive wrapper automatically updates UI components when database changes occur, providing a seamless developer experience. + +## What Was Created + +### 1. Reactive SQLite Package (`/packages/reactive-sqlite/`) + +A local package that wraps `expo-sqlite` to make it reactive: + +- **`ReactiveQuery`**: A query class that manages subscriptions and notifies listeners +- **`useReactiveQuery`**: React hook for using reactive queries in components +- **`createReactiveDatabase()`**: Factory function to create a reactive database wrapper +- **Automatic Updates**: Components automatically re-render when data changes + +**Key Features:** + +- ✅ Type-safe TypeScript implementation +- ✅ Subscription-based reactivity +- ✅ Query caching and invalidation +- ✅ Loading and error states +- ✅ Zero dependencies (uses only expo-sqlite and React) + +### 2. Todos Hook (`/hooks/useTodos.ts`) + +A custom hook that demonstrates the reactive SQLite wrapper in action: + +```typescript +const { todos, toggleTodo, deleteTodo, update, create } = useTodos(); +``` + +**API:** + +- `todos`: Array of todo items (automatically updates) +- `isLoading`: Loading state +- `error`: Error state +- `create(title)`: Add a new todo +- `update(id, title)`: Update a todo's title +- `toggleTodo(id)`: Toggle completion status +- `deleteTodo(id)`: Delete a single todo +- `deleteCompleted()`: Delete all completed todos +- `refresh()`: Manually refresh the list + +### 3. Todos Screen (`/components/screens/todos.tsx`) + +A beautiful, SwiftUI-style todo app with: + +- ✨ Glass morphism design using `@expo/ui/swift-ui` +- 📊 Statistics showing active and completed counts +- ➕ Add new todos with inline form +- ✏️ Edit todos inline +- ✅ Toggle completion with animated checkbox +- 🗑️ Delete individual or all completed todos +- 🎨 Modern, polished UI with proper spacing and typography +- 📱 Native iOS SF Symbols icons + +### 4. Navigation Integration + +Added a new "Todos" tab to the main navigation: + +- Tab icon: Checkmark circle (SF Symbol: `checkmark.circle.fill`) +- Positioned between "Basic" and "Settings" tabs +- Full screen layout with proper routing + +## How It Works + +### The Reactive Pattern + +1. **Query Creation**: Create a reactive query with a unique key + + ```typescript + const query = reactiveDb.createQuery("todos", async () => { + return await db.getAllAsync("SELECT * FROM todos"); + }); + ``` + +2. **Component Usage**: Use the query in React components + + ```typescript + const { data, isLoading } = useReactiveQuery(query); + ``` + +3. **Data Updates**: When data changes, invalidate the query + + ```typescript + await db.runAsync("INSERT INTO todos ..."); + await reactiveDb.invalidateQuery("todos"); + ``` + +4. **Auto-Update**: All subscribed components automatically re-render! + +### Database Schema + +```sql +CREATE TABLE todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + completed INTEGER DEFAULT 0, + createdAt INTEGER NOT NULL +); +``` + +## File Structure + +``` +/Users/beto/Desktop/apps/expo-ui-playground/ +├── packages/ +│ └── reactive-sqlite/ +│ ├── index.ts # Core reactive wrapper +│ ├── package.json # Package definition +│ └── README.md # Package documentation +├── hooks/ +│ └── useTodos.ts # Todos hook implementation +├── components/ +│ └── screens/ +│ └── todos.tsx # Todos UI screen +├── app/ +│ ├── _layout.tsx # Updated with todos tab +│ └── todos/ +│ ├── _layout.tsx # Todos stack navigator +│ └── index.tsx # Todos route +└── tsconfig.json # Updated with @local/* paths +``` + +## Configuration Changes + +### `tsconfig.json` + +Added path alias for local packages: + +```json +{ + "compilerOptions": { + "paths": { + "@local/*": ["./packages/*"] + } + } +} +``` + +## Usage Example + +```typescript +import { useTodos } from "@/hooks/useTodos"; + +function MyComponent() { + const { todos, create, toggleTodo } = useTodos(); + + return ( + + {todos.map((todo) => ( + toggleTodo(todo.id)}> + {todo.title} + + ))} + + + ); +} +``` + +## Benefits + +### For Developers + +- **Simple API**: No complex state management needed +- **Automatic Updates**: UI stays in sync with database +- **Type Safety**: Full TypeScript support +- **Reusable**: Easy to extend for other features + +### For Users + +- **Fast**: Instant UI updates +- **Reliable**: SQLite-backed persistence +- **Beautiful**: Modern, native-feeling UI +- **Smooth**: No flickering or manual refreshes + +## Testing the Feature + +1. Open the Expo app +2. Navigate to the "Todos" tab +3. Add some todos using the input field +4. Toggle completion by tapping on todos +5. Edit todos using the pencil icon +6. Delete todos individually or clear all completed +7. Notice how all changes are instantly reflected and persist across app restarts! + +## Extending the System + +To create your own reactive hooks: + +```typescript +// 1. Get the database +const { db, reactiveDb } = await getDatabase(); + +// 2. Create a query +const myQuery = reactiveDb.createQuery("mydata", async () => { + return await db.getAllAsync("SELECT * FROM mytable"); +}); + +// 3. Use in components +function MyComponent() { + const { data } = useReactiveQuery(myQuery); + // Component automatically updates when data changes! +} + +// 4. Invalidate when data changes +async function updateData() { + await db.runAsync("INSERT INTO mytable ..."); + await reactiveDb.invalidateQuery("mydata"); +} +``` + +## Future Enhancements + +Possible improvements: + +- [ ] Add optimistic updates for better perceived performance +- [ ] Implement debounced invalidation for rapid changes +- [ ] Add query parameters support +- [ ] Create a query builder abstraction +- [ ] Add offline support with sync capabilities +- [ ] Implement undo/redo functionality +- [ ] Add categories and tags for todos +- [ ] Implement search and filtering + +## Notes + +- Database file: `todos.db` (stored in SQLite default location) +- WAL mode enabled for better performance +- All operations are async for non-blocking UI +- Singleton database pattern prevents multiple connections +- Proper cleanup with useEffect unsubscribe + +--- + +Built with ❤️ using Expo, SQLite, and React Native diff --git a/app.json b/app.json index b6b2534..b1809bf 100644 --- a/app.json +++ b/app.json @@ -39,7 +39,8 @@ ], "expo-font", "expo-web-browser", - "expo-video" + "expo-video", + "expo-sqlite" ], "experiments": { "typedRoutes": true diff --git a/app/_layout.tsx b/app/_layout.tsx index 3db672b..a02b5bb 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -11,6 +11,10 @@ export default function TabLayout() { + + + + diff --git a/app/todos/_layout.tsx b/app/todos/_layout.tsx new file mode 100644 index 0000000..27df14e --- /dev/null +++ b/app/todos/_layout.tsx @@ -0,0 +1,15 @@ +import { Stack } from "expo-router"; + +export default function TodosLayout() { + return ( + + + + ); +} diff --git a/app/todos/index.tsx b/app/todos/index.tsx new file mode 100644 index 0000000..54a0041 --- /dev/null +++ b/app/todos/index.tsx @@ -0,0 +1 @@ +export { default } from "@/components/screens/todos"; diff --git a/bun.lockb b/bun.lockb index 552e3c9..ca8769d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/components/liquid-glass/ProfileSection.tsx b/components/liquid-glass/ProfileSection.tsx index 284ed90..fd653d9 100644 --- a/components/liquid-glass/ProfileSection.tsx +++ b/components/liquid-glass/ProfileSection.tsx @@ -105,10 +105,7 @@ export function ProfileSection() { - @@ -132,34 +129,15 @@ export function ProfileSection() { - {isLiquidGlassAvailable() && ( - - - - )} - {isLiquidGlassAvailable() && ( - - - - )} - (null); + const [editingText, setEditingText] = React.useState(""); + + const handleCreate = async () => { + if (newTodoText.trim()) { + await create(newTodoText.trim()); + setNewTodoText(""); + } + }; + + const handleEdit = (id: number, currentTitle: string) => { + setEditingId(id); + setEditingText(currentTitle); + }; + + const handleSaveEdit = async () => { + if (editingId !== null && editingText.trim()) { + await update(editingId, editingText.trim()); + setEditingId(null); + setEditingText(""); + } + }; + + const handleCancelEdit = () => { + setEditingId(null); + setEditingText(""); + }; + + const completedCount = todos.filter((t) => t.completed === 1).length; + const activeCount = todos.length - completedCount; + + if (isLoading) { + return ( + + + Loading todos... + + ); + } + + return ( + + {/* Header */} + + Todo List + + + {activeCount} + Active + + + {completedCount} + Completed + + + + + {/* Add New Todo */} + + + + + + {/* Todo List */} + + {todos.length === 0 ? ( + + 📝 + No todos yet + Add your first todo above + + ) : ( + todos.map((todo) => ( + + {editingId === todo.id ? ( + // Edit Mode + + + + + + + + ) : ( + // View Mode + <> + toggleTodo(todo.id)} + > + + {todo.completed === 1 && ( + + )} + + + + {todo.title} + + + {new Date(todo.createdAt).toLocaleDateString()} + + + + + + + )} + + {/* Info */} + + + ✨ Todos are stored locally using SQLite + + + 🔄 Updates are reactive and automatic + + + + ); +} + +function Button( + props: React.ComponentProps & { + style?: StyleProp; + } +) { + const { style, ...restProps } = props; + return ( + + {props.children} + + ); +} + +const styles = StyleSheet.create({ + container: { + padding: 16, + gap: 20, + }, + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + gap: 12, + }, + loadingText: { + fontSize: 16, + color: "#666", + }, + header: { + gap: 12, + }, + title: { + fontSize: 34, + fontWeight: "bold", + color: "#000", + }, + statsContainer: { + flexDirection: "row", + gap: 12, + }, + statBadge: { + backgroundColor: "rgba(0, 0, 0, 0.05)", + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 12, + alignItems: "center", + flexDirection: "row", + gap: 8, + }, + statNumber: { + fontSize: 20, + fontWeight: "bold", + color: "#007AFF", + }, + statLabel: { + fontSize: 14, + color: "#666", + }, + addSection: { + gap: 12, + }, + input: { + backgroundColor: "rgba(255, 255, 255, 0.9)", + borderWidth: 1, + borderColor: "rgba(0, 0, 0, 0.1)", + borderRadius: 12, + padding: 16, + fontSize: 16, + color: "#000", + }, + addButton: { + width: "100%", + }, + listSection: { + gap: 12, + }, + emptyState: { + alignItems: "center", + paddingVertical: 60, + gap: 8, + }, + emptyIcon: { + fontSize: 64, + marginBottom: 8, + }, + emptyText: { + fontSize: 20, + fontWeight: "600", + color: "#666", + }, + emptySubtext: { + fontSize: 16, + color: "#999", + }, + todoItem: { + backgroundColor: "rgba(255, 255, 255, 0.9)", + borderRadius: 16, + padding: 16, + borderWidth: 1, + borderColor: "rgba(0, 0, 0, 0.05)", + gap: 12, + }, + todoContent: { + flexDirection: "row", + alignItems: "center", + gap: 12, + flex: 1, + }, + checkbox: { + width: 28, + height: 28, + borderRadius: 14, + borderWidth: 2, + borderColor: "#007AFF", + justifyContent: "center", + alignItems: "center", + }, + checkboxChecked: { + backgroundColor: "#007AFF", + }, + checkmark: { + color: "white", + fontSize: 16, + fontWeight: "bold", + }, + todoTextContainer: { + flex: 1, + gap: 4, + }, + todoText: { + fontSize: 17, + color: "#000", + fontWeight: "500", + }, + todoTextCompleted: { + textDecorationLine: "line-through", + color: "#999", + }, + todoDate: { + fontSize: 13, + color: "#999", + }, + todoActions: { + flexDirection: "row", + gap: 8, + justifyContent: "flex-end", + }, + iconButton: { + minWidth: 44, + }, + editContainer: { + gap: 12, + }, + editInput: { + backgroundColor: "rgba(0, 0, 0, 0.05)", + borderRadius: 8, + padding: 12, + fontSize: 16, + color: "#000", + }, + editActions: { + flexDirection: "row", + gap: 8, + justifyContent: "flex-end", + }, + smallButton: { + minWidth: 80, + }, + actionsSection: { + gap: 12, + }, + infoSection: { + gap: 8, + paddingTop: 12, + alignItems: "center", + }, + infoText: { + fontSize: 14, + color: "#999", + textAlign: "center", + }, +}); diff --git a/eslint.config.js b/eslint.config.js index 5025da6..33f0ef7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,10 +1,20 @@ // https://docs.expo.dev/guides/using-eslint/ -const { defineConfig } = require('eslint/config'); -const expoConfig = require('eslint-config-expo/flat'); +const { defineConfig } = require("eslint/config"); +const expoConfig = require("eslint-config-expo/flat"); module.exports = defineConfig([ expoConfig, { - ignores: ['dist/*'], + ignores: ["dist/*"], + }, + { + rules: { + "import/no-unresolved": [ + "error", + { + ignore: ["^@local/"], + }, + ], + }, }, ]); diff --git a/hooks/useTodos.ts b/hooks/useTodos.ts new file mode 100644 index 0000000..cb3a5dc --- /dev/null +++ b/hooks/useTodos.ts @@ -0,0 +1,159 @@ +import * as SQLite from "expo-sqlite"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + createReactiveDatabase, + useReactiveQuery, +} from "../packages/reactive-sqlite"; + +export interface Todo { + id: number; + title: string; + completed: number; // SQLite doesn't have boolean, so we use 0/1 + createdAt: number; +} + +// Singleton database instance +let dbInstance: SQLite.SQLiteDatabase | null = null; +let reactiveDbInstance: ReturnType | null = null; + +async function getDatabase() { + if (!dbInstance) { + dbInstance = await SQLite.openDatabaseAsync("todos.db"); + + // Initialize database schema + await dbInstance.execAsync(` + PRAGMA journal_mode = WAL; + CREATE TABLE IF NOT EXISTS todos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + completed INTEGER DEFAULT 0, + createdAt INTEGER NOT NULL + ); + `); + + reactiveDbInstance = createReactiveDatabase(dbInstance); + } + + return { db: dbInstance, reactiveDb: reactiveDbInstance! }; +} + +/** + * Reactive hook for managing todos + * Automatically updates when todos are added, updated, or deleted + */ +export function useTodos() { + const [reactiveDb, setReactiveDb] = useState | null>(null); + const [db, setDb] = useState(null); + const initialized = useRef(false); + + // Initialize database + useEffect(() => { + if (initialized.current) return; + initialized.current = true; + + getDatabase().then(({ db, reactiveDb }) => { + setDb(db); + setReactiveDb(reactiveDb); + }); + }, []); + + // Create reactive query for todos + const todosQuery = useMemo(() => { + if (!reactiveDb || !db) return null; + return reactiveDb.createQuery("todos", async () => { + const todos = await db.getAllAsync( + "SELECT * FROM todos ORDER BY createdAt DESC" + ); + return todos; + }); + }, [reactiveDb, db]); + + // Use the reactive query hook + const { + data: todos, + isLoading, + error, + refresh, + } = useReactiveQuery(todosQuery); + + // Create a new todo + const create = useCallback( + async (title: string) => { + if (!db || !reactiveDb) return; + + const createdAt = Date.now(); + await db.runAsync( + "INSERT INTO todos (title, completed, createdAt) VALUES (?, ?, ?)", + title, + 0, + createdAt + ); + + // Invalidate query to trigger refresh + await reactiveDb.invalidateQuery("todos"); + }, + [db, reactiveDb] + ); + + // Update a todo + const update = useCallback( + async (id: number, title: string) => { + if (!db || !reactiveDb) return; + + await db.runAsync("UPDATE todos SET title = ? WHERE id = ?", title, id); + + await reactiveDb.invalidateQuery("todos"); + }, + [db, reactiveDb] + ); + + // Toggle todo completion + const toggleTodo = useCallback( + async (id: number) => { + if (!db || !reactiveDb) return; + + await db.runAsync( + "UPDATE todos SET completed = CASE WHEN completed = 0 THEN 1 ELSE 0 END WHERE id = ?", + id + ); + + await reactiveDb.invalidateQuery("todos"); + }, + [db, reactiveDb] + ); + + // Delete a todo + const deleteTodo = useCallback( + async (id: number) => { + if (!db || !reactiveDb) return; + + await db.runAsync("DELETE FROM todos WHERE id = ?", id); + + await reactiveDb.invalidateQuery("todos"); + }, + [db, reactiveDb] + ); + + // Delete all completed todos + const deleteCompleted = useCallback(async () => { + if (!db || !reactiveDb) return; + + await db.runAsync("DELETE FROM todos WHERE completed = 1"); + + await reactiveDb.invalidateQuery("todos"); + }, [db, reactiveDb]); + + return { + todos: todos || [], + isLoading: !db || isLoading, + error, + create, + update, + toggleTodo, + deleteTodo, + deleteCompleted, + refresh, + }; +} diff --git a/package.json b/package.json index a661a57..73b466f 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "expo-mesh-gradient": "0.4.8-canary-20250930-9dc59d3", "expo-router": "6.1.0-canary-20250930-9dc59d3", "expo-splash-screen": "31.0.11-canary-20250930-9dc59d3", + "expo-sqlite": "16.0.9-canary-20250930-9dc59d3", "expo-status-bar": "3.0.9-canary-20250930-9dc59d3", "expo-symbols": "1.0.8-canary-20250930-9dc59d3", "expo-system-ui": "6.0.8-canary-20250930-9dc59d3", diff --git a/packages/reactive-sqlite/README.md b/packages/reactive-sqlite/README.md new file mode 100644 index 0000000..997d367 --- /dev/null +++ b/packages/reactive-sqlite/README.md @@ -0,0 +1,134 @@ +# @local/reactive-sqlite + +A reactive wrapper for expo-sqlite that provides automatic UI updates when database changes occur. + +## Features + +- 🔄 **Reactive Updates**: Components automatically re-render when data changes +- 🪝 **React Hooks**: Simple hook-based API for React components +- 🚀 **Lightweight**: Minimal overhead, built on top of expo-sqlite +- 🎯 **Type-Safe**: Full TypeScript support +- 💾 **Query Caching**: Efficient query result caching and invalidation + +## Installation + +This is a local package, no installation needed. Just import and use. + +## Basic Usage + +### 1. Create a Reactive Database + +```typescript +import * as SQLite from "expo-sqlite"; +import { + createReactiveDatabase, + useReactiveQuery, +} from "@local/reactive-sqlite"; + +// Initialize database +const db = await SQLite.openDatabaseAsync("myapp.db"); +const reactiveDb = createReactiveDatabase(db); +``` + +### 2. Create Reactive Queries + +```typescript +// Create a query that will automatically update components +const usersQuery = reactiveDb.createQuery("users", async () => { + return await db.getAllAsync("SELECT * FROM users"); +}); +``` + +### 3. Use in React Components + +```typescript +function UsersList() { + const { data: users, isLoading, error } = useReactiveQuery(usersQuery); + + if (isLoading) return ; + if (error) return ; + + return ( + + {users.map((user) => ( + + ))} + + ); +} +``` + +### 4. Trigger Updates + +```typescript +async function addUser(name: string) { + await db.runAsync("INSERT INTO users (name) VALUES (?)", name); + + // Automatically update all components using this query + await reactiveDb.invalidateQuery("users"); +} +``` + +## API Reference + +### `createReactiveDatabase(db: SQLiteDatabase)` + +Creates a reactive wrapper around an SQLite database. + +**Returns:** `ReactiveDatabase` + +### `ReactiveDatabase.createQuery(key: string, queryFn: () => Promise)` + +Creates a reactive query that can be used with `useReactiveQuery`. + +**Parameters:** + +- `key`: Unique identifier for the query +- `queryFn`: Async function that returns query results + +**Returns:** `ReactiveQuery` + +### `ReactiveDatabase.invalidateQuery(key: string)` + +Invalidates a query, causing all components using it to refresh. + +**Parameters:** + +- `key`: Query identifier + +### `useReactiveQuery(query: ReactiveQuery | null)` + +React hook to use a reactive query in a component. Accepts null to handle cases where the query hasn't been initialized yet. + +**Parameters:** + +- `query`: ReactiveQuery instance or null + +**Returns:** + +- `data`: Query results +- `isLoading`: Loading state +- `error`: Error state +- `refresh`: Manual refresh function + +## Example: Todo App + +See `/hooks/useTodos.ts` and `/components/screens/todos.tsx` for a complete example implementation. + +## How It Works + +1. **Query Creation**: Queries are registered with a unique key +2. **Subscription**: Components subscribe to query changes via React hooks +3. **Invalidation**: When data changes, invalidate the query +4. **Auto-Update**: All subscribed components automatically re-render with new data + +## Benefits + +- **Separation of Concerns**: Database logic separate from UI components +- **Automatic Updates**: No manual state management needed +- **Performance**: Only affected components re-render +- **Developer Experience**: Simple, intuitive API + +## License + +MIT diff --git a/packages/reactive-sqlite/index.ts b/packages/reactive-sqlite/index.ts new file mode 100644 index 0000000..9595a60 --- /dev/null +++ b/packages/reactive-sqlite/index.ts @@ -0,0 +1,156 @@ +import * as SQLite from "expo-sqlite"; +import { useCallback, useEffect, useRef, useState } from "react"; + +type Listener = (data: T[]) => void; +type QueryFunction = () => Promise; + +/** + * Reactive SQLite wrapper that automatically updates React components + * when database changes occur + */ +export class ReactiveQuery { + private listeners: Set> = new Set(); + private currentData: T[] = []; + private queryFn: QueryFunction; + + constructor(queryFn: QueryFunction) { + this.queryFn = queryFn; + } + + /** + * Subscribe to data changes + */ + subscribe(listener: Listener) { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + /** + * Notify all subscribers of data change + */ + private notify() { + this.listeners.forEach((listener) => listener(this.currentData)); + } + + /** + * Refresh data from database and notify subscribers + */ + async refresh() { + try { + this.currentData = await this.queryFn(); + this.notify(); + } catch (error) { + console.error("Error refreshing reactive query:", error); + throw error; + } + } + + /** + * Get current data without triggering refresh + */ + getCurrentData() { + return this.currentData; + } +} + +/** + * Hook to use reactive SQLite queries in React components + */ +export function useReactiveQuery(query: ReactiveQuery | null): { + data: T[]; + isLoading: boolean; + error: Error | null; + refresh: () => Promise; +} { + const [data, setData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const mounted = useRef(true); + + const refresh = useCallback(async () => { + if (!query) return; + try { + setIsLoading(true); + setError(null); + await query.refresh(); + } catch (err) { + if (mounted.current) { + setError(err instanceof Error ? err : new Error("Unknown error")); + } + } finally { + if (mounted.current) { + setIsLoading(false); + } + } + }, [query]); + + useEffect(() => { + if (!query) return; + + mounted.current = true; + + // Subscribe to changes + const unsubscribe = query.subscribe((newData) => { + if (mounted.current) { + setData(newData); + } + }); + + // Initial load + refresh(); + + return () => { + mounted.current = false; + unsubscribe(); + }; + }, [query, refresh]); + + return { data, isLoading, error, refresh }; +} + +/** + * Create a reactive database wrapper + */ +export function createReactiveDatabase(db: SQLite.SQLiteDatabase) { + const queries = new Map>(); + + return { + /** + * Create or get a reactive query + */ + createQuery(key: string, queryFn: QueryFunction): ReactiveQuery { + if (!queries.has(key)) { + queries.set(key, new ReactiveQuery(queryFn)); + } + return queries.get(key)!; + }, + + /** + * Invalidate a query to trigger refresh + */ + async invalidateQuery(key: string) { + const query = queries.get(key); + if (query) { + await query.refresh(); + } + }, + + /** + * Invalidate multiple queries + */ + async invalidateQueries(keys: string[]) { + await Promise.all(keys.map((key) => this.invalidateQuery(key))); + }, + + /** + * Get the underlying database + */ + getDatabase() { + return db; + }, + }; +} + +export type ReactiveDatabase = ReturnType; diff --git a/packages/reactive-sqlite/package.json b/packages/reactive-sqlite/package.json new file mode 100644 index 0000000..e7111fe --- /dev/null +++ b/packages/reactive-sqlite/package.json @@ -0,0 +1,11 @@ +{ + "name": "@local/reactive-sqlite", + "version": "1.0.0", + "main": "index.ts", + "types": "index.ts", + "private": true, + "peerDependencies": { + "expo-sqlite": "*", + "react": "*" + } +} diff --git a/tsconfig.json b/tsconfig.json index 909e901..37727a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,15 +3,9 @@ "compilerOptions": { "strict": true, "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"], + "@local/*": ["./packages/*"] } }, - "include": [ - "**/*.ts", - "**/*.tsx", - ".expo/types/**/*.ts", - "expo-env.d.ts" - ] + "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] }