@adobe/aio-lib-db 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,557 @@
1
+ # aio-lib-db
2
+
3
+ **aio-lib-db** is a powerful document database library for Adobe I/O Runtime applications. It provides structured, queryable, and flexible data persistence with MongoDB-like query capabilities.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ Install `aio-lib-db` from npm:
10
+
11
+ ```bash
12
+ npm install @adobe/aio-lib-db
13
+ ```
14
+
15
+ Or add it to your `package.json`:
16
+
17
+ ```json
18
+ {
19
+ "dependencies": {
20
+ "@adobe/aio-lib-db": "^0.1.0"
21
+ }
22
+ }
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Quick Start
28
+
29
+ ### Setup
30
+
31
+ First, set your credentials in your `.env` file:
32
+
33
+ ```env
34
+ __OW_NAMESPACE=your_namespace
35
+ __OW_API_KEY=user:password
36
+ ```
37
+
38
+ > To find runtime namespace and credentials, click "Download all" in the Adobe Developer Console for your project workspace and the values will be under `project.workspace.details.runtime.namespaces`.
39
+
40
+ ### Basic Usage
41
+
42
+ > When calling `libDb.init()`, you can pass `{ region: '<region>>' }` to specify the region where your database is provisioned.
43
+ > Valid regions are `amer` (default), `emea`, and `apac`.
44
+
45
+ ```javascript
46
+ const libDb = require('@adobe/aio-lib-db');
47
+
48
+ async function main() {
49
+ try {
50
+ // Initialize and connect
51
+ const db = await libDb.init({ region: 'amer' });
52
+ const client = await db.connect();
53
+
54
+ // Get a collection
55
+ const users = client.collection('users');
56
+
57
+ // Insert a document
58
+ await users.insertOne({ name: 'John Doe', email: 'john@example.com' });
59
+
60
+ // Find documents
61
+ const cursor = users.find({ name: 'John Doe' });
62
+ const results = await cursor.toArray();
63
+ }
64
+ finally {
65
+ // Close any open cursors when the application is done
66
+ await client.close();
67
+ }
68
+ }
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Collection Operations
74
+
75
+ ### Insert Operations
76
+
77
+ ```javascript
78
+ // Insert a single document
79
+ const result = await collection.insertOne({
80
+ name: 'Jane Smith',
81
+ email: 'jane@example.com',
82
+ age: 30
83
+ });
84
+
85
+ // Insert multiple documents
86
+ const result = await collection.insertMany([
87
+ { name: 'Alice', email: 'alice@example.com' },
88
+ { name: 'Bob', email: 'bob@example.com' }
89
+ ]);
90
+ ```
91
+
92
+ ### Find Operations
93
+
94
+ ```javascript
95
+ // Find one document
96
+ const user = await collection.findOne({ email: 'john@example.com' });
97
+
98
+ // Find all documents matching a filter
99
+ const cursor = collection.find({ age: { $gte: 18 } });
100
+ const adults = await cursor.toArray();
101
+
102
+ // Find with projection and sorting
103
+ const cursor = collection.find({ age: { $gte: 18 } })
104
+ .project({ name: 1, email: 1 })
105
+ .sort({ name: 1 })
106
+ .limit(10);
107
+ ```
108
+
109
+ ### Update Operations
110
+
111
+ ```javascript
112
+ // Update one document
113
+ const result = await collection.updateOne(
114
+ { email: 'john@example.com' },
115
+ { $set: { age: 31 } }
116
+ );
117
+
118
+ // Update multiple documents
119
+ const result = await collection.updateMany(
120
+ { age: { $lt: 18 } },
121
+ { $set: { category: 'minor' } }
122
+ );
123
+
124
+ // Find and update
125
+ const updatedUser = await collection.findOneAndUpdate(
126
+ { email: 'john@example.com' },
127
+ { $set: { lastLogin: new Date() } },
128
+ { returnDocument: 'after' }
129
+ );
130
+ ```
131
+
132
+ ### Delete Operations
133
+
134
+ ```javascript
135
+ // Delete one document
136
+ const result = await collection.deleteOne({ email: 'john@example.com' });
137
+
138
+ // Delete multiple documents
139
+ const result = await collection.deleteMany({ age: { $lt: 0 } });
140
+
141
+ // Find and delete
142
+ const deletedUser = await collection.findOneAndDelete({ email: 'john@example.com' });
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Query Building with Cursors
148
+
149
+ > Cursors will close themselves after all results have been processed, but they can be closed early to release resources by calling `cursor.close()`, and best practice is to close them explicitly once they're no longer needed. The `client.close()` method will close all open cursors and connections, so it should be called when the application is shutting down or no longer needs database access.
150
+
151
+ ### FindCursor Methods
152
+
153
+ The `find()` method returns a `FindCursor` that supports method chaining:
154
+
155
+ ```javascript
156
+ const cursor = collection.find({ status: 'active' })
157
+ .filter({ category: 'premium' }) // Additional filtering
158
+ .sort({ createdAt: -1 }) // Sort by creation date (newest first)
159
+ .project({ name: 1, email: 1, _id: 0 }) // Only include name and email
160
+ .limit(20) // Limit to 20 results
161
+ .skip(10) // Skip first 10 results
162
+ .batchSize(5); // Process in batches of 5
163
+ ```
164
+
165
+ ### Cursor Iteration
166
+
167
+ ```javascript
168
+ // Using toArray() - loads all results into memory
169
+ const results = await cursor.toArray();
170
+
171
+ // Using iteration - memory efficient
172
+ while (await cursor.hasNext()) {
173
+ const doc = await cursor.next();
174
+ console.log(doc);
175
+ }
176
+
177
+ // Using for await...of - most convenient
178
+ for await (const doc of cursor) {
179
+ console.log(doc);
180
+ }
181
+
182
+ // Using streams
183
+ const stream = cursor.stream();
184
+ stream.on('data', (doc) => {
185
+ console.log(doc);
186
+ });
187
+
188
+ // Check cursor properties
189
+ console.log('Cursor ID:', cursor.id);
190
+ console.log('Is closed:', cursor.closed);
191
+ ```
192
+
193
+ ### Cursor Transformations
194
+
195
+ ```javascript
196
+ // Transform documents as they're retrieved
197
+ const cursor = collection.find({ status: 'active' })
198
+ .map(doc => ({
199
+ ...doc,
200
+ displayName: `${doc.firstName} ${doc.lastName}`,
201
+ isVip: doc.tier === 'premium'
202
+ }));
203
+
204
+ // Chain multiple transformations
205
+ const cursor = collection.find({ status: 'active' })
206
+ .map(doc => ({ ...doc, processed: true }))
207
+ .map(doc => ({ ...doc, timestamp: new Date() }));
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Aggregation Pipeline
213
+
214
+ ### Basic Aggregation
215
+
216
+ ```javascript
217
+ // Simple aggregation
218
+ const pipeline = [
219
+ { $match: { status: 'active' } },
220
+ { $group: { _id: '$category', count: { $sum: 1 } } },
221
+ { $sort: { count: -1 } }
222
+ ];
223
+
224
+ const cursor = collection.aggregate(pipeline);
225
+ const results = await cursor.toArray();
226
+ ```
227
+
228
+ ### Chained Aggregation Building
229
+
230
+ ```javascript
231
+ // Build aggregation pipeline using method chaining
232
+ const cursor = collection.aggregate()
233
+ .match({ status: 'active' })
234
+ .group({ _id: '$category', total: { $sum: '$amount' } })
235
+ .sort({ total: -1 })
236
+ .limit(10)
237
+ .project({ category: '$_id', total: 1, _id: 0 });
238
+
239
+ const topCategories = await cursor.toArray();
240
+
241
+ // Geospatial aggregation example
242
+ const nearbyStores = await collection.aggregate()
243
+ .geoNear({
244
+ near: { type: 'Point', coordinates: [-122.4194, 37.7749] }, // San Francisco
245
+ distanceField: 'distance',
246
+ maxDistance: 1000, // 1km radius
247
+ spherical: true
248
+ })
249
+ .match({ status: 'open' })
250
+ .limit(10)
251
+ .toArray();
252
+ ```
253
+
254
+ ### Advanced Aggregation
255
+
256
+ ```javascript
257
+ // Complex aggregation with multiple stages
258
+ const cursor = collection.aggregate()
259
+ .match({ dateCreated: { $gte: new Date('2024-01-01') } })
260
+ .lookup({
261
+ from: 'categories',
262
+ localField: 'categoryId',
263
+ foreignField: '_id',
264
+ as: 'category'
265
+ })
266
+ .unwind('$category')
267
+ .redact({
268
+ $cond: {
269
+ if: { $eq: ['$category.status', 'active'] },
270
+ then: '$$DESCEND',
271
+ else: '$$PRUNE'
272
+ }
273
+ })
274
+ .group({
275
+ _id: '$category.name',
276
+ totalSales: { $sum: '$amount' },
277
+ averageOrder: { $avg: '$amount' },
278
+ orderCount: { $sum: 1 }
279
+ })
280
+ .sort({ totalSales: -1 })
281
+ .limit(5)
282
+ .out('sales_summary'); // Output results to a new collection
283
+ ```
284
+
285
+ ---
286
+
287
+ ## Advanced Features
288
+
289
+ ### Indexing
290
+
291
+ ```javascript
292
+ // Create indexes for better query performance
293
+ await collection.createIndex({ email: 1 }, { unique: true });
294
+ await collection.createIndex({ 'profile.age': 1, status: 1 });
295
+
296
+ // List all indexes
297
+ const indexes = await collection.getIndexes();
298
+
299
+ // Drop an index
300
+ await collection.dropIndex('email_1');
301
+ ```
302
+
303
+ ### Counting Documents
304
+
305
+ ```javascript
306
+ // Fast count estimate (uses collection metadata)
307
+ const estimate = await collection.estimatedDocumentCount();
308
+
309
+ // Accurate count with filter (scans documents)
310
+ const activeUsers = await collection.countDocuments({ status: 'active' });
311
+
312
+ // Count all documents accurately
313
+ const totalExact = await collection.countDocuments({});
314
+ ```
315
+
316
+ ### Bulk Operations
317
+
318
+ ```javascript
319
+ // Perform multiple operations in a single request
320
+ const operations = [
321
+ { insertOne: { document: { name: 'Alice' } } },
322
+ { updateOne: { filter: { name: 'Bob' }, update: { $set: { age: 30 } } } },
323
+ { deleteOne: { filter: { name: 'Charlie' } } }
324
+ ];
325
+
326
+ const result = await collection.bulkWrite(operations);
327
+ ```
328
+
329
+ ### Collection Management
330
+
331
+ ```javascript
332
+ // Drop a collection (permanently delete)
333
+ await collection.drop();
334
+
335
+ // Rename a collection
336
+ await collection.renameCollection('new_collection_name');
337
+
338
+ // Create a new collection with options
339
+ const newCollection = await client.createCollection('analytics', {
340
+ validator: {
341
+ $jsonSchema: {
342
+ required: ['userId', 'action', 'timestamp'],
343
+ properties: {
344
+ userId: { type: 'string' },
345
+ action: { type: 'string' },
346
+ timestamp: { type: 'date' }
347
+ }
348
+ }
349
+ }
350
+ });
351
+ ```
352
+
353
+ ### Query Options
354
+
355
+ ```javascript
356
+ // Advanced query options
357
+ const cursor = collection.find({ status: 'active' })
358
+ .hint({ status: 1 }) // Use specific index
359
+ .maxTimeMS(5000) // Set query timeout
360
+ .readConcern({ level: 'majority' }) // Set read concern
361
+ .collation({ locale: 'en', strength: 2 }) // Case-insensitive sorting
362
+ .noCursorTimeout(true); // Disable cursor timeout
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Error Handling
368
+
369
+ ```javascript
370
+ const { DbError } = require('@adobe/aio-lib-db');
371
+
372
+ try {
373
+ await collection.insertOne({ email: 'invalid-email' });
374
+ } catch (error) {
375
+ if (error instanceof DbError) {
376
+ console.error('Database error:', error.message);
377
+ } else {
378
+ console.error('Unexpected error:', error);
379
+ }
380
+ }
381
+ ```
382
+
383
+ ---
384
+
385
+ ## Best Practices
386
+
387
+ ### 1. **Always Close Connections**
388
+
389
+ ```javascript
390
+ const client = await db.connect();
391
+ try {
392
+ // Your database operations
393
+ } finally {
394
+ await client.close();
395
+ }
396
+ ```
397
+
398
+ ### 2. **Use Projections for Large Documents**
399
+
400
+ ```javascript
401
+ // Only fetch needed fields
402
+ const users = await collection.find({})
403
+ .project({ name: 1, email: 1, _id: 0 })
404
+ .toArray();
405
+ ```
406
+
407
+ ### 3. **Use Indexes for Frequent Queries**
408
+
409
+ ```javascript
410
+ // Create indexes for frequently queried fields
411
+ await collection.createIndex({ email: 1 });
412
+ await collection.createIndex({ status: 1, createdAt: -1 });
413
+ ```
414
+
415
+ ### 4. **Handle Large Result Sets with Cursors**
416
+
417
+ ```javascript
418
+ // For large datasets, use cursor iteration instead of toArray()
419
+ for await (const doc of collection.find({})) {
420
+ // Process one document at a time
421
+ await processDocument(doc);
422
+ }
423
+ ```
424
+
425
+ ### 5. **Use Aggregation for Complex Queries**
426
+
427
+ ```javascript
428
+ // Use aggregation for complex data processing
429
+ const report = await collection.aggregate()
430
+ .match({ date: { $gte: startDate } })
431
+ .group({ _id: '$category', total: { $sum: '$amount' } })
432
+ .sort({ total: -1 })
433
+ .toArray();
434
+ ```
435
+
436
+ ---
437
+
438
+ ## API Reference
439
+
440
+ ### DbClient
441
+
442
+ - `dbStats()` - Get database statistics
443
+ - `listCollections(filter?, options?)` - List collections
444
+ - `collection(name)` - Get collection instance
445
+ - `createCollection(name, options?)` - Create new collection
446
+ - `close()` - Close the connection and all open cursors
447
+
448
+ ### DbCollection
449
+
450
+ **Insert Operations:**
451
+ - `insertOne(document, options?)` - Insert single document
452
+ - `insertMany(documents, options?)` - Insert multiple documents
453
+
454
+ **Find Operations:**
455
+ - `findOne(filter, options?)` - Find single document
456
+ - `find(filter?, options?)` - Find multiple documents (returns FindCursor)
457
+ - `findArray(filter?, options?)` - Find single batch as array
458
+
459
+ **Update Operations:**
460
+ - `updateOne(filter, update, options?)` - Update single document
461
+ - `updateMany(filter, update, options?)` - Update multiple documents
462
+ - `findOneAndUpdate(filter, update, options?)` - Find and update
463
+ - `replaceOne(filter, replacement, options?)` - Replace document
464
+
465
+ **Delete Operations:**
466
+ - `deleteOne(filter, options?)` - Delete single document
467
+ - `deleteMany(filter, options?)` - Delete multiple documents
468
+ - `findOneAndDelete(filter, options?)` - Find and delete
469
+
470
+ **Aggregation:**
471
+ - `aggregate(pipeline?, options?)` - Run aggregation (returns AggregateCursor)
472
+
473
+ **Utility Operations:**
474
+ - `countDocuments(filter?, options?)` - Count documents
475
+ - `estimatedDocumentCount(options?)` - Estimate document count from metadata
476
+ - `distinct(field, filter?, options?)` - Get distinct values
477
+ - `bulkWrite(operations, options?)` - Bulk operations
478
+
479
+ **Statistics & Monitoring:**
480
+ - `stats(options?)` - Get collection statistics
481
+
482
+ **Index Operations:**
483
+ - `createIndex(specification, options?)` - Create index
484
+ - `getIndexes()` - List indexes
485
+ - `dropIndex(indexName, options?)` - Drop index
486
+
487
+ **Collection Management:**
488
+ - `drop(options?)` - Drop the collection
489
+ - `renameCollection(newName, options?)` - Rename collection
490
+
491
+ ### FindCursor
492
+
493
+ **Query Building:**
494
+ - `filter(filter)` - Set query filter
495
+ - `sort(sort, direction?)` - Set sort order
496
+ - `project(projection)` - Set field projection
497
+ - `limit(limit)` - Set result limit
498
+ - `skip(skip)` - Set number to skip
499
+ - `batchSize(size)` - Set batch size
500
+
501
+ **Iteration:**
502
+ - `hasNext()` - Check if more results available
503
+ - `next()` - Get next document
504
+ - `toArray()` - Get all results as array
505
+
506
+ **Properties:**
507
+ - `id` - Get cursor ID
508
+ - `closed` - Check if cursor is closed and exhausted
509
+
510
+ **Utilities:**
511
+ - `map(transform)` - Transform documents
512
+ - `stream(transform?)` - Get readable stream
513
+ - `close()` - Close the cursor and release resources
514
+
515
+ ### AggregateCursor
516
+
517
+ **Pipeline Building:**
518
+ - `match(filter)` - Add $match stage
519
+ - `group(groupSpec)` - Add $group stage
520
+ - `sort(sort)` - Add $sort stage
521
+ - `project(projection)` - Add $project stage
522
+ - `limit(limit)` - Add $limit stage
523
+ - `skip(skip)` - Add $skip stage
524
+ - `lookup(lookupSpec)` - Add $lookup stage
525
+ - `unwind(path)` - Add $unwind stage
526
+ - `out(outSpec)` - Add $out stage (output to collection)
527
+ - `redact(redactSpec)` - Add $redact stage (conditional filtering)
528
+ - `geoNear(geoNearSpec)` - Add $geoNear stage (geospatial queries)
529
+ - `addStage(stage)` - Add custom stage
530
+
531
+ **Iteration:** (Same as FindCursor)
532
+ - `hasNext()`, `next()`, `toArray()`, `stream()`, etc.
533
+
534
+ **Properties:**
535
+ - `id` - Get cursor ID
536
+ - `closed` - Check if cursor is closed and exhausted
537
+
538
+ **Utilities:**
539
+ - `close()` - Close the cursor and release resources
540
+
541
+ ---
542
+
543
+ ## Why Use aio-lib-db?
544
+
545
+ - **MongoDB-like Syntax**: Familiar query language and operations
546
+ - **Powerful Querying**: Complex filtering, sorting, and aggregation
547
+ - **Cursor Support**: Memory-efficient iteration over large datasets
548
+ - **Method Chaining**: Fluent API for building complex queries
549
+ - **Type Safety**: Comprehensive input validation and error handling
550
+ - **Streaming Support**: Process large datasets without memory issues
551
+ - **Native Integration**: Built specifically for Adobe I/O Runtime
552
+
553
+ ---
554
+
555
+ ## Support
556
+
557
+ For issues, feature requests, or questions, please refer to the project's issue tracker or documentation.
package/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /*
2
+ Copyright 2025 Adobe. All rights reserved.
3
+ This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License. You may obtain a copy
5
+ of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software distributed under
8
+ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ OF ANY KIND, either express or implied. See the License for the specific language
10
+ governing permissions and limitations under the License.
11
+ */
12
+ module.exports = require('./lib/init')