zync-nest-data-module 1.0.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.
Files changed (84) hide show
  1. package/README.md +672 -0
  2. package/dist/backup/backup.config.d.ts +4 -0
  3. package/dist/backup/backup.config.js +84 -0
  4. package/dist/backup/backup.config.js.map +1 -0
  5. package/dist/backup/backup.interface.d.ts +21 -0
  6. package/dist/backup/backup.interface.js +3 -0
  7. package/dist/backup/backup.interface.js.map +1 -0
  8. package/dist/backup/backup.module.d.ts +2 -0
  9. package/dist/backup/backup.module.js +24 -0
  10. package/dist/backup/backup.module.js.map +1 -0
  11. package/dist/backup/backup.service.d.ts +19 -0
  12. package/dist/backup/backup.service.js +229 -0
  13. package/dist/backup/backup.service.js.map +1 -0
  14. package/dist/backup/index.d.ts +4 -0
  15. package/dist/backup/index.js +21 -0
  16. package/dist/backup/index.js.map +1 -0
  17. package/dist/database/database.module.d.ts +2 -0
  18. package/dist/database/database.module.js +34 -0
  19. package/dist/database/database.module.js.map +1 -0
  20. package/dist/database/database.repository.d.ts +62 -0
  21. package/dist/database/database.repository.js +259 -0
  22. package/dist/database/database.repository.js.map +1 -0
  23. package/dist/database/database.scheme.d.ts +45 -0
  24. package/dist/database/database.scheme.js +187 -0
  25. package/dist/database/database.scheme.js.map +1 -0
  26. package/dist/database/database.service.d.ts +7 -0
  27. package/dist/database/database.service.js +39 -0
  28. package/dist/database/database.service.js.map +1 -0
  29. package/dist/database/database.sync.d.ts +10 -0
  30. package/dist/database/database.sync.js +44 -0
  31. package/dist/database/database.sync.js.map +1 -0
  32. package/dist/database/database.transaction.d.ts +17 -0
  33. package/dist/database/database.transaction.js +101 -0
  34. package/dist/database/database.transaction.js.map +1 -0
  35. package/dist/database/database.uniqueId.d.ts +25 -0
  36. package/dist/database/database.uniqueId.js +68 -0
  37. package/dist/database/database.uniqueId.js.map +1 -0
  38. package/dist/database/database.utils.d.ts +10 -0
  39. package/dist/database/database.utils.js +119 -0
  40. package/dist/database/database.utils.js.map +1 -0
  41. package/dist/database/index.d.ts +8 -0
  42. package/dist/database/index.js +25 -0
  43. package/dist/database/index.js.map +1 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +20 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/service/index.d.ts +1 -0
  48. package/dist/service/index.js +18 -0
  49. package/dist/service/index.js.map +1 -0
  50. package/dist/service/service.d.ts +40 -0
  51. package/dist/service/service.js +182 -0
  52. package/dist/service/service.js.map +1 -0
  53. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  54. package/libs/src/app.controller.ts +84 -0
  55. package/libs/src/app.module.ts +31 -0
  56. package/libs/src/backup/backup.config.ts +45 -0
  57. package/libs/src/backup/backup.interface.ts +21 -0
  58. package/libs/src/backup/backup.module.ts +11 -0
  59. package/libs/src/backup/backup.service.ts +283 -0
  60. package/libs/src/backup/index.ts +4 -0
  61. package/libs/src/database/database.module.ts +26 -0
  62. package/libs/src/database/database.repository.ts +358 -0
  63. package/libs/src/database/database.scheme.ts +128 -0
  64. package/libs/src/database/database.service.ts +36 -0
  65. package/libs/src/database/database.sync.ts +61 -0
  66. package/libs/src/database/database.transaction.ts +101 -0
  67. package/libs/src/database/database.uniqueId.ts +59 -0
  68. package/libs/src/database/database.utils.ts +99 -0
  69. package/libs/src/database/index.ts +8 -0
  70. package/libs/src/index.ts +3 -0
  71. package/libs/src/main.ts +62 -0
  72. package/libs/src/service/index.ts +1 -0
  73. package/libs/src/service/service.ts +201 -0
  74. package/libs/src/test/test.dto.ts +41 -0
  75. package/libs/src/test/test.module.ts +20 -0
  76. package/libs/src/test/test.repository.ts +44 -0
  77. package/libs/src/test/test.resolver.ts +44 -0
  78. package/libs/src/test/test.schema.ts +21 -0
  79. package/libs/src/test/test.service.ts +19 -0
  80. package/libs/tsconfig.lib.json +19 -0
  81. package/nest-cli.json +16 -0
  82. package/package.json +89 -0
  83. package/tsconfig.json +29 -0
  84. package/update-links.js +159 -0
@@ -0,0 +1,358 @@
1
+ import { InjectModel } from "@nestjs/mongoose";
2
+ import moment from "moment";
3
+ import { Document, FilterQuery, PipelineStage, Schema, Types } from "mongoose";
4
+ import { SoftDeleteModel } from "mongoose-delete";
5
+ import {
6
+ handlePageFacet,
7
+ handlePageResult,
8
+ IPageParams,
9
+ IPageResult,
10
+ } from "./database.scheme";
11
+ import { TransactionSession } from "./database.transaction";
12
+ import { ApUniqueIdGenerator, UniqueKeyTypes } from "./database.uniqueId";
13
+ import { DbUtils } from "./database.utils";
14
+ import { Inject } from '@nestjs/common';
15
+
16
+ export interface IRefInput<T> {
17
+ prefix?: string;
18
+ key?: UniqueKeyTypes;
19
+ filter?: FilterQuery<T>;
20
+ }
21
+
22
+ export abstract class AbstractBaseRepository<
23
+ T extends Omit<Document<any, any, any>, "delete">
24
+ > {
25
+ private _session: TransactionSession = null;
26
+
27
+ @Inject(ApUniqueIdGenerator)
28
+ private uniqueIdGenerator: ApUniqueIdGenerator ;
29
+
30
+ constructor(
31
+ @InjectModel("MODEL_NAME")
32
+ protected dbModel: SoftDeleteModel<T>
33
+ ) {}
34
+
35
+ protected abstract buildQuery(query: Partial<T>): any;
36
+
37
+ public abstract mapData(data: any, isCreate: boolean): Promise<T>;
38
+
39
+ public set session(v: TransactionSession) {
40
+ this._session = v;
41
+ }
42
+
43
+ public get session(): TransactionSession {
44
+ return this._session;
45
+ }
46
+
47
+ private get hasSession(): boolean {
48
+ return !!this._session && !this._session.hasEnded;
49
+ }
50
+
51
+ protected mapIds(query: Partial<T>): any {
52
+ return DbUtils.mapToObjectIds<T>(query);
53
+ }
54
+
55
+ public async create(data: Partial<T> | T): Promise<T> {
56
+ return await this._create(data);
57
+ }
58
+
59
+ public async findById(id: string): Promise<T> {
60
+ return await this._findById(id);
61
+ }
62
+
63
+ public async findOne(query: FilterQuery<T>): Promise<T> {
64
+ return await this._findOne(query);
65
+ }
66
+
67
+ public async find(query: FilterQuery<T>): Promise<T[]> {
68
+ return await this._find(query);
69
+ }
70
+
71
+ public async update(id: string, data: Partial<T>): Promise<T> {
72
+ return this._update(id, data);
73
+ }
74
+
75
+ public async updateMany(query: Partial<T>, model: Partial<T>): Promise<void> {
76
+ await this._updateMany(query, model);
77
+ }
78
+
79
+ public async delete(id: string): Promise<void> {
80
+ await this._delete(id);
81
+ }
82
+
83
+ public async deleteMany(query: Partial<T>): Promise<void> {
84
+ await this._deleteMany(query);
85
+ }
86
+
87
+ public async aggregate(pipeline?: PipelineStage[]): Promise<T[]> {
88
+ if (!this.hasSession)
89
+ return await this.dbModel.aggregate(pipeline).session(this.session);
90
+
91
+ return await this.dbModel.aggregate(pipeline).session(this.session);
92
+ }
93
+
94
+ public async page(page: IPageParams): Promise<IPageResult<T>> {
95
+ return await this._page(page);
96
+ }
97
+
98
+ public async findLast(query: FilterQuery<T>): Promise<T> {
99
+ return await this._findLast(query);
100
+ }
101
+
102
+ public async count(query?: FilterQuery<T>): Promise<number> {
103
+ return this._count(query);
104
+ }
105
+
106
+ protected async _count(query: FilterQuery<T>): Promise<number> {
107
+ query = this.mapIds(query) as FilterQuery<T>;
108
+
109
+ return this.dbModel.countDocuments(query as FilterQuery<T>);
110
+ }
111
+
112
+ protected async _create(data: Partial<T> | T): Promise<T> {
113
+ const payload = await this.mapData(data, true);
114
+
115
+ payload._id = payload._id
116
+ ? new Types.ObjectId(payload._id?.toString())
117
+ : this.getObjectId();
118
+
119
+ const created = new this.dbModel(payload);
120
+
121
+ if (!this.hasSession) {
122
+ const saved = await created.save();
123
+
124
+ return await this.findById(saved._id);
125
+ }
126
+
127
+ const saved = await created.save({ session: this.session });
128
+
129
+ return await this.findById(saved._id);
130
+ }
131
+
132
+ protected async _findById(id: string): Promise<T> {
133
+ const findQuery = this.dbModel.findById(new Types.ObjectId(id?.toString()));
134
+
135
+ if (this.hasSession) {
136
+ findQuery.session(this.session);
137
+ }
138
+
139
+ return findQuery.exec().then((res) => (res as any)?._doc || res);
140
+ }
141
+
142
+ protected async _findOne(query: FilterQuery<T>): Promise<T> {
143
+ query = this.mapIds(query) as FilterQuery<T>;
144
+
145
+ const findQuery = this.dbModel.findOne(query as FilterQuery<T>);
146
+
147
+ if (this.hasSession) {
148
+ findQuery.session(this.session);
149
+ }
150
+
151
+ return findQuery.exec().then((res) => (res as any)?._doc || res);
152
+ }
153
+
154
+ protected async _find(query: FilterQuery<T>): Promise<T[]> {
155
+ if (query._id) {
156
+ query._id = new Types.ObjectId(query._id);
157
+ }
158
+
159
+ query = this.mapIds(query) as FilterQuery<T>;
160
+
161
+ const findQuery = this.dbModel
162
+ .find(query as FilterQuery<T>)
163
+ .sort({ createdAt: -1 });
164
+
165
+ if (this.hasSession) {
166
+ findQuery.session(this.session);
167
+ }
168
+
169
+ return findQuery.exec().then((res) => (res as any)?._doc || res);
170
+ }
171
+
172
+ protected async _update(id: string, data: Partial<T>): Promise<T> {
173
+ delete data._id;
174
+
175
+ const payload = await this.mapData(data, false);
176
+
177
+ await this.dbModel
178
+ .updateOne(
179
+ { _id: new Types.ObjectId(id) },
180
+ { $set: payload },
181
+ {
182
+ ...(this.hasSession && { session: this.session }),
183
+ }
184
+ )
185
+ .exec();
186
+
187
+ return await this.findById(id);
188
+ }
189
+
190
+ protected async _updateMany(
191
+ query: Partial<T>,
192
+ model: Partial<T>
193
+ ): Promise<void> {
194
+ delete model._id;
195
+
196
+ query = this.mapIds(query) as FilterQuery<T>;
197
+
198
+ model = this.mapIds(model);
199
+
200
+ await this.dbModel
201
+ .updateMany(
202
+ query as FilterQuery<T>,
203
+ {
204
+ ...model,
205
+ updatedAt: Date.now(),
206
+ },
207
+ this.hasSession ? { new: true, session: this.session } : {}
208
+ )
209
+ .exec();
210
+ }
211
+
212
+ protected async _delete(id: string): Promise<void> {
213
+ const deleteQuery = this.dbModel.findByIdAndDelete(new Types.ObjectId(id));
214
+
215
+ if (this.hasSession) {
216
+ deleteQuery.session(this.session);
217
+ }
218
+
219
+ await deleteQuery.exec();
220
+ }
221
+
222
+ protected async _deleteMany(query: Partial<T>): Promise<void> {
223
+ query = this.mapIds(query) as FilterQuery<T>;
224
+
225
+ const deleteQuery = this.dbModel.deleteMany(query as FilterQuery<T>);
226
+
227
+ if (this.hasSession) {
228
+ deleteQuery.session(this.session);
229
+ }
230
+
231
+ await deleteQuery.exec();
232
+ }
233
+
234
+ protected async _findLast(query: FilterQuery<T>): Promise<T> {
235
+ query = this.mapIds(query) as FilterQuery<T>;
236
+
237
+ const findQuery = this.dbModel
238
+ .find(query as FilterQuery<T>)
239
+ .limit(1)
240
+ .sort({ createdAt: -1 });
241
+
242
+ if (this.hasSession) {
243
+ findQuery.session(this.session);
244
+ }
245
+
246
+ const result = await findQuery.exec();
247
+
248
+ return result.length === 0 ? null : result[0];
249
+ }
250
+
251
+ protected async _page(
252
+ page: IPageParams,
253
+ $lookups?: any[]
254
+ ): Promise<IPageResult<T>> {
255
+ const query = this.buildQuery(page as any);
256
+ return this.aggregate([
257
+ ...(!!$lookups?.length ? $lookups : []),
258
+ ...(Array.isArray(query) ? query : [query]),
259
+ { $sort: { createdAt: -1 } },
260
+ { ...handlePageFacet(page) },
261
+ ]).then(handlePageResult<T>);
262
+ }
263
+
264
+ public schemaKeysQuery(
265
+ schema: Schema,
266
+ options: {
267
+ query: Partial<
268
+ T & {
269
+ fromDate?: number;
270
+ toDate?: number;
271
+ ref?: string;
272
+ keyword?: string;
273
+ }
274
+ >;
275
+ searchKeys?: string[];
276
+ ignoreKeys?: string[];
277
+ },
278
+ addMatchCondition: (condition: any) => void
279
+ ) {
280
+ const { query, ignoreKeys } = options;
281
+
282
+ Object.keys(schema.obj).forEach((key) => {
283
+ if (query[key] && !ignoreKeys?.includes(key)) {
284
+ addMatchCondition({
285
+ [key]:
286
+ schema.path(key).instance === "Mixed"
287
+ ? this.toObjectId(query[key])
288
+ : query[key],
289
+ });
290
+ }
291
+ });
292
+
293
+ if (query?.keyword && options.searchKeys?.length) {
294
+ const keywordValue = query?.keyword;
295
+ const globalSearchConditions = options.searchKeys?.map((field) => ({
296
+ [field]: { $regex: keywordValue, $options: "i" },
297
+ }));
298
+
299
+ if (globalSearchConditions.length > 0) {
300
+ addMatchCondition({ $or: globalSearchConditions });
301
+ }
302
+ }
303
+
304
+ if (query?.fromDate && query?.toDate) {
305
+ addMatchCondition({
306
+ createdAt: {
307
+ $gte: moment(query.fromDate).startOf("day").valueOf(),
308
+ $lte: moment(query.toDate).endOf("day").valueOf(),
309
+ },
310
+ });
311
+ }
312
+
313
+ if (query?.ref) {
314
+ addMatchCondition({
315
+ ref: { $regex: query.ref?.toUpperCase(), $options: "i" },
316
+ });
317
+ }
318
+ }
319
+
320
+ public cleanupQuery(andConditions: any[]) {
321
+ // If no conditions, match all
322
+ if (andConditions.length === 0) {
323
+ return { $match: {} };
324
+ }
325
+
326
+ // If only one condition, avoid unnecessary $and
327
+ if (andConditions.length === 1) {
328
+ return { $match: andConditions[0] };
329
+ }
330
+
331
+ // Otherwise, use $and
332
+ return { $match: { $and: andConditions } };
333
+ }
334
+
335
+ public getObjectId(): string {
336
+ return this.uniqueIdGenerator.getObjectId();
337
+ }
338
+
339
+ public toObjectId(value: string | any): Types.ObjectId {
340
+ return new Types.ObjectId(value?.toString());
341
+ }
342
+
343
+ public async generateUniqueId(key: UniqueKeyTypes): Promise<number> {
344
+ return await this.uniqueIdGenerator.getNextUniqueId(key);
345
+ }
346
+
347
+ public async generateRef(ref?: IRefInput<T>): Promise<string> {
348
+ const key = ref?.key || "uniqueId";
349
+ const prefix = ref?.prefix;
350
+ try {
351
+ const newRefNumber = await this.uniqueIdGenerator.getNextUniqueId(key);
352
+ return prefix ? `${prefix}-${newRefNumber}` : `${newRefNumber}`;
353
+ } catch (error) {
354
+ console.error("Error generating reference:", error);
355
+ throw new Error("Failed to generate reference");
356
+ }
357
+ }
358
+ }
@@ -0,0 +1,128 @@
1
+ import { Prop } from "@nestjs/mongoose";
2
+ import moment from "moment";
3
+ import mongoose, { Types } from "mongoose";
4
+
5
+ export class BaseSchema {
6
+ @Prop({ set: (val) => BaseSchema.toObjectId(val) })
7
+ _id: Types.ObjectId;
8
+ @Prop({ required: true, unique: true })
9
+ ref: string;
10
+ @Prop({ set: (val) => BaseSchema.toObjectId(val) })
11
+ branchId: Types.ObjectId;
12
+ @Prop({ set: (val) => moment(val).valueOf() })
13
+ createdAt: number;
14
+ @Prop({ set: (val) => new Types.ObjectId(val) })
15
+ createdBy: Types.ObjectId;
16
+ @Prop({ set: (val) => moment(val).valueOf() })
17
+ updatedAt: number;
18
+ @Prop({ set: (val) => new Types.ObjectId(val) })
19
+ updatedBy: Types.ObjectId;
20
+ @Prop({ set: (val) => moment(val).valueOf() })
21
+ deletedAt: number;
22
+ @Prop({ set: (val) => new Types.ObjectId(val) })
23
+ deletedBy: Types.ObjectId;
24
+ @Prop({ set: (val) => moment(val).valueOf() })
25
+ deleted: boolean;
26
+
27
+ @Prop({ set: (val) => moment(val).valueOf() })
28
+ documentDate: number;
29
+
30
+ @Prop({
31
+ validate: {
32
+ validator: function (v) {
33
+ // If empty, skip unique validation (allow multiple empty)
34
+ return (
35
+ v === undefined ||
36
+ v === null ||
37
+ v === "" ||
38
+ this.model(this.constructor.modelName)
39
+ .countDocuments({
40
+ documentNumber: v,
41
+ _id: { $ne: BaseSchema.toObjectId(this._id) },
42
+ })
43
+ .then((count) => count === 0)
44
+ );
45
+ },
46
+ message: "documentNumber must be unique if not empty",
47
+ },
48
+ })
49
+ documentNumber: string;
50
+
51
+ @Prop({ default: false })
52
+ canDelete: boolean;
53
+
54
+ @Prop({ default: false })
55
+ canUpdate: boolean;
56
+
57
+ @Prop({ set: (val) => (val === "" ? undefined : new Types.ObjectId(val)) })
58
+ storeId: Types.ObjectId;
59
+
60
+ @Prop({ set: (val) => new Types.ObjectId(val) })
61
+ companyId: Types.ObjectId;
62
+
63
+ @Prop({ set: (val) => new Types.ObjectId(val) })
64
+ employeeId: Types.ObjectId;
65
+
66
+ static toObjectId(value: string) {
67
+ try {
68
+ if (Array.isArray(value)) {
69
+ return value.map((v) =>
70
+ mongoose.Types.ObjectId.isValid(v)
71
+ ? new mongoose.Types.ObjectId(v)
72
+ : v
73
+ );
74
+ } else if (mongoose.Types.ObjectId.isValid(value)) {
75
+ return new mongoose.Types.ObjectId(value);
76
+ } else if (typeof value === "string") {
77
+ return new mongoose.Types.ObjectId(value);
78
+ }
79
+ } catch (error) {
80
+ return value;
81
+ }
82
+ }
83
+
84
+ static toUnixTimestamp(value: number | string) {
85
+ if (!value) {
86
+ return moment().valueOf();
87
+ }
88
+
89
+ if (typeof value === "number" && !Number.isNaN(value)) {
90
+ return value;
91
+ }
92
+
93
+ const momentDate = moment(value);
94
+ if (momentDate.isValid()) {
95
+ return momentDate.valueOf();
96
+ }
97
+
98
+ return moment().valueOf();
99
+ }
100
+ }
101
+
102
+ export interface IPageParams extends BaseSchema {
103
+ skip: number;
104
+ take: number;
105
+ }
106
+
107
+ export interface IPageResult<T> {
108
+ totalRecords: number;
109
+ data: Array<T>;
110
+ }
111
+
112
+ export const handlePageFacet = (page: any) => {
113
+ return {
114
+ $facet: {
115
+ data: [{ $skip: Number(page.skip) }, { $limit: Number(page.take) }],
116
+ totalRecords: [{ $count: "count" }],
117
+ },
118
+ };
119
+ };
120
+
121
+ export const handlePageResult = <T>(res: any): IPageResult<T> => {
122
+ let rs = res[0] as any;
123
+ if (rs.totalRecords.length)
124
+ rs = { ...rs, totalRecords: rs.totalRecords[0].count };
125
+ else rs = { ...rs, totalRecords: 0 };
126
+
127
+ return rs;
128
+ };
@@ -0,0 +1,36 @@
1
+ import { Injectable } from "@nestjs/common";
2
+ import { Connection, createConnection } from "mongoose";
3
+
4
+ @Injectable({})
5
+ export class DatabaseService {
6
+ private _connectionId: string = process.env.mongodb_default_db_name;
7
+ private connections: Map<string, Connection> = new Map();
8
+
9
+ public setConnection(companyId: string): Connection {
10
+ this._connectionId = companyId
11
+ ? companyId
12
+ : process.env.mongodb_default_db_name;
13
+
14
+ // Check if the connection already exists
15
+ if (this.connections.has(companyId)) {
16
+ return this.connections.get(companyId);
17
+ }
18
+
19
+ // Create new connection
20
+ const connection = createConnection(
21
+ process.env.mongodb_url?.replace("{{db_name}}", companyId)
22
+ );
23
+
24
+ this.connections.set(companyId, connection);
25
+
26
+ return connection;
27
+ }
28
+
29
+ public getConnection(): Connection {
30
+ // Check if the connection already exists
31
+ if (this.connections.has(this._connectionId)) {
32
+ return this.connections.get(this._connectionId);
33
+ }
34
+ return this.setConnection(this._connectionId);
35
+ }
36
+ }
@@ -0,0 +1,61 @@
1
+ import { config } from "dotenv";
2
+ import { MongoClient, ChangeStreamDocument, Document, WithId } from "mongodb";
3
+ config();
4
+
5
+ interface RealtimeSyncOptions {
6
+ sourceUri: string;
7
+ targetUri: string;
8
+ collection: string;
9
+ sourceClient: MongoClient;
10
+ targetClient: MongoClient;
11
+ }
12
+
13
+ export async function realtimeSync(options: RealtimeSyncOptions): Promise<void> {
14
+ const { sourceUri, targetUri, collection, sourceClient, targetClient } = options;
15
+ // const sourceClient = new MongoClient(sourceUri);
16
+ // const targetClient = new MongoClient(targetUri);
17
+
18
+ await sourceClient.connect();
19
+ await targetClient.connect();
20
+
21
+ // Extract DB name from connection string
22
+ const getDbName = (uri: string) => {
23
+ const match = uri.match(/\/(\w+)(\?|$)/);
24
+ return match ? match[1] : undefined;
25
+ };
26
+ const sourceDB = getDbName(sourceUri);
27
+ const targetDB = getDbName(targetUri);
28
+
29
+ console.log(`🔗 Connected to source DB: ${sourceDB}`);
30
+ console.log(`🔗 Connected to target DB: ${targetDB}`);
31
+
32
+ if (!sourceDB || !targetDB) {
33
+ throw new Error("Database name must be specified in both connection strings.");
34
+ }
35
+
36
+ const source = sourceClient.db(sourceDB).collection(collection);
37
+ const target = targetClient.db(targetDB).collection(collection);
38
+
39
+ const changeStream = source.watch([], { fullDocument: "updateLookup" });
40
+
41
+ console.log(`📡 Watching changes on ${sourceDB}.${collection}...`);
42
+
43
+ changeStream.on("change", async (change: ChangeStreamDocument<WithId<Document>>) => {
44
+ const { operationType, fullDocument, documentKey }: any = change;
45
+
46
+ try {
47
+ if (operationType === "insert" && fullDocument) {
48
+ // Use replaceOne with upsert to handle duplicates gracefully
49
+ await target.replaceOne({ _id: fullDocument._id }, fullDocument, { upsert: true });
50
+ } else if ((operationType === "update" || operationType === "replace") && fullDocument) {
51
+ await target.replaceOne({ _id: documentKey._id }, fullDocument, { upsert: true });
52
+ } else if (operationType === "delete") {
53
+ await target.deleteOne({ _id: documentKey._id });
54
+ }
55
+
56
+ console.log(`🔄 Synced ${operationType} for _id=${documentKey._id}`);
57
+ } catch (error) {
58
+ console.error(`❌ Error syncing ${operationType} for _id=${documentKey._id}:`, error);
59
+ }
60
+ });
61
+ }
@@ -0,0 +1,101 @@
1
+ import { Injectable, Scope } from "@nestjs/common";
2
+ import { InjectConnection } from "@nestjs/mongoose";
3
+ import { MongoServerError } from "mongodb"; // Import MongoServerError if using native MongoDB driver
4
+ import mongodb, { ClientSession, Connection } from "mongoose";
5
+ import { DatabaseService } from "./database.service";
6
+
7
+ export type TransactionSession = mongodb.ClientSession;
8
+
9
+ @Injectable({ scope: Scope.DEFAULT })
10
+ export class TransactionManager {
11
+ private _session: ClientSession;
12
+ private _sessionName: string;
13
+ constructor(
14
+ @InjectConnection() private readonly connection: Connection,
15
+ private dbSvc: DatabaseService
16
+ ) { }
17
+
18
+ private async getSession(): Promise<ClientSession> {
19
+ return await this.dbSvc.getConnection().startSession();
20
+ }
21
+
22
+ public async startTransaction(): Promise<TransactionSession> {
23
+ this._session = await this.getSession();
24
+
25
+ this._session.startTransaction({
26
+ readConcern: { level: "local" },
27
+ writeConcern: { w: "majority", j: true },
28
+ readPreference: "primary",
29
+ });
30
+ return this._session as TransactionSession;
31
+ }
32
+
33
+ public async commitTransaction(retries: number = 3): Promise<void> {
34
+ // if (this._session && this._session.transaction.isActive) {
35
+ // try {
36
+ // await this._session.commitTransaction();
37
+ // } catch (error) {
38
+ // console.error("Transaction commit failed:", error);
39
+ // await this.abortTransaction();
40
+ // } finally {
41
+ // await this._session.endSession();
42
+ // }
43
+ // }
44
+ if (this._session && this._session.transaction.isActive) {
45
+ let attempt = 0;
46
+ while (attempt < retries) {
47
+ try {
48
+ await this._session.commitTransaction();
49
+ return;
50
+ } catch (error) {
51
+ if (
52
+ error instanceof MongoServerError &&
53
+ error.message.includes("Write conflict during plan execution")
54
+ ) {
55
+ attempt++;
56
+ if (attempt >= retries) {
57
+ throw error;
58
+ }
59
+ console.warn(
60
+ `Write conflict detected. Retrying commit (${attempt}/${retries})...`
61
+ );
62
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait for 1 second before retrying
63
+ } else {
64
+ throw error;
65
+ }
66
+ } finally {
67
+ await this._session.endSession();
68
+ // await this.dbSvc.getConnection().close();
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ public async abortTransaction(): Promise<void> {
75
+ if (this._session && this._session.transaction.isActive) {
76
+ try {
77
+ await this._session.abortTransaction();
78
+ await this._session.endSession();
79
+ } catch (error) {
80
+ } finally {
81
+ await this._session.endSession();
82
+ }
83
+ }
84
+ }
85
+
86
+ public async endTransaction(): Promise<void> {
87
+ if (this._session) {
88
+ try {
89
+ await this._session.endSession();
90
+ } catch (error) { }
91
+ }
92
+ }
93
+
94
+ public get sessionName(): string {
95
+ return this._sessionName;
96
+ }
97
+
98
+ public set sessionName(name: string) {
99
+ this._sessionName = name;
100
+ }
101
+ }