|
| 1 | +# @objectstack/service-user-preferences |
| 2 | + |
| 3 | +User Preferences Service for ObjectStack — implements IUserPreferencesService and IUserFavoritesService with ObjectQL persistence and REST routes. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- **Scalar Preferences**: Simple key-value storage for user settings (theme, locale, etc.) |
| 8 | +- **Structured Data**: Store complex data structures (favorites, recent items) |
| 9 | +- **ObjectQL Persistence**: Leverages IDataEngine for database-agnostic storage |
| 10 | +- **REST API**: Full HTTP routes for preferences and favorites management |
| 11 | +- **Type Safety**: Complete TypeScript support with Zod schemas |
| 12 | +- **Multi-tenant**: User-scoped preferences with isolation |
| 13 | +- **Prefix Filtering**: Query preferences by key prefix (e.g., all `plugin.ai.*` settings) |
| 14 | + |
| 15 | +## Installation |
| 16 | + |
| 17 | +```bash |
| 18 | +pnpm add @objectstack/service-user-preferences |
| 19 | +``` |
| 20 | + |
| 21 | +## Usage |
| 22 | + |
| 23 | +### Basic Setup |
| 24 | + |
| 25 | +```typescript |
| 26 | +import { ObjectKernel } from '@objectstack/core'; |
| 27 | +import { ObjectQLPlugin } from '@objectstack/objectql'; |
| 28 | +import { DriverPlugin } from '@objectstack/runtime'; |
| 29 | +import { InMemoryDriver } from '@objectstack/driver-memory'; |
| 30 | +import { UserPreferencesServicePlugin } from '@objectstack/service-user-preferences'; |
| 31 | + |
| 32 | +const kernel = new ObjectKernel(); |
| 33 | + |
| 34 | +await kernel.use(new ObjectQLPlugin()); |
| 35 | +await kernel.use(new DriverPlugin(new InMemoryDriver())); |
| 36 | +await kernel.use(new UserPreferencesServicePlugin()); |
| 37 | + |
| 38 | +await kernel.bootstrap(); |
| 39 | + |
| 40 | +const prefs = kernel.getService<IUserPreferencesService>('user-preferences'); |
| 41 | +``` |
| 42 | + |
| 43 | +### Scalar Preferences |
| 44 | + |
| 45 | +```typescript |
| 46 | +// Set a preference |
| 47 | +await prefs.set('user123', 'theme', 'dark'); |
| 48 | + |
| 49 | +// Get a preference |
| 50 | +const theme = await prefs.get('user123', 'theme'); // => 'dark' |
| 51 | + |
| 52 | +// Set multiple preferences at once |
| 53 | +await prefs.setMany('user123', { |
| 54 | + theme: 'dark', |
| 55 | + locale: 'en-US', |
| 56 | + sidebar_collapsed: true, |
| 57 | +}); |
| 58 | + |
| 59 | +// Get all preferences |
| 60 | +const all = await prefs.getAll('user123'); |
| 61 | +// => { theme: 'dark', locale: 'en-US', sidebar_collapsed: true } |
| 62 | + |
| 63 | +// Delete a preference |
| 64 | +await prefs.delete('user123', 'theme'); |
| 65 | + |
| 66 | +// Check if a preference exists |
| 67 | +const hasTheme = await prefs.has('user123', 'theme'); |
| 68 | +``` |
| 69 | + |
| 70 | +### Structured Data (Favorites) |
| 71 | + |
| 72 | +```typescript |
| 73 | +const favorites = kernel.getService<IUserFavoritesService>('user-favorites'); |
| 74 | + |
| 75 | +// Add a favorite |
| 76 | +const entry = await favorites.add('user123', { |
| 77 | + type: 'view', |
| 78 | + target: 'kanban_tasks', |
| 79 | + label: 'My Tasks', |
| 80 | + icon: 'kanban', |
| 81 | +}); |
| 82 | + |
| 83 | +// List all favorites |
| 84 | +const allFavorites = await favorites.list('user123'); |
| 85 | + |
| 86 | +// Remove a favorite |
| 87 | +await favorites.remove('user123', entry.id); |
| 88 | + |
| 89 | +// Check if an item is favorited |
| 90 | +const isFav = await favorites.has('user123', 'view', 'kanban_tasks'); |
| 91 | + |
| 92 | +// Toggle a favorite (add if not exists, remove if exists) |
| 93 | +const added = await favorites.toggle('user123', { |
| 94 | + type: 'view', |
| 95 | + target: 'kanban_tasks', |
| 96 | +}); |
| 97 | +``` |
| 98 | + |
| 99 | +### Prefix Filtering |
| 100 | + |
| 101 | +```typescript |
| 102 | +// Set plugin-specific preferences |
| 103 | +await prefs.setMany('user123', { |
| 104 | + 'plugin.ai.auto_save': true, |
| 105 | + 'plugin.ai.model': 'gpt-4', |
| 106 | + 'plugin.security.mfa_enabled': false, |
| 107 | +}); |
| 108 | + |
| 109 | +// Get all AI plugin preferences |
| 110 | +const aiPrefs = await prefs.getAll('user123', { prefix: 'plugin.ai.' }); |
| 111 | +// => { 'plugin.ai.auto_save': true, 'plugin.ai.model': 'gpt-4' } |
| 112 | + |
| 113 | +// Clear all AI plugin preferences |
| 114 | +await prefs.clear('user123', { prefix: 'plugin.ai.' }); |
| 115 | +``` |
| 116 | + |
| 117 | +## REST API |
| 118 | + |
| 119 | +The plugin automatically registers HTTP routes when started: |
| 120 | + |
| 121 | +### Preferences Routes |
| 122 | + |
| 123 | +- **GET `/api/v1/user/preferences`** - Get all preferences (with optional `?prefix=` query param) |
| 124 | +- **GET `/api/v1/user/preferences/:key`** - Get a single preference |
| 125 | +- **POST `/api/v1/user/preferences`** - Batch set preferences (body: `{ preferences: { ... } }`) |
| 126 | +- **PUT `/api/v1/user/preferences/:key`** - Set a single preference (body: `{ value: ... }`) |
| 127 | +- **DELETE `/api/v1/user/preferences/:key`** - Delete a preference |
| 128 | + |
| 129 | +### Favorites Routes |
| 130 | + |
| 131 | +- **GET `/api/v1/user/favorites`** - List all favorites |
| 132 | +- **POST `/api/v1/user/favorites`** - Add a favorite (body: `{ type, target, label?, icon?, metadata? }`) |
| 133 | +- **DELETE `/api/v1/user/favorites/:id`** - Remove a favorite |
| 134 | +- **POST `/api/v1/user/favorites/toggle`** - Toggle a favorite (body: `{ type, target, label?, icon?, metadata? }`) |
| 135 | + |
| 136 | +All routes require authentication and the user's ID is automatically extracted from the request context. |
| 137 | + |
| 138 | +## Well-Known Preference Keys |
| 139 | + |
| 140 | +The following preference keys are reserved for system-level settings: |
| 141 | + |
| 142 | +- `theme` - UI theme (`'light'` | `'dark'` | `'system'`) |
| 143 | +- `locale` - User's preferred locale (`'en-US'`, `'zh-CN'`, etc.) |
| 144 | +- `timezone` - User's timezone (`'America/New_York'`, etc.) |
| 145 | +- `favorites` - User's favorite items (structured array) |
| 146 | +- `recent_items` - Recently accessed items (structured array) |
| 147 | +- `sidebar_collapsed` - UI: sidebar state (boolean) |
| 148 | +- `page_size` - Default pagination size (number) |
| 149 | + |
| 150 | +Plugins can define custom keys using their own namespace, e.g., `plugin.ai.auto_save`. |
| 151 | + |
| 152 | +## Schema |
| 153 | + |
| 154 | +### UserPreferenceEntry |
| 155 | + |
| 156 | +```typescript |
| 157 | +{ |
| 158 | + id: string; // Auto-generated (e.g., 'pref_abc123') |
| 159 | + userId: string; // User ID |
| 160 | + key: string; // Preference key |
| 161 | + value: unknown; // JSON-serializable value |
| 162 | + valueType?: string; // Type hint: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null' |
| 163 | + createdAt: string; // ISO timestamp |
| 164 | + updatedAt: string; // ISO timestamp |
| 165 | +} |
| 166 | +``` |
| 167 | + |
| 168 | +### FavoriteEntry |
| 169 | + |
| 170 | +```typescript |
| 171 | +{ |
| 172 | + id: string; // Auto-generated (e.g., 'fav_xyz789') |
| 173 | + type: 'object' | 'view' | 'app' | 'dashboard' | 'report' | 'record'; |
| 174 | + target: string; // Target reference (object name, view name, etc.) |
| 175 | + label?: string; // Display label override |
| 176 | + icon?: string; // Icon override |
| 177 | + metadata?: Record<string, unknown>; // Custom metadata |
| 178 | + createdAt: string; // ISO timestamp |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +## Database |
| 183 | + |
| 184 | +The plugin creates a `user_preferences` object in ObjectQL with the following schema: |
| 185 | + |
| 186 | +- `id` (text, primary) - Unique identifier |
| 187 | +- `user_id` (text, indexed) - User who owns the preference |
| 188 | +- `key` (text, indexed) - Preference key |
| 189 | +- `value` (textarea) - JSON-serialized value |
| 190 | +- `value_type` (select) - Type hint for client-side type safety |
| 191 | +- `created_at` (datetime) - Creation timestamp |
| 192 | +- `updated_at` (datetime) - Last update timestamp |
| 193 | + |
| 194 | +Unique composite index: `(user_id, key)` |
| 195 | + |
| 196 | +## Architecture |
| 197 | + |
| 198 | +The service follows ObjectStack's standard patterns: |
| 199 | + |
| 200 | +1. **Spec Layer** (`@objectstack/spec/identity`) - Zod schemas for preferences and favorites |
| 201 | +2. **Contract Layer** (`@objectstack/spec/contracts`) - Service interfaces (IUserPreferencesService, IUserFavoritesService) |
| 202 | +3. **Implementation Layer** - ObjectQL-based adapter for persistence |
| 203 | +4. **Plugin Layer** - Kernel plugin with service registration and HTTP routes |
| 204 | +5. **Client Layer** - Type-safe client SDK (future enhancement) |
| 205 | + |
| 206 | +## License |
| 207 | + |
| 208 | +Apache-2.0 © ObjectStack |
0 commit comments