@owlmeans/client-resource 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -679
- package/package.json +6 -5
- package/tsconfig.json +5 -10
package/README.md
CHANGED
|
@@ -1,714 +1,68 @@
|
|
|
1
1
|
# @owlmeans/client-resource
|
|
2
2
|
|
|
3
|
-
Client-side resource
|
|
3
|
+
Client-side resource persistence layer — browser key-value storage backed resource for the OwlMeans `Resource<T>` interface.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- **Resource Management**: Structured data management with type safety
|
|
12
|
-
- **Context Integration**: Seamless integration with OwlMeans context system
|
|
13
|
-
- **List Management**: Efficient listing and pagination of records
|
|
14
|
-
- **Data Integrity**: Built-in validation and error handling
|
|
15
|
-
- **Storage Abstraction**: Database-agnostic storage interface
|
|
16
|
-
|
|
17
|
-
This package is part of the OwlMeans resource management ecosystem:
|
|
18
|
-
- **@owlmeans/resource**: Base resource interfaces and utilities
|
|
19
|
-
- **@owlmeans/client-resource**: Client-side resource implementation *(this package)*
|
|
20
|
-
- **@owlmeans/server-resource**: Server-side resource implementation
|
|
7
|
+
- `appendClientResource(context, alias)` — registers a client-side resource under `alias` in the context
|
|
8
|
+
- `ClientResource<T>` — extends `Resource<T>` with `db` accessor and `erase()` method
|
|
9
|
+
- `ClientDbService` / `ClientDb` — low-level key-value store interface (`get`, `set`, `has`, `del`)
|
|
10
|
+
- Used by `client-did`, `client-auth`, `web-flow`, and `web-db` to persist data in the browser
|
|
21
11
|
|
|
22
12
|
## Installation
|
|
23
13
|
|
|
24
14
|
```bash
|
|
25
|
-
|
|
15
|
+
bun add @owlmeans/client-resource
|
|
26
16
|
```
|
|
27
17
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
### Resources
|
|
31
|
-
Resources represent collections of data with consistent CRUD operations. Each resource is backed by a client-side database and provides type-safe access to stored records.
|
|
32
|
-
|
|
33
|
-
### Client Database Service
|
|
34
|
-
The underlying database service provides an abstraction over various client-side storage mechanisms (IndexedDB, localStorage, etc.).
|
|
35
|
-
|
|
36
|
-
### Record Management
|
|
37
|
-
All data is stored as records with unique identifiers, supporting both automatic ID generation and custom IDs.
|
|
18
|
+
## Usage
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
### Factory Functions
|
|
42
|
-
|
|
43
|
-
#### `appendClientResource<C, T>(context: T, alias: string): T`
|
|
44
|
-
|
|
45
|
-
Appends a client resource to the application context with the specified alias.
|
|
20
|
+
Register a client resource in context setup:
|
|
46
21
|
|
|
47
22
|
```typescript
|
|
48
23
|
import { appendClientResource } from '@owlmeans/client-resource'
|
|
49
|
-
import { makeClientContext } from '@owlmeans/client-context'
|
|
50
|
-
|
|
51
|
-
const context = makeClientContext(config)
|
|
52
|
-
appendClientResource(context, 'users')
|
|
53
|
-
|
|
54
|
-
// Access the resource
|
|
55
|
-
const userResource = context.resource<ClientResource<UserRecord>>('users')
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
**Parameters:**
|
|
59
|
-
- `context`: T - The client context to append the resource to
|
|
60
|
-
- `alias`: string - Unique identifier for the resource
|
|
61
|
-
|
|
62
|
-
**Returns:** Enhanced context with the registered resource
|
|
63
|
-
|
|
64
|
-
### Core Interfaces
|
|
65
|
-
|
|
66
|
-
#### `ClientResource<T extends ResourceRecord>`
|
|
67
|
-
|
|
68
|
-
Main interface for client-side resource management with full CRUD capabilities.
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
interface ClientResource<T extends ResourceRecord = ResourceRecord> extends Resource<T> {
|
|
72
|
-
db?: ClientDb // Underlying database instance
|
|
73
|
-
erase(): Promise<void> // Completely erase all data
|
|
74
|
-
|
|
75
|
-
// Inherited from Resource:
|
|
76
|
-
get(id: string): Promise<T> // Get record (throws if not found)
|
|
77
|
-
load(id: string): Promise<T | null> // Load record (returns null if not found)
|
|
78
|
-
list(criteria?, opts?): Promise<ListResult<T>> // List records with pagination
|
|
79
|
-
create(record: Partial<T>): Promise<T> // Create new record
|
|
80
|
-
update(record: Partial<T> & { id: string }): Promise<T> // Update existing record
|
|
81
|
-
delete(id: string | T): Promise<T | null> // Delete record
|
|
82
|
-
pick(id: string | T): Promise<T> // Remove and return record
|
|
83
|
-
save(record: Partial<T>): Promise<T> // Save (create or update)
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
#### `ClientDbService`
|
|
88
|
-
|
|
89
|
-
Service interface for managing client-side databases.
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
interface ClientDbService extends InitializedService {
|
|
93
|
-
initialize(alias?: string): Promise<ClientDb> // Initialize database instance
|
|
94
|
-
erase(): Promise<void> // Erase all databases
|
|
95
|
-
}
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
#### `ClientDb`
|
|
99
|
-
|
|
100
|
-
Low-level database interface for direct storage operations.
|
|
101
|
-
|
|
102
|
-
```typescript
|
|
103
|
-
interface ClientDb {
|
|
104
|
-
get<T>(id: string): Promise<T> // Get value by ID
|
|
105
|
-
set<T>(id: string, value: T): Promise<void> // Set value by ID
|
|
106
|
-
has(id: string): Promise<boolean> // Check if ID exists
|
|
107
|
-
del(id: string): Promise<boolean> // Delete by ID
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Resource Methods Detailed Reference
|
|
112
|
-
|
|
113
|
-
#### Data Retrieval Methods
|
|
114
|
-
|
|
115
|
-
**`get(id: string): Promise<T>`**
|
|
116
|
-
- **Purpose**: Retrieves a record by ID, throwing an error if not found
|
|
117
|
-
- **Behavior**: Guaranteed to return a record or throw `UnknownRecordError`
|
|
118
|
-
- **Usage**: When you know the record should exist
|
|
119
|
-
- **Throws**: `UnknownRecordError` if record doesn't exist
|
|
120
|
-
|
|
121
|
-
```typescript
|
|
122
|
-
const userResource = context.resource<ClientResource<UserRecord>>('users')
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const user = await userResource.get('user123')
|
|
126
|
-
console.log('User found:', user.name)
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.error('User not found:', error.message)
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
**`load(id: string): Promise<T | null>`**
|
|
133
|
-
- **Purpose**: Loads a record by ID, returning null if not found
|
|
134
|
-
- **Behavior**: Safe retrieval that never throws for missing records
|
|
135
|
-
- **Usage**: When you need to check if a record exists
|
|
136
|
-
- **Returns**: Record object or null
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
const user = await userResource.load('user123')
|
|
140
|
-
if (user) {
|
|
141
|
-
console.log('User exists:', user.name)
|
|
142
|
-
} else {
|
|
143
|
-
console.log('User not found')
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
24
|
|
|
147
|
-
|
|
148
|
-
- **Purpose**: Lists records with optional filtering and pagination
|
|
149
|
-
- **Behavior**: Efficiently paginates through large datasets
|
|
150
|
-
- **Usage**: For displaying lists of data with optional filtering
|
|
151
|
-
- **Returns**: Object with `items` array and `pager` information
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
// List all users
|
|
155
|
-
const allUsers = await userResource.list()
|
|
156
|
-
|
|
157
|
-
// List with pagination
|
|
158
|
-
const pagedUsers = await userResource.list({}, {
|
|
159
|
-
pager: { page: 0, size: 10 }
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
// List with filtering
|
|
163
|
-
const activeUsers = await userResource.list({
|
|
164
|
-
status: 'active'
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
// Combined filtering and pagination
|
|
168
|
-
const result = await userResource.list(
|
|
169
|
-
{ role: 'admin' },
|
|
170
|
-
{ pager: { page: 1, size: 5 } }
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
console.log('Users:', result.items)
|
|
174
|
-
console.log('Total:', result.pager.total)
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
#### Data Modification Methods
|
|
178
|
-
|
|
179
|
-
**`create(record: Partial<T>): Promise<T>`**
|
|
180
|
-
- **Purpose**: Creates a new record with automatic ID generation
|
|
181
|
-
- **Behavior**: Generates unique ID if not provided, validates uniqueness
|
|
182
|
-
- **Usage**: For creating new records
|
|
183
|
-
- **Throws**: `RecordExists` if ID already exists
|
|
184
|
-
- **Returns**: Created record with generated/validated ID
|
|
185
|
-
|
|
186
|
-
```typescript
|
|
187
|
-
const newUser = await userResource.create({
|
|
188
|
-
name: 'John Doe',
|
|
189
|
-
email: 'john@example.com',
|
|
190
|
-
role: 'user'
|
|
191
|
-
})
|
|
192
|
-
console.log('Created user with ID:', newUser.id)
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
**`update(record: Partial<T> & { id: string }): Promise<T>`**
|
|
196
|
-
- **Purpose**: Updates an existing record with partial data
|
|
197
|
-
- **Behavior**: Merges provided data with existing record
|
|
198
|
-
- **Usage**: For modifying existing records
|
|
199
|
-
- **Throws**: `UnknownRecordError` if record doesn't exist
|
|
200
|
-
- **Returns**: Updated record
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
const updatedUser = await userResource.update({
|
|
204
|
-
id: 'user123',
|
|
205
|
-
name: 'Jane Doe',
|
|
206
|
-
lastLogin: new Date()
|
|
207
|
-
})
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
**`save(record: Partial<T>): Promise<T>`**
|
|
211
|
-
- **Purpose**: Smart save that creates or updates based on existence
|
|
212
|
-
- **Behavior**: Creates if ID is missing or record doesn't exist, updates otherwise
|
|
213
|
-
- **Usage**: When you want create-or-update semantics
|
|
214
|
-
- **Returns**: Saved record
|
|
215
|
-
|
|
216
|
-
```typescript
|
|
217
|
-
// Will create if user doesn't exist
|
|
218
|
-
const user1 = await userResource.save({
|
|
219
|
-
name: 'New User',
|
|
220
|
-
email: 'new@example.com'
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
// Will update if user exists
|
|
224
|
-
const user2 = await userResource.save({
|
|
225
|
-
id: 'existing-user',
|
|
226
|
-
name: 'Updated Name'
|
|
227
|
-
})
|
|
25
|
+
appendClientResource(context, 'my-resource')
|
|
228
26
|
```
|
|
229
27
|
|
|
230
|
-
|
|
231
|
-
- **Purpose**: Deletes a record by ID or record object
|
|
232
|
-
- **Behavior**: Removes record and updates internal lists
|
|
233
|
-
- **Usage**: For permanent record removal
|
|
234
|
-
- **Returns**: Deleted record or null if not found
|
|
28
|
+
Use the resource via the context:
|
|
235
29
|
|
|
236
30
|
```typescript
|
|
237
|
-
|
|
238
|
-
const deletedUser = await userResource.delete('user123')
|
|
31
|
+
import type { ClientResource } from '@owlmeans/client-resource'
|
|
239
32
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
33
|
+
const res = context.resource<ClientResource<MyRecord>>('my-resource')
|
|
34
|
+
await res.save({ id: 'key', ...data })
|
|
35
|
+
const record = await res.load('key')
|
|
36
|
+
await res.erase() // clear all stored data
|
|
245
37
|
```
|
|
246
38
|
|
|
247
|
-
|
|
248
|
-
- **Purpose**: Removes and returns a record atomically
|
|
249
|
-
- **Behavior**: Combines delete operation with return of the deleted record
|
|
250
|
-
- **Usage**: When you need to extract a record from storage
|
|
251
|
-
- **Throws**: `UnknownRecordError` if record doesn't exist
|
|
252
|
-
- **Returns**: Picked record
|
|
39
|
+
## API
|
|
253
40
|
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const pickedUser = await userResource.pick('user123')
|
|
257
|
-
console.log('Picked user:', pickedUser.name)
|
|
258
|
-
// User is now removed from storage
|
|
259
|
-
} catch (error) {
|
|
260
|
-
console.error('User not found for picking')
|
|
261
|
-
}
|
|
262
|
-
```
|
|
41
|
+
### `appendClientResource<C, T>(context, alias): T`
|
|
263
42
|
|
|
264
|
-
|
|
265
|
-
- **Purpose**: Completely erases all data in the resource
|
|
266
|
-
- **Behavior**: Removes all records and resets the resource to empty state
|
|
267
|
-
- **Usage**: For data cleanup or reset operations
|
|
268
|
-
- **Warning**: This operation is irreversible
|
|
43
|
+
Appends a client resource with the given `alias` to `context`. Returns the context for chaining.
|
|
269
44
|
|
|
270
|
-
|
|
271
|
-
// Completely clear all user data
|
|
272
|
-
await userResource.erase()
|
|
273
|
-
console.log('All user data erased')
|
|
274
|
-
```
|
|
45
|
+
### `ClientResource<T>`
|
|
275
46
|
|
|
276
|
-
|
|
47
|
+
Extends `Resource<T>` with:
|
|
48
|
+
- `db?: ClientDb` — underlying key-value store
|
|
49
|
+
- `erase(): Promise<void>` — wipe all stored records
|
|
277
50
|
|
|
278
|
-
|
|
51
|
+
### `ClientDb`
|
|
279
52
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
{
|
|
286
|
-
alias: 'users',
|
|
287
|
-
service: 'client-db',
|
|
288
|
-
schema: 'app-users',
|
|
289
|
-
host: []
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
alias: 'settings',
|
|
293
|
-
service: 'client-db',
|
|
294
|
-
schema: 'app-settings',
|
|
295
|
-
host: []
|
|
296
|
-
}
|
|
297
|
-
]
|
|
298
|
-
})
|
|
299
|
-
```
|
|
53
|
+
Low-level browser storage interface:
|
|
54
|
+
- `get<T>(id): Promise<T>`
|
|
55
|
+
- `set<T>(id, value): Promise<void>`
|
|
56
|
+
- `has(id): Promise<boolean>`
|
|
57
|
+
- `del(id): Promise<boolean>`
|
|
300
58
|
|
|
301
59
|
### Constants
|
|
302
60
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
#### `LIST_KEY`
|
|
307
|
-
Internal key used for maintaining record lists (`'_list'`).
|
|
308
|
-
|
|
309
|
-
## Usage Examples
|
|
310
|
-
|
|
311
|
-
### Basic Resource Setup
|
|
312
|
-
|
|
313
|
-
```typescript
|
|
314
|
-
import { appendClientResource } from '@owlmeans/client-resource'
|
|
315
|
-
import { makeClientContext } from '@owlmeans/client-context'
|
|
316
|
-
|
|
317
|
-
interface UserRecord extends ResourceRecord {
|
|
318
|
-
id: string
|
|
319
|
-
name: string
|
|
320
|
-
email: string
|
|
321
|
-
role: 'admin' | 'user'
|
|
322
|
-
createdAt: Date
|
|
323
|
-
lastLogin?: Date
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Create context and add resource
|
|
327
|
-
const context = makeClientContext(config)
|
|
328
|
-
appendClientResource(context, 'users')
|
|
329
|
-
|
|
330
|
-
// Initialize context
|
|
331
|
-
await context.configure().init()
|
|
332
|
-
|
|
333
|
-
// Access the resource
|
|
334
|
-
const userResource = context.resource<ClientResource<UserRecord>>('users')
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Complete CRUD Operations
|
|
338
|
-
|
|
339
|
-
```typescript
|
|
340
|
-
// Create a new user
|
|
341
|
-
const newUser = await userResource.create({
|
|
342
|
-
name: 'Alice Johnson',
|
|
343
|
-
email: 'alice@example.com',
|
|
344
|
-
role: 'user',
|
|
345
|
-
createdAt: new Date()
|
|
346
|
-
})
|
|
347
|
-
console.log('Created user:', newUser.id)
|
|
348
|
-
|
|
349
|
-
// Load a user
|
|
350
|
-
const user = await userResource.load(newUser.id)
|
|
351
|
-
if (user) {
|
|
352
|
-
console.log('User found:', user.name)
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Update the user
|
|
356
|
-
const updatedUser = await userResource.update({
|
|
357
|
-
id: newUser.id,
|
|
358
|
-
lastLogin: new Date(),
|
|
359
|
-
role: 'admin'
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
// List users with pagination
|
|
363
|
-
const userList = await userResource.list({}, {
|
|
364
|
-
pager: { page: 0, size: 10 }
|
|
365
|
-
})
|
|
366
|
-
console.log('Users:', userList.items.length)
|
|
367
|
-
console.log('Total users:', userList.pager.total)
|
|
368
|
-
|
|
369
|
-
// Delete the user
|
|
370
|
-
const deletedUser = await userResource.delete(newUser.id)
|
|
371
|
-
console.log('Deleted user:', deletedUser?.name)
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### Advanced Filtering and Pagination
|
|
375
|
-
|
|
376
|
-
```typescript
|
|
377
|
-
interface ProductRecord extends ResourceRecord {
|
|
378
|
-
id: string
|
|
379
|
-
name: string
|
|
380
|
-
category: string
|
|
381
|
-
price: number
|
|
382
|
-
inStock: boolean
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const productResource = context.resource<ClientResource<ProductRecord>>('products')
|
|
386
|
-
|
|
387
|
-
// Filter by category
|
|
388
|
-
const electronics = await productResource.list({
|
|
389
|
-
category: 'electronics'
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
// Filter by availability
|
|
393
|
-
const availableProducts = await productResource.list({
|
|
394
|
-
inStock: true
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
// Paginated results
|
|
398
|
-
let page = 0
|
|
399
|
-
const pageSize = 20
|
|
400
|
-
let hasMore = true
|
|
401
|
-
|
|
402
|
-
while (hasMore) {
|
|
403
|
-
const result = await productResource.list({}, {
|
|
404
|
-
pager: { page, size: pageSize }
|
|
405
|
-
})
|
|
406
|
-
|
|
407
|
-
console.log(`Page ${page + 1}:`, result.items.length)
|
|
408
|
-
|
|
409
|
-
hasMore = (page + 1) * pageSize < result.pager.total
|
|
410
|
-
page++
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### Error Handling
|
|
415
|
-
|
|
416
|
-
```typescript
|
|
417
|
-
import { RecordExists, UnknownRecordError, ResourceError } from '@owlmeans/resource'
|
|
418
|
-
|
|
419
|
-
try {
|
|
420
|
-
// Attempt to create user with specific ID
|
|
421
|
-
const user = await userResource.create({
|
|
422
|
-
id: 'specific-id',
|
|
423
|
-
name: 'Test User',
|
|
424
|
-
email: 'test@example.com'
|
|
425
|
-
})
|
|
426
|
-
} catch (error) {
|
|
427
|
-
if (error instanceof RecordExists) {
|
|
428
|
-
console.error('User with this ID already exists')
|
|
429
|
-
} else if (error instanceof ResourceError) {
|
|
430
|
-
console.error('Resource error:', error.message)
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
// Attempt to get non-existent user
|
|
436
|
-
const user = await userResource.get('non-existent-id')
|
|
437
|
-
} catch (error) {
|
|
438
|
-
if (error instanceof UnknownRecordError) {
|
|
439
|
-
console.error('User not found')
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### Multiple Resources Management
|
|
445
|
-
|
|
446
|
-
```typescript
|
|
447
|
-
interface UserRecord extends ResourceRecord {
|
|
448
|
-
id: string
|
|
449
|
-
name: string
|
|
450
|
-
email: string
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
interface PostRecord extends ResourceRecord {
|
|
454
|
-
id: string
|
|
455
|
-
title: string
|
|
456
|
-
content: string
|
|
457
|
-
authorId: string
|
|
458
|
-
publishedAt: Date
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
// Setup multiple resources
|
|
462
|
-
const context = makeClientContext(config)
|
|
463
|
-
appendClientResource(context, 'users')
|
|
464
|
-
appendClientResource(context, 'posts')
|
|
465
|
-
|
|
466
|
-
await context.configure().init()
|
|
467
|
-
|
|
468
|
-
const userResource = context.resource<ClientResource<UserRecord>>('users')
|
|
469
|
-
const postResource = context.resource<ClientResource<PostRecord>>('posts')
|
|
470
|
-
|
|
471
|
-
// Create user and posts
|
|
472
|
-
const author = await userResource.create({
|
|
473
|
-
name: 'John Doe',
|
|
474
|
-
email: 'john@example.com'
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
const post1 = await postResource.create({
|
|
478
|
-
title: 'First Post',
|
|
479
|
-
content: 'Hello world!',
|
|
480
|
-
authorId: author.id,
|
|
481
|
-
publishedAt: new Date()
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
const post2 = await postResource.create({
|
|
485
|
-
title: 'Second Post',
|
|
486
|
-
content: 'More content here',
|
|
487
|
-
authorId: author.id,
|
|
488
|
-
publishedAt: new Date()
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
// Find posts by author
|
|
492
|
-
const authorPosts = await postResource.list({
|
|
493
|
-
authorId: author.id
|
|
494
|
-
})
|
|
495
|
-
console.log(`${author.name} has ${authorPosts.items.length} posts`)
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
### Resource with Custom Database Configuration
|
|
499
|
-
|
|
500
|
-
```typescript
|
|
501
|
-
const context = makeClientContext({
|
|
502
|
-
service: 'advanced-app',
|
|
503
|
-
// ... other config
|
|
504
|
-
dbs: [
|
|
505
|
-
{
|
|
506
|
-
alias: 'user-data',
|
|
507
|
-
service: 'indexed-db-service',
|
|
508
|
-
schema: 'users-v2',
|
|
509
|
-
host: []
|
|
510
|
-
},
|
|
511
|
-
{
|
|
512
|
-
alias: 'cache-data',
|
|
513
|
-
service: 'memory-db-service',
|
|
514
|
-
schema: 'temp-cache',
|
|
515
|
-
host: []
|
|
516
|
-
}
|
|
517
|
-
]
|
|
518
|
-
})
|
|
519
|
-
|
|
520
|
-
// Resources will use the configured database services
|
|
521
|
-
appendClientResource(context, 'user-data')
|
|
522
|
-
appendClientResource(context, 'cache-data')
|
|
523
|
-
```
|
|
524
|
-
|
|
525
|
-
### Reactive Resource Updates
|
|
526
|
-
|
|
527
|
-
```typescript
|
|
528
|
-
import { useState, useEffect } from 'react'
|
|
529
|
-
import { useContext } from '@owlmeans/client'
|
|
530
|
-
|
|
531
|
-
function UserList() {
|
|
532
|
-
const context = useContext()
|
|
533
|
-
const [users, setUsers] = useState([])
|
|
534
|
-
|
|
535
|
-
useEffect(() => {
|
|
536
|
-
const loadUsers = async () => {
|
|
537
|
-
const userResource = context.resource('users')
|
|
538
|
-
const result = await userResource.list()
|
|
539
|
-
setUsers(result.items)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
loadUsers()
|
|
543
|
-
}, [])
|
|
544
|
-
|
|
545
|
-
const addUser = async (userData) => {
|
|
546
|
-
const userResource = context.resource('users')
|
|
547
|
-
const newUser = await userResource.create(userData)
|
|
548
|
-
setUsers(prev => [...prev, newUser])
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const deleteUser = async (userId) => {
|
|
552
|
-
const userResource = context.resource('users')
|
|
553
|
-
await userResource.delete(userId)
|
|
554
|
-
setUsers(prev => prev.filter(u => u.id !== userId))
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
return (
|
|
558
|
-
<div>
|
|
559
|
-
{users.map(user => (
|
|
560
|
-
<div key={user.id}>
|
|
561
|
-
{user.name}
|
|
562
|
-
<button onClick={() => deleteUser(user.id)}>Delete</button>
|
|
563
|
-
</div>
|
|
564
|
-
))}
|
|
565
|
-
</div>
|
|
566
|
-
)
|
|
567
|
-
}
|
|
568
|
-
```
|
|
569
|
-
|
|
570
|
-
### Data Migration and Cleanup
|
|
571
|
-
|
|
572
|
-
```typescript
|
|
573
|
-
// Migration helper
|
|
574
|
-
const migrateUserData = async () => {
|
|
575
|
-
const userResource = context.resource<ClientResource<UserRecord>>('users')
|
|
576
|
-
const users = await userResource.list()
|
|
577
|
-
|
|
578
|
-
for (const user of users.items) {
|
|
579
|
-
if (!user.createdAt) {
|
|
580
|
-
await userResource.update({
|
|
581
|
-
id: user.id,
|
|
582
|
-
createdAt: new Date()
|
|
583
|
-
})
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Cleanup old data
|
|
589
|
-
const cleanupOldUsers = async () => {
|
|
590
|
-
const userResource = context.resource<ClientResource<UserRecord>>('users')
|
|
591
|
-
const users = await userResource.list()
|
|
592
|
-
|
|
593
|
-
const oneYearAgo = new Date()
|
|
594
|
-
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1)
|
|
595
|
-
|
|
596
|
-
for (const user of users.items) {
|
|
597
|
-
if (user.lastLogin && user.lastLogin < oneYearAgo) {
|
|
598
|
-
await userResource.delete(user.id)
|
|
599
|
-
console.log('Deleted inactive user:', user.name)
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
// Complete resource reset
|
|
605
|
-
const resetAllData = async () => {
|
|
606
|
-
const userResource = context.resource('users')
|
|
607
|
-
const postResource = context.resource('posts')
|
|
608
|
-
|
|
609
|
-
await Promise.all([
|
|
610
|
-
userResource.erase(),
|
|
611
|
-
postResource.erase()
|
|
612
|
-
])
|
|
613
|
-
|
|
614
|
-
console.log('All data erased')
|
|
615
|
-
}
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
## Error Handling
|
|
619
|
-
|
|
620
|
-
The package integrates with the OwlMeans error system and may throw the following errors:
|
|
621
|
-
|
|
622
|
-
### `ResourceError`
|
|
623
|
-
General resource operation errors.
|
|
624
|
-
|
|
625
|
-
### `UnknownRecordError`
|
|
626
|
-
Thrown when attempting to access a record that doesn't exist.
|
|
627
|
-
|
|
628
|
-
### `RecordExists`
|
|
629
|
-
Thrown when attempting to create a record with an ID that already exists.
|
|
630
|
-
|
|
631
|
-
```typescript
|
|
632
|
-
import { ResourceError, UnknownRecordError, RecordExists } from '@owlmeans/resource'
|
|
633
|
-
|
|
634
|
-
const handleResourceOperation = async () => {
|
|
635
|
-
try {
|
|
636
|
-
await userResource.create({ id: 'existing-id', name: 'Test' })
|
|
637
|
-
} catch (error) {
|
|
638
|
-
if (error instanceof RecordExists) {
|
|
639
|
-
console.error('Record already exists')
|
|
640
|
-
} else if (error instanceof UnknownRecordError) {
|
|
641
|
-
console.error('Record not found')
|
|
642
|
-
} else if (error instanceof ResourceError) {
|
|
643
|
-
console.error('Resource error:', error.message)
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
## Performance Considerations
|
|
650
|
-
|
|
651
|
-
1. **Pagination**: Use pagination for large datasets to avoid memory issues
|
|
652
|
-
2. **Indexing**: Consider database indexing for frequently queried fields
|
|
653
|
-
3. **Batch Operations**: Group related operations together when possible
|
|
654
|
-
4. **Memory Management**: Use `pick()` instead of `get()` + `delete()` when extracting data
|
|
655
|
-
5. **Database Choice**: Choose appropriate database service based on data size and usage patterns
|
|
656
|
-
|
|
657
|
-
## Integration with Other Packages
|
|
658
|
-
|
|
659
|
-
### Client Context Integration
|
|
660
|
-
```typescript
|
|
661
|
-
import { makeClientContext } from '@owlmeans/client-context'
|
|
662
|
-
import { appendClientResource } from '@owlmeans/client-resource'
|
|
663
|
-
|
|
664
|
-
const context = makeClientContext(config)
|
|
665
|
-
appendClientResource(context, 'users')
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
### Authentication Integration
|
|
669
|
-
```typescript
|
|
670
|
-
import { appendClientResource } from '@owlmeans/client-resource'
|
|
671
|
-
import { AUTH_RESOURCE } from '@owlmeans/client-auth'
|
|
672
|
-
|
|
673
|
-
// Setup authentication resource
|
|
674
|
-
appendClientResource(context, AUTH_RESOURCE)
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
### Database Service Integration
|
|
678
|
-
```typescript
|
|
679
|
-
import { WebDbService } from '@owlmeans/web-db'
|
|
680
|
-
import { appendClientResource } from '@owlmeans/client-resource'
|
|
681
|
-
|
|
682
|
-
// Register database service first
|
|
683
|
-
context.registerService(webDbService)
|
|
684
|
-
|
|
685
|
-
// Then add resources that use it
|
|
686
|
-
appendClientResource(context, 'users')
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
## Best Practices
|
|
690
|
-
|
|
691
|
-
1. **Type Safety**: Always use TypeScript interfaces for your records
|
|
692
|
-
2. **Resource Naming**: Use descriptive names for resource aliases
|
|
693
|
-
3. **Error Handling**: Implement comprehensive error handling for all operations
|
|
694
|
-
4. **Data Validation**: Validate data before storing in resources
|
|
695
|
-
5. **Memory Management**: Use pagination for large datasets
|
|
696
|
-
6. **Consistent IDs**: Use consistent ID generation strategies
|
|
697
|
-
7. **Database Configuration**: Configure appropriate database services for your use case
|
|
698
|
-
|
|
699
|
-
## Dependencies
|
|
700
|
-
|
|
701
|
-
This package depends on:
|
|
702
|
-
- `@owlmeans/resource` - Base resource interfaces and utilities
|
|
703
|
-
- `@owlmeans/client-context` - Client context management
|
|
704
|
-
- `@owlmeans/context` - Core context system
|
|
705
|
-
- `@noble/hashes` - Cryptographic utilities for ID generation
|
|
706
|
-
- `@scure/base` - Base encoding for ID generation
|
|
61
|
+
- `DEFAULT_DB_ALIAS` — `'client-db'` — default alias for the underlying DB service
|
|
62
|
+
- `LIST_KEY` — `'_list'` — key used to store the record index
|
|
707
63
|
|
|
708
64
|
## Related Packages
|
|
709
65
|
|
|
710
|
-
- [`@owlmeans/resource`](../resource)
|
|
711
|
-
- [`@owlmeans/
|
|
712
|
-
- [`@owlmeans/
|
|
713
|
-
- [`@owlmeans/server-resource`](../server-resource) - Server-side resources
|
|
714
|
-
- [`@owlmeans/client-auth`](../client-auth) - Authentication with resource storage
|
|
66
|
+
- [`@owlmeans/resource`](../resource) — `Resource<T>`, `ResourceRecord` base interfaces
|
|
67
|
+
- [`@owlmeans/web-db`](../web-db) — IndexedDB-backed implementation of `ClientDbService`
|
|
68
|
+
- [`@owlmeans/client-did`](../client-did) — uses `appendClientResource` to persist DID keys
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/client-resource",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -22,14 +22,15 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@noble/hashes": "^1.5.0",
|
|
25
|
-
"@owlmeans/client-context": "^0.1.
|
|
26
|
-
"@owlmeans/context": "^0.1.
|
|
27
|
-
"@owlmeans/resource": "^0.1.
|
|
25
|
+
"@owlmeans/client-context": "^0.1.4",
|
|
26
|
+
"@owlmeans/context": "^0.1.4",
|
|
27
|
+
"@owlmeans/resource": "^0.1.4",
|
|
28
28
|
"@scure/base": "^1.1.9"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
+
"@owlmeans/dep-config": "workspace:*",
|
|
31
32
|
"nodemon": "^3.1.11",
|
|
32
|
-
"typescript": "^
|
|
33
|
+
"typescript": "^6.0.2"
|
|
33
34
|
},
|
|
34
35
|
"publishConfig": {
|
|
35
36
|
"access": "public"
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": [
|
|
3
|
-
"
|
|
3
|
+
"@owlmeans/dep-config/tsconfig.base.json"
|
|
4
4
|
],
|
|
5
5
|
"compilerOptions": {
|
|
6
|
-
"rootDir": "./src/",
|
|
7
|
-
"outDir": "./build/"
|
|
8
|
-
"moduleResolution": "Bundler",
|
|
6
|
+
"rootDir": "./src/",
|
|
7
|
+
"outDir": "./build/"
|
|
9
8
|
},
|
|
10
|
-
"exclude": [
|
|
11
|
-
|
|
12
|
-
"./build/**/*",
|
|
13
|
-
"./*.ts"
|
|
14
|
-
]
|
|
15
|
-
}
|
|
9
|
+
"exclude": ["./dist/**/*", "./build/**/*", "./*.ts"]
|
|
10
|
+
}
|