@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 +21 -0
- package/README.md +714 -0
- package/build/.gitkeep +0 -0
- package/build/consts.d.ts +3 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +3 -0
- package/build/consts.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/build/resource.d.ts +7 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +156 -0
- package/build/resource.js.map +1 -0
- package/build/types.d.ts +17 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/package.json +37 -0
- package/src/consts.ts +4 -0
- package/src/index.ts +4 -0
- package/src/resource.ts +200 -0
- package/src/types.ts +19 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
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 @@
|
|
|
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"}
|
package/build/consts.js
ADDED
|
@@ -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"}
|
package/build/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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":"AACA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAEpF,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAExD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAKjD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAyC,OAAU,EAAE,KAAa,EAAK,EAAE;IAE3G,MAAM,QAAQ,GAAG,mBAAmB,KAAK,EAAE,CAAA;IAE3C,MAAM,MAAM,GAAG,GAAa,EAAE;QAC5B,IAAI,QAAQ,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;YACxB,MAAM,IAAI,aAAa,CAAC,QAAQ,QAAQ,EAAE,CAAC,CAAA;QAC7C,CAAC;QAED,OAAO,QAAQ,CAAC,EAAE,CAAA;IACpB,CAAC,CAAA;IAED,MAAM,QAAQ,GAAmC,gBAAgB,CAAiC,KAAK,EAAE;QACvG,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,MAAM,OAAO,GAAG,aAAa,CAAkB,QAAQ,CAAC,GAAc,EAAE,QAAQ,CAAC,CAAA;YACjF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,KAAK,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;YACzG,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAkB,MAAM,CAAC,OAAO,CAAC,CAAA;YAClE,QAAQ,CAAC,EAAE,GAAG,MAAM,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,IAAI,KAAK,CAAC,CAAA;QAClE,CAAC;QAED,GAAG,EAAE,KAAK,EAAC,EAAE,EAAC,EAAE;YACd,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YAEtC,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,kBAAkB,CAAC,EAAE,CAAC,CAAA;YAClC,CAAC;YAED,OAAO,MAAa,CAAA;QACtB,CAAC;QAGD,IAAI,EAAE,KAAK,EAAC,EAAE,EAAC,EAAE;YACf,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;YAEnB,OAAO,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAe,CAAA;QACvC,CAAC;QAED,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;YAC7B,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;gBACrB,QAAQ,GAAG,EAAE,CAAA;gBACb,IAAI,GAAG,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,CAAA;YACzC,CAAC;YACD,IAAI,OAAO,IAAI,QAAQ,EAAE,CAAC;gBACxB,IAAI,GAAG,QAAQ,CAAA;gBACf,QAAQ,GAAG,QAAQ,CAAC,QAAwB,CAAA;YAC9C,CAAC;YACD,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;YACnB,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,GAAG,CAAW,QAAQ,CAAC,CAAA;YAE7C,MAAM,KAAK,GAAc,IAAI,EAAE,KAAK,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAA;YAC7D,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,QAAwB,IAAI,EAAE,CAAC,CAAA;YACjE,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,CAAC,CAAA;YAC5B,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,IAAI,EAAE,CAAA;YAE7B,MAAM,MAAM,GAAqB,EAAE,CAAA;YACnC,IAAI,IAAI,GAAG,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,IAAI,CAAA;YAClC,IAAI,KAAK,GAAG,CAAC,CAAA;YACb,IAAI,KAAK,GAAG,CAAC,CAAA;YACb,KAAK,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC;gBACtB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAiB,EAAE,CAAC,CAAA;gBAC/C,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;oBACnB,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,KAAK,CAC/C,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,GAA0B,CAAC,KAAK,KAAK,CAC/D,CAAC,EAAE,CAAC;wBACH,SAAQ;oBACV,CAAC;oBACD,IAAI,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;wBACtC,KAAK,EAAE,CAAA;wBACP,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;oBACrB,CAAC;oBACD,EAAE,KAAK,CAAA;gBACT,CAAC;YACH,CAAC;YAED,OAAO,EAAE,KAAK,EAAE,MAAe,EAAE,KAAK,EAAE,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,EAAE,CAAA;QAC/D,CAAC;QAED,MAAM,EAAE,KAAK,EAAC,MAAM,EAAC,EAAE;YACrB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;YAEnB,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAA;YAEvD,IAAI,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC5B,MAAM,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YACnC,CAAC;YAED,MAAM,IAAI,GAAa,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;YACnD,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC7B,MAAM,IAAI,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YACnC,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YACpB,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;YAC5B,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;YAE/B,OAAO,MAAa,CAAA;QACtB,CAAC;QAED,MAAM,EAAE,KAAK,EAAC,MAAM,EAAC,EAAE;YACrB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;YAEnB,IAAI,MAAM,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;gBACtB,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAA;YACxC,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YAC7C,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;YACzC,CAAC;YAED,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;YAE7B,MAAM,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;YAE/B,OAAO,MAAa,CAAA;QACtB,CAAC;QAED,MAAM,EAAE,KAAK,EAAC,EAAE,EAAC,EAAE;YACjB,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;gBAC3B,IAAI,EAAE,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;oBAClB,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,CAAA;gBACxC,CAAC;gBACD,OAAO,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;YAC/B,CAAC;YAED,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;YACnB,IAAI,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;gBACtB,OAAO,IAAI,CAAA;YACb,CAAC;YAED,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC/B,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,IAAI,WAAW,CAAC,iEAAiE,CAAC,CAAA;YAC1F,CAAC;YACD,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAChB,MAAM,IAAI,GAAa,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;YACnD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YAC5B,IAAI,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;gBACb,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;gBACnB,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;YAC9B,CAAC;YAED,OAAO,MAAa,CAAA;QACtB,CAAC;QAED,IAAI,EAAE,KAAK,EAAC,EAAE,EAAC,EAAE;YACf,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;gBAC3B,IAAI,EAAE,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC;oBAClB,MAAM,IAAI,kBAAkB,CAAC,MAAM,CAAC,CAAA;gBACtC,CAAC;gBACD,OAAO,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,CAAA;YAC7B,CAAC;YAED,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;YACnB,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAC/B,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;YAClB,CAAC;iBAAM,CAAC;gBACN,MAAM,IAAI,kBAAkB,CAAC,EAAE,CAAC,CAAA;YAClC,CAAC;YACD,MAAM,IAAI,GAAa,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;YACnD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YAC5B,IAAI,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;gBACb,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;gBACnB,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;YAC9B,CAAC;YAED,OAAO,MAAa,CAAA;QACtB,CAAC;QAED,IAAI,EAAE,KAAK,EAAC,MAAM,EAAC,EAAE;YACnB,IAAI,MAAM,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,CAAC;gBAClE,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAChC,CAAC;YAED,OAAO,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QAChC,CAAC;QAED,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;YACnB,MAAM,IAAI,GAAa,MAAM,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;YACnD,MAAM,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;QACxD,CAAC;KACF,CAAC,CAAA;IAEF,OAAO,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAA;IAElC,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
|
package/build/types.d.ts
ADDED
|
@@ -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 @@
|
|
|
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
package/src/index.ts
ADDED
package/src/resource.ts
ADDED
|
@@ -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"}
|