@wonderlandlabs/rxjs-sqlite
Version:
A reactive adapter for SQLite using RxJS, with RxDB adapter support
258 lines (205 loc) • 6.77 kB
Markdown
# Recipe App Example
This fully functional recipe management application demonstrates how to use the
RxJS SQLite adapter in a practical scenario. It follows the "dogfood principle"
by using the adapter in a real application.
## Architecture
The Recipe App uses a server-client architecture:
- **Backend**: Node.js server with Express, using the RxJS SQLite adapter
- **Frontend**: Simple HTML/JavaScript client (with an optional React UI)
## Features
- Store and retrieve recipes with ingredients, instructions, and metadata
- Search recipes by name, tags, or ingredients
- View detailed recipe information
- Bookmark favorite recipes
- Handle complex data relationships (recipes, ingredients, tags)
- Demonstrate proper handling of nullable fields
## Running the Recipe App
### Starting the Server
```bash
# Install dependencies
npm install
# Start the server
npm run start
```
The server will start on http://localhost:3000 by default.
### Using the App
Once the server is running, you can:
1. Open your browser to http://localhost:3000
2. Browse the recipe list
3. Click on a recipe to view details
4. Use the search box to find recipes
5. Bookmark recipes by clicking the star icon
## Code Structure
The Recipe App consists of these main components:
### Backend (Node.js)
- `server/recipe-server.cjs`: Express server with API endpoints
- `server/data/`: SQLite database files
### Frontend
- `server/recipe-client.html`: Simple HTML client
- `recipe-ui/`: React-based UI (optional)
## API Endpoints
The server exposes these REST API endpoints:
- `GET /api/recipes`: List all recipes (paginated, with basic info)
- `GET /api/recipes/:id`: Get detailed recipe information
- `GET /api/recipes/search?q=query`: Search recipes
- `POST /api/recipes`: Create a new recipe
- `PUT /api/recipes/:id`: Update a recipe
- `DELETE /api/recipes/:id`: Delete a recipe
- `GET /api/ingredients`: List all ingredients
- `GET /api/tags`: List all tags
## Database Schema
The app uses these collections:
### Recipes Collection
```typescript
const recipeSchema = {
title: 'recipe schema',
version: 0,
primaryKey: 'id',
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
description: { type: 'string' },
preparationTime: { type: 'number' },
cookingTime: { type: 'number' },
servings: { type: 'number' },
difficulty: { type: 'string', enum: ['easy', 'medium', 'hard'] },
instructions: { type: 'string' },
// Nullable fields using multi-type arrays
imageUrl: { type: ['string', 'null'], default: null },
sourceUrl: { type: ['string', 'null'], default: null },
notes: { type: ['string', 'null'], default: null },
// Arrays
ingredients: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
amount: { type: 'number' },
unit: { type: 'string' },
notes: { type: ['string', 'null'], default: null }
}
}
},
tags: {
type: 'array',
items: { type: 'string' }
},
createdAt: { type: 'string' },
updatedAt: { type: 'string' }
},
required: ['id', 'name', 'instructions']
};
```
### Bookmarks Collection
```typescript
const bookmarkSchema = {
title: 'bookmark schema',
version: 0,
primaryKey: 'id',
type: 'object',
properties: {
id: { type: 'string' },
recipeId: { type: 'string' },
userId: { type: 'string' },
createdAt: { type: 'string' }
},
required: ['id', 'recipeId', 'userId']
};
```
## Key Implementation Details
### Database Setup
```typescript
// Create a database with the SQLite adapter
const db = await createRxDatabase({
name: 'recipes',
storage: getRxStorageSQLite({
filename: './server/data/recipes.sqlite'
}),
// Dev mode with proper validation strategy for nullable fields
devMode: true,
validationStrategy: {
validateBeforeInsert: true,
validateBeforeSave: false,
validateOnQuery: false
}
});
// Add collections
const collections = await db.addCollections({
recipes: { schema: recipeSchema },
bookmarks: { schema: bookmarkSchema }
});
```
### Search Implementation
The search functionality demonstrates how to use the SQLite adapter's query
capabilities:
```typescript
// Search recipes by name, tags, or ingredients
app.get('/api/recipes/search', async (req, res) => {
const query = req.query.q || '';
if (!query) {
return res.json([]);
}
try {
// Use RxDB query with $or operator
const results = await collections.recipes.find({
selector: {
$or: [
{ name: { $regex: query, $options: 'i' } },
{ 'tags': { $elemMatch: { $regex: query, $options: 'i' } } },
{ 'ingredients.name': { $regex: query, $options: 'i' } }
]
}
}).exec();
res.json(results.map(doc => ({
id: doc.id,
name: doc.name,
description: doc.description,
tags: doc.tags
})));
} catch (error) {
console.error('Search error:', error);
res.status(500).json({ error: 'Search failed' });
}
});
```
### Direct SQLite Access
The app also demonstrates how to access the underlying SQLite database directly:
```typescript
// Get the SQLite database instance
const sqliteDb = getRxStorageSQLite.getDBByName('recipes');
// Execute a complex query directly with SQL
app.get('/api/recipes/popular', (req, res) => {
try {
// Use raw SQL for complex queries that are difficult to express with RxDB
const results = sqliteDb.prepare(`
SELECT r.id, r.name, COUNT(b.id) as bookmark_count
FROM recipes_recipes r
LEFT JOIN recipes_bookmarks b ON json_extract(b.data, '$.recipeId') = r.id
GROUP BY r.id
ORDER BY bookmark_count DESC
LIMIT 10
`).all();
res.json(results);
} catch (error) {
console.error('Error getting popular recipes:', error);
res.status(500).json({ error: 'Failed to get popular recipes' });
}
});
```
## Learning from the Recipe App
This example demonstrates several important concepts:
1. **Proper Schema Design**: How to design schemas with nullable fields using
multi-type arrays
2. **Validation Strategy**: How to configure validation to handle nullable fields
correctly
3. **Query Patterns**: How to use RxDB queries and when to use direct SQLite
access for complex queries
4. **Reactive Programming**: How to leverage RxJS observables for real-time
updates
5. **Server-Client Architecture**: How to use the SQLite adapter in a Node.js
backend while serving a browser frontend
By studying and extending this example, you'll gain practical experience with
the RxJS SQLite adapter in a realistic application context.