@owlmeans/redis-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,560 @@
1
+ # @owlmeans/redis-resource
2
+
3
+ The **@owlmeans/redis-resource** package provides Redis-based resource storage implementation for OwlMeans Common Libraries, designed for fullstack microservices and microclients development with focus on security and proper authentication and authorization.
4
+
5
+ ## Purpose
6
+
7
+ This package serves as a Redis integration layer for the OwlMeans resource system that:
8
+
9
+ - **Provides Redis-backed storage** for resources with high performance and scalability
10
+ - **Supports pub/sub messaging** for real-time data synchronization across services
11
+ - **Implements full CRUD operations** with Redis-optimized performance
12
+ - **Offers TTL support** for automatic expiration of records
13
+ - **Enables horizontal scaling** through Redis clustering
14
+ - **Integrates with context system** for dependency injection and service management
15
+
16
+ ## Core Concepts
17
+
18
+ ### Redis Resource
19
+ A Redis resource is a storage implementation that uses Redis as the backend, providing fast key-value storage with advanced features like pub/sub, TTL, and clustering support.
20
+
21
+ ### Key Management
22
+ Records are stored in Redis using a configurable prefix and key structure, allowing for namespace separation and efficient data organization.
23
+
24
+ ### Pub/Sub Integration
25
+ The package provides built-in pub/sub functionality for real-time data synchronization, enabling reactive updates across distributed systems.
26
+
27
+ ### Database Service Integration
28
+ Integrates with the OwlMeans database service pattern, allowing for connection pooling and database management.
29
+
30
+ ## API Reference
31
+
32
+ ### Factory Functions
33
+
34
+ #### `makeRedisResource<R, T>(alias, dbAlias?, serviceAlias?, makeCustomResource?): T`
35
+
36
+ Creates a new Redis resource instance.
37
+
38
+ ```typescript
39
+ import { makeRedisResource } from '@owlmeans/redis-resource'
40
+
41
+ interface User {
42
+ id: string
43
+ name: string
44
+ email: string
45
+ }
46
+
47
+ const userResource = makeRedisResource<User>('users', 'redis-main')
48
+ ```
49
+
50
+ **Parameters:**
51
+ - `alias`: string - Resource alias for context registration
52
+ - `dbAlias`: string (optional) - Database alias, defaults to `DEFAULT_DB_ALIAS` ('redis')
53
+ - `serviceAlias`: string (optional) - Service alias, defaults to `DEFAULT_DB_ALIAS`
54
+ - `makeCustomResource`: ResourceMaker (optional) - Custom resource factory function
55
+
56
+ **Returns:** RedisResource instance with full CRUD and pub/sub capabilities
57
+
58
+ ### Resource Methods
59
+
60
+ The Redis resource implements the standard `Resource<ResourceRecord>` interface with additional Redis-specific methods:
61
+
62
+ #### Core CRUD Operations
63
+
64
+ #### `get<T>(id, field?, opts?): Promise<T>`
65
+
66
+ Retrieves a record by ID or field value. Throws `UnknownRecordError` if not found.
67
+
68
+ ```typescript
69
+ const user = await userResource.get<User>('user-123')
70
+ ```
71
+
72
+ #### `load<T>(id, field?, opts?): Promise<T | null>`
73
+
74
+ Loads a record by ID or field value. Returns `null` if not found.
75
+
76
+ ```typescript
77
+ const user = await userResource.load<User>('user-123')
78
+ if (user) {
79
+ console.log('User found:', user.name)
80
+ }
81
+ ```
82
+
83
+ #### `create<T>(record, opts?): Promise<T>`
84
+
85
+ Creates a new record in Redis storage.
86
+
87
+ ```typescript
88
+ const user = await userResource.create({
89
+ id: 'user-123',
90
+ name: 'John Doe',
91
+ email: 'john@example.com'
92
+ }, { ttl: 3600 }) // TTL in seconds
93
+ ```
94
+
95
+ **Options:**
96
+ - `ttl`: number | Date - Time to live in seconds (number) or expiration date (Date)
97
+
98
+ #### `update<T>(record, opts?): Promise<T>`
99
+
100
+ Updates an existing record.
101
+
102
+ ```typescript
103
+ const updatedUser = await userResource.update({
104
+ id: 'user-123',
105
+ name: 'John Smith',
106
+ email: 'john.smith@example.com'
107
+ }, { ttl: 7200 })
108
+ ```
109
+
110
+ #### `save<T>(record, opts?): Promise<T>`
111
+
112
+ Creates or updates a record (upsert operation).
113
+
114
+ ```typescript
115
+ const user = await userResource.save({
116
+ id: 'user-123',
117
+ name: 'John Doe',
118
+ email: 'john@example.com'
119
+ })
120
+ ```
121
+
122
+ #### `delete(id, opts?): Promise<T | null>`
123
+
124
+ Deletes a record and returns it, or `null` if not found.
125
+
126
+ ```typescript
127
+ const deletedUser = await userResource.delete('user-123')
128
+ ```
129
+
130
+ #### `pick<T>(id, opts?): Promise<T>`
131
+
132
+ Deletes a record and returns it. Throws `UnknownRecordError` if not found.
133
+
134
+ ```typescript
135
+ const pickedUser = await userResource.pick('user-123')
136
+ ```
137
+
138
+ #### `list<T>(criteria?, opts?): Promise<ListResult<T>>`
139
+
140
+ Lists records with pagination support.
141
+
142
+ ```typescript
143
+ const result = await userResource.list<User>({}, {
144
+ pager: { page: 0, size: 20 }
145
+ })
146
+ console.log('Users:', result.items)
147
+ console.log('Total:', result.pager?.total)
148
+ ```
149
+
150
+ ### Redis-Specific Methods
151
+
152
+ #### `key(key?): string`
153
+
154
+ Generates a Redis key with the configured prefix.
155
+
156
+ ```typescript
157
+ const redisKey = userResource.key('user-123')
158
+ console.log(redisKey) // "prefix:user-123"
159
+ ```
160
+
161
+ #### `subscribe<Type>(handler, key?): Promise<() => Promise<void>>`
162
+
163
+ Subscribes to Redis pub/sub messages for real-time updates.
164
+
165
+ ```typescript
166
+ const unsubscribe = await userResource.subscribe<User>(
167
+ async (user) => {
168
+ console.log('User updated:', user)
169
+ },
170
+ 'user-updates'
171
+ )
172
+
173
+ // Later, unsubscribe
174
+ await unsubscribe()
175
+ ```
176
+
177
+ **Parameters:**
178
+ - `handler`: (value: Type) => Promise<void> - Message handler function
179
+ - `key`: SubOpts (optional) - Subscription options or key
180
+
181
+ #### `publish<Type>(value, key?): Promise<void>`
182
+
183
+ Publishes a message to Redis pub/sub.
184
+
185
+ ```typescript
186
+ await userResource.publish<User>({
187
+ id: 'user-123',
188
+ name: 'John Doe',
189
+ email: 'john@example.com'
190
+ }, 'user-updates')
191
+ ```
192
+
193
+ ### Types
194
+
195
+ #### `RedisResource<T>`
196
+
197
+ Main interface for Redis resources extending the base Resource interface.
198
+
199
+ ```typescript
200
+ interface RedisResource<T extends ResourceRecord> extends Resource<T> {
201
+ name?: string
202
+ db: RedisDb
203
+ key: (key?: string) => string
204
+ subscribe: <Type extends T>(handler: (value: Type) => Promise<void>, key?: SubOpts) => Promise<() => Promise<void>>
205
+ publish: <Type extends T>(value: Type, key?: string) => Promise<void>
206
+ }
207
+ ```
208
+
209
+ #### `RedisDb`
210
+
211
+ Redis database connection interface.
212
+
213
+ ```typescript
214
+ interface RedisDb {
215
+ client: RedisCommander
216
+ prefix: string
217
+ }
218
+ ```
219
+
220
+ #### `RedisDbService`
221
+
222
+ Service interface for Redis database management.
223
+
224
+ ```typescript
225
+ interface RedisDbService extends ResourceDbService<RedisDb, RedisClient> {}
226
+ ```
227
+
228
+ #### `RedisClient`
229
+
230
+ Type alias for Redis client (supports both single Redis and Cluster).
231
+
232
+ ```typescript
233
+ type RedisClient = Redis | Cluster
234
+ ```
235
+
236
+ #### `SubscriptionOptions`
237
+
238
+ Options for Redis subscriptions.
239
+
240
+ ```typescript
241
+ interface SubscriptionOptions extends LifecycleOptions {
242
+ key?: string
243
+ once?: boolean
244
+ }
245
+ ```
246
+
247
+ ### Constants
248
+
249
+ #### `DEFAULT_DB_ALIAS`
250
+
251
+ Default database alias for Redis connections.
252
+
253
+ ```typescript
254
+ const DEFAULT_DB_ALIAS = 'redis'
255
+ ```
256
+
257
+ #### `DEFAULT_PAGE_SIZE`
258
+
259
+ Default page size for list operations.
260
+
261
+ ```typescript
262
+ const DEFAULT_PAGE_SIZE = 10
263
+ ```
264
+
265
+ ## Usage Examples
266
+
267
+ ### Basic Usage
268
+
269
+ ```typescript
270
+ import { makeRedisResource } from '@owlmeans/redis-resource'
271
+
272
+ interface Product {
273
+ id: string
274
+ name: string
275
+ price: number
276
+ category: string
277
+ }
278
+
279
+ // Create Redis resource
280
+ const productResource = makeRedisResource<Product>('products')
281
+
282
+ // Create products
283
+ await productResource.create({
284
+ id: 'prod-1',
285
+ name: 'Laptop',
286
+ price: 999.99,
287
+ category: 'electronics'
288
+ })
289
+
290
+ await productResource.create({
291
+ id: 'prod-2',
292
+ name: 'Book',
293
+ price: 19.99,
294
+ category: 'books'
295
+ }, { ttl: 86400 }) // Expires in 24 hours
296
+
297
+ // Retrieve products
298
+ const laptop = await productResource.get('prod-1')
299
+ console.log('Product:', laptop.name, laptop.price)
300
+
301
+ // List all products
302
+ const { items, pager } = await productResource.list()
303
+ console.log('Total products:', pager?.total)
304
+ ```
305
+
306
+ ### Context Integration
307
+
308
+ ```typescript
309
+ import { makeRedisResource } from '@owlmeans/redis-resource'
310
+ import { makeServerContext, makeServerConfig } from '@owlmeans/server-context'
311
+ import { AppType } from '@owlmeans/context'
312
+
313
+ // Create server context
314
+ const config = makeServerConfig(AppType.Backend, 'product-service')
315
+ const context = makeServerContext(config)
316
+
317
+ // Create and register Redis resource
318
+ const productResource = makeRedisResource<Product>('products', 'redis-main')
319
+ context.registerResource(productResource)
320
+
321
+ // Configure and initialize context
322
+ context.configure()
323
+ await context.init()
324
+
325
+ // Use resource through context
326
+ const resource = context.resource<typeof productResource>('products')
327
+ ```
328
+
329
+ ### Pub/Sub Usage
330
+
331
+ ```typescript
332
+ import { makeRedisResource } from '@owlmeans/redis-resource'
333
+
334
+ const orderResource = makeRedisResource<Order>('orders')
335
+
336
+ // Subscribe to order updates
337
+ const unsubscribe = await orderResource.subscribe<Order>(
338
+ async (order) => {
339
+ console.log('Order updated:', order.id, order.status)
340
+
341
+ // Process order update
342
+ if (order.status === 'completed') {
343
+ await processCompletedOrder(order)
344
+ }
345
+ },
346
+ { key: 'order-updates', once: false }
347
+ )
348
+
349
+ // Publish order updates
350
+ await orderResource.publish({
351
+ id: 'order-123',
352
+ status: 'completed',
353
+ total: 299.99
354
+ }, 'order-updates')
355
+
356
+ // Clean up subscription
357
+ await unsubscribe()
358
+ ```
359
+
360
+ ### TTL and Expiration
361
+
362
+ ```typescript
363
+ const sessionResource = makeRedisResource<Session>('sessions')
364
+
365
+ // Create session with TTL
366
+ await sessionResource.create({
367
+ id: 'session-abc123',
368
+ userId: 'user-456',
369
+ createdAt: new Date()
370
+ }, { ttl: 3600 }) // Expires in 1 hour
371
+
372
+ // Update with new expiration
373
+ await sessionResource.update({
374
+ id: 'session-abc123',
375
+ lastActivity: new Date()
376
+ }, { ttl: new Date(Date.now() + 7200000) }) // Expires in 2 hours
377
+ ```
378
+
379
+ ### Real-time Synchronization
380
+
381
+ ```typescript
382
+ // Service A - Updates user data
383
+ const userResource = makeRedisResource<User>('users')
384
+
385
+ await userResource.update({
386
+ id: 'user-123',
387
+ lastLogin: new Date(),
388
+ status: 'online'
389
+ })
390
+
391
+ // Notify other services
392
+ await userResource.publish({
393
+ id: 'user-123',
394
+ event: 'status-change',
395
+ status: 'online'
396
+ }, 'user-events')
397
+
398
+ // Service B - Listens for user events
399
+ const unsubscribe = await userResource.subscribe<UserEvent>(
400
+ async (event) => {
401
+ console.log('User event:', event.event, event.id)
402
+ await updateLocalCache(event)
403
+ },
404
+ 'user-events'
405
+ )
406
+ ```
407
+
408
+ ### Error Handling
409
+
410
+ ```typescript
411
+ import { UnknownRecordError, RecordExists, MisshapedRecord } from '@owlmeans/resource'
412
+
413
+ const userResource = makeRedisResource<User>('users')
414
+
415
+ try {
416
+ // This will throw UnknownRecordError
417
+ await userResource.get('non-existent-user')
418
+ } catch (error) {
419
+ if (error instanceof UnknownRecordError) {
420
+ console.log('User not found:', error.message)
421
+ }
422
+ }
423
+
424
+ try {
425
+ await userResource.create({ id: 'user-1', name: 'Alice', email: 'alice@example.com' })
426
+ // This will throw RecordExists
427
+ await userResource.create({ id: 'user-1', name: 'Bob', email: 'bob@example.com' })
428
+ } catch (error) {
429
+ if (error instanceof RecordExists) {
430
+ console.log('User already exists:', error.message)
431
+ // Use save() for upsert instead
432
+ await userResource.save({ id: 'user-1', name: 'Bob', email: 'bob@example.com' })
433
+ }
434
+ }
435
+ ```
436
+
437
+ ### Advanced Filtering and Pagination
438
+
439
+ ```typescript
440
+ const productResource = makeRedisResource<Product>('products')
441
+
442
+ // List with pagination
443
+ const page1 = await productResource.list({}, {
444
+ pager: { page: 0, size: 10 }
445
+ })
446
+
447
+ const page2 = await productResource.list({}, {
448
+ pager: { page: 1, size: 10 }
449
+ })
450
+
451
+ console.log('Page 1:', page1.items.length)
452
+ console.log('Page 2:', page2.items.length)
453
+ console.log('Total products:', page1.pager?.total)
454
+ ```
455
+
456
+ ## Integration Patterns
457
+
458
+ ### Microservices Architecture
459
+
460
+ ```typescript
461
+ // Product Service
462
+ const productResource = makeRedisResource<Product>('products', 'redis-products')
463
+
464
+ // Order Service
465
+ const orderResource = makeRedisResource<Order>('orders', 'redis-orders')
466
+
467
+ // Shared event bus
468
+ const eventResource = makeRedisResource<Event>('events', 'redis-events')
469
+
470
+ // Cross-service communication
471
+ await productResource.subscribe<ProductEvent>(
472
+ async (event) => {
473
+ if (event.type === 'price-updated') {
474
+ await orderResource.publish({
475
+ type: 'product-price-changed',
476
+ productId: event.productId,
477
+ newPrice: event.price
478
+ }, 'order-events')
479
+ }
480
+ },
481
+ 'product-events'
482
+ )
483
+ ```
484
+
485
+ ### Caching Layer
486
+
487
+ ```typescript
488
+ // Use Redis resource as a caching layer
489
+ const cacheResource = makeRedisResource<CacheEntry>('cache')
490
+
491
+ const getCachedData = async <T>(key: string, fetcher: () => Promise<T>): Promise<T> => {
492
+ try {
493
+ const cached = await cacheResource.load(key)
494
+ if (cached && cached.expiresAt > Date.now()) {
495
+ return cached.data as T
496
+ }
497
+ } catch (error) {
498
+ // Cache miss, continue to fetch
499
+ }
500
+
501
+ const data = await fetcher()
502
+
503
+ // Cache for 5 minutes
504
+ await cacheResource.save({
505
+ id: key,
506
+ data,
507
+ expiresAt: Date.now() + 300000
508
+ }, { ttl: 300 })
509
+
510
+ return data
511
+ }
512
+
513
+ // Usage
514
+ const userData = await getCachedData(`user:${userId}`, () => fetchUserFromDB(userId))
515
+ ```
516
+
517
+ ## Performance Considerations
518
+
519
+ ### Connection Management
520
+ - **Connection Pooling** - Use Redis connection pools for high-traffic applications
521
+ - **Cluster Support** - Leverage Redis clustering for horizontal scaling
522
+ - **Pipeline Operations** - Batch multiple operations for better performance
523
+
524
+ ### Data Organization
525
+ - **Key Prefixes** - Use meaningful prefixes to organize data and avoid conflicts
526
+ - **TTL Management** - Set appropriate TTL values to manage memory usage
527
+ - **Pagination** - Use pagination for large datasets to avoid memory issues
528
+
529
+ ### Best Practices
530
+
531
+ 1. **Use appropriate TTL** - Set TTL for temporary data to prevent memory bloat
532
+ 2. **Monitor Redis memory** - Keep track of Redis memory usage and key counts
533
+ 3. **Implement error handling** - Handle Redis connection failures gracefully
534
+ 4. **Use pub/sub wisely** - Avoid creating too many subscriptions that could impact performance
535
+ 5. **Batch operations** - Use pipelines for multiple operations when possible
536
+
537
+ ## Error Handling
538
+
539
+ The package uses standard error types from `@owlmeans/resource`:
540
+
541
+ - **`UnknownRecordError`** - Thrown when trying to access a non-existent record
542
+ - **`RecordExists`** - Thrown when trying to create a record with an existing ID
543
+ - **`MisshapedRecord`** - Thrown when record structure is invalid
544
+ - **`UnsupportedArgumentError`** - Thrown when unsupported arguments are provided
545
+
546
+ Additional Redis-specific errors may be thrown by the underlying ioredis client.
547
+
548
+ ## Dependencies
549
+
550
+ This package depends on:
551
+ - `@owlmeans/context` - For contextual resource management
552
+ - `@owlmeans/resource` - For resource interface and error types
553
+ - `ioredis` - Redis client library for Node.js
554
+
555
+ ## Related Packages
556
+
557
+ - [`@owlmeans/resource`](../resource) - Base resource interface and utilities
558
+ - [`@owlmeans/mongo-resource`](../mongo-resource) - MongoDB resource implementation
559
+ - [`@owlmeans/static-resource`](../static-resource) - In-memory resource implementation
560
+ - [`@owlmeans/server-context`](../server-context) - Server-side context management
package/build/.gitkeep ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_DB_ALIAS = "redis";
2
+ export declare const DEFAULT_PAGE_SIZE = 10;
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,UAAU,CAAA;AAEvC,eAAO,MAAM,iBAAiB,KAAK,CAAA"}
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_DB_ALIAS = 'redis';
2
+ export const DEFAULT_PAGE_SIZE = 10;
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,OAAO,CAAA;AAEvC,MAAM,CAAC,MAAM,iBAAiB,GAAG,EAAE,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":"AAAA,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":"AACA,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA"}
@@ -0,0 +1,4 @@
1
+ import type { ResourceMaker, ResourceRecord } from '@owlmeans/resource';
2
+ import type { RedisResource } from './types.js';
3
+ export declare const makeRedisResource: <R extends ResourceRecord, T extends RedisResource<R> = RedisResource<R>>(alias: string, dbAlias?: string, serviceAlias?: string, makeCustomResource?: ResourceMaker<R, T>) => T;
4
+ //# 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,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAEvE,OAAO,KAAK,EAA+B,aAAa,EAAE,MAAM,YAAY,CAAA;AAU5E,eAAO,MAAM,iBAAiB,GAC5B,CAAC,SAAS,cAAc,EAAE,CAAC,SAAS,aAAa,CAAC,CAAC,CAAC,4BAE7C,MAAM,gEACQ,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,KACvC,CAoRF,CAAA"}