@owlmeans/client-resource 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,714 @@
1
+ # @owlmeans/client-resource
2
+
3
+ Client-side resource management library for OwlMeans Common applications. This package provides a comprehensive data persistence and management system for React applications, with support for local database storage, CRUD operations, and seamless integration with the OwlMeans context system.
4
+
5
+ ## Overview
6
+
7
+ The `@owlmeans/client-resource` package extends the base `@owlmeans/resource` package with client-specific functionality. It provides:
8
+
9
+ - **Local Database Integration**: Persistent data storage using client-side databases
10
+ - **CRUD Operations**: Complete Create, Read, Update, Delete functionality
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
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @owlmeans/client-resource
26
+ ```
27
+
28
+ ## Core Concepts
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.
38
+
39
+ ## API Reference
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.
46
+
47
+ ```typescript
48
+ 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
+
147
+ **`list(criteria?, opts?): Promise<ListResult<T>>`**
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
+ })
228
+ ```
229
+
230
+ **`delete(id: string | T): Promise<T | null>`**
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
235
+
236
+ ```typescript
237
+ // Delete by ID
238
+ const deletedUser = await userResource.delete('user123')
239
+
240
+ // Delete by record
241
+ const userToDelete = await userResource.load('user456')
242
+ if (userToDelete) {
243
+ await userResource.delete(userToDelete)
244
+ }
245
+ ```
246
+
247
+ **`pick(id: string | T): Promise<T>`**
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
253
+
254
+ ```typescript
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
+ ```
263
+
264
+ **`erase(): Promise<void>`**
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
269
+
270
+ ```typescript
271
+ // Completely clear all user data
272
+ await userResource.erase()
273
+ console.log('All user data erased')
274
+ ```
275
+
276
+ ### Database Configuration
277
+
278
+ Resources can be configured through the context configuration:
279
+
280
+ ```typescript
281
+ const context = makeClientContext({
282
+ service: 'my-app',
283
+ // ... other config
284
+ dbs: [
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
+ ```
300
+
301
+ ### Constants
302
+
303
+ #### `DEFAULT_DB_ALIAS`
304
+ Default database service alias (`'client-db'`).
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
707
+
708
+ ## Related Packages
709
+
710
+ - [`@owlmeans/resource`](../resource) - Base resource interfaces
711
+ - [`@owlmeans/client-context`](../client-context) - Client context management
712
+ - [`@owlmeans/web-db`](../web-db) - Web database implementation
713
+ - [`@owlmeans/server-resource`](../server-resource) - Server-side resources
714
+ - [`@owlmeans/client-auth`](../client-auth) - Authentication with resource storage
package/build/.gitkeep ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_DB_ALIAS = "client-db";
2
+ export declare const LIST_KEY = "_list";
3
+ //# sourceMappingURL=consts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,gBAAgB,cAAc,CAAA;AAE3C,eAAO,MAAM,QAAQ,UAAU,CAAA"}
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_DB_ALIAS = 'client-db';
2
+ export const LIST_KEY = '_list';
3
+ //# sourceMappingURL=consts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,gBAAgB,GAAG,WAAW,CAAA;AAE3C,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAA"}
@@ -0,0 +1,4 @@
1
+ export type * from './types.js';
2
+ export * from './consts.js';
3
+ export * from './resource.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
package/build/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from './consts.js';
2
+ export * from './resource.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,7 @@
1
+ import type { ClientConfig, ClientContext } from '@owlmeans/client-context';
2
+ type Config = ClientConfig;
3
+ interface Context<C extends Config = Config> extends ClientContext<C> {
4
+ }
5
+ export declare const appendClientResource: <C extends Config, T extends Context<C>>(context: T, alias: string) => T;
6
+ export {};
7
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AAS3E,KAAK,MAAM,GAAG,YAAY,CAAA;AAC1B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI;AAEzE,eAAO,MAAM,oBAAoB,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,SAAS,MAAM,KAAG,CA2LxG,CAAA"}
@@ -0,0 +1,156 @@
1
+ import { appendContextual, assertContext } from '@owlmeans/context';
2
+ import { RecordExists, ResourceError, UnknownRecordError } from '@owlmeans/resource';
3
+ import { DEFAULT_DB_ALIAS, LIST_KEY } from './consts.js';
4
+ import { base58 } from '@scure/base';
5
+ import { randomBytes } from '@noble/hashes/utils';
6
+ export const appendClientResource = (context, alias) => {
7
+ const location = `client-resource:${alias}`;
8
+ const assert = () => {
9
+ if (resource.db == null) {
10
+ throw new ResourceError(`nodb-${location}`);
11
+ }
12
+ return resource.db;
13
+ };
14
+ const resource = appendContextual(alias, {
15
+ init: async () => {
16
+ const context = assertContext(resource.ctx, location);
17
+ const config = context.cfg.dbs?.find(db => db.alias === alias) ?? { service: DEFAULT_DB_ALIAS, host: [] };
18
+ const dbService = context.service(config.service);
19
+ resource.db = await dbService.initialize(config.schema ?? alias);
20
+ },
21
+ get: async (id) => {
22
+ const record = await resource.load(id);
23
+ if (record == null) {
24
+ throw new UnknownRecordError(id);
25
+ }
26
+ return record;
27
+ },
28
+ load: async (id) => {
29
+ const db = assert();
30
+ return await db.get(id);
31
+ },
32
+ list: async (criteria, opts) => {
33
+ if (criteria == null) {
34
+ criteria = {};
35
+ opts = { pager: { page: 0, size: 10 } };
36
+ }
37
+ if ("pager" in criteria) {
38
+ opts = criteria;
39
+ criteria = criteria.criteria;
40
+ }
41
+ const db = assert();
42
+ const list = await db.get(LIST_KEY);
43
+ const pager = opts?.pager ?? { page: 0, size: 10 };
44
+ const conditions = Object.entries(criteria ?? {});
45
+ pager.page = pager.page ?? 0;
46
+ pager.size = pager.size ?? 10;
47
+ const result = [];
48
+ let skip = pager.page * pager.size;
49
+ let total = 0;
50
+ let added = 0;
51
+ for (const id of list) {
52
+ const record = await db.get(id);
53
+ if (record != null) {
54
+ if (!(conditions.length === 0 || conditions.every(([key, value]) => record[key] === value))) {
55
+ continue;
56
+ }
57
+ if (skip-- <= 0 && added < pager.size) {
58
+ added++;
59
+ result.push(record);
60
+ }
61
+ ++total;
62
+ }
63
+ }
64
+ return { items: result, pager: { ...pager, total } };
65
+ },
66
+ create: async (record) => {
67
+ const db = assert();
68
+ record.id = record.id ?? base58.encode(randomBytes(32));
69
+ if (await db.has(record.id)) {
70
+ throw new RecordExists(record.id);
71
+ }
72
+ const list = await db.get(LIST_KEY) ?? [];
73
+ if (list.includes(record.id)) {
74
+ throw new RecordExists(record.id);
75
+ }
76
+ list.push(record.id);
77
+ await db.set(LIST_KEY, list);
78
+ await db.set(record.id, record);
79
+ return record;
80
+ },
81
+ update: async (record) => {
82
+ const db = assert();
83
+ if (record.id == null) {
84
+ throw new UnknownRecordError('update');
85
+ }
86
+ const update = await resource.load(record.id);
87
+ if (update == null) {
88
+ throw new UnknownRecordError(record.id);
89
+ }
90
+ Object.assign(update, record);
91
+ await db.set(record.id, update);
92
+ return record;
93
+ },
94
+ delete: async (id) => {
95
+ if (typeof id === 'object') {
96
+ if (id.id == null) {
97
+ throw new UnknownRecordError('delete');
98
+ }
99
+ return resource.delete(id.id);
100
+ }
101
+ const db = assert();
102
+ if (!await db.has(id)) {
103
+ return null;
104
+ }
105
+ const record = await db.get(id);
106
+ if (record == null) {
107
+ throw new SyntaxError('We should not try to delete record that we know that not exists');
108
+ }
109
+ await db.del(id);
110
+ const list = await db.get(LIST_KEY) ?? [];
111
+ const idx = list.indexOf(id);
112
+ if (idx > -1) {
113
+ list.splice(idx, 1);
114
+ await db.set(LIST_KEY, list);
115
+ }
116
+ return record;
117
+ },
118
+ pick: async (id) => {
119
+ if (typeof id === 'object') {
120
+ if (id.id == null) {
121
+ throw new UnknownRecordError('pick');
122
+ }
123
+ return resource.pick(id.id);
124
+ }
125
+ const db = assert();
126
+ const record = await db.get(id);
127
+ if (record != null) {
128
+ await db.del(id);
129
+ }
130
+ else {
131
+ throw new UnknownRecordError(id);
132
+ }
133
+ const list = await db.get(LIST_KEY) ?? [];
134
+ const idx = list.indexOf(id);
135
+ if (idx > -1) {
136
+ list.splice(idx, 1);
137
+ await db.set(LIST_KEY, list);
138
+ }
139
+ return record;
140
+ },
141
+ save: async (record) => {
142
+ if (record.id == null || (await resource.load(record.id) == null)) {
143
+ return resource.create(record);
144
+ }
145
+ return resource.update(record);
146
+ },
147
+ erase: async () => {
148
+ const db = assert();
149
+ const list = await db.get(LIST_KEY) ?? [];
150
+ await Promise.all(list.map(id => resource.delete(id)));
151
+ }
152
+ });
153
+ context.registerResource(resource);
154
+ return context;
155
+ };
156
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { InitializedService } from '@owlmeans/context';
2
+ import type { Resource, ResourceRecord } from '@owlmeans/resource';
3
+ export interface ClientDbService extends InitializedService {
4
+ initialize: (alias?: string) => Promise<ClientDb>;
5
+ erase: () => Promise<void>;
6
+ }
7
+ export interface ClientDb {
8
+ get: <T>(id: string) => Promise<T>;
9
+ set: <T>(id: string, value: T) => Promise<void>;
10
+ has: (id: string) => Promise<boolean>;
11
+ del: (id: string) => Promise<boolean>;
12
+ }
13
+ export interface ClientResource<T extends ResourceRecord = ResourceRecord> extends Resource<T> {
14
+ db?: ClientDb;
15
+ erase: () => Promise<void>;
16
+ }
17
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAElE,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IACjD,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAA;IAClC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/C,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;IACrC,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CACtC;AAED,MAAM,WAAW,cAAc,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,CAAE,SAAQ,QAAQ,CAAC,CAAC,CAAC;IAC5F,EAAE,CAAC,EAAE,QAAQ,CAAA;IACb,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B"}
package/build/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@owlmeans/client-resource",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "build": "tsc -b",
7
+ "dev": "sleep 120 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
8
+ "watch": "tsc -b -w --preserveWatchOutput --pretty"
9
+ },
10
+ "main": "build/index.js",
11
+ "module": "build/index.js",
12
+ "types": "build/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": "./build/index.js",
16
+ "require": "./build/index.js",
17
+ "default": "./build/index.js",
18
+ "module": "./build/index.js",
19
+ "types": "./build/index.d.ts"
20
+ }
21
+ },
22
+ "dependencies": {
23
+ "@noble/hashes": "^1.5.0",
24
+ "@owlmeans/client-context": "^0.1.0",
25
+ "@owlmeans/context": "^0.1.0",
26
+ "@owlmeans/resource": "^0.1.0",
27
+ "@scure/base": "^1.1.9"
28
+ },
29
+ "devDependencies": {
30
+ "nodemon": "^3.1.7",
31
+ "typescript": "^5.6.3"
32
+ },
33
+ "private": false,
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
package/src/consts.ts ADDED
@@ -0,0 +1,4 @@
1
+
2
+ export const DEFAULT_DB_ALIAS = 'client-db'
3
+
4
+ export const LIST_KEY = '_list'
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './consts.js'
4
+ export * from './resource.js'
@@ -0,0 +1,200 @@
1
+ import type { ClientConfig, ClientContext } from '@owlmeans/client-context'
2
+ import { appendContextual, assertContext } from '@owlmeans/context'
3
+ import { RecordExists, ResourceError, UnknownRecordError } from '@owlmeans/resource'
4
+ import type { ListCriteria, ListPager, ResourceRecord } from '@owlmeans/resource'
5
+ import { DEFAULT_DB_ALIAS, LIST_KEY } from './consts.js'
6
+ import type { ClientDb, ClientDbService, ClientResource } from './types.js'
7
+ import { base58 } from '@scure/base'
8
+ import { randomBytes } from '@noble/hashes/utils'
9
+
10
+ type Config = ClientConfig
11
+ interface Context<C extends Config = Config> extends ClientContext<C> { }
12
+
13
+ export const appendClientResource = <C extends Config, T extends Context<C>>(context: T, alias: string): T => {
14
+
15
+ const location = `client-resource:${alias}`
16
+
17
+ const assert = (): ClientDb => {
18
+ if (resource.db == null) {
19
+ throw new ResourceError(`nodb-${location}`)
20
+ }
21
+
22
+ return resource.db
23
+ }
24
+
25
+ const resource: ClientResource<ResourceRecord> = appendContextual<ClientResource<ResourceRecord>>(alias, {
26
+ init: async () => {
27
+ const context = assertContext<Config, Context>(resource.ctx as Context, location)
28
+ const config = context.cfg.dbs?.find(db => db.alias === alias) ?? { service: DEFAULT_DB_ALIAS, host: [] }
29
+ const dbService = context.service<ClientDbService>(config.service)
30
+ resource.db = await dbService.initialize(config.schema ?? alias)
31
+ },
32
+
33
+ get: async id => {
34
+ const record = await resource.load(id)
35
+
36
+ if (record == null) {
37
+ throw new UnknownRecordError(id)
38
+ }
39
+
40
+ return record as any
41
+ },
42
+
43
+
44
+ load: async id => {
45
+ const db = assert()
46
+
47
+ return await db.get(id) as any | null
48
+ },
49
+
50
+ list: async (criteria, opts) => {
51
+ if (criteria == null) {
52
+ criteria = {}
53
+ opts = { pager: { page: 0, size: 10 } }
54
+ }
55
+ if ("pager" in criteria) {
56
+ opts = criteria
57
+ criteria = criteria.criteria as ListCriteria
58
+ }
59
+ const db = assert()
60
+ const list = await db.get<string[]>(LIST_KEY)
61
+
62
+ const pager: ListPager = opts?.pager ?? { page: 0, size: 10 }
63
+ const conditions = Object.entries(criteria as ListCriteria ?? {})
64
+ pager.page = pager.page ?? 0
65
+ pager.size = pager.size ?? 10
66
+
67
+ const result: ResourceRecord[] = []
68
+ let skip = pager.page * pager.size
69
+ let total = 0
70
+ let added = 0
71
+ for (const id of list) {
72
+ const record = await db.get<ResourceRecord>(id)
73
+ if (record != null) {
74
+ if (!(conditions.length === 0 || conditions.every(
75
+ ([key, value]) => record[key as keyof typeof record] === value
76
+ ))) {
77
+ continue
78
+ }
79
+ if (skip-- <= 0 && added < pager.size) {
80
+ added++
81
+ result.push(record)
82
+ }
83
+ ++total
84
+ }
85
+ }
86
+
87
+ return { items: result as any[], pager: { ...pager, total } }
88
+ },
89
+
90
+ create: async record => {
91
+ const db = assert()
92
+
93
+ record.id = record.id ?? base58.encode(randomBytes(32))
94
+
95
+ if (await db.has(record.id)) {
96
+ throw new RecordExists(record.id)
97
+ }
98
+
99
+ const list: string[] = await db.get(LIST_KEY) ?? []
100
+ if (list.includes(record.id)) {
101
+ throw new RecordExists(record.id)
102
+ }
103
+ list.push(record.id)
104
+ await db.set(LIST_KEY, list)
105
+ await db.set(record.id, record)
106
+
107
+ return record as any
108
+ },
109
+
110
+ update: async record => {
111
+ const db = assert()
112
+
113
+ if (record.id == null) {
114
+ throw new UnknownRecordError('update')
115
+ }
116
+
117
+ const update = await resource.load(record.id)
118
+ if (update == null) {
119
+ throw new UnknownRecordError(record.id)
120
+ }
121
+
122
+ Object.assign(update, record)
123
+
124
+ await db.set(record.id, update)
125
+
126
+ return record as any
127
+ },
128
+
129
+ delete: async id => {
130
+ if (typeof id === 'object') {
131
+ if (id.id == null) {
132
+ throw new UnknownRecordError('delete')
133
+ }
134
+ return resource.delete(id.id)
135
+ }
136
+
137
+ const db = assert()
138
+ if (!await db.has(id)) {
139
+ return null
140
+ }
141
+
142
+ const record = await db.get(id)
143
+ if (record == null) {
144
+ throw new SyntaxError('We should not try to delete record that we know that not exists')
145
+ }
146
+ await db.del(id)
147
+ const list: string[] = await db.get(LIST_KEY) ?? []
148
+ const idx = list.indexOf(id)
149
+ if (idx > -1) {
150
+ list.splice(idx, 1)
151
+ await db.set(LIST_KEY, list)
152
+ }
153
+
154
+ return record as any
155
+ },
156
+
157
+ pick: async id => {
158
+ if (typeof id === 'object') {
159
+ if (id.id == null) {
160
+ throw new UnknownRecordError('pick')
161
+ }
162
+ return resource.pick(id.id)
163
+ }
164
+
165
+ const db = assert()
166
+ const record = await db.get(id)
167
+ if (record != null) {
168
+ await db.del(id)
169
+ } else {
170
+ throw new UnknownRecordError(id)
171
+ }
172
+ const list: string[] = await db.get(LIST_KEY) ?? []
173
+ const idx = list.indexOf(id)
174
+ if (idx > -1) {
175
+ list.splice(idx, 1)
176
+ await db.set(LIST_KEY, list)
177
+ }
178
+
179
+ return record as any
180
+ },
181
+
182
+ save: async record => {
183
+ if (record.id == null || (await resource.load(record.id) == null)) {
184
+ return resource.create(record)
185
+ }
186
+
187
+ return resource.update(record)
188
+ },
189
+
190
+ erase: async () => {
191
+ const db = assert()
192
+ const list: string[] = await db.get(LIST_KEY) ?? []
193
+ await Promise.all(list.map(id => resource.delete(id)))
194
+ }
195
+ })
196
+
197
+ context.registerResource(resource)
198
+
199
+ return context
200
+ }
package/src/types.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { InitializedService } from '@owlmeans/context'
2
+ import type { Resource, ResourceRecord } from '@owlmeans/resource'
3
+
4
+ export interface ClientDbService extends InitializedService {
5
+ initialize: (alias?: string) => Promise<ClientDb>
6
+ erase: () => Promise<void>
7
+ }
8
+
9
+ export interface ClientDb {
10
+ get: <T>(id: string) => Promise<T>
11
+ set: <T>(id: string, value: T) => Promise<void>
12
+ has: (id: string) => Promise<boolean>
13
+ del: (id: string) => Promise<boolean>
14
+ }
15
+
16
+ export interface ClientResource<T extends ResourceRecord = ResourceRecord> extends Resource<T> {
17
+ db?: ClientDb
18
+ erase: () => Promise<void>
19
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": [
3
+ "../tsconfig.default.json"
4
+ ],
5
+ "compilerOptions": {
6
+ "rootDir": "./src/", /* Specify the root folder within your source files. */
7
+ "outDir": "./build/", /* Specify an output folder for all emitted files. */
8
+ "moduleResolution": "Bundler",
9
+ },
10
+ "exclude": [
11
+ "./dist/**/*",
12
+ "./build/**/*",
13
+ "./*.ts"
14
+ ]
15
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/consts.ts","./src/index.ts","./src/resource.ts","./src/types.ts"],"version":"5.6.3"}