@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 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,3 @@
1
+ export declare const DEFAULT_ALIAS = "mongo";
2
+ export declare const DEF_REPLSET = "rs-main";
3
+ //# sourceMappingURL=consts.d.ts.map
@@ -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"}
@@ -0,0 +1,4 @@
1
+ import { DEFAULT_DB_ALIAS } from '@owlmeans/mongo-resource';
2
+ export const DEFAULT_ALIAS = DEFAULT_DB_ALIAS;
3
+ export const DEF_REPLSET = 'rs-main';
4
+ //# sourceMappingURL=consts.js.map
@@ -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"}
@@ -0,0 +1,4 @@
1
+ export type * from './types.js';
2
+ export * from './service.js';
3
+ export * from './consts.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * from './service.js';
2
+ export * from './consts.js';
3
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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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"}
@@ -0,0 +1,4 @@
1
+ export interface MongoMeta {
2
+ replicaSet?: string;
3
+ }
4
+ //# sourceMappingURL=types.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,4 @@
1
+ import type { MongoClient } from 'mongodb';
2
+ import type { DbConfig } from '@owlmeans/resource';
3
+ export declare const setUpCluster: (client: MongoClient, config: DbConfig) => Promise<boolean>;
4
+ //# sourceMappingURL=cluster.d.ts.map
@@ -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,3 @@
1
+ export * from './cluster.js';
2
+ export * from './config.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * from './cluster.js';
2
+ export * from './config.js';
3
+ //# sourceMappingURL=index.js.map
@@ -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
@@ -0,0 +1,6 @@
1
+
2
+ import { DEFAULT_DB_ALIAS } from '@owlmeans/mongo-resource'
3
+
4
+ export const DEFAULT_ALIAS = DEFAULT_DB_ALIAS
5
+
6
+ export const DEF_REPLSET = 'rs-main'
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+
2
+ export type * from './types.js'
3
+ export * from './service.js'
4
+ export * from './consts.js'
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,4 @@
1
+
2
+ export interface MongoMeta {
3
+ replicaSet?: string
4
+ }
@@ -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}`
@@ -0,0 +1,3 @@
1
+
2
+ export * from './cluster.js'
3
+ export * from './config.js'
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"}