@owlmeans/mongo-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 +807 -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/helper.d.ts +3 -0
- package/build/helper.d.ts.map +1 -0
- package/build/helper.js +3 -0
- package/build/helper.js.map +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/resource.d.ts +4 -0
- package/build/resource.d.ts.map +1 -0
- package/build/resource.js +268 -0
- package/build/resource.js.map +1 -0
- package/build/types.d.ts +20 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/build/utils/index.d.ts +5 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +5 -0
- package/build/utils/index.js.map +1 -0
- package/build/utils/indexes.d.ts +5 -0
- package/build/utils/indexes.d.ts.map +1 -0
- package/build/utils/indexes.js +34 -0
- package/build/utils/indexes.js.map +1 -0
- package/build/utils/life-cycle.d.ts +7 -0
- package/build/utils/life-cycle.d.ts.map +1 -0
- package/build/utils/life-cycle.js +48 -0
- package/build/utils/life-cycle.js.map +1 -0
- package/build/utils/name.d.ts +4 -0
- package/build/utils/name.d.ts.map +1 -0
- package/build/utils/name.js +2 -0
- package/build/utils/name.js.map +1 -0
- package/build/utils/schema.d.ts +4 -0
- package/build/utils/schema.d.ts.map +1 -0
- package/build/utils/schema.js +63 -0
- package/build/utils/schema.js.map +1 -0
- package/package.json +41 -0
- package/src/consts.ts +4 -0
- package/src/helper.ts +5 -0
- package/src/index.ts +5 -0
- package/src/resource.ts +349 -0
- package/src/types.ts +17 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/indexes.ts +38 -0
- package/src/utils/life-cycle.ts +64 -0
- package/src/utils/name.ts +5 -0
- package/src/utils/schema.ts +82 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
# @owlmeans/mongo-resource
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
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.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```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
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Factory Functions
|
|
100
|
+
|
|
101
|
+
#### `makeMongoResource<R extends ResourceRecord, T extends MongoResource<R>>(alias: string, dbAlias?: string, serviceAlias?: string, makeCustomResource?: ResourceMaker<R, T>): T`
|
|
102
|
+
|
|
103
|
+
Creates a MongoDB resource instance with full CRUD capabilities.
|
|
104
|
+
|
|
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
|
+
```typescript
|
|
115
|
+
import { makeMongoResource } from '@owlmeans/mongo-resource'
|
|
116
|
+
|
|
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
|
|
142
|
+
|
|
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')
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
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>`
|
|
337
|
+
|
|
338
|
+
Gets default values from the AJV schema.
|
|
339
|
+
|
|
340
|
+
**Example:**
|
|
341
|
+
```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()
|
|
347
|
+
```
|
|
348
|
+
|
|
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.
|
|
364
|
+
|
|
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
|
+
```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
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Helper Functions
|
|
386
|
+
|
|
387
|
+
#### `getSchemaSecureFeilds(schema: AnySchema): string[]`
|
|
388
|
+
|
|
389
|
+
Extracts fields marked as secure in the AJV schema.
|
|
390
|
+
|
|
391
|
+
**Parameters:**
|
|
392
|
+
- `schema`: AJV schema object
|
|
393
|
+
|
|
394
|
+
**Returns:** Array of secure field names
|
|
395
|
+
|
|
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
|
+
}
|
|
408
|
+
|
|
409
|
+
const secureFields = getSchemaSecureFeilds(schema)
|
|
410
|
+
// ['password', 'balance']
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Constants
|
|
414
|
+
|
|
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
|
+
```
|
|
801
|
+
|
|
802
|
+
## Related Packages
|
|
803
|
+
|
|
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
|