@diia-inhouse/db 10.8.12 → 12.3.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/dist/_virtual/_rolldown/runtime.js +27 -0
- package/dist/cli/cleanupExpiredData.js +83 -136
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +85 -100
- package/dist/cli/runMigration.js +64 -105
- package/dist/cli/syncIndexes.js +38 -77
- package/dist/index.d.ts +28 -0
- package/dist/index.js +23 -22
- package/dist/interfaces/database.d.ts +10 -0
- package/dist/interfaces/dbConfig.d.ts +37 -0
- package/dist/interfaces/dbConfig.js +8 -9
- package/dist/interfaces/errors.d.ts +43 -0
- package/dist/interfaces/errors.js +42 -50
- package/dist/interfaces/index.d.ts +15 -0
- package/dist/interfaces/index.js +11 -25
- package/dist/interfaces/metrics.d.ts +22 -0
- package/dist/interfaces/metrics.js +38 -19
- package/dist/interfaces/migrateMongoConfig.d.ts +17 -0
- package/dist/interfaces/models/counter.d.ts +8 -0
- package/dist/interfaces/models/encryptedStorage.d.ts +9 -0
- package/dist/interfaces/mongoStatus.d.ts +16 -0
- package/dist/interfaces/mongoStatus.js +11 -12
- package/dist/mongo.d.ts +12 -0
- package/dist/mongo.js +71 -89
- package/dist/postgres.d.ts +4 -0
- package/dist/postgres.js +5 -19
- package/dist/postgresUtils.d.ts +8 -0
- package/dist/postgresUtils.js +14 -18
- package/dist/schemas/counter.d.ts +7 -0
- package/dist/schemas/counter.js +19 -14
- package/dist/schemas/encryptedStorage.d.ts +7 -0
- package/dist/schemas/encryptedStorage.js +17 -15
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +4 -20
- package/dist/schemas/statusHistoryItem.d.ts +7 -0
- package/dist/schemas/statusHistoryItem.js +16 -10
- package/dist/services/counter.d.ts +19 -0
- package/dist/services/counter.js +70 -72
- package/dist/services/database.d.ts +50 -0
- package/dist/services/database.js +255 -281
- package/dist/services/encryptedStorage.d.ts +73 -0
- package/dist/services/encryptedStorage.js +178 -214
- package/dist/services/index.d.ts +4 -0
- package/dist/services/index.js +5 -21
- package/dist/services/postgresDatabase.d.ts +12 -0
- package/dist/services/postgresDatabase.js +20 -25
- package/dist/tables/counter.d.ts +4 -0
- package/dist/tables/counter.js +11 -12
- package/dist/tables/encryptedStorage.d.ts +4 -0
- package/dist/tables/encryptedStorage.js +12 -13
- package/dist/tables/index.d.ts +9 -0
- package/dist/tables/index.js +3 -19
- package/package.json +47 -48
- package/dist/cli/cleanupExpiredData.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/runMigration.js.map +0 -1
- package/dist/cli/syncIndexes.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/interfaces/database.js +0 -3
- package/dist/interfaces/database.js.map +0 -1
- package/dist/interfaces/dbConfig.js.map +0 -1
- package/dist/interfaces/errors.js.map +0 -1
- package/dist/interfaces/index.js.map +0 -1
- package/dist/interfaces/metrics.js.map +0 -1
- package/dist/interfaces/migrateMongoConfig.js +0 -3
- package/dist/interfaces/migrateMongoConfig.js.map +0 -1
- package/dist/interfaces/models/counter.js +0 -3
- package/dist/interfaces/models/counter.js.map +0 -1
- package/dist/interfaces/models/encryptedStorage.js +0 -3
- package/dist/interfaces/models/encryptedStorage.js.map +0 -1
- package/dist/interfaces/models/index.js +0 -19
- package/dist/interfaces/models/index.js.map +0 -1
- package/dist/interfaces/mongoStatus.js.map +0 -1
- package/dist/mongo.js.map +0 -1
- package/dist/postgres.js.map +0 -1
- package/dist/postgresUtils.js.map +0 -1
- package/dist/schemas/counter.js.map +0 -1
- package/dist/schemas/encryptedStorage.js.map +0 -1
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/statusHistoryItem.js.map +0 -1
- package/dist/services/counter.js.map +0 -1
- package/dist/services/database.js.map +0 -1
- package/dist/services/encryptedStorage.js.map +0 -1
- package/dist/services/index.js.map +0 -1
- package/dist/services/postgresDatabase.js.map +0 -1
- package/dist/tables/counter.js.map +0 -1
- package/dist/tables/encryptedStorage.js.map +0 -1
- package/dist/tables/index.js.map +0 -1
- package/dist/types/cli/cleanupExpiredData.d.ts +0 -11
- package/dist/types/cli/index.d.ts +0 -2
- package/dist/types/cli/runMigration.d.ts +0 -11
- package/dist/types/cli/syncIndexes.d.ts +0 -7
- package/dist/types/index.d.ts +0 -5
- package/dist/types/interfaces/database.d.ts +0 -8
- package/dist/types/interfaces/dbConfig.d.ts +0 -33
- package/dist/types/interfaces/errors.d.ts +0 -40
- package/dist/types/interfaces/index.d.ts +0 -8
- package/dist/types/interfaces/metrics.d.ts +0 -20
- package/dist/types/interfaces/migrateMongoConfig.d.ts +0 -13
- package/dist/types/interfaces/models/counter.d.ts +0 -5
- package/dist/types/interfaces/models/encryptedStorage.d.ts +0 -6
- package/dist/types/interfaces/models/index.d.ts +0 -2
- package/dist/types/interfaces/mongoStatus.d.ts +0 -12
- package/dist/types/mongo.d.ts +0 -9
- package/dist/types/postgres.d.ts +0 -2
- package/dist/types/postgresUtils.d.ts +0 -26
- package/dist/types/schemas/counter.d.ts +0 -12
- package/dist/types/schemas/encryptedStorage.d.ts +0 -12
- package/dist/types/schemas/index.d.ts +0 -3
- package/dist/types/schemas/statusHistoryItem.d.ts +0 -3
- package/dist/types/services/counter.d.ts +0 -15
- package/dist/types/services/database.d.ts +0 -43
- package/dist/types/services/encryptedStorage.d.ts +0 -69
- package/dist/types/services/index.d.ts +0 -4
- package/dist/types/services/postgresDatabase.d.ts +0 -8
- package/dist/types/tables/counter.d.ts +0 -94
- package/dist/types/tables/encryptedStorage.d.ts +0 -111
- package/dist/types/tables/index.d.ts +0 -2
|
@@ -1,282 +1,256 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import "../interfaces/dbConfig.js";
|
|
2
|
+
import "../interfaces/mongoStatus.js";
|
|
3
|
+
import { mongodbOperationsAllowedFields, mongodbOperationsDefaultBuckets } from "../interfaces/metrics.js";
|
|
4
|
+
import "../interfaces/index.js";
|
|
5
|
+
import mongoose from "mongoose";
|
|
6
|
+
import lodash from "lodash";
|
|
7
|
+
import { Histogram, Observer } from "@diia-inhouse/diia-metrics";
|
|
8
|
+
import { DatabaseError } from "@diia-inhouse/errors";
|
|
9
|
+
import { HttpStatusCode } from "@diia-inhouse/types";
|
|
10
|
+
//#region src/services/database.ts
|
|
11
|
+
const { cloneDeep } = lodash;
|
|
12
|
+
var DatabaseService = class {
|
|
13
|
+
databaseAdapter;
|
|
14
|
+
dbConfigs;
|
|
15
|
+
envService;
|
|
16
|
+
logger;
|
|
17
|
+
defaultModelsDir = "models";
|
|
18
|
+
db = {};
|
|
19
|
+
dbOperationsHistogram;
|
|
20
|
+
dbOplogObserver;
|
|
21
|
+
observerInterval;
|
|
22
|
+
dbOplog = {};
|
|
23
|
+
isMonitoringEnabled = false;
|
|
24
|
+
dbStateCodeToName = {
|
|
25
|
+
[mongoose.ConnectionStates.disconnected]: "disconnected",
|
|
26
|
+
[mongoose.ConnectionStates.connected]: "connected",
|
|
27
|
+
[mongoose.ConnectionStates.connecting]: "connecting",
|
|
28
|
+
[mongoose.ConnectionStates.disconnecting]: "disconnecting"
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* https://www.mongodb.com/docs/manual/core/transactions/#read-concern-write-concern-read-preference
|
|
32
|
+
*/
|
|
33
|
+
defaultTransactionOptions = { readPreference: "primary" };
|
|
34
|
+
constructor(databaseAdapter, dbConfigs, envService, logger) {
|
|
35
|
+
this.databaseAdapter = databaseAdapter;
|
|
36
|
+
this.dbConfigs = dbConfigs;
|
|
37
|
+
this.envService = envService;
|
|
38
|
+
this.logger = logger;
|
|
39
|
+
this.isMonitoringEnabled = this.dbConfigs.main?.metrics.enabled || false;
|
|
40
|
+
if (this.isMonitoringEnabled) {
|
|
41
|
+
this.dbOperationsHistogram = new Histogram("diia_mongodb_operation_seconds", mongodbOperationsAllowedFields, "Mongodb operation duration in seconds", this.dbConfigs.main?.metrics.buckets || mongodbOperationsDefaultBuckets);
|
|
42
|
+
this.dbOplogObserver = new Observer("diia_mongodb_oplog_cache_size", [], "Amount of operations stored in in-memory cache for monitoring");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async onInit() {
|
|
46
|
+
if (this.databaseAdapter !== "mongo") return;
|
|
47
|
+
const tasks = Object.entries(this.dbConfigs).map(async ([type, config]) => {
|
|
48
|
+
const dbType = type;
|
|
49
|
+
const connection = await this.createDbConnection(dbType, config);
|
|
50
|
+
if (connection) this.db[dbType] = connection;
|
|
51
|
+
});
|
|
52
|
+
await Promise.all(tasks);
|
|
53
|
+
if (this.isMonitoringEnabled) this.observerInterval = setInterval(() => {
|
|
54
|
+
this.oplogObserver();
|
|
55
|
+
}, 5e3);
|
|
56
|
+
}
|
|
57
|
+
async onDestroy() {
|
|
58
|
+
if (this.isMonitoringEnabled) clearInterval(this.observerInterval);
|
|
59
|
+
for (const db of Object.values(this.db)) await db.connection.close();
|
|
60
|
+
}
|
|
61
|
+
async onHealthCheck() {
|
|
62
|
+
const dbStatus = {};
|
|
63
|
+
for (const [type, db] of Object.entries(this.db)) {
|
|
64
|
+
const dbType = type;
|
|
65
|
+
try {
|
|
66
|
+
await db.connection.db?.listCollections({}, {
|
|
67
|
+
nameOnly: true,
|
|
68
|
+
authorizedCollections: true
|
|
69
|
+
}).toArray();
|
|
70
|
+
} catch (err) {
|
|
71
|
+
this.logger.error("Mongo list collections error", {
|
|
72
|
+
err,
|
|
73
|
+
dbType
|
|
74
|
+
});
|
|
75
|
+
dbStatus[dbType] = "op_failed";
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
dbStatus[dbType] = this.dbStateCodeToName[db.connection.readyState];
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
status: Object.values(dbStatus).some((s) => s !== "connected") ? HttpStatusCode.SERVICE_UNAVAILABLE : HttpStatusCode.OK,
|
|
82
|
+
details: { mongodb: dbStatus }
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async createDbConnection(type, config) {
|
|
86
|
+
const { isEnabled } = config;
|
|
87
|
+
if (typeof isEnabled === "boolean" && !isEnabled) {
|
|
88
|
+
this.logger.info(`Database is disabled: ${type}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const { host, port, database, authSource, user, password, replicaSet, replicaSetNodes, readPreference, authMechanism } = config;
|
|
93
|
+
const connectionOptions = {};
|
|
94
|
+
let hosts = [];
|
|
95
|
+
if (host) if (port) hosts.push(`${host}:${port}`);
|
|
96
|
+
else hosts.push(host);
|
|
97
|
+
if (user && password) connectionOptions.auth = {
|
|
98
|
+
username: user,
|
|
99
|
+
password
|
|
100
|
+
};
|
|
101
|
+
if (replicaSet) connectionOptions.replicaSet = replicaSet;
|
|
102
|
+
if (replicaSetNodes) {
|
|
103
|
+
if (host) {
|
|
104
|
+
const errMsg = "Must be only `host` and `port` or `replicaSetNodes` config";
|
|
105
|
+
this.logger.error("Wrong database configuration:", errMsg);
|
|
106
|
+
throw new Error(errMsg);
|
|
107
|
+
}
|
|
108
|
+
hosts = replicaSetNodes.map(({ replicaHost }) => `${replicaHost}:${port}`);
|
|
109
|
+
}
|
|
110
|
+
let connectionString = `mongodb://${hosts.join(",")}/`;
|
|
111
|
+
if (database) connectionOptions.dbName = database;
|
|
112
|
+
const query = [];
|
|
113
|
+
if (authSource) query.push(`authSource=${authSource}`);
|
|
114
|
+
if (readPreference) query.push(`readPreference=${readPreference}`);
|
|
115
|
+
if (authMechanism) query.push(`authMechanism=${authMechanism}`);
|
|
116
|
+
if (query.length > 0) connectionString += `?${query.join("&")}`;
|
|
117
|
+
const logOptions = cloneDeep(connectionOptions);
|
|
118
|
+
if (logOptions.auth) logOptions.auth = {
|
|
119
|
+
username: "********",
|
|
120
|
+
password: "********"
|
|
121
|
+
};
|
|
122
|
+
this.logger.info(`Connecting to DB ${connectionString} ${type}`, logOptions);
|
|
123
|
+
if (this.envService.isLocal() || this.envService.isTest()) {
|
|
124
|
+
this.logger.debug("Mongoose set to Debug");
|
|
125
|
+
mongoose.set("debug", (coll, method, dbQuery, doc, options) => {
|
|
126
|
+
this.logger.debug("Mongo: ", {
|
|
127
|
+
coll,
|
|
128
|
+
method,
|
|
129
|
+
query: dbQuery,
|
|
130
|
+
doc,
|
|
131
|
+
options
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
let connection;
|
|
136
|
+
if (type === "main") {
|
|
137
|
+
await mongoose.connect(connectionString, {
|
|
138
|
+
...connectionOptions,
|
|
139
|
+
monitorCommands: this.isMonitoringEnabled
|
|
140
|
+
});
|
|
141
|
+
connection = mongoose.connection;
|
|
142
|
+
} else connection = await mongoose.createConnection(connectionString, connectionOptions).asPromise();
|
|
143
|
+
if (this.isMonitoringEnabled) this.connectMonitor(connection);
|
|
144
|
+
connection.on("error", (err) => {
|
|
145
|
+
this.logger.error("Mongo connection error", {
|
|
146
|
+
err,
|
|
147
|
+
type
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
connection,
|
|
152
|
+
connectionString,
|
|
153
|
+
connectionOptions
|
|
154
|
+
};
|
|
155
|
+
} catch (err) {
|
|
156
|
+
this.logger.error("Failed to connect to Database", {
|
|
157
|
+
type,
|
|
158
|
+
err
|
|
159
|
+
});
|
|
160
|
+
throw new DatabaseError("Failed to connect to Database");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Begins a new MongoDB transaction by starting a session and transaction
|
|
165
|
+
*
|
|
166
|
+
* @param options - Optional transaction options to override the default transaction settings
|
|
167
|
+
* @returns A ClientSession with an active transaction
|
|
168
|
+
* @throws {DatabaseError} If the database connection is undefined or transaction fails to start
|
|
169
|
+
*
|
|
170
|
+
* @remarks
|
|
171
|
+
* - Uses the main database connection to start the session
|
|
172
|
+
* - Applies default transaction options (readPreference: primary)
|
|
173
|
+
* - Will abort transaction and end session if any errors occur during setup
|
|
174
|
+
*/
|
|
175
|
+
async beginTransaction(options) {
|
|
176
|
+
const connection = this.db["main"]?.connection;
|
|
177
|
+
if (!connection) throw new DatabaseError("Connection is undefined");
|
|
178
|
+
const session = await connection.startSession();
|
|
179
|
+
try {
|
|
180
|
+
const transactionOptions = {
|
|
181
|
+
...this.defaultTransactionOptions,
|
|
182
|
+
...options
|
|
183
|
+
};
|
|
184
|
+
session.startTransaction(transactionOptions);
|
|
185
|
+
return session;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
await session.abortTransaction();
|
|
188
|
+
await session.endSession();
|
|
189
|
+
throw new DatabaseError("Unable to begin transaction", { err });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
connectMonitor(connection) {
|
|
193
|
+
try {
|
|
194
|
+
const client = connection.getClient();
|
|
195
|
+
client.on("commandStarted", (event) => {
|
|
196
|
+
const db = event.databaseName;
|
|
197
|
+
const opType = event.commandName;
|
|
198
|
+
const key = `${event.requestId}${event.connectionId}`;
|
|
199
|
+
const parsedCollection = Number.parseInt(event.command[opType] || "", 10);
|
|
200
|
+
const collection = Number.isNaN(parsedCollection) ? event.command[opType] : void 0;
|
|
201
|
+
this.dbOplog[key] = {
|
|
202
|
+
database: db,
|
|
203
|
+
collection,
|
|
204
|
+
opType
|
|
205
|
+
};
|
|
206
|
+
this.logger.debug("Mongodb commandStarted operation", {
|
|
207
|
+
database: db,
|
|
208
|
+
collection,
|
|
209
|
+
operation: opType,
|
|
210
|
+
command: event.command
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
client.on("commandSucceeded", (event) => {
|
|
214
|
+
const key = `${event.requestId}${event.connectionId}`;
|
|
215
|
+
const oplogEntry = this.dbOplog[key];
|
|
216
|
+
const durationMs = event.duration;
|
|
217
|
+
delete this.dbOplog[key];
|
|
218
|
+
this.logger.debug("Mongodb commandSucceeded operation", {
|
|
219
|
+
database: oplogEntry.database,
|
|
220
|
+
operation: oplogEntry.opType,
|
|
221
|
+
collection: oplogEntry.collection
|
|
222
|
+
});
|
|
223
|
+
this.dbOperationsHistogram?.observe({
|
|
224
|
+
operation: oplogEntry.opType,
|
|
225
|
+
status: "successful",
|
|
226
|
+
database: oplogEntry.database,
|
|
227
|
+
...oplogEntry.collection && { collection: oplogEntry.collection }
|
|
228
|
+
}, durationMs / 1e3);
|
|
229
|
+
});
|
|
230
|
+
client.on("commandFailed", (event) => {
|
|
231
|
+
const key = `${event.requestId}${event.connectionId}`;
|
|
232
|
+
const oplogEntry = this.dbOplog[key];
|
|
233
|
+
const durationMs = event.duration;
|
|
234
|
+
delete this.dbOplog[key];
|
|
235
|
+
this.logger.debug("Mongodb commandFailed operation", {
|
|
236
|
+
database: oplogEntry.database,
|
|
237
|
+
operation: oplogEntry.opType,
|
|
238
|
+
collection: oplogEntry.collection
|
|
239
|
+
});
|
|
240
|
+
this.dbOperationsHistogram?.observe({
|
|
241
|
+
operation: oplogEntry.opType,
|
|
242
|
+
status: "failed",
|
|
243
|
+
database: oplogEntry.database,
|
|
244
|
+
...oplogEntry.collection && { collection: oplogEntry.collection }
|
|
245
|
+
}, durationMs / 1e3);
|
|
246
|
+
});
|
|
247
|
+
} catch (err) {
|
|
248
|
+
this.logger.error("Couldn't attach commands monitor", { err });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
oplogObserver() {
|
|
252
|
+
this.dbOplogObserver?.observe({}, Object.keys(this.dbOplog).length);
|
|
253
|
+
}
|
|
4
254
|
};
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const lodash_1 = require("lodash");
|
|
8
|
-
const mongoose_1 = __importDefault(require("mongoose"));
|
|
9
|
-
const diia_metrics_1 = require("@diia-inhouse/diia-metrics");
|
|
10
|
-
const errors_1 = require("@diia-inhouse/errors");
|
|
11
|
-
const types_1 = require("@diia-inhouse/types");
|
|
12
|
-
const interfaces_1 = require("../interfaces");
|
|
13
|
-
class DatabaseService {
|
|
14
|
-
databaseAdapter;
|
|
15
|
-
dbConfigs;
|
|
16
|
-
envService;
|
|
17
|
-
logger;
|
|
18
|
-
defaultModelsDir = 'models';
|
|
19
|
-
db = {};
|
|
20
|
-
dbOperationsHistogram;
|
|
21
|
-
dbOplogObserver;
|
|
22
|
-
observerInterval;
|
|
23
|
-
dbOplog = {};
|
|
24
|
-
isMonitoringEnabled = false;
|
|
25
|
-
dbStateCodeToName = {
|
|
26
|
-
[mongoose_1.default.ConnectionStates.disconnected]: interfaces_1.DbConnectionStatus.Disconnected,
|
|
27
|
-
[mongoose_1.default.ConnectionStates.connected]: interfaces_1.DbConnectionStatus.Connected,
|
|
28
|
-
[mongoose_1.default.ConnectionStates.connecting]: interfaces_1.DbConnectionStatus.Connecting,
|
|
29
|
-
[mongoose_1.default.ConnectionStates.disconnecting]: interfaces_1.DbConnectionStatus.Disconnecting,
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* https://www.mongodb.com/docs/manual/core/transactions/#read-concern-write-concern-read-preference
|
|
33
|
-
*/
|
|
34
|
-
defaultTransactionOptions = {
|
|
35
|
-
readPreference: 'primary',
|
|
36
|
-
};
|
|
37
|
-
constructor(databaseAdapter, dbConfigs, envService, logger) {
|
|
38
|
-
this.databaseAdapter = databaseAdapter;
|
|
39
|
-
this.dbConfigs = dbConfigs;
|
|
40
|
-
this.envService = envService;
|
|
41
|
-
this.logger = logger;
|
|
42
|
-
this.isMonitoringEnabled = this.dbConfigs.main?.metrics.enabled || false;
|
|
43
|
-
if (this.isMonitoringEnabled) {
|
|
44
|
-
this.dbOperationsHistogram = new diia_metrics_1.Histogram('diia_mongodb_operation_seconds', interfaces_1.mongodbOperationsAllowedFields, 'Mongodb operation duration in seconds', this.dbConfigs.main?.metrics.buckets || interfaces_1.mongodbOperationsDefaultBuckets);
|
|
45
|
-
this.dbOplogObserver = new diia_metrics_1.Observer('diia_mongodb_oplog_cache_size', [], 'Amount of operations stored in in-memory cache for monitoring');
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
async onInit() {
|
|
49
|
-
if (this.databaseAdapter !== 'mongo') {
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const tasks = Object.entries(this.dbConfigs).map(async ([type, config]) => {
|
|
53
|
-
const dbType = type;
|
|
54
|
-
const connection = await this.createDbConnection(dbType, config);
|
|
55
|
-
if (connection) {
|
|
56
|
-
this.db[dbType] = connection;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
await Promise.all(tasks);
|
|
60
|
-
if (this.isMonitoringEnabled) {
|
|
61
|
-
this.observerInterval = setInterval(() => {
|
|
62
|
-
this.oplogObserver();
|
|
63
|
-
}, 5000);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
async onDestroy() {
|
|
67
|
-
if (this.isMonitoringEnabled) {
|
|
68
|
-
clearInterval(this.observerInterval);
|
|
69
|
-
}
|
|
70
|
-
for (const db of Object.values(this.db)) {
|
|
71
|
-
await db.connection.close();
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async onHealthCheck() {
|
|
75
|
-
const dbStatus = {};
|
|
76
|
-
for (const [type, db] of Object.entries(this.db)) {
|
|
77
|
-
const dbType = type;
|
|
78
|
-
try {
|
|
79
|
-
await db.connection.db
|
|
80
|
-
?.listCollections({}, {
|
|
81
|
-
nameOnly: true,
|
|
82
|
-
authorizedCollections: true,
|
|
83
|
-
})
|
|
84
|
-
.toArray();
|
|
85
|
-
}
|
|
86
|
-
catch (err) {
|
|
87
|
-
this.logger.error('Mongo list collections error', { err, dbType });
|
|
88
|
-
dbStatus[dbType] = interfaces_1.DbConnectionStatus.OpFailed;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
dbStatus[dbType] = this.dbStateCodeToName[db.connection.readyState];
|
|
92
|
-
}
|
|
93
|
-
const status = Object.values(dbStatus).some((s) => s !== interfaces_1.DbConnectionStatus.Connected)
|
|
94
|
-
? types_1.HttpStatusCode.SERVICE_UNAVAILABLE
|
|
95
|
-
: types_1.HttpStatusCode.OK;
|
|
96
|
-
return {
|
|
97
|
-
status,
|
|
98
|
-
details: { mongodb: dbStatus },
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
async createDbConnection(type, config) {
|
|
102
|
-
const { isEnabled } = config;
|
|
103
|
-
if (typeof isEnabled === 'boolean' && !isEnabled) {
|
|
104
|
-
this.logger.info(`Database is disabled: ${type}`);
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const { host, port, database, authSource, user, password, replicaSet, replicaSetNodes, readPreference, authMechanism } = config;
|
|
109
|
-
const connectionOptions = {};
|
|
110
|
-
let hosts = [];
|
|
111
|
-
if (host) {
|
|
112
|
-
if (port) {
|
|
113
|
-
hosts.push(`${host}:${port}`);
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
hosts.push(`${host}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
if (user && password) {
|
|
120
|
-
connectionOptions.auth = { username: user, password };
|
|
121
|
-
}
|
|
122
|
-
if (replicaSet) {
|
|
123
|
-
connectionOptions.replicaSet = replicaSet;
|
|
124
|
-
}
|
|
125
|
-
if (replicaSetNodes) {
|
|
126
|
-
if (host) {
|
|
127
|
-
const errMsg = 'Must be only `host` and `port` or `replicaSetNodes` config';
|
|
128
|
-
this.logger.error('Wrong database configuration:', errMsg);
|
|
129
|
-
throw new Error(errMsg);
|
|
130
|
-
}
|
|
131
|
-
hosts = replicaSetNodes.map(({ replicaHost }) => `${replicaHost}:${port}`);
|
|
132
|
-
}
|
|
133
|
-
let connectionString = `mongodb://${hosts.join(',')}/`;
|
|
134
|
-
if (database) {
|
|
135
|
-
connectionOptions.dbName = database;
|
|
136
|
-
}
|
|
137
|
-
const query = [];
|
|
138
|
-
if (authSource) {
|
|
139
|
-
query.push(`authSource=${authSource}`);
|
|
140
|
-
}
|
|
141
|
-
if (readPreference) {
|
|
142
|
-
query.push(`readPreference=${readPreference}`);
|
|
143
|
-
}
|
|
144
|
-
if (authMechanism) {
|
|
145
|
-
query.push(`authMechanism=${authMechanism}`);
|
|
146
|
-
}
|
|
147
|
-
if (query.length > 0) {
|
|
148
|
-
connectionString += `?${query.join('&')}`;
|
|
149
|
-
}
|
|
150
|
-
const logOptions = (0, lodash_1.cloneDeep)(connectionOptions);
|
|
151
|
-
if (logOptions.auth) {
|
|
152
|
-
logOptions.auth = {
|
|
153
|
-
username: '********',
|
|
154
|
-
password: '********',
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
this.logger.info(`Connecting to DB ${connectionString} ${type}`, logOptions);
|
|
158
|
-
if (this.envService.isLocal() || this.envService.isTest()) {
|
|
159
|
-
this.logger.debug('Mongoose set to Debug');
|
|
160
|
-
mongoose_1.default.set('debug', (coll, method, dbQuery, doc, options) => {
|
|
161
|
-
this.logger.debug('Mongo: ', { coll, method, query: dbQuery, doc, options });
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
let connection;
|
|
165
|
-
if (type === interfaces_1.DbType.Main) {
|
|
166
|
-
await mongoose_1.default.connect(connectionString, { ...connectionOptions, monitorCommands: this.isMonitoringEnabled });
|
|
167
|
-
connection = mongoose_1.default.connection;
|
|
168
|
-
}
|
|
169
|
-
else {
|
|
170
|
-
connection = await mongoose_1.default.createConnection(connectionString, connectionOptions).asPromise();
|
|
171
|
-
}
|
|
172
|
-
if (this.isMonitoringEnabled) {
|
|
173
|
-
this.connectMonitor(connection);
|
|
174
|
-
}
|
|
175
|
-
connection.on('error', (err) => {
|
|
176
|
-
this.logger.error('Mongo connection error', { err, type });
|
|
177
|
-
});
|
|
178
|
-
return { connection, connectionString, connectionOptions };
|
|
179
|
-
}
|
|
180
|
-
catch (err) {
|
|
181
|
-
this.logger.error('Failed to connect to Database', { type, err });
|
|
182
|
-
throw new errors_1.DatabaseError('Failed to connect to Database');
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Begins a new MongoDB transaction by starting a session and transaction
|
|
187
|
-
*
|
|
188
|
-
* @param options - Optional transaction options to override the default transaction settings
|
|
189
|
-
* @returns A ClientSession with an active transaction
|
|
190
|
-
* @throws {DatabaseError} If the database connection is undefined or transaction fails to start
|
|
191
|
-
*
|
|
192
|
-
* @remarks
|
|
193
|
-
* - Uses the main database connection to start the session
|
|
194
|
-
* - Applies default transaction options (readPreference: primary)
|
|
195
|
-
* - Will abort transaction and end session if any errors occur during setup
|
|
196
|
-
*/
|
|
197
|
-
async beginTransaction(options) {
|
|
198
|
-
const connection = this.db[interfaces_1.DbType.Main]?.connection;
|
|
199
|
-
if (!connection) {
|
|
200
|
-
throw new errors_1.DatabaseError('Connection is undefined');
|
|
201
|
-
}
|
|
202
|
-
const session = await connection.startSession();
|
|
203
|
-
try {
|
|
204
|
-
const transactionOptions = {
|
|
205
|
-
...this.defaultTransactionOptions,
|
|
206
|
-
...options,
|
|
207
|
-
};
|
|
208
|
-
session.startTransaction(transactionOptions);
|
|
209
|
-
return session;
|
|
210
|
-
}
|
|
211
|
-
catch (err) {
|
|
212
|
-
await session.abortTransaction();
|
|
213
|
-
await session.endSession();
|
|
214
|
-
throw new errors_1.DatabaseError('Unable to begin transaction', { err });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
connectMonitor(connection) {
|
|
218
|
-
try {
|
|
219
|
-
const client = connection.getClient();
|
|
220
|
-
client.on('commandStarted', (event) => {
|
|
221
|
-
const db = event.databaseName;
|
|
222
|
-
const opType = event.commandName;
|
|
223
|
-
const key = `${event.requestId}${event.connectionId}`;
|
|
224
|
-
const parsedCollection = Number.parseInt(event.command[opType] || '', 10);
|
|
225
|
-
const collection = Number.isNaN(parsedCollection) ? event.command[opType] : undefined;
|
|
226
|
-
this.dbOplog[key] = {
|
|
227
|
-
database: db,
|
|
228
|
-
collection,
|
|
229
|
-
opType,
|
|
230
|
-
};
|
|
231
|
-
this.logger.debug('Mongodb commandStarted operation', {
|
|
232
|
-
database: db,
|
|
233
|
-
collection,
|
|
234
|
-
operation: opType,
|
|
235
|
-
command: event.command,
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
client.on('commandSucceeded', (event) => {
|
|
239
|
-
const key = `${event.requestId}${event.connectionId}`;
|
|
240
|
-
const oplogEntry = this.dbOplog[key];
|
|
241
|
-
const durationMs = event.duration;
|
|
242
|
-
delete this.dbOplog[key];
|
|
243
|
-
this.logger.debug('Mongodb commandSucceeded operation', {
|
|
244
|
-
database: oplogEntry.database,
|
|
245
|
-
operation: oplogEntry.opType,
|
|
246
|
-
collection: oplogEntry.collection,
|
|
247
|
-
});
|
|
248
|
-
this.dbOperationsHistogram?.observe({
|
|
249
|
-
operation: oplogEntry.opType,
|
|
250
|
-
status: interfaces_1.MongodbOperationResult.Successful,
|
|
251
|
-
database: oplogEntry.database,
|
|
252
|
-
...(oplogEntry.collection && { collection: oplogEntry.collection }),
|
|
253
|
-
}, durationMs / 1000);
|
|
254
|
-
});
|
|
255
|
-
client.on('commandFailed', (event) => {
|
|
256
|
-
const key = `${event.requestId}${event.connectionId}`;
|
|
257
|
-
const oplogEntry = this.dbOplog[key];
|
|
258
|
-
const durationMs = event.duration;
|
|
259
|
-
delete this.dbOplog[key];
|
|
260
|
-
this.logger.debug('Mongodb commandFailed operation', {
|
|
261
|
-
database: oplogEntry.database,
|
|
262
|
-
operation: oplogEntry.opType,
|
|
263
|
-
collection: oplogEntry.collection,
|
|
264
|
-
});
|
|
265
|
-
this.dbOperationsHistogram?.observe({
|
|
266
|
-
operation: oplogEntry.opType,
|
|
267
|
-
status: interfaces_1.MongodbOperationResult.Failed,
|
|
268
|
-
database: oplogEntry.database,
|
|
269
|
-
...(oplogEntry.collection && { collection: oplogEntry.collection }),
|
|
270
|
-
}, durationMs / 1000);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
catch (err) {
|
|
274
|
-
this.logger.error("Couldn't attach commands monitor", { err });
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
oplogObserver() {
|
|
278
|
-
this.dbOplogObserver?.observe({}, Object.keys(this.dbOplog).length);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
exports.DatabaseService = DatabaseService;
|
|
282
|
-
//# sourceMappingURL=database.js.map
|
|
255
|
+
//#endregion
|
|
256
|
+
export { DatabaseService };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { DatabaseAdapterType, PostgresDatabase } from "../interfaces/database.js";
|
|
2
|
+
import { EncryptedStorage } from "../interfaces/models/encryptedStorage.js";
|
|
3
|
+
import { Model, QueryOptions } from "mongoose";
|
|
4
|
+
import { Logger } from "@diia-inhouse/types";
|
|
5
|
+
import { EnvService } from "@diia-inhouse/env";
|
|
6
|
+
import { AuthService } from "@diia-inhouse/crypto";
|
|
7
|
+
|
|
8
|
+
//#region src/services/encryptedStorage.d.ts
|
|
9
|
+
/**
|
|
10
|
+
* EncryptedStorageService provides CRUD operations for managing encrypted storage entries
|
|
11
|
+
* in both Postgres and MongoDB databases. The service abstracts database implementation details
|
|
12
|
+
* using a strategy pattern depending on the configured database adapter type.
|
|
13
|
+
*
|
|
14
|
+
* Suitable for storing and managing sensitive data in an encrypted manner with specific expiration time.
|
|
15
|
+
*
|
|
16
|
+
* This service should be wrapped in a repository.
|
|
17
|
+
*
|
|
18
|
+
* Register this service in the DI container with the following (MongoDB) configuration (AuthService is located in @diia-inhouse/crypto):
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // src/deps.ts
|
|
21
|
+
* encryptedStorage: asClass(EncryptedStorageService, { injector: () => ({ encryptedStorageModel }) }).singleton(),
|
|
22
|
+
* auth: asClass(AuthService, { injector: () => ({ authConfig: config.auth }) }).singleton()
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* // src/config.ts
|
|
27
|
+
* auth: {
|
|
28
|
+
* jwk: await envService.getSecret('JWE_SECRET_DATA_JWK'),
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* ```typescript
|
|
33
|
+
* // src/models/encryptedStorage.ts
|
|
34
|
+
* import { EncryptedStorage, Model, encryptedStorageSchema, model, models } from '@diia-inhouse/db'
|
|
35
|
+
*
|
|
36
|
+
* export default (models.EncryptedStorage as Model<EncryptedStorage>) || model('EncryptedStorage', encryptedStorageSchema)
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* Use the following command to generate a JWE secret data JWK:
|
|
40
|
+
* node -e "const crypto = require('crypto'); const { privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 }); const jwk = privateKey.export({ format: 'jwk' }); jwk.use = 'enc'; console.log(JSON.stringify(jwk));"
|
|
41
|
+
* ```typescript
|
|
42
|
+
* // .env.example
|
|
43
|
+
* JWE_SECRET_DATA_JWK={your-jwk-here}
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
declare class EncryptedStorageService {
|
|
47
|
+
private readonly logger;
|
|
48
|
+
private readonly auth;
|
|
49
|
+
private readonly envService;
|
|
50
|
+
private readonly databaseAdapter;
|
|
51
|
+
private readonly encryptedStorageModel;
|
|
52
|
+
private readonly postgresDatabase;
|
|
53
|
+
private readonly saveEntityStrategy;
|
|
54
|
+
private readonly updateEntityStrategy;
|
|
55
|
+
private readonly findEntityStrategy;
|
|
56
|
+
private readonly deleteManyStrategy;
|
|
57
|
+
private readonly removeEntityStrategy;
|
|
58
|
+
private readonly findExpirationStrategy;
|
|
59
|
+
constructor(logger: Logger, auth: AuthService, envService: EnvService, databaseAdapter: DatabaseAdapterType, encryptedStorageModel?: Model<EncryptedStorage> | null, postgresDatabase?: PostgresDatabase | null);
|
|
60
|
+
save<T>(data: T, expiration: number): Promise<string>;
|
|
61
|
+
get<T>(id: string, options?: QueryOptions): Promise<T>;
|
|
62
|
+
getSafe<T>(id: string, options?: QueryOptions): Promise<T | undefined>;
|
|
63
|
+
update<T>(id: string, data: T): Promise<void>;
|
|
64
|
+
remove(id: string): Promise<void>;
|
|
65
|
+
deleteMany(ids: string[]): Promise<void>;
|
|
66
|
+
setExpiration(id: string, expiration: number): Promise<void>;
|
|
67
|
+
getExpiration(id: string): Promise<Date>;
|
|
68
|
+
private getStorageItemById;
|
|
69
|
+
private getModel;
|
|
70
|
+
private getPostgresDatabase;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { EncryptedStorageService };
|