@owlmeans/mongo-resource 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,807 +1,81 @@
1
1
  # @owlmeans/mongo-resource
2
2
 
3
- MongoDB resource implementation for OwlMeans Common applications. This package provides a complete MongoDB integration for the OwlMeans resource system, offering document storage, querying, indexing, and schema validation with the familiar OwlMeans resource interface.
3
+ MongoDB-backed `Resource<T>` implementation the primary database resource for OwlMeans server apps.
4
4
 
5
5
  ## Overview
6
6
 
7
- The `@owlmeans/mongo-resource` package extends the base `@owlmeans/resource` system with MongoDB-specific functionality. It provides:
8
-
9
- - **MongoDB Resource Implementation**: Complete resource interface backed by MongoDB collections
10
- - **Document Management**: Full CRUD operations with MongoDB ObjectId handling
11
- - **Schema Integration**: AJV schema validation with MongoDB document structure
12
- - **Index Management**: Automated index creation and management
13
- - **Query Support**: MongoDB query capabilities with pagination and sorting
14
- - **Locking Mechanisms**: Document-level locking for concurrent access control
15
- - **Collection Lifecycle**: Automated collection and index initialization
16
- - **Type Safety**: Full TypeScript support with MongoDB-specific types
17
-
18
- This package is part of the OwlMeans database integration ecosystem, providing MongoDB as a storage backend for resources.
7
+ - `makeMongoResource<R, T>(alias, dbAlias?, serviceAlias?, maker?)` factory for MongoDB resources
8
+ - `MongoResource<T>` — extends `Resource<T>` with MongoDB collection, indexing, and field encryption
9
+ - Supports CRUD, list/pagination, AJV schema validation, and field-level locking (encryption)
10
+ - Used for all persistent data models in server applications
19
11
 
20
12
  ## Installation
21
13
 
22
14
  ```bash
23
- npm install @owlmeans/mongo-resource mongodb ajv
24
- ```
25
-
26
- ## Dependencies
27
-
28
- This package requires:
29
- - `@owlmeans/resource`: Core resource system
30
- - `@owlmeans/server-context`: Server context management
31
- - `mongodb`: MongoDB Node.js driver (peer dependency)
32
- - `ajv`: JSON Schema validation (peer dependency)
33
-
34
- ## Core Concepts
35
-
36
- ### MongoDB Resource
37
-
38
- A MongoDB resource wraps a MongoDB collection with the OwlMeans resource interface, providing seamless integration between MongoDB documents and the resource system.
39
-
40
- ### ObjectId Handling
41
-
42
- The package automatically converts MongoDB ObjectIds to string IDs in the resource interface while maintaining MongoDB-native ObjectId usage internally.
43
-
44
- ### Schema Validation
45
-
46
- JSON Schema validation is applied to documents before storage, ensuring data integrity and consistency.
47
-
48
- ### Collection Management
49
-
50
- Collections are automatically created and configured with indexes during resource initialization.
51
-
52
- ## API Reference
53
-
54
- ### Types
55
-
56
- #### `MongoResource<T extends ResourceRecord>`
57
-
58
- Main MongoDB resource interface that extends the base Resource interface with MongoDB-specific capabilities.
59
-
60
- ```typescript
61
- interface MongoResource<T extends ResourceRecord> extends Resource<T>, ResourceLocker<T> {
62
- name?: string // Collection name override
63
- schema?: AnySchema // AJV schema for validation
64
- indexes?: Array<IndexDefinition> // Index definitions
65
- collection: Collection // MongoDB collection instance
66
- db(): Promise<Db> // Get database instance
67
- client(): Promise<MongoClient> // Get MongoDB client
68
- index<Type extends MongoResource<T>>( // Add index definition
69
- name: string,
70
- index: IndexSpecification,
71
- options?: CreateIndexesOptions
72
- ): Type
73
- getDefaults(): Partial<T> // Get default values from schema
74
- }
75
- ```
76
-
77
- #### `MongoDbService`
78
-
79
- Database service interface for MongoDB operations and connection management.
80
-
81
- ```typescript
82
- interface MongoDbService extends ResourceDbService<Db, MongoClient>, DbLocker<ResourceRecord> {
83
- // Inherits database service methods and locking capabilities
84
- }
85
- ```
86
-
87
- #### `IndexDefinition`
88
-
89
- Type for defining MongoDB indexes.
90
-
91
- ```typescript
92
- interface IndexDefinition {
93
- name: string // Index name
94
- index: IndexSpecification // MongoDB index specification
95
- options?: CreateIndexesOptions // Index creation options
96
- }
15
+ bun add @owlmeans/mongo-resource
97
16
  ```
98
17
 
99
- ### Factory Functions
100
-
101
- #### `makeMongoResource<R extends ResourceRecord, T extends MongoResource<R>>(alias: string, dbAlias?: string, serviceAlias?: string, makeCustomResource?: ResourceMaker<R, T>): T`
18
+ ## Usage
102
19
 
103
- Creates a MongoDB resource instance with full CRUD capabilities.
20
+ Define a resource:
104
21
 
105
- **Parameters:**
106
- - `alias`: Resource alias for registration
107
- - `dbAlias`: Database alias (default: 'mongo')
108
- - `serviceAlias`: Service alias (default: same as dbAlias)
109
- - `makeCustomResource`: Optional custom resource factory
110
-
111
- **Returns:** MongoResource instance
112
-
113
- **Example:**
114
22
  ```typescript
115
23
  import { makeMongoResource } from '@owlmeans/mongo-resource'
24
+ import type { MongoResource } from '@owlmeans/mongo-resource'
25
+ import type { ResourceMaker } from '@owlmeans/resource'
116
26
 
117
- interface User extends ResourceRecord {
118
- id?: string
119
- name: string
120
- email: string
121
- createdAt?: Date
122
- }
123
-
124
- const userResource = makeMongoResource<User>('users')
125
-
126
- // With custom collection and service
127
- const customResource = makeMongoResource<User>('users', 'primary-db', 'mongo-service')
128
- ```
129
-
130
- ### MongoDB Resource Methods
131
-
132
- #### `get(id: string, field?: string, opts?: LifecycleOptions): Promise<T>`
133
-
134
- Retrieves a document by ID or specified field. Throws `UnknownRecordError` if not found.
135
-
136
- **Parameters:**
137
- - `id`: Document identifier
138
- - `field`: Field to search by (default: '_id')
139
- - `opts`: Lifecycle options
140
-
141
- **Returns:** Promise resolving to the document
27
+ export interface ProjectResource extends MongoResource<ProjectRecord> {}
142
28
 
143
- **Example:**
144
- ```typescript
145
- // Get by MongoDB ObjectId (converted from string)
146
- const user = await userResource.get('507f1f77bcf86cd799439011')
147
-
148
- // Get by custom field
149
- const userByEmail = await userResource.get('user@example.com', 'email')
150
- ```
151
-
152
- #### `load(id: string, field?: string, opts?: LifecycleOptions): Promise<T | null>`
153
-
154
- Loads a document by ID or specified field. Returns `null` if not found.
155
-
156
- **Example:**
157
- ```typescript
158
- const user = await userResource.load('507f1f77bcf86cd799439011')
159
- if (user) {
160
- console.log('User found:', user.name)
161
- } else {
162
- console.log('User not found')
29
+ export const makeProjectResource: ResourceMaker<ProjectRecord, ProjectResource> = (dbAlias, serviceAlias) => {
30
+ const resource = makeMongoResource<ProjectRecord>(
31
+ RES_PROJECT, dbAlias, serviceAlias, makeProjectResource
32
+ )
33
+ resource.schema = ProjectSchema
34
+ resource.index('entity', { entityId: 1 })
35
+ resource.index('alias', { alias: 1 })
36
+ return resource
163
37
  }
164
38
  ```
165
39
 
166
- #### `create(record: Partial<T>, opts?: LifecycleOptions): Promise<T>`
167
-
168
- Creates a new document. Automatically applies schema defaults and generates ObjectId.
169
-
170
- **Parameters:**
171
- - `record`: Document data (id will be generated)
172
- - `opts`: Lifecycle options
173
-
174
- **Returns:** Promise resolving to the created document
175
-
176
- **Throws:** `RecordExists` if record already has an ID
177
-
178
- **Example:**
179
- ```typescript
180
- const newUser = await userResource.create({
181
- name: 'John Doe',
182
- email: 'john@example.com'
183
- })
184
-
185
- console.log('Created user with ID:', newUser.id)
186
- ```
187
-
188
- #### `save(record: Partial<T>, opts?: Getter): Promise<T>`
189
-
190
- Saves a document (creates if new, updates if exists).
191
-
192
- **Example:**
193
- ```typescript
194
- // Create new user (no id)
195
- const user1 = await userResource.save({
196
- name: 'Jane Doe',
197
- email: 'jane@example.com'
198
- })
199
-
200
- // Update existing user (with id)
201
- const user2 = await userResource.save({
202
- id: user1.id,
203
- name: 'Jane Smith'
204
- })
205
- ```
206
-
207
- #### `update(record: Partial<T>, opts?: Getter): Promise<T>`
208
-
209
- Updates an existing document. Throws `UnknownRecordError` if not found.
210
-
211
- **Example:**
212
- ```typescript
213
- const updatedUser = await userResource.update({
214
- id: '507f1f77bcf86cd799439011',
215
- name: 'Updated Name'
216
- })
217
- ```
218
-
219
- #### `delete(id: string | T, opts?: Getter): Promise<T | null>`
220
-
221
- Deletes a document by ID or document object.
222
-
223
- **Parameters:**
224
- - `id`: Document ID or document object
225
- - `opts`: Additional options or field name
226
-
227
- **Returns:** Promise resolving to deleted document or null
228
-
229
- **Example:**
230
- ```typescript
231
- // Delete by ID
232
- const deleted = await userResource.delete('507f1f77bcf86cd799439011')
233
-
234
- // Delete by document
235
- const deleted2 = await userResource.delete(userObject)
236
-
237
- // Delete by custom field
238
- const deleted3 = await userResource.delete('user@example.com', 'email')
239
- ```
240
-
241
- #### `pick(id: string | T, opts?: Getter): Promise<T>`
242
-
243
- Deletes and returns a document. Throws `UnknownRecordError` if not found.
244
-
245
- **Example:**
246
- ```typescript
247
- try {
248
- const removedUser = await userResource.pick('507f1f77bcf86cd799439011')
249
- console.log('Removed user:', removedUser.name)
250
- } catch (error) {
251
- console.error('User not found for removal')
252
- }
253
- ```
254
-
255
- #### `list(criteria?: ListOptions | ListCriteria, opts?: ListOptions): Promise<ListResult<T>>`
256
-
257
- Lists documents with optional filtering, pagination, and sorting.
258
-
259
- **Parameters:**
260
- - `criteria`: MongoDB query criteria or list options
261
- - `opts`: Additional list options
262
-
263
- **Returns:** Promise resolving to paginated results
264
-
265
- **Example:**
266
- ```typescript
267
- // List all users with pagination
268
- const result = await userResource.list({
269
- pager: { page: 0, size: 10 }
270
- })
271
-
272
- // List with MongoDB query
273
- const activeUsers = await userResource.list({
274
- criteria: { status: 'active' },
275
- pager: { page: 0, size: 20, sort: [['createdAt', false]] }
276
- })
277
-
278
- console.log(`Found ${result.pager?.total} users`)
279
- result.items.forEach(user => console.log(user.name))
280
- ```
281
-
282
- ### Database Access Methods
283
-
284
- #### `db(): Promise<Db>`
285
-
286
- Gets the MongoDB database instance.
287
-
288
- **Example:**
289
- ```typescript
290
- const db = await userResource.db()
291
- const stats = await db.stats()
292
- console.log('Database stats:', stats)
293
- ```
294
-
295
- #### `client(): Promise<MongoClient>`
296
-
297
- Gets the MongoDB client instance.
298
-
299
- **Example:**
300
- ```typescript
301
- const client = await userResource.client()
302
- const admin = client.db().admin()
303
- const serverStatus = await admin.serverStatus()
304
- ```
305
-
306
- ### Index Management
307
-
308
- #### `index<Type>(name: string, index: IndexSpecification, options?: CreateIndexesOptions): Type`
309
-
310
- Adds an index definition to the resource. Indexes are created during resource initialization.
311
-
312
- **Parameters:**
313
- - `name`: Index name
314
- - `index`: MongoDB index specification
315
- - `options`: Index creation options
316
-
317
- **Returns:** Resource instance for method chaining
318
-
319
- **Example:**
320
- ```typescript
321
- // Single field index
322
- userResource.index('email-unique', { email: 1 }, { unique: true })
323
-
324
- // Compound index
325
- userResource.index('name-email', { name: 1, email: 1 })
326
-
327
- // Text index
328
- userResource.index('text-search', { name: 'text', email: 'text' })
329
-
330
- // TTL index
331
- userResource.index('expire-sessions', { createdAt: 1 }, { expireAfterSeconds: 3600 })
332
- ```
333
-
334
- ### Schema and Defaults
335
-
336
- #### `getDefaults(): Partial<T>`
40
+ Register in context:
337
41
 
338
- Gets default values from the AJV schema.
339
-
340
- **Example:**
341
42
  ```typescript
342
- // Assuming schema has default values
343
- const defaults = userResource.getDefaults()
344
- console.log('Default values:', defaults)
345
-
346
- // Defaults are automatically applied during create()
43
+ context.registerResource(makeProjectResource())
347
44
  ```
348
45
 
349
- ### Locking Methods
350
-
351
- #### `lock(record: Partial<T>, fields?: string[]): Promise<T>`
352
-
353
- Locks specified fields of a document for concurrent access control.
354
-
355
- **Parameters:**
356
- - `record`: Document to lock
357
- - `fields`: Fields to lock (default: secure fields from schema)
358
-
359
- **Returns:** Promise resolving to locked document
360
-
361
- #### `unlock(record: Partial<T>, fields?: string[]): Promise<T>`
362
-
363
- Unlocks specified fields of a document.
46
+ Use in a handler:
364
47
 
365
- **Parameters:**
366
- - `record`: Document to unlock
367
- - `fields`: Fields to unlock (default: secure fields from schema)
368
-
369
- **Returns:** Promise resolving to unlocked document
370
-
371
- **Example:**
372
48
  ```typescript
373
- // Lock user for critical update
374
- const lockedUser = await userResource.lock({ id: userId }, ['balance'])
375
-
376
- try {
377
- // Perform critical operations
378
- await updateUserBalance(lockedUser)
379
- } finally {
380
- // Always unlock
381
- await userResource.unlock({ id: userId }, ['balance'])
382
- }
49
+ const projects = context.resource<ProjectResource>(RES_PROJECT)
50
+ const record = await projects.create({ entityId, alias, title })
51
+ const list = await projects.list({ criteria: { entityId } })
383
52
  ```
384
53
 
385
- ### Helper Functions
54
+ ## API
386
55
 
387
- #### `getSchemaSecureFeilds(schema: AnySchema): string[]`
56
+ ### `makeMongoResource<R, T>(alias, dbAlias?, serviceAlias?, maker?): T`
388
57
 
389
- Extracts fields marked as secure in the AJV schema.
58
+ Creates a MongoDB resource. `dbAlias` defaults to `DEFAULT_DB_ALIAS` (`'mongo'`).
390
59
 
391
- **Parameters:**
392
- - `schema`: AJV schema object
60
+ ### `MongoResource<T>`
393
61
 
394
- **Returns:** Array of secure field names
62
+ Extends `Resource<T>` with:
63
+ - `collection: Collection` — MongoDB collection
64
+ - `db(): Promise<Db>` — get the MongoDB database
65
+ - `index(name, spec, options?): this` — define a collection index
66
+ - `lock(record, fields?)` / `unlock(record, fields?)` — encrypt/decrypt secure fields
67
+ - `getDefaults(): Partial<T>` — default values derived from schema
395
68
 
396
- **Example:**
397
- ```typescript
398
- import { getSchemaSecureFeilds } from '@owlmeans/mongo-resource'
399
-
400
- const schema = {
401
- type: 'object',
402
- properties: {
403
- password: { type: 'string', secure: true },
404
- balance: { type: 'number', secure: true },
405
- name: { type: 'string' }
406
- }
407
- }
69
+ ### `Resource<T>` methods (all implemented)
408
70
 
409
- const secureFields = getSchemaSecureFeilds(schema)
410
- // ['password', 'balance']
411
- ```
71
+ `get`, `load`, `create`, `update`, `save`, `delete`, `pick`, `list`
412
72
 
413
73
  ### Constants
414
74
 
415
- ```typescript
416
- const DEFAULT_DB_ALIAS = 'mongo' // Default database alias
417
- const DEFAULT_PAGE_SIZE = 10 // Default pagination size
418
- ```
419
-
420
- ## Usage Examples
421
-
422
- ### Basic Resource Setup
423
-
424
- ```typescript
425
- import { makeMongoResource } from '@owlmeans/mongo-resource'
426
- import { makeServerContext } from '@owlmeans/server-context'
427
-
428
- // Define document interface
429
- interface Product extends ResourceRecord {
430
- id?: string
431
- name: string
432
- price: number
433
- category: string
434
- inStock: boolean
435
- createdAt?: Date
436
- }
437
-
438
- // Create context with MongoDB configuration
439
- const context = makeServerContext({
440
- service: 'product-service',
441
- dbs: [{
442
- service: 'mongo',
443
- alias: 'primary',
444
- host: 'localhost',
445
- port: 27017,
446
- schema: 'products_db'
447
- }]
448
- })
449
-
450
- // Create and configure resource
451
- const productResource = makeMongoResource<Product>('products', 'primary')
452
-
453
- // Add indexes
454
- productResource
455
- .index('category-price', { category: 1, price: -1 })
456
- .index('name-text', { name: 'text' })
457
- .index('inStock', { inStock: 1 })
458
-
459
- // Register resource with context
460
- context.registerResource(productResource)
461
-
462
- // Initialize context
463
- await context.configure().init()
464
- ```
465
-
466
- ### CRUD Operations
467
-
468
- ```typescript
469
- // Create product
470
- const newProduct = await productResource.create({
471
- name: 'Gaming Laptop',
472
- price: 1299.99,
473
- category: 'electronics',
474
- inStock: true
475
- })
476
-
477
- // Get product
478
- const product = await productResource.get(newProduct.id!)
479
-
480
- // Update product
481
- const updatedProduct = await productResource.update({
482
- id: product.id,
483
- price: 1199.99
484
- })
485
-
486
- // List products with filtering
487
- const electronicsResult = await productResource.list({
488
- criteria: { category: 'electronics', inStock: true },
489
- pager: {
490
- page: 0,
491
- size: 20,
492
- sort: [['price', true]] // Sort by price descending
493
- }
494
- })
495
-
496
- // Delete product
497
- await productResource.delete(product.id!)
498
- ```
499
-
500
- ### Schema Validation Integration
501
-
502
- ```typescript
503
- import Ajv, { JSONSchemaType } from 'ajv'
504
-
505
- // Define schema with validation and defaults
506
- const productSchema: JSONSchemaType<Product> = {
507
- type: 'object',
508
- properties: {
509
- id: { type: 'string', nullable: true },
510
- name: { type: 'string', minLength: 1, maxLength: 100 },
511
- price: { type: 'number', minimum: 0 },
512
- category: { type: 'string', enum: ['electronics', 'clothing', 'books'] },
513
- inStock: { type: 'boolean', default: true },
514
- createdAt: { type: 'string', format: 'date-time', nullable: true }
515
- },
516
- required: ['name', 'price', 'category'],
517
- additionalProperties: false
518
- }
519
-
520
- // Create resource with schema
521
- const productResource = makeMongoResource<Product>('products')
522
- productResource.schema = productSchema
523
-
524
- // Now all operations will validate against schema
525
- try {
526
- await productResource.create({
527
- name: '', // Will fail validation (minLength: 1)
528
- price: -10, // Will fail validation (minimum: 0)
529
- category: 'invalid' // Will fail validation (not in enum)
530
- })
531
- } catch (error) {
532
- console.error('Validation failed:', error)
533
- }
534
- ```
535
-
536
- ### Advanced Querying
537
-
538
- ```typescript
539
- // Complex MongoDB queries
540
- const advancedResults = await productResource.list({
541
- criteria: {
542
- $and: [
543
- { price: { $gte: 100, $lte: 1000 } },
544
- { category: { $in: ['electronics', 'books'] } },
545
- { inStock: true }
546
- ]
547
- },
548
- pager: {
549
- page: 0,
550
- size: 15,
551
- sort: [['createdAt', false], ['price', true]]
552
- }
553
- })
554
-
555
- // Text search (requires text index)
556
- const searchResults = await productResource.list({
557
- criteria: { $text: { $search: 'gaming laptop' } }
558
- })
559
-
560
- // Aggregation via direct database access
561
- const db = await productResource.db()
562
- const categoryStats = await db.collection('products').aggregate([
563
- { $group: { _id: '$category', count: { $sum: 1 }, avgPrice: { $avg: '$price' } } }
564
- ]).toArray()
565
- ```
566
-
567
- ### Index Management
568
-
569
- ```typescript
570
- // Add various types of indexes
571
- productResource
572
- // Unique index
573
- .index('sku-unique', { sku: 1 }, { unique: true })
574
-
575
- // Compound index for common queries
576
- .index('category-instock-price', {
577
- category: 1,
578
- inStock: 1,
579
- price: -1
580
- })
581
-
582
- // Text search index
583
- .index('search', {
584
- name: 'text',
585
- description: 'text'
586
- }, {
587
- weights: { name: 10, description: 5 }
588
- })
589
-
590
- // TTL index for temporary records
591
- .index('temp-expire', {
592
- createdAt: 1
593
- }, {
594
- expireAfterSeconds: 86400 // 24 hours
595
- })
596
-
597
- // Partial index
598
- .index('active-products', {
599
- category: 1,
600
- price: 1
601
- }, {
602
- partialFilterExpression: { inStock: true }
603
- })
604
-
605
- // Indexes are automatically created during resource initialization
606
- ```
607
-
608
- ### Document Locking
609
-
610
- ```typescript
611
- // Schema with secure fields
612
- const userSchema = {
613
- type: 'object',
614
- properties: {
615
- id: { type: 'string', nullable: true },
616
- username: { type: 'string' },
617
- balance: { type: 'number', secure: true }, // Secure field
618
- password: { type: 'string', secure: true } // Secure field
619
- },
620
- required: ['username']
621
- }
622
-
623
- const userResource = makeMongoResource<User>('users')
624
- userResource.schema = userSchema
625
-
626
- // Lock user for balance update
627
- const userId = 'user123'
628
- const lockedUser = await userResource.lock({ id: userId })
629
-
630
- try {
631
- // Perform balance update
632
- const user = await userResource.get(userId)
633
- await userResource.update({
634
- id: userId,
635
- balance: user.balance + 100
636
- })
637
- } finally {
638
- // Always unlock
639
- await userResource.unlock({ id: userId })
640
- }
641
- ```
642
-
643
- ### Multiple Database Support
644
-
645
- ```typescript
646
- // Configure multiple MongoDB databases
647
- const context = makeServerContext({
648
- dbs: [
649
- {
650
- service: 'mongo',
651
- alias: 'primary',
652
- host: 'primary-mongo.example.com',
653
- schema: 'main_db'
654
- },
655
- {
656
- service: 'mongo',
657
- alias: 'analytics',
658
- host: 'analytics-mongo.example.com',
659
- schema: 'analytics_db'
660
- }
661
- ]
662
- })
663
-
664
- // Create resources for different databases
665
- const userResource = makeMongoResource<User>('users', 'primary')
666
- const analyticsResource = makeMongoResource<AnalyticsRecord>('events', 'analytics')
667
-
668
- // Both resources work independently with their respective databases
669
- ```
670
-
671
- ### Error Handling
672
-
673
- ```typescript
674
- import {
675
- UnknownRecordError,
676
- RecordExists,
677
- RecordUpdateFailed,
678
- MisshapedRecord
679
- } from '@owlmeans/resource'
680
-
681
- try {
682
- // Various operations that can fail
683
- const user = await userResource.get('nonexistent-id')
684
- } catch (error) {
685
- if (error instanceof UnknownRecordError) {
686
- console.error('User not found:', error.id)
687
- }
688
- }
689
-
690
- try {
691
- await userResource.create({ id: 'existing-id', name: 'Test' })
692
- } catch (error) {
693
- if (error instanceof RecordExists) {
694
- console.error('User already exists')
695
- }
696
- }
697
- ```
698
-
699
- ## Advanced Features
700
-
701
- ### Custom Collection Names
702
-
703
- ```typescript
704
- // Override collection name
705
- const customResource = makeMongoResource<User>('users')
706
- customResource.name = 'custom_users_collection'
707
- ```
708
-
709
- ### Direct MongoDB Operations
710
-
711
- ```typescript
712
- // Access MongoDB collection directly for advanced operations
713
- const collection = userResource.collection
714
-
715
- // Use MongoDB-specific features
716
- const bulkOps = collection.initializeUnorderedBulkOp()
717
- bulkOps.insert({ name: 'User 1' })
718
- bulkOps.insert({ name: 'User 2' })
719
- await bulkOps.execute()
720
-
721
- // Aggregation pipelines
722
- const pipeline = [
723
- { $match: { active: true } },
724
- { $group: { _id: '$department', count: { $sum: 1 } } }
725
- ]
726
- const results = await collection.aggregate(pipeline).toArray()
727
- ```
728
-
729
- ### Schema Defaults Integration
730
-
731
- ```typescript
732
- const schemaWithDefaults = {
733
- type: 'object',
734
- properties: {
735
- status: { type: 'string', default: 'active' },
736
- createdAt: { type: 'string', format: 'date-time', default: new Date().toISOString() },
737
- settings: {
738
- type: 'object',
739
- default: { theme: 'light', notifications: true }
740
- }
741
- }
742
- }
743
-
744
- const resource = makeMongoResource<MyRecord>('records')
745
- resource.schema = schemaWithDefaults
746
-
747
- // Defaults are automatically applied during creation
748
- const record = await resource.create({ name: 'Test' })
749
- // record.status === 'active'
750
- // record.settings === { theme: 'light', notifications: true }
751
- ```
752
-
753
- ## Performance Considerations
754
-
755
- - **Indexing**: Create appropriate indexes for your query patterns
756
- - **Pagination**: Use pagination for large result sets
757
- - **Connection Pooling**: MongoDB driver handles connection pooling automatically
758
- - **Schema Validation**: Validation happens before database operations
759
- - **Document Size**: MongoDB has a 16MB document size limit
760
- - **Query Optimization**: Use MongoDB explain() to optimize queries
761
-
762
- ## Best Practices
763
-
764
- 1. **Index Strategy**: Create indexes that match your query patterns
765
- 2. **Schema Design**: Design schemas that reflect your document structure
766
- 3. **Error Handling**: Handle MongoDB-specific errors appropriately
767
- 4. **Resource Cleanup**: Properly close MongoDB connections
768
- 5. **Security**: Use secure fields for sensitive data that requires locking
769
- 6. **Validation**: Always use schema validation for data integrity
770
- 7. **Pagination**: Implement pagination for list operations
771
-
772
- ## Integration with OwlMeans Ecosystem
773
-
774
- ### Server Context Integration
775
-
776
- ```typescript
777
- import { makeServerContext } from '@owlmeans/server-context'
778
-
779
- // MongoDB resources integrate seamlessly with server context
780
- const context = makeServerContext(config)
781
- context.registerResource(mongoResource)
782
- ```
783
-
784
- ### Service Integration
785
-
786
- ```typescript
787
- import { createDbService } from '@owlmeans/resource'
788
-
789
- // MongoDB service provides database access
790
- const mongoService = createDbService('mongo', mongoConfig)
791
- context.registerService(mongoService)
792
- ```
793
-
794
- ### Authentication Integration
795
-
796
- ```typescript
797
- // MongoDB resources work with authentication scopes
798
- const authenticatedResource = makeMongoResource<ProtectedRecord>('protected')
799
- // Access control is handled at the application level
800
- ```
75
+ - `DEFAULT_DB_ALIAS` — `'mongo'`
76
+ - `DEFAULT_PAGE_SIZE` `10`
801
77
 
802
78
  ## Related Packages
803
79
 
804
- - [`@owlmeans/resource`](../resource) - Core resource system
805
- - [`@owlmeans/server-context`](../server-context) - Server context management
806
- - [`@owlmeans/redis-resource`](../redis-resource) - Redis resource implementation
807
- - [`@owlmeans/static-resource`](../static-resource) - Static file resources
80
+ - [`@owlmeans/resource`](../resource) `Resource<T>`, `ResourceRecord`, `ResourceMaker` base
81
+ - [`@owlmeans/mongo`](../mongo) MongoDB connection service required by this package
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@owlmeans/mongo-resource",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsc -b",
8
8
  "dev": "sleep 198 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
9
- "watch": "tsc -b -w --preserveWatchOutput --pretty"
9
+ "watch": "tsc -b -w --preserveWatchOutput --pretty",
10
+ "test": "bun test ./tests"
10
11
  },
11
12
  "main": "build/index.js",
12
13
  "module": "build/index.js",
@@ -25,15 +26,19 @@
25
26
  "mongodb": "*"
26
27
  },
27
28
  "dependencies": {
28
- "@owlmeans/context": "^0.1.2",
29
- "@owlmeans/resource": "^0.1.2",
30
- "@owlmeans/server-context": "^0.1.2"
29
+ "@owlmeans/context": "^0.1.4",
30
+ "@owlmeans/resource": "^0.1.4",
31
+ "@owlmeans/server-context": "^0.1.4"
31
32
  },
32
33
  "devDependencies": {
34
+ "@owlmeans/dep-config": "workspace:*",
35
+ "@owlmeans/test-integration": "^0.1.4",
36
+ "@types/bun": "^1.3.0",
33
37
  "@types/node": "^24.10.1",
38
+ "mongodb": "^6.9.0",
34
39
  "nodemon": "^3.1.11",
35
40
  "npm-check": "^6.0.1",
36
- "typescript": "^5.8.3"
41
+ "typescript": "^6.0.2"
37
42
  },
38
43
  "publishConfig": {
39
44
  "access": "public"
@@ -0,0 +1,42 @@
1
+ import { mongoGate, randomNamespace, registerCleanup } from '@owlmeans/test-integration'
2
+ import type { IntegrationGate, MongoEnv } from '@owlmeans/test-integration'
3
+
4
+ export interface MongoTestEnv {
5
+ gate: IntegrationGate<MongoEnv>
6
+ dbName: string
7
+ }
8
+
9
+ let cached: MongoTestEnv | null = null
10
+
11
+ /**
12
+ * Per-package test env for mongo-resource. Reads `mongoGate()` once;
13
+ * if the gate is open, derives a randomly-namespaced database name and
14
+ * registers a cleanup to drop that database after the suite. If the
15
+ * gate is closed, returns the same shape so specs can `gate.skip`-self
16
+ * without branching on truthiness.
17
+ *
18
+ * NOTE: this pilot intentionally does NOT instantiate the real
19
+ * @owlmeans/mongo service yet — that wiring belongs to the full
20
+ * integration test pass and depends on the consuming app's config
21
+ * shape. The pilot proves the gate + namespacing + cleanup pattern.
22
+ */
23
+ export const getTestEnv = (): MongoTestEnv => {
24
+ if (cached != null) return cached
25
+ const gate = mongoGate()
26
+ const prefix = process.env.MONGO_TEST_DB_PREFIX ?? 'omt'
27
+ const dbName = randomNamespace(prefix)
28
+ cached = { gate, dbName }
29
+ if (!gate.skip) {
30
+ registerCleanup(async () => {
31
+ const { MongoClient } = await import('mongodb')
32
+ const client = new MongoClient(gate.env.MONGO_URL as string)
33
+ try {
34
+ await client.connect()
35
+ await client.db(dbName).dropDatabase()
36
+ } finally {
37
+ await client.close()
38
+ }
39
+ })
40
+ }
41
+ return cached
42
+ }
@@ -0,0 +1,31 @@
1
+ import { afterAll, describe, expect, test } from 'bun:test'
2
+ import { runCleanups } from '@owlmeans/test-integration'
3
+ import { getTestEnv } from './context.js'
4
+
5
+ const env = getTestEnv()
6
+ const it = env.gate.skip ? test.skip : test
7
+
8
+ afterAll(async () => {
9
+ await runCleanups()
10
+ })
11
+
12
+ describe('@owlmeans/mongo-resource — connection round-trip', () => {
13
+ if (env.gate.skip) {
14
+ test.skip(env.gate.reason ?? 'mongo gate closed', () => {})
15
+ return
16
+ }
17
+
18
+ it('inserts and reads a document via the namespaced test database', async () => {
19
+ const { MongoClient } = await import('mongodb')
20
+ const client = new MongoClient(env.gate.env.MONGO_URL as string)
21
+ await client.connect()
22
+ try {
23
+ const col = client.db(env.dbName).collection<{ id: string, value: number }>('pilot')
24
+ await col.insertOne({ id: 'a', value: 42 })
25
+ const doc = await col.findOne({ id: 'a' })
26
+ expect(doc?.value).toBe(42)
27
+ } finally {
28
+ await client.close()
29
+ }
30
+ })
31
+ })
package/tsconfig.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "extends": [
3
- "../tsconfig.default.json",
3
+ "@owlmeans/dep-config/tsconfig.base.json",
4
+ "@owlmeans/dep-config/tsconfig.node.json"
4
5
  ],
5
6
  "compilerOptions": {
6
- "rootDir": "./src/", /* Specify the root folder within your source files. */
7
- "outDir": "./build/", /* Specify an output folder for all emitted files. */
7
+ "rootDir": "./src/",
8
+ "outDir": "./build/"
8
9
  },
9
10
  "exclude": [
10
11
  "./dist/**/*",
11
12
  "./build/**/*",
13
+ "./tests/**/*",
12
14
  "./*.ts"
13
15
  ]
14
- }
16
+ }