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.
- package/README.md +672 -0
- package/dist/backup/backup.config.d.ts +4 -0
- package/dist/backup/backup.config.js +84 -0
- package/dist/backup/backup.config.js.map +1 -0
- package/dist/backup/backup.interface.d.ts +21 -0
- package/dist/backup/backup.interface.js +3 -0
- package/dist/backup/backup.interface.js.map +1 -0
- package/dist/backup/backup.module.d.ts +2 -0
- package/dist/backup/backup.module.js +24 -0
- package/dist/backup/backup.module.js.map +1 -0
- package/dist/backup/backup.service.d.ts +19 -0
- package/dist/backup/backup.service.js +229 -0
- package/dist/backup/backup.service.js.map +1 -0
- package/dist/backup/index.d.ts +4 -0
- package/dist/backup/index.js +21 -0
- package/dist/backup/index.js.map +1 -0
- package/dist/database/database.module.d.ts +2 -0
- package/dist/database/database.module.js +34 -0
- package/dist/database/database.module.js.map +1 -0
- package/dist/database/database.repository.d.ts +62 -0
- package/dist/database/database.repository.js +259 -0
- package/dist/database/database.repository.js.map +1 -0
- package/dist/database/database.scheme.d.ts +45 -0
- package/dist/database/database.scheme.js +187 -0
- package/dist/database/database.scheme.js.map +1 -0
- package/dist/database/database.service.d.ts +7 -0
- package/dist/database/database.service.js +39 -0
- package/dist/database/database.service.js.map +1 -0
- package/dist/database/database.sync.d.ts +10 -0
- package/dist/database/database.sync.js +44 -0
- package/dist/database/database.sync.js.map +1 -0
- package/dist/database/database.transaction.d.ts +17 -0
- package/dist/database/database.transaction.js +101 -0
- package/dist/database/database.transaction.js.map +1 -0
- package/dist/database/database.uniqueId.d.ts +25 -0
- package/dist/database/database.uniqueId.js +68 -0
- package/dist/database/database.uniqueId.js.map +1 -0
- package/dist/database/database.utils.d.ts +10 -0
- package/dist/database/database.utils.js +119 -0
- package/dist/database/database.utils.js.map +1 -0
- package/dist/database/index.d.ts +8 -0
- package/dist/database/index.js +25 -0
- package/dist/database/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/service/index.d.ts +1 -0
- package/dist/service/index.js +18 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/service.d.ts +40 -0
- package/dist/service/service.js +182 -0
- package/dist/service/service.js.map +1 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/libs/src/app.controller.ts +84 -0
- package/libs/src/app.module.ts +31 -0
- package/libs/src/backup/backup.config.ts +45 -0
- package/libs/src/backup/backup.interface.ts +21 -0
- package/libs/src/backup/backup.module.ts +11 -0
- package/libs/src/backup/backup.service.ts +283 -0
- package/libs/src/backup/index.ts +4 -0
- package/libs/src/database/database.module.ts +26 -0
- package/libs/src/database/database.repository.ts +358 -0
- package/libs/src/database/database.scheme.ts +128 -0
- package/libs/src/database/database.service.ts +36 -0
- package/libs/src/database/database.sync.ts +61 -0
- package/libs/src/database/database.transaction.ts +101 -0
- package/libs/src/database/database.uniqueId.ts +59 -0
- package/libs/src/database/database.utils.ts +99 -0
- package/libs/src/database/index.ts +8 -0
- package/libs/src/index.ts +3 -0
- package/libs/src/main.ts +62 -0
- package/libs/src/service/index.ts +1 -0
- package/libs/src/service/service.ts +201 -0
- package/libs/src/test/test.dto.ts +41 -0
- package/libs/src/test/test.module.ts +20 -0
- package/libs/src/test/test.repository.ts +44 -0
- package/libs/src/test/test.resolver.ts +44 -0
- package/libs/src/test/test.schema.ts +21 -0
- package/libs/src/test/test.service.ts +19 -0
- package/libs/tsconfig.lib.json +19 -0
- package/nest-cli.json +16 -0
- package/package.json +89 -0
- package/tsconfig.json +29 -0
- 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
|
+
}
|