@owlmeans/mongo 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 +451 -0
- package/build/consts.d.ts +3 -0
- package/build/consts.d.ts.map +1 -0
- package/build/consts.js +4 -0
- package/build/consts.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/build/service.d.ts +9 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +104 -0
- package/build/service.js.map +1 -0
- package/build/types.d.ts +4 -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/cluster.d.ts +4 -0
- package/build/utils/cluster.d.ts.map +1 -0
- package/build/utils/cluster.js +62 -0
- package/build/utils/cluster.js.map +1 -0
- package/build/utils/config.d.ts +5 -0
- package/build/utils/config.d.ts.map +1 -0
- package/build/utils/config.js +16 -0
- package/build/utils/config.js.map +1 -0
- package/build/utils/index.d.ts +3 -0
- package/build/utils/index.d.ts.map +1 -0
- package/build/utils/index.js +3 -0
- package/build/utils/index.js.map +1 -0
- package/package.json +40 -0
- package/src/consts.ts +6 -0
- package/src/index.ts +4 -0
- package/src/service.ts +149 -0
- package/src/types.ts +4 -0
- package/src/utils/cluster.ts +70 -0
- package/src/utils/config.ts +26 -0
- package/src/utils/index.ts +3 -0
- package/tsconfig.json +14 -0
- package/tsconfig.tsbuildinfo +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 OwlMeans Common — Fullstack typescript framework
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# @owlmeans/mongo
|
|
2
|
+
|
|
3
|
+
MongoDB service integration for OwlMeans Common server applications. This package provides a server-side MongoDB service implementation with clustering support, field-level encryption, and connection management designed for secure, multi-layer applications.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The `@owlmeans/mongo` package extends the OwlMeans resource system to provide MongoDB-specific functionality including:
|
|
8
|
+
|
|
9
|
+
- **MongoDB Service Integration**: Factory functions for creating MongoDB services with connection management
|
|
10
|
+
- **Field-Level Encryption**: Built-in encryption/decryption for sensitive database fields using OwlMeans cryptographic keys
|
|
11
|
+
- **Cluster Support**: Automatic cluster setup and replica set configuration
|
|
12
|
+
- **Multi-Layer Support**: Integration with OwlMeans context layer system for proper data isolation
|
|
13
|
+
- **Connection Pooling**: Efficient MongoDB connection management with proper cleanup
|
|
14
|
+
|
|
15
|
+
This package follows the OwlMeans "quadra" pattern as a server-side implementation complementing the basic `@owlmeans/mongo-resource` package.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @owlmeans/mongo
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Dependencies
|
|
24
|
+
|
|
25
|
+
This package requires MongoDB driver and integrates with:
|
|
26
|
+
- `@owlmeans/mongo-resource`: Base MongoDB resource definitions
|
|
27
|
+
- `@owlmeans/server-context`: Server context management
|
|
28
|
+
- `@owlmeans/basic-keys`: Cryptographic operations for field encryption
|
|
29
|
+
- `mongodb`: Official MongoDB Node.js driver
|
|
30
|
+
|
|
31
|
+
## Core Concepts
|
|
32
|
+
|
|
33
|
+
### MongoDB Service
|
|
34
|
+
|
|
35
|
+
The MongoDB service provides database connection management and extends the base database service with MongoDB-specific functionality like field encryption and cluster support.
|
|
36
|
+
|
|
37
|
+
### Field Encryption
|
|
38
|
+
|
|
39
|
+
Built-in support for encrypting/decrypting specific database fields using OwlMeans cryptographic keys, providing application-level encryption for sensitive data.
|
|
40
|
+
|
|
41
|
+
### Layer Integration
|
|
42
|
+
|
|
43
|
+
Supports multi-layer data isolation through the OwlMeans context layer system, allowing service-specific and entity-specific database configurations.
|
|
44
|
+
|
|
45
|
+
## API Reference
|
|
46
|
+
|
|
47
|
+
### Types
|
|
48
|
+
|
|
49
|
+
#### `MongoMeta`
|
|
50
|
+
Metadata interface for MongoDB-specific configuration.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
interface MongoMeta {
|
|
54
|
+
replicaSet?: string // Replica set name for clustering
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Factory Functions
|
|
59
|
+
|
|
60
|
+
#### `makeMongoDbService(alias?: string): MongoDbService`
|
|
61
|
+
|
|
62
|
+
Creates a MongoDB service instance with connection management and encryption capabilities.
|
|
63
|
+
|
|
64
|
+
**Parameters:**
|
|
65
|
+
- `alias` (optional): Service alias (default: `DEFAULT_ALIAS`)
|
|
66
|
+
|
|
67
|
+
**Returns:** `MongoDbService` instance
|
|
68
|
+
|
|
69
|
+
**Methods:**
|
|
70
|
+
- **`db(configAlias?: string): Promise<Db>`**: Gets MongoDB database instance for the configuration
|
|
71
|
+
- **`initialize(configAlias?: string): Promise<void>`**: Initializes MongoDB connection with cluster setup
|
|
72
|
+
- **`lock(alias: string, record: object, fields: string[]): Promise<object>`**: Encrypts specified fields in a record
|
|
73
|
+
- **`unlock(alias: string, record: object, fields: string[]): Promise<object>`**: Decrypts specified fields in a record
|
|
74
|
+
- **`reinitializeContext<T>(context: BasicContext<ServerConfig>): T`**: Reinitializes service with new context
|
|
75
|
+
|
|
76
|
+
**Example:**
|
|
77
|
+
```typescript
|
|
78
|
+
import { makeMongoDbService } from '@owlmeans/mongo'
|
|
79
|
+
|
|
80
|
+
const mongoService = makeMongoDbService('main-db')
|
|
81
|
+
|
|
82
|
+
// Initialize with configuration
|
|
83
|
+
await mongoService.initialize('prod-config')
|
|
84
|
+
|
|
85
|
+
// Get database instance
|
|
86
|
+
const db = await mongoService.db('prod-config')
|
|
87
|
+
|
|
88
|
+
// Use database
|
|
89
|
+
const collection = db.collection('users')
|
|
90
|
+
const users = await collection.find().toArray()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### `appendMongo<C, T>(context: T, alias?: string): T`
|
|
94
|
+
|
|
95
|
+
Convenience function to create and register a MongoDB service with a context.
|
|
96
|
+
|
|
97
|
+
**Parameters:**
|
|
98
|
+
- `context`: Server context instance
|
|
99
|
+
- `alias` (optional): Service alias (default: `DEFAULT_ALIAS`)
|
|
100
|
+
|
|
101
|
+
**Returns:** The context with MongoDB service registered
|
|
102
|
+
|
|
103
|
+
**Example:**
|
|
104
|
+
```typescript
|
|
105
|
+
import { appendMongo } from '@owlmeans/mongo'
|
|
106
|
+
import { makeServerContext } from '@owlmeans/server-context'
|
|
107
|
+
|
|
108
|
+
const context = makeServerContext(serverConfig)
|
|
109
|
+
const contextWithMongo = appendMongo(context, 'app-db')
|
|
110
|
+
|
|
111
|
+
await contextWithMongo.configure().init()
|
|
112
|
+
|
|
113
|
+
// Access MongoDB service
|
|
114
|
+
const mongoService = context.service('app-db')
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Field Encryption
|
|
118
|
+
|
|
119
|
+
The MongoDB service provides built-in field-level encryption for sensitive data:
|
|
120
|
+
|
|
121
|
+
#### `lock(alias: string, record: object, fields: string[]): Promise<object>`
|
|
122
|
+
|
|
123
|
+
Encrypts specified fields in a database record.
|
|
124
|
+
|
|
125
|
+
**Parameters:**
|
|
126
|
+
- `alias`: Configuration alias with encryption key
|
|
127
|
+
- `record`: Database record object
|
|
128
|
+
- `fields`: Array of field names to encrypt
|
|
129
|
+
|
|
130
|
+
**Returns:** Promise resolving to record with encrypted fields
|
|
131
|
+
|
|
132
|
+
**Throws:**
|
|
133
|
+
- `SyntaxError` if no encryption key configured
|
|
134
|
+
- `SyntaxError` if no fields specified
|
|
135
|
+
|
|
136
|
+
**Example:**
|
|
137
|
+
```typescript
|
|
138
|
+
const sensitiveUser = {
|
|
139
|
+
id: '123',
|
|
140
|
+
name: 'John Doe',
|
|
141
|
+
email: 'john@example.com',
|
|
142
|
+
ssn: '123-45-6789',
|
|
143
|
+
creditCard: '4111-1111-1111-1111'
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Encrypt sensitive fields before saving
|
|
147
|
+
const encryptedUser = await mongoService.lock('prod-config', sensitiveUser, ['ssn', 'creditCard'])
|
|
148
|
+
|
|
149
|
+
// Save to database with encrypted fields
|
|
150
|
+
await collection.insertOne(encryptedUser)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### `unlock(alias: string, record: object, fields: string[]): Promise<object>`
|
|
154
|
+
|
|
155
|
+
Decrypts specified fields in a database record.
|
|
156
|
+
|
|
157
|
+
**Parameters:**
|
|
158
|
+
- `alias`: Configuration alias with encryption key
|
|
159
|
+
- `record`: Database record object with encrypted fields
|
|
160
|
+
- `fields`: Array of field names to decrypt
|
|
161
|
+
|
|
162
|
+
**Returns:** Promise resolving to record with decrypted fields
|
|
163
|
+
|
|
164
|
+
**Example:**
|
|
165
|
+
```typescript
|
|
166
|
+
// Retrieve from database
|
|
167
|
+
const encryptedUser = await collection.findOne({ id: '123' })
|
|
168
|
+
|
|
169
|
+
// Decrypt sensitive fields after retrieval
|
|
170
|
+
const decryptedUser = await mongoService.unlock('prod-config', encryptedUser, ['ssn', 'creditCard'])
|
|
171
|
+
|
|
172
|
+
console.log(decryptedUser.ssn) // '123-45-6789' (decrypted)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Cluster Support
|
|
176
|
+
|
|
177
|
+
The service automatically handles MongoDB cluster setup and replica set configuration:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// Configuration with cluster hosts
|
|
181
|
+
const config = {
|
|
182
|
+
alias: 'cluster-config',
|
|
183
|
+
host: ['mongo1.example.com', 'mongo2.example.com', 'mongo3.example.com'],
|
|
184
|
+
port: 27017,
|
|
185
|
+
database: 'app-db',
|
|
186
|
+
replicaSet: 'rs-main'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Service automatically detects cluster and sets up connections
|
|
190
|
+
await mongoService.initialize('cluster-config')
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Constants
|
|
194
|
+
|
|
195
|
+
#### `DEFAULT_ALIAS`
|
|
196
|
+
Default service alias for MongoDB services.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
const DEFAULT_ALIAS = DEFAULT_DB_ALIAS // From @owlmeans/mongo-resource
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
#### `DEF_REPLSET`
|
|
203
|
+
Default replica set name for clustering.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const DEF_REPLSET = 'rs-main'
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Usage Examples
|
|
210
|
+
|
|
211
|
+
### Basic MongoDB Service Setup
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { makeMongoDbService } from '@owlmeans/mongo'
|
|
215
|
+
import { makeServerContext } from '@owlmeans/server-context'
|
|
216
|
+
|
|
217
|
+
// Create server context with MongoDB configuration
|
|
218
|
+
const context = makeServerContext({
|
|
219
|
+
service: 'my-app',
|
|
220
|
+
type: AppType.Backend,
|
|
221
|
+
layer: Layer.Service,
|
|
222
|
+
dbs: [{
|
|
223
|
+
alias: 'main-db',
|
|
224
|
+
service: 'mongo',
|
|
225
|
+
host: 'localhost',
|
|
226
|
+
port: 27017,
|
|
227
|
+
database: 'myapp'
|
|
228
|
+
}]
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
// Create and register MongoDB service
|
|
232
|
+
const mongoService = makeMongoDbService('mongo')
|
|
233
|
+
context.registerService(mongoService)
|
|
234
|
+
|
|
235
|
+
// Initialize context
|
|
236
|
+
await context.configure().init()
|
|
237
|
+
|
|
238
|
+
// Use MongoDB
|
|
239
|
+
const db = await mongoService.db('main-db')
|
|
240
|
+
const users = db.collection('users')
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Using appendMongo Helper
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { appendMongo } from '@owlmeans/mongo'
|
|
247
|
+
|
|
248
|
+
const context = makeServerContext(config)
|
|
249
|
+
const contextWithMongo = appendMongo(context)
|
|
250
|
+
|
|
251
|
+
await contextWithMongo.configure().init()
|
|
252
|
+
|
|
253
|
+
const mongoService = context.service('mongo')
|
|
254
|
+
const db = await mongoService.db()
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Field Encryption in Practice
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// Configure encryption key in database config
|
|
261
|
+
const config = {
|
|
262
|
+
alias: 'secure-db',
|
|
263
|
+
service: 'mongo',
|
|
264
|
+
host: 'localhost',
|
|
265
|
+
database: 'secure-app',
|
|
266
|
+
encryptionKey: 'xchacha:base64encryptionkey...'
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const mongoService = makeMongoDbService()
|
|
270
|
+
await mongoService.initialize('secure-db')
|
|
271
|
+
|
|
272
|
+
// Encrypt before saving
|
|
273
|
+
const user = { name: 'Alice', ssn: '123-45-6789', balance: 1000 }
|
|
274
|
+
const encrypted = await mongoService.lock('secure-db', user, ['ssn', 'balance'])
|
|
275
|
+
|
|
276
|
+
const db = await mongoService.db('secure-db')
|
|
277
|
+
await db.collection('users').insertOne(encrypted)
|
|
278
|
+
|
|
279
|
+
// Decrypt after retrieval
|
|
280
|
+
const retrieved = await db.collection('users').findOne({ name: 'Alice' })
|
|
281
|
+
const decrypted = await mongoService.unlock('secure-db', retrieved, ['ssn', 'balance'])
|
|
282
|
+
|
|
283
|
+
console.log(decrypted.ssn) // '123-45-6789'
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Multi-Database Configuration
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
const context = makeServerContext({
|
|
290
|
+
service: 'multi-db-app',
|
|
291
|
+
type: AppType.Backend,
|
|
292
|
+
layer: Layer.Service,
|
|
293
|
+
dbs: [
|
|
294
|
+
{
|
|
295
|
+
alias: 'user-db',
|
|
296
|
+
service: 'mongo',
|
|
297
|
+
host: 'users.db.example.com',
|
|
298
|
+
database: 'users',
|
|
299
|
+
encryptionKey: 'xchacha:userkey...'
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
alias: 'analytics-db',
|
|
303
|
+
service: 'mongo',
|
|
304
|
+
host: 'analytics.db.example.com',
|
|
305
|
+
database: 'analytics'
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
const mongoService = makeMongoDbService('mongo')
|
|
311
|
+
context.registerService(mongoService)
|
|
312
|
+
|
|
313
|
+
await context.configure().init()
|
|
314
|
+
|
|
315
|
+
// Use different databases
|
|
316
|
+
const userDb = await mongoService.db('user-db')
|
|
317
|
+
const analyticsDb = await mongoService.db('analytics-db')
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Cluster Configuration
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
const clusterConfig = {
|
|
324
|
+
service: 'cluster-app',
|
|
325
|
+
type: AppType.Backend,
|
|
326
|
+
layer: Layer.Service,
|
|
327
|
+
dbs: [{
|
|
328
|
+
alias: 'cluster-db',
|
|
329
|
+
service: 'mongo',
|
|
330
|
+
host: [
|
|
331
|
+
'mongo1.cluster.example.com',
|
|
332
|
+
'mongo2.cluster.example.com',
|
|
333
|
+
'mongo3.cluster.example.com'
|
|
334
|
+
],
|
|
335
|
+
port: 27017,
|
|
336
|
+
database: 'clustered-app',
|
|
337
|
+
replicaSet: 'production-rs'
|
|
338
|
+
}]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const context = makeServerContext(clusterConfig)
|
|
342
|
+
const mongoService = makeMongoDbService('mongo')
|
|
343
|
+
context.registerService(mongoService)
|
|
344
|
+
|
|
345
|
+
// Service automatically handles cluster setup
|
|
346
|
+
await context.configure().init()
|
|
347
|
+
|
|
348
|
+
const db = await mongoService.db('cluster-db')
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Service Reinitialization
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// Original context
|
|
355
|
+
const originalContext = makeServerContext(config)
|
|
356
|
+
const mongoService = makeMongoDbService()
|
|
357
|
+
originalContext.registerService(mongoService)
|
|
358
|
+
|
|
359
|
+
// Later, reinitialize with new context
|
|
360
|
+
const newContext = makeServerContext(newConfig)
|
|
361
|
+
const reinitializedService = mongoService.reinitializeContext(newContext)
|
|
362
|
+
|
|
363
|
+
// Service now uses new context configuration
|
|
364
|
+
await reinitializedService.initialize()
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Configuration
|
|
368
|
+
|
|
369
|
+
MongoDB service configuration is handled through the server context's database configuration:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
interface DatabaseConfig {
|
|
373
|
+
alias: string // Configuration alias
|
|
374
|
+
service: string // Service name ('mongo')
|
|
375
|
+
host: string | string[] // Database host(s)
|
|
376
|
+
port?: number // Database port
|
|
377
|
+
database: string // Database name
|
|
378
|
+
username?: string // Authentication username
|
|
379
|
+
password?: string // Authentication password
|
|
380
|
+
encryptionKey?: string // Field encryption key
|
|
381
|
+
replicaSet?: string // Replica set name
|
|
382
|
+
serviceSensitive?: boolean // Enable service-layer isolation
|
|
383
|
+
entitySensitive?: boolean // Enable entity-layer isolation
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Error Handling
|
|
388
|
+
|
|
389
|
+
The package provides descriptive error messages for common issues:
|
|
390
|
+
|
|
391
|
+
- **Missing encryption key**: Thrown when attempting encryption/decryption without configured key
|
|
392
|
+
- **No fields specified**: Thrown when lock/unlock called without fields
|
|
393
|
+
- **Client replacement**: Thrown when attempting to replace existing MongoDB client
|
|
394
|
+
- **Context assertion**: Thrown when service context is invalid
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
try {
|
|
398
|
+
await mongoService.lock('config-alias', record, ['field'])
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (error.message.includes('No encryption key')) {
|
|
401
|
+
// Handle missing encryption configuration
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Security Considerations
|
|
407
|
+
|
|
408
|
+
### Field Encryption
|
|
409
|
+
- Use strong encryption keys for field-level encryption
|
|
410
|
+
- Rotate encryption keys regularly
|
|
411
|
+
- Store encryption keys securely outside the database
|
|
412
|
+
|
|
413
|
+
### Connection Security
|
|
414
|
+
- Use authentication credentials for production databases
|
|
415
|
+
- Configure TLS/SSL for database connections
|
|
416
|
+
- Restrict database access to necessary IP addresses
|
|
417
|
+
|
|
418
|
+
### Data Isolation
|
|
419
|
+
- Use layer-sensitive configurations for multi-tenant applications
|
|
420
|
+
- Separate database configurations by security level
|
|
421
|
+
|
|
422
|
+
## Performance Considerations
|
|
423
|
+
|
|
424
|
+
- **Connection Pooling**: MongoDB driver handles connection pooling automatically
|
|
425
|
+
- **Encryption Overhead**: Field encryption adds computational overhead
|
|
426
|
+
- **Cluster Latency**: Cluster setup may add initialization time
|
|
427
|
+
- **Memory Usage**: Multiple database connections increase memory usage
|
|
428
|
+
|
|
429
|
+
## Integration with OwlMeans Ecosystem
|
|
430
|
+
|
|
431
|
+
This package integrates with:
|
|
432
|
+
|
|
433
|
+
- **@owlmeans/mongo-resource**: Base MongoDB resource types and interfaces
|
|
434
|
+
- **@owlmeans/server-context**: Server context and configuration management
|
|
435
|
+
- **@owlmeans/basic-keys**: Cryptographic operations for field encryption
|
|
436
|
+
- **@owlmeans/resource**: Base resource service patterns
|
|
437
|
+
|
|
438
|
+
## Best Practices
|
|
439
|
+
|
|
440
|
+
1. **Use field encryption** for sensitive data like PII, financial information
|
|
441
|
+
2. **Configure replica sets** for production high-availability
|
|
442
|
+
3. **Separate database configurations** by environment and sensitivity level
|
|
443
|
+
4. **Handle encryption errors** gracefully with fallback mechanisms
|
|
444
|
+
5. **Monitor connection health** and implement proper cleanup
|
|
445
|
+
|
|
446
|
+
## Related Packages
|
|
447
|
+
|
|
448
|
+
- **@owlmeans/mongo-resource**: Base MongoDB resource definitions
|
|
449
|
+
- **@owlmeans/redis**: Redis service implementation
|
|
450
|
+
- **@owlmeans/server-context**: Server context management
|
|
451
|
+
- **@owlmeans/resource**: Base resource service patterns
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,aAAa,UAAmB,CAAA;AAE7C,eAAO,MAAM,WAAW,YAAY,CAAA"}
|
package/build/consts.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAE3D,MAAM,CAAC,MAAM,aAAa,GAAG,gBAAgB,CAAA;AAE7C,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,mBAAmB,YAAY,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ServerContext, ServerConfig } from '@owlmeans/server-context';
|
|
2
|
+
import type { MongoDbService } from '@owlmeans/mongo-resource';
|
|
3
|
+
type Config = ServerConfig;
|
|
4
|
+
interface Context<C extends Config = Config> extends ServerContext<C> {
|
|
5
|
+
}
|
|
6
|
+
export declare const makeMongoDbService: (alias?: string) => MongoDbService;
|
|
7
|
+
export declare const appendMongo: <C extends Config, T extends Context<C> = Context<C>>(context: T, alias?: string) => T;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAK3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAI9D,KAAK,MAAM,GAAG,YAAY,CAAA;AAC1B,UAAU,OAAO,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAE,SAAQ,aAAa,CAAC,CAAC,CAAC;CAAI;AAEzE,eAAO,MAAM,kBAAkB,WAAW,MAAM,KAAmB,cA2HlE,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,OAAO,CAAC,CAAC,CAAC,wBACvD,CAAC,UAAS,MAAM,KACxB,CAMF,CAAA"}
|
package/build/service.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assertContext, Layer } from '@owlmeans/context';
|
|
2
|
+
import { DEFAULT_ALIAS } from './consts.js';
|
|
3
|
+
import { MongoClient } from 'mongodb';
|
|
4
|
+
import { prepareConfig } from './utils/config.js';
|
|
5
|
+
import { setUpCluster } from './utils/cluster.js';
|
|
6
|
+
import { createDbService } from '@owlmeans/resource';
|
|
7
|
+
import { makeKeyPairModel } from '@owlmeans/basic-keys';
|
|
8
|
+
export const makeMongoDbService = (alias = DEFAULT_ALIAS) => {
|
|
9
|
+
const location = `mongo:${alias}`;
|
|
10
|
+
const service = createDbService(alias, {
|
|
11
|
+
db: async (configAlias) => {
|
|
12
|
+
const client = await service.client(configAlias);
|
|
13
|
+
const name = await service.name(configAlias);
|
|
14
|
+
return client.db(name);
|
|
15
|
+
},
|
|
16
|
+
initialize: async (configAlias) => {
|
|
17
|
+
configAlias = service.ensureConfigAlias(configAlias);
|
|
18
|
+
const config = service.config(configAlias);
|
|
19
|
+
if (service.clients[configAlias] != null) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (service.layers == null) {
|
|
23
|
+
service.layers = [Layer.Global];
|
|
24
|
+
}
|
|
25
|
+
if (config.serviceSensitive && service.layers.includes(Layer.Service)) {
|
|
26
|
+
service.layers.push(Layer.Service);
|
|
27
|
+
}
|
|
28
|
+
if (config.entitySensitive && service.layers.includes(Layer.Entity)) {
|
|
29
|
+
service.layers.push(Layer.Entity);
|
|
30
|
+
}
|
|
31
|
+
let [url, options] = prepareConfig(config);
|
|
32
|
+
let client = new MongoClient(url, options);
|
|
33
|
+
if (Array.isArray(config.host)) {
|
|
34
|
+
// @TODO Number of tries can be configurable
|
|
35
|
+
for (let i = 0; i < 3; ++i) {
|
|
36
|
+
if (await setUpCluster(client, config)) {
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await client.close();
|
|
41
|
+
let [url, options] = prepareConfig(config, false);
|
|
42
|
+
client = new MongoClient(url, options);
|
|
43
|
+
}
|
|
44
|
+
process.on('SIGTERM', () => {
|
|
45
|
+
client.close();
|
|
46
|
+
});
|
|
47
|
+
if (service.clients[configAlias] != null) {
|
|
48
|
+
throw new SyntaxError(`Cannot replace existing mongo client: ${configAlias} - ${service.alias}`);
|
|
49
|
+
}
|
|
50
|
+
service.clients[configAlias] = client;
|
|
51
|
+
},
|
|
52
|
+
lock: async (alias, record, fields) => {
|
|
53
|
+
alias = service.ensureConfigAlias(alias);
|
|
54
|
+
const config = service.config(alias);
|
|
55
|
+
if (config.encryptionKey == null) {
|
|
56
|
+
throw new SyntaxError(`No encryption key for locking: ${alias}`);
|
|
57
|
+
}
|
|
58
|
+
if ((fields.length ?? 0) < 1) {
|
|
59
|
+
throw new SyntaxError(`No fields to lock: ${JSON.stringify(record)}`);
|
|
60
|
+
}
|
|
61
|
+
const key = makeKeyPairModel(config.encryptionKey);
|
|
62
|
+
return Object.fromEntries(await Promise.all(Object.entries(record).map(async ([field, value]) => [
|
|
63
|
+
field,
|
|
64
|
+
fields.includes(field) ? await key.encrypt(value) : value
|
|
65
|
+
])));
|
|
66
|
+
},
|
|
67
|
+
unlock: async (alias, record, fields) => {
|
|
68
|
+
alias = service.ensureConfigAlias(alias);
|
|
69
|
+
const config = service.config(alias);
|
|
70
|
+
if (config.encryptionKey == null) {
|
|
71
|
+
throw new SyntaxError(`No encryption key for unlocking: ${alias}`);
|
|
72
|
+
}
|
|
73
|
+
if ((fields.length ?? 0) < 1) {
|
|
74
|
+
throw new SyntaxError(`No fields to unlock: ${JSON.stringify(record)}`);
|
|
75
|
+
}
|
|
76
|
+
const key = makeKeyPairModel(config.encryptionKey);
|
|
77
|
+
return Object.fromEntries(await Promise.all(Object.entries(record).map(async ([field, value]) => [
|
|
78
|
+
field,
|
|
79
|
+
fields.includes(field) ? await key.decrypt(value).catch(() => value) : value
|
|
80
|
+
])));
|
|
81
|
+
},
|
|
82
|
+
reinitializeContext: (context) => {
|
|
83
|
+
const _service = makeMongoDbService(alias);
|
|
84
|
+
_service.ctx = context;
|
|
85
|
+
_service.layers = service.layers;
|
|
86
|
+
return _service;
|
|
87
|
+
}
|
|
88
|
+
}, service => async () => {
|
|
89
|
+
const context = assertContext(service.ctx, location);
|
|
90
|
+
// Try to initialize all connections
|
|
91
|
+
await context.cfg.dbs?.filter(dbConfig => dbConfig.service === alias).reduce(async (prev, dbConfig) => {
|
|
92
|
+
await prev;
|
|
93
|
+
await service.config(dbConfig.alias);
|
|
94
|
+
}, Promise.resolve());
|
|
95
|
+
service.initialized = true;
|
|
96
|
+
});
|
|
97
|
+
return service;
|
|
98
|
+
};
|
|
99
|
+
export const appendMongo = (context, alias = DEFAULT_ALIAS) => {
|
|
100
|
+
const service = makeMongoDbService(alias);
|
|
101
|
+
context.registerService(service);
|
|
102
|
+
return context;
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AACA,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../src/utils/cluster.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAMlD,eAAO,MAAM,YAAY,WAAkB,WAAW,UAAU,QAAQ,KAAG,OAAO,CAAC,OAAO,CA6DzF,CAAA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { MongoServerError } from 'mongodb';
|
|
2
|
+
import { DEF_REPLSET } from '../consts.js';
|
|
3
|
+
import { port } from './config.js';
|
|
4
|
+
export const setUpCluster = async (client, config) => {
|
|
5
|
+
const meta = config.meta;
|
|
6
|
+
const hosts = config.host;
|
|
7
|
+
await client.connect();
|
|
8
|
+
const admin = client.db('admin').admin();
|
|
9
|
+
try {
|
|
10
|
+
await admin.replSetGetStatus();
|
|
11
|
+
// Replicaset can be missconfigured after server restarts
|
|
12
|
+
const { config: currentConfig } = await admin.command({ replSetGetConfig: 1 });
|
|
13
|
+
if (hosts.some(host => {
|
|
14
|
+
const _host = port(host, { ...config, port: config.port ?? 27017 });
|
|
15
|
+
if (currentConfig.members.some((member) => member.host === _host)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
})) {
|
|
20
|
+
const error = new MongoServerError({});
|
|
21
|
+
error.codeName = 'InvalidReplicaSetConfig';
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
try {
|
|
28
|
+
if (e instanceof MongoServerError && 'NotYetInitialized' === e.codeName) {
|
|
29
|
+
// INITIALIZE REPLICASET
|
|
30
|
+
await admin.command({
|
|
31
|
+
replSetInitiate: {
|
|
32
|
+
_id: meta?.replicaSet ?? DEF_REPLSET,
|
|
33
|
+
members: hosts.map((host, index) => ({ _id: index, host: port(host, config) }))
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else if (e instanceof MongoServerError && e.codeName === 'InvalidReplicaSetConfig') {
|
|
38
|
+
// FIX REPLICASET IF NODES CHANGED THEIR ADDRESSES
|
|
39
|
+
const { config: currentConfig } = await admin.command({ replSetGetConfig: 1 });
|
|
40
|
+
currentConfig.version++;
|
|
41
|
+
currentConfig.members = hosts.map((host, index) => ({
|
|
42
|
+
...currentConfig.members[index], _id: index, host: port(host, config)
|
|
43
|
+
}));
|
|
44
|
+
await admin.command({ replSetReconfig: currentConfig, force: true });
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
throw e;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
if (e instanceof MongoServerError && e.codeName === 'ConfigurationInProgress') {
|
|
52
|
+
// @TODO 2 seconds can't be enough
|
|
53
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
throw e;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
//# sourceMappingURL=cluster.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cluster.js","sourceRoot":"","sources":["../../src/utils/cluster.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MongoClientOptions } from 'mongodb';
|
|
2
|
+
import type { DbConfig } from '@owlmeans/resource';
|
|
3
|
+
export declare const prepareConfig: (config: DbConfig, single?: boolean) => [string, MongoClientOptions];
|
|
4
|
+
export declare const port: (host: string, config: DbConfig) => string;
|
|
5
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAIlD,eAAO,MAAM,aAAa,WAAY,QAAQ,WAAU,OAAO,KAAU,CAAC,MAAM,EAAE,kBAAkB,CAgBnG,CAAA;AAED,eAAO,MAAM,IAAI,SAAU,MAAM,UAAU,QAAQ,KAAG,MACC,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DEF_REPLSET } from '../consts.js';
|
|
2
|
+
export const prepareConfig = (config, single = true) => {
|
|
3
|
+
let host = Array.isArray(config.host) ? config.host[0] : config.host;
|
|
4
|
+
const hosts = Array.isArray(config.host) ? config.host : [config.host];
|
|
5
|
+
if (config.user != null) {
|
|
6
|
+
host = `mongodb://${single ? port(host, config) : hosts.map(host => port(host, config)).join(',')}`;
|
|
7
|
+
}
|
|
8
|
+
const meta = config.meta;
|
|
9
|
+
host = `${host}/?replicaSet=${meta?.replicaSet ?? DEF_REPLSET}`;
|
|
10
|
+
return [host, config.user != null ? {
|
|
11
|
+
auth: { username: config.user, password: config.secret },
|
|
12
|
+
directConnection: single
|
|
13
|
+
} : {}];
|
|
14
|
+
};
|
|
15
|
+
export const port = (host, config) => config.port == null ? host : `${host}:${config.port}`;
|
|
16
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE1C,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAgB,EAAE,SAAkB,IAAI,EAAgC,EAAE;IACtG,IAAI,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAA;IACpE,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAEtE,IAAI,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,GAAG,aAAa,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;IACrG,CAAC;IAED,MAAM,IAAI,GAA0B,MAAM,CAAC,IAAI,CAAA;IAE/C,IAAI,GAAG,GAAG,IAAI,gBAAgB,IAAI,EAAE,UAAU,IAAI,WAAW,EAAE,CAAA;IAE/D,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC;YAClC,IAAI,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;YACxD,gBAAgB,EAAE,MAAM;SACzB,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;AACT,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,MAAgB,EAAU,EAAE,CAC7D,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,EAAE,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@owlmeans/mongo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc -b",
|
|
7
|
+
"dev": "sleep 192 && nodemon -e ts,tsx,json --watch src --exec \"tsc -p ./tsconfig.json\"",
|
|
8
|
+
"watch": "tsc -b -w --preserveWatchOutput --pretty"
|
|
9
|
+
},
|
|
10
|
+
"main": "build/index.js",
|
|
11
|
+
"module": "build/index.js",
|
|
12
|
+
"types": "build/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./build/index.js",
|
|
16
|
+
"require": "./build/index.js",
|
|
17
|
+
"default": "./build/index.js",
|
|
18
|
+
"module": "./build/index.js",
|
|
19
|
+
"types": "./build/index.d.ts"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.7.8",
|
|
24
|
+
"nodemon": "^3.1.7",
|
|
25
|
+
"typescript": "^5.6.3"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@noble/hashes": "^1.5.0",
|
|
29
|
+
"@owlmeans/basic-keys": "^0.1.0",
|
|
30
|
+
"@owlmeans/context": "^0.1.0",
|
|
31
|
+
"@owlmeans/mongo-resource": "^0.1.0",
|
|
32
|
+
"@owlmeans/server-context": "^0.1.0",
|
|
33
|
+
"@scure/base": "^1.1.9",
|
|
34
|
+
"mongodb": "^6.9.0"
|
|
35
|
+
},
|
|
36
|
+
"private": false,
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/src/consts.ts
ADDED
package/src/index.ts
ADDED
package/src/service.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { assertContext, Layer } from '@owlmeans/context'
|
|
2
|
+
import type { BasicContext } from '@owlmeans/context'
|
|
3
|
+
import { DEFAULT_ALIAS } from './consts.js'
|
|
4
|
+
import type { ServerContext, ServerConfig } from '@owlmeans/server-context'
|
|
5
|
+
import { MongoClient } from 'mongodb'
|
|
6
|
+
import type { Db } from 'mongodb'
|
|
7
|
+
import { prepareConfig } from './utils/config.js'
|
|
8
|
+
import { setUpCluster } from './utils/cluster.js'
|
|
9
|
+
import type { MongoDbService } from '@owlmeans/mongo-resource'
|
|
10
|
+
import { createDbService } from '@owlmeans/resource'
|
|
11
|
+
import { makeKeyPairModel } from '@owlmeans/basic-keys'
|
|
12
|
+
|
|
13
|
+
type Config = ServerConfig
|
|
14
|
+
interface Context<C extends Config = Config> extends ServerContext<C> { }
|
|
15
|
+
|
|
16
|
+
export const makeMongoDbService = (alias: string = DEFAULT_ALIAS): MongoDbService => {
|
|
17
|
+
const location = `mongo:${alias}`
|
|
18
|
+
|
|
19
|
+
const service: MongoDbService = createDbService<Db, MongoClient, MongoDbService>(alias, {
|
|
20
|
+
db: async configAlias => {
|
|
21
|
+
const client = await service.client(configAlias)
|
|
22
|
+
|
|
23
|
+
const name = await service.name(configAlias)
|
|
24
|
+
|
|
25
|
+
return client.db(name)
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
initialize: async configAlias => {
|
|
29
|
+
configAlias = service.ensureConfigAlias(configAlias)
|
|
30
|
+
const config = service.config(configAlias)
|
|
31
|
+
|
|
32
|
+
if (service.clients[configAlias] != null) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (service.layers == null) {
|
|
37
|
+
service.layers = [Layer.Global]
|
|
38
|
+
}
|
|
39
|
+
if (config.serviceSensitive && service.layers.includes(Layer.Service)) {
|
|
40
|
+
service.layers.push(Layer.Service)
|
|
41
|
+
}
|
|
42
|
+
if (config.entitySensitive && service.layers.includes(Layer.Entity)) {
|
|
43
|
+
service.layers.push(Layer.Entity)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let [url, options] = prepareConfig(config)
|
|
47
|
+
|
|
48
|
+
let client = new MongoClient(url, options)
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(config.host)) {
|
|
51
|
+
// @TODO Number of tries can be configurable
|
|
52
|
+
for (let i = 0; i < 3; ++i) {
|
|
53
|
+
if (await setUpCluster(client, config)) {
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
await client.close()
|
|
58
|
+
let [url, options] = prepareConfig(config, false)
|
|
59
|
+
client = new MongoClient(url, options)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
process.on('SIGTERM', () => {
|
|
63
|
+
client.close()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if (service.clients[configAlias] != null) {
|
|
67
|
+
throw new SyntaxError(`Cannot replace existing mongo client: ${configAlias} - ${service.alias}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
service.clients[configAlias] = client
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
lock: async (alias, record, fields) => {
|
|
74
|
+
alias = service.ensureConfigAlias(alias)
|
|
75
|
+
const config = service.config(alias)
|
|
76
|
+
if (config.encryptionKey == null) {
|
|
77
|
+
throw new SyntaxError(`No encryption key for locking: ${alias}`)
|
|
78
|
+
}
|
|
79
|
+
if ((fields.length ?? 0) < 1) {
|
|
80
|
+
throw new SyntaxError(`No fields to lock: ${JSON.stringify(record)}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const key = makeKeyPairModel(config.encryptionKey)
|
|
84
|
+
|
|
85
|
+
return Object.fromEntries(
|
|
86
|
+
await Promise.all(Object.entries(record).map(
|
|
87
|
+
async ([field, value]) => [
|
|
88
|
+
field,
|
|
89
|
+
fields.includes(field) ? await key.encrypt(value) : value
|
|
90
|
+
]
|
|
91
|
+
))
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
unlock: async (alias, record, fields) => {
|
|
96
|
+
alias = service.ensureConfigAlias(alias)
|
|
97
|
+
const config = service.config(alias)
|
|
98
|
+
if (config.encryptionKey == null) {
|
|
99
|
+
throw new SyntaxError(`No encryption key for unlocking: ${alias}`)
|
|
100
|
+
}
|
|
101
|
+
if ((fields.length ?? 0) < 1) {
|
|
102
|
+
throw new SyntaxError(`No fields to unlock: ${JSON.stringify(record)}`)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const key = makeKeyPairModel(config.encryptionKey)
|
|
106
|
+
|
|
107
|
+
return Object.fromEntries(
|
|
108
|
+
await Promise.all(Object.entries(record).map(
|
|
109
|
+
async ([field, value]) => [
|
|
110
|
+
field,
|
|
111
|
+
fields.includes(field) ? await key.decrypt(value).catch(() => value) : value
|
|
112
|
+
]
|
|
113
|
+
))
|
|
114
|
+
)
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
reinitializeContext: <T>(context: BasicContext<ServerConfig>) => {
|
|
118
|
+
const _service = makeMongoDbService(alias)
|
|
119
|
+
|
|
120
|
+
_service.ctx = context
|
|
121
|
+
|
|
122
|
+
_service.layers = service.layers
|
|
123
|
+
|
|
124
|
+
return _service as T
|
|
125
|
+
}
|
|
126
|
+
}, service => async () => {
|
|
127
|
+
const context = assertContext<Config, Context>(service.ctx as Context, location)
|
|
128
|
+
|
|
129
|
+
// Try to initialize all connections
|
|
130
|
+
await context.cfg.dbs?.filter(dbConfig => dbConfig.service === alias).reduce(async (prev, dbConfig) => {
|
|
131
|
+
await prev
|
|
132
|
+
await service.config(dbConfig.alias)
|
|
133
|
+
}, Promise.resolve())
|
|
134
|
+
|
|
135
|
+
service.initialized = true
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return service
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const appendMongo = <C extends Config, T extends Context<C> = Context<C>>(
|
|
142
|
+
context: T, alias: string = DEFAULT_ALIAS
|
|
143
|
+
): T => {
|
|
144
|
+
const service = makeMongoDbService(alias)
|
|
145
|
+
|
|
146
|
+
context.registerService(service)
|
|
147
|
+
|
|
148
|
+
return context
|
|
149
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { MongoClient } from 'mongodb'
|
|
2
|
+
import type { MongoMeta } from '../types.js'
|
|
3
|
+
import type { DbConfig } from '@owlmeans/resource'
|
|
4
|
+
|
|
5
|
+
import { MongoServerError } from 'mongodb'
|
|
6
|
+
import { DEF_REPLSET } from '../consts.js'
|
|
7
|
+
import { port } from './config.js'
|
|
8
|
+
|
|
9
|
+
export const setUpCluster = async (client: MongoClient, config: DbConfig): Promise<boolean> => {
|
|
10
|
+
const meta: MongoMeta | undefined = config.meta
|
|
11
|
+
const hosts = config.host as string[]
|
|
12
|
+
|
|
13
|
+
await client.connect()
|
|
14
|
+
const admin = client.db('admin').admin()
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await admin.replSetGetStatus()
|
|
18
|
+
// Replicaset can be missconfigured after server restarts
|
|
19
|
+
const { config: currentConfig } = await admin.command({ replSetGetConfig: 1 })
|
|
20
|
+
|
|
21
|
+
if (hosts.some(host => {
|
|
22
|
+
const _host = port(host, { ...config, port: config.port ?? 27017 })
|
|
23
|
+
if (currentConfig.members.some((member: { host: string }) => member.host === _host)) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return true
|
|
28
|
+
})) {
|
|
29
|
+
const error = new MongoServerError({})
|
|
30
|
+
error.codeName = 'InvalidReplicaSetConfig'
|
|
31
|
+
throw error
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return true
|
|
35
|
+
} catch (e) {
|
|
36
|
+
try {
|
|
37
|
+
if (e instanceof MongoServerError && 'NotYetInitialized' === e.codeName) {
|
|
38
|
+
// INITIALIZE REPLICASET
|
|
39
|
+
await admin.command({
|
|
40
|
+
replSetInitiate: {
|
|
41
|
+
_id: meta?.replicaSet ?? DEF_REPLSET,
|
|
42
|
+
members: hosts.map((host, index) => ({ _id: index, host: port(host, config) }))
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
} else if (e instanceof MongoServerError && e.codeName === 'InvalidReplicaSetConfig') {
|
|
46
|
+
// FIX REPLICASET IF NODES CHANGED THEIR ADDRESSES
|
|
47
|
+
|
|
48
|
+
const { config: currentConfig } = await admin.command({ replSetGetConfig: 1 })
|
|
49
|
+
currentConfig.version++
|
|
50
|
+
|
|
51
|
+
currentConfig.members = hosts.map((host, index) => ({
|
|
52
|
+
...currentConfig.members[index], _id: index, host: port(host, config)
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
await admin.command({ replSetReconfig: currentConfig, force: true })
|
|
56
|
+
} else {
|
|
57
|
+
throw e
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (e instanceof MongoServerError && e.codeName === 'ConfigurationInProgress') {
|
|
61
|
+
// @TODO 2 seconds can't be enough
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 2000))
|
|
63
|
+
} else {
|
|
64
|
+
throw e
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MongoClientOptions } from 'mongodb'
|
|
2
|
+
import type { MongoMeta } from '../types.js'
|
|
3
|
+
import type { DbConfig } from '@owlmeans/resource'
|
|
4
|
+
|
|
5
|
+
import { DEF_REPLSET } from '../consts.js'
|
|
6
|
+
|
|
7
|
+
export const prepareConfig = (config: DbConfig, single: boolean = true): [string, MongoClientOptions] => {
|
|
8
|
+
let host = Array.isArray(config.host) ? config.host[0] : config.host
|
|
9
|
+
const hosts = Array.isArray(config.host) ? config.host : [config.host]
|
|
10
|
+
|
|
11
|
+
if (config.user != null) {
|
|
12
|
+
host = `mongodb://${single ? port(host, config) : hosts.map(host => port(host, config)).join(',')}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const meta: MongoMeta | undefined = config.meta
|
|
16
|
+
|
|
17
|
+
host = `${host}/?replicaSet=${meta?.replicaSet ?? DEF_REPLSET}`
|
|
18
|
+
|
|
19
|
+
return [host, config.user != null ? {
|
|
20
|
+
auth: { username: config.user, password: config.secret },
|
|
21
|
+
directConnection: single
|
|
22
|
+
} : {}]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const port = (host: string, config: DbConfig): string =>
|
|
26
|
+
config.port == null ? host : `${host}:${config.port}`
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": [
|
|
3
|
+
"../tsconfig.default.json",
|
|
4
|
+
],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"rootDir": "./src/", /* Specify the root folder within your source files. */
|
|
7
|
+
"outDir": "./build/", /* Specify an output folder for all emitted files. */
|
|
8
|
+
},
|
|
9
|
+
"exclude": [
|
|
10
|
+
"./dist/**/*",
|
|
11
|
+
"./build/**/*",
|
|
12
|
+
"./*.ts"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/consts.ts","./src/index.ts","./src/service.ts","./src/types.ts","./src/utils/cluster.ts","./src/utils/config.ts","./src/utils/index.ts"],"version":"5.6.3"}
|