@treatwell/moleculer-essentials 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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/context-factory-BWO3xPWE.d.cts +520 -0
  4. package/dist/context-factory-BWO3xPWE.d.mts +520 -0
  5. package/dist/index-82e1CXJX.cjs +11 -0
  6. package/dist/index-BV1ZqQrU.mjs +351 -0
  7. package/dist/index-DNJWwcZu.mjs +8 -0
  8. package/dist/index-rZl77S1z.cjs +375 -0
  9. package/dist/index.cjs +1570 -0
  10. package/dist/index.d.cts +373 -0
  11. package/dist/index.d.mts +373 -0
  12. package/dist/index.mjs +1535 -0
  13. package/dist/mixins/database.mixin.cjs +1673 -0
  14. package/dist/mixins/database.mixin.d.cts +958 -0
  15. package/dist/mixins/database.mixin.d.mts +958 -0
  16. package/dist/mixins/database.mixin.mjs +1645 -0
  17. package/dist/mixins/encryptor.mixin.cjs +84 -0
  18. package/dist/mixins/encryptor.mixin.d.cts +31 -0
  19. package/dist/mixins/encryptor.mixin.d.mts +31 -0
  20. package/dist/mixins/encryptor.mixin.mjs +81 -0
  21. package/dist/mixins/global-store.mixin.cjs +56 -0
  22. package/dist/mixins/global-store.mixin.d.cts +39 -0
  23. package/dist/mixins/global-store.mixin.d.mts +39 -0
  24. package/dist/mixins/global-store.mixin.mjs +54 -0
  25. package/dist/mixins/jwt.mixin.cjs +118 -0
  26. package/dist/mixins/jwt.mixin.d.cts +43 -0
  27. package/dist/mixins/jwt.mixin.d.mts +43 -0
  28. package/dist/mixins/jwt.mixin.mjs +115 -0
  29. package/dist/mixins/queue.mixin.cjs +420 -0
  30. package/dist/mixins/queue.mixin.d.cts +150 -0
  31. package/dist/mixins/queue.mixin.d.mts +150 -0
  32. package/dist/mixins/queue.mixin.mjs +414 -0
  33. package/dist/mixins/redis.mixin.cjs +50 -0
  34. package/dist/mixins/redis.mixin.d.cts +27 -0
  35. package/dist/mixins/redis.mixin.d.mts +27 -0
  36. package/dist/mixins/redis.mixin.mjs +48 -0
  37. package/dist/mixins/redlock.mixin.cjs +76 -0
  38. package/dist/mixins/redlock.mixin.d.cts +30 -0
  39. package/dist/mixins/redlock.mixin.d.mts +30 -0
  40. package/dist/mixins/redlock.mixin.mjs +74 -0
  41. package/package.json +181 -0
@@ -0,0 +1,1645 @@
1
+ import { ZodType, z, ZodObject } from 'zod/v4';
2
+ import { o as omitFields, d as optionalFields, S as SCHEMA_REF_NAME, O as OBJECTID_TYPE, C as COERCE_ARRAY_ATTRIBUTE, h as zodObjectId, i as zodCoerceArray, j as createOpenAPIResponses } from '../index-BV1ZqQrU.mjs';
3
+ import { Errors } from 'moleculer';
4
+ import { isMatch, isEqual, partition, omit, pick } from 'lodash';
5
+ import { MongoClient } from 'mongodb';
6
+ import { w as wrapMixin } from '../index-DNJWwcZu.mjs';
7
+ import { GlobalStoreMixin } from './global-store.mixin.mjs';
8
+ import 'bson';
9
+ import 'date-fns';
10
+ import 'zod';
11
+
12
+ const { MoleculerClientError } = Errors;
13
+ class EntityNotFoundError extends MoleculerClientError {
14
+ constructor(id) {
15
+ super("Entity not found", 404, "ENTITY_NOT_FOUND", { id });
16
+ }
17
+ }
18
+
19
+ function removeMongoId(schema, refName) {
20
+ return omitFields(schema, ["_id"], refName);
21
+ }
22
+ function optionalMongoId(schema, refName) {
23
+ return optionalFields(schema, ["_id"], refName);
24
+ }
25
+ function optimizeQuery(query) {
26
+ const { $or, ...rest } = query;
27
+ if (!Array.isArray($or) || $or.length === 0) {
28
+ return query;
29
+ }
30
+ return {
31
+ $or: $or.map((predicate) => ({ ...predicate, ...rest }))
32
+ };
33
+ }
34
+
35
+ var QueryOp = /* @__PURE__ */ ((QueryOp2) => {
36
+ QueryOp2["GT"] = "$gt";
37
+ QueryOp2["GTE"] = "$gte";
38
+ QueryOp2["LT"] = "$lt";
39
+ QueryOp2["LTE"] = "$lte";
40
+ QueryOp2["IN"] = "$in";
41
+ QueryOp2["EQ"] = "$eq";
42
+ QueryOp2["NE"] = "$ne";
43
+ return QueryOp2;
44
+ })(QueryOp || {});
45
+
46
+ class AjvActionSchemaFactory {
47
+ constructor(opts) {
48
+ this.opts = opts;
49
+ const { schema, tenantField } = opts;
50
+ if (schema) {
51
+ if (tenantField) {
52
+ this.tenantFieldType = schema.properties[tenantField];
53
+ }
54
+ this._idFieldType = schema.properties._id;
55
+ }
56
+ }
57
+ tenantFieldType;
58
+ _idFieldType;
59
+ hasIdField() {
60
+ return !!this._idFieldType;
61
+ }
62
+ hasTenantIdField() {
63
+ return !!this.tenantFieldType;
64
+ }
65
+ createSchemaWithDbFields() {
66
+ const { timestamps, schema, schemaName } = this.opts;
67
+ if (!schema) {
68
+ throw new Error("Schema is not defined");
69
+ }
70
+ const requiredSet = new Set(schema.required);
71
+ requiredSet.add("_id");
72
+ if (timestamps) {
73
+ requiredSet.add("createdAt");
74
+ requiredSet.add("updatedAt");
75
+ }
76
+ return {
77
+ ...schema,
78
+ [SCHEMA_REF_NAME]: schemaName ? `Full${schemaName}` : void 0,
79
+ required: [...requiredSet.values()]
80
+ };
81
+ }
82
+ createFindParams() {
83
+ const { tenantField, softDelete } = this.opts;
84
+ const { tenantFieldType } = this;
85
+ const additionalProps = {};
86
+ const required = [];
87
+ if (tenantField && tenantFieldType) {
88
+ required.push(tenantField);
89
+ additionalProps[tenantField] = tenantFieldType;
90
+ }
91
+ if (softDelete) {
92
+ additionalProps.scope = {
93
+ type: "string",
94
+ enum: ["include-deleted", "only-deleted", "no-deleted"]
95
+ };
96
+ }
97
+ return {
98
+ type: "object",
99
+ additionalProperties: false,
100
+ required,
101
+ properties: {
102
+ query: { type: "object", additionalProperties: true, required: [] },
103
+ fields: { type: "array", items: { type: "string" } },
104
+ sort: { type: "array", items: { type: "string" } },
105
+ limit: { type: "integer", minimum: 0 },
106
+ offset: { type: "integer", minimum: 0 },
107
+ collation: { type: "object" },
108
+ ...additionalProps
109
+ }
110
+ };
111
+ }
112
+ createGetParams(params) {
113
+ const { _idFieldType, tenantFieldType } = this;
114
+ const { tenantField, softDelete } = this.opts;
115
+ const { allowFields } = params;
116
+ const additionalProps = {};
117
+ const required = ["_id"];
118
+ if (tenantField && tenantFieldType) {
119
+ required.push(tenantField);
120
+ additionalProps[tenantField] = tenantFieldType;
121
+ }
122
+ if (softDelete) {
123
+ additionalProps.scope = {
124
+ type: "string",
125
+ enum: ["include-deleted", "only-deleted", "no-deleted"]
126
+ };
127
+ }
128
+ if (allowFields) {
129
+ additionalProps.fields = { type: "array", items: { type: "string" } };
130
+ }
131
+ return {
132
+ type: "object",
133
+ additionalProperties: false,
134
+ required,
135
+ properties: {
136
+ _id: _idFieldType || OBJECTID_TYPE,
137
+ ...additionalProps
138
+ }
139
+ };
140
+ }
141
+ createCountParams(params) {
142
+ const { tenantFieldType } = this;
143
+ const { tenantField, softDelete } = this.opts;
144
+ const { queryType } = params;
145
+ const properties = {};
146
+ const required = [];
147
+ if (tenantField && tenantFieldType) {
148
+ required.push(tenantField);
149
+ properties[tenantField] = tenantFieldType;
150
+ }
151
+ if (softDelete) {
152
+ properties.scope = {
153
+ type: "string",
154
+ enum: ["include-deleted", "only-deleted", "no-deleted"]
155
+ };
156
+ }
157
+ if (queryType === "stringified") {
158
+ properties.sQuery = { type: "string" };
159
+ } else if (queryType === "object") {
160
+ properties.query = { type: "object", additionalProperties: true };
161
+ }
162
+ return {
163
+ type: "object",
164
+ additionalProperties: false,
165
+ required,
166
+ properties
167
+ };
168
+ }
169
+ createListParams(params) {
170
+ const { tenantField, softDelete } = this.opts;
171
+ const { tenantFieldType } = this;
172
+ const { queryType, maxPageSize } = params;
173
+ const additionalProperties = {};
174
+ const required = [];
175
+ if (tenantField && tenantFieldType) {
176
+ required.push(tenantField);
177
+ additionalProperties[tenantField] = tenantFieldType;
178
+ }
179
+ if (softDelete) {
180
+ additionalProperties.scope = {
181
+ type: "string",
182
+ enum: ["include-deleted", "only-deleted", "no-deleted"]
183
+ };
184
+ }
185
+ if (queryType === "stringified") {
186
+ additionalProperties.sQuery = { type: "string" };
187
+ } else if (queryType === "object") {
188
+ additionalProperties.query = {
189
+ type: "object",
190
+ additionalProperties: true
191
+ };
192
+ }
193
+ return {
194
+ type: "object",
195
+ additionalProperties: false,
196
+ required,
197
+ properties: {
198
+ page: { type: "integer", minimum: 0 },
199
+ pageSize: {
200
+ type: "integer",
201
+ minimum: 1,
202
+ maximum: maxPageSize || 100
203
+ },
204
+ sort: {
205
+ type: "array",
206
+ items: { type: "string" },
207
+ [COERCE_ARRAY_ATTRIBUTE]: true
208
+ },
209
+ ...additionalProperties
210
+ }
211
+ };
212
+ }
213
+ createListResponse() {
214
+ return {
215
+ type: "object",
216
+ required: ["rows", "page", "pageSize", "total", "totalPages"],
217
+ additionalProperties: false,
218
+ properties: {
219
+ rows: { type: "array", items: this.createSchemaWithDbFields() },
220
+ page: { type: "integer", minimum: 0 },
221
+ pageSize: { type: "integer", minimum: 1 },
222
+ total: { type: "integer", minimum: 0 },
223
+ totalPages: { type: "integer", minimum: 0 }
224
+ }
225
+ };
226
+ }
227
+ createCreateParams(params) {
228
+ const { schema } = this.opts;
229
+ const { allowClientId } = params;
230
+ if (!schema) {
231
+ throw new Error("Schema is not defined");
232
+ }
233
+ if (allowClientId) {
234
+ return schema;
235
+ }
236
+ return removeMongoId(schema);
237
+ }
238
+ createUpdateParams() {
239
+ const { tenantField, schema } = this.opts;
240
+ if (!schema) {
241
+ throw new Error("Schema is not defined");
242
+ }
243
+ const required = ["_id"];
244
+ if (tenantField) {
245
+ required.push(tenantField);
246
+ }
247
+ return { ...schema, required };
248
+ }
249
+ createRemoveParams() {
250
+ const { tenantFieldType, _idFieldType } = this;
251
+ const { tenantField } = this.opts;
252
+ const required = ["_id"];
253
+ const additionalProps = {};
254
+ if (tenantField && tenantFieldType) {
255
+ required.push(tenantField);
256
+ additionalProps[tenantField] = tenantFieldType;
257
+ }
258
+ return {
259
+ type: "object",
260
+ additionalProperties: false,
261
+ required,
262
+ properties: {
263
+ _id: _idFieldType || OBJECTID_TYPE,
264
+ ...additionalProps
265
+ }
266
+ };
267
+ }
268
+ }
269
+ function addQueryOps(schema, queryOps) {
270
+ const ops = Object.fromEntries(queryOps.map((op) => [op, true]));
271
+ return {
272
+ oneOf: [
273
+ schema,
274
+ {
275
+ type: "object",
276
+ additionalProperties: false,
277
+ required: [],
278
+ properties: {
279
+ ...ops[QueryOp.GT] ? { $gt: schema } : {},
280
+ ...ops[QueryOp.GTE] ? { $gte: schema } : {},
281
+ ...ops[QueryOp.LT] ? { $lt: schema } : {},
282
+ ...ops[QueryOp.LTE] ? { $lte: schema } : {},
283
+ ...ops[QueryOp.IN] ? { $in: { type: "array", items: schema } } : {},
284
+ ...ops[QueryOp.EQ] ? { $eq: schema } : {},
285
+ ...ops[QueryOp.NE] ? { $ne: schema } : {}
286
+ }
287
+ }
288
+ ]
289
+ };
290
+ }
291
+
292
+ function parseStringifiedQuery(sQuery) {
293
+ if (!sQuery) {
294
+ return {};
295
+ }
296
+ let query = {};
297
+ try {
298
+ query = JSON.parse(sQuery || "{}");
299
+ } catch {
300
+ throw new Errors.ValidationError("Invalid query format");
301
+ }
302
+ return query;
303
+ }
304
+ function parseAndValidateQuery(validator, schema, sQuery) {
305
+ if (!schema) {
306
+ return {};
307
+ }
308
+ let query = parseStringifiedQuery(sQuery);
309
+ if (schema instanceof ZodType) {
310
+ query = validator.validate(query, schema);
311
+ } else {
312
+ validator.validate(query, schema);
313
+ }
314
+ return query;
315
+ }
316
+
317
+ const ScopeSchema = z.enum(["include-deleted", "only-deleted", "no-deleted"]);
318
+ class ZodActionSchemaFactory {
319
+ constructor(opts) {
320
+ this.opts = opts;
321
+ const { schema, tenantField } = opts;
322
+ if (schema) {
323
+ if (!(schema instanceof ZodObject)) {
324
+ throw new Error("Schema must be a ZodObject");
325
+ }
326
+ if (tenantField) {
327
+ this.tenantFieldType = schema.shape[tenantField];
328
+ }
329
+ this._idFieldType = schema.shape._id;
330
+ }
331
+ }
332
+ tenantFieldType;
333
+ _idFieldType;
334
+ schemaWithDbFields;
335
+ hasIdField() {
336
+ return !!this._idFieldType;
337
+ }
338
+ hasTenantIdField() {
339
+ return !!this.tenantFieldType;
340
+ }
341
+ createSchemaWithDbFields() {
342
+ if (this.schemaWithDbFields) {
343
+ return this.schemaWithDbFields;
344
+ }
345
+ const { timestamps, schema, schemaName } = this.opts;
346
+ if (!schema || !(schema instanceof ZodObject)) {
347
+ throw new Error("Schema is not a ZodObject");
348
+ }
349
+ let res = schema.required({ _id: true });
350
+ if (timestamps) {
351
+ res = res.required({ createdAt: true, updatedAt: true });
352
+ }
353
+ if (schemaName) {
354
+ res = res.meta({ id: `Full${schemaName}` });
355
+ }
356
+ this.schemaWithDbFields = res;
357
+ return res;
358
+ }
359
+ createFindParams() {
360
+ const { tenantField, softDelete } = this.opts;
361
+ const { tenantFieldType } = this;
362
+ const shape = {
363
+ query: z.looseObject({}).optional(),
364
+ fields: z.array(z.string()).optional(),
365
+ sort: z.array(z.string()).optional(),
366
+ limit: z.uint32().optional(),
367
+ offset: z.uint32().optional(),
368
+ collation: z.looseObject({}).optional()
369
+ };
370
+ if (tenantField && tenantFieldType) {
371
+ shape[tenantField] = tenantFieldType;
372
+ }
373
+ if (softDelete) {
374
+ shape.scope = ScopeSchema.optional();
375
+ }
376
+ return z.object(shape);
377
+ }
378
+ createGetParams(params) {
379
+ const { _idFieldType, tenantFieldType } = this;
380
+ const { tenantField, softDelete } = this.opts;
381
+ const { allowFields } = params;
382
+ const shape = {
383
+ _id: _idFieldType || zodObjectId
384
+ };
385
+ if (tenantField && tenantFieldType) {
386
+ shape[tenantField] = tenantFieldType;
387
+ }
388
+ if (softDelete) {
389
+ shape.scope = ScopeSchema.optional();
390
+ }
391
+ if (allowFields) {
392
+ shape.fields = z.array(z.string()).optional();
393
+ }
394
+ return z.object(shape);
395
+ }
396
+ createCountParams(params) {
397
+ const { tenantFieldType } = this;
398
+ const { tenantField, softDelete } = this.opts;
399
+ const { queryType } = params;
400
+ const shape = {};
401
+ if (queryType === "stringified") {
402
+ shape.sQuery = z.string().optional();
403
+ } else if (queryType === "object") {
404
+ shape.query = z.looseObject({}).optional();
405
+ }
406
+ if (tenantField && tenantFieldType) {
407
+ shape[tenantField] = tenantFieldType;
408
+ }
409
+ if (softDelete) {
410
+ shape.scope = ScopeSchema.optional();
411
+ }
412
+ return z.object(shape);
413
+ }
414
+ createListParams(params) {
415
+ const { tenantField, softDelete } = this.opts;
416
+ const { tenantFieldType } = this;
417
+ const { queryType, maxPageSize } = params;
418
+ const shape = {
419
+ page: z.coerce.number().int().min(0).optional(),
420
+ pageSize: z.coerce.number().int().min(1).max(maxPageSize || 100).optional(),
421
+ sort: zodCoerceArray(z.string()).optional()
422
+ };
423
+ if (queryType === "stringified") {
424
+ shape.sQuery = z.string().optional();
425
+ } else if (queryType === "object") {
426
+ shape.query = z.looseObject({}).optional();
427
+ }
428
+ if (tenantField && tenantFieldType) {
429
+ shape[tenantField] = tenantFieldType;
430
+ }
431
+ if (softDelete) {
432
+ shape.scope = ScopeSchema.optional();
433
+ }
434
+ return z.object(shape);
435
+ }
436
+ createListResponse() {
437
+ return z.strictObject({
438
+ rows: z.array(this.createSchemaWithDbFields()),
439
+ page: z.uint32(),
440
+ pageSize: z.int().min(1),
441
+ total: z.uint32(),
442
+ totalPages: z.uint32()
443
+ });
444
+ }
445
+ createCreateParams(params) {
446
+ const { schema } = this.opts;
447
+ const { allowClientId } = params;
448
+ if (!schema || !(schema instanceof ZodObject)) {
449
+ throw new Error("Schema is not a ZodObject");
450
+ }
451
+ if (allowClientId) {
452
+ return schema;
453
+ }
454
+ return schema.omit({ _id: true });
455
+ }
456
+ createUpdateParams() {
457
+ const { tenantField, schema } = this.opts;
458
+ if (!schema || !(schema instanceof ZodObject)) {
459
+ throw new Error("Schema is not a ZodObject");
460
+ }
461
+ const mask = { _id: true };
462
+ if (tenantField) {
463
+ mask[tenantField] = true;
464
+ }
465
+ return schema.partial().required(mask);
466
+ }
467
+ createRemoveParams() {
468
+ const { tenantFieldType, _idFieldType } = this;
469
+ const { tenantField } = this.opts;
470
+ const shape = {
471
+ _id: _idFieldType || zodObjectId
472
+ };
473
+ if (tenantField && tenantFieldType) {
474
+ shape[tenantField] = tenantFieldType;
475
+ }
476
+ return z.object(shape);
477
+ }
478
+ }
479
+ function addZodQueryOps(fieldValue, queryOps) {
480
+ const shape = {};
481
+ for (const op of queryOps) {
482
+ if (op === QueryOp.IN) {
483
+ shape.$in = z.array(fieldValue).optional();
484
+ } else {
485
+ shape[op] = fieldValue.optional();
486
+ }
487
+ }
488
+ return z.union([fieldValue, z.object(shape)]);
489
+ }
490
+
491
+ const PUBLISHABLE_ACTIONS = [
492
+ "get",
493
+ "count",
494
+ "list",
495
+ "create",
496
+ "update",
497
+ "remove"
498
+ ];
499
+ function createActions(opts) {
500
+ const actions = {};
501
+ const factory = opts.actions?.schemaFactory || (opts.actions?.schema instanceof ZodType ? new ZodActionSchemaFactory({
502
+ schemaName: opts.actions.schemaName,
503
+ schema: opts.actions.schema,
504
+ timestamps: opts.timestamps,
505
+ softDelete: opts.softDelete,
506
+ tenantField: opts.tenantField
507
+ }) : new AjvActionSchemaFactory({
508
+ schemaName: opts.actions?.schemaName,
509
+ schema: opts.actions?.schema,
510
+ timestamps: opts.timestamps,
511
+ softDelete: opts.softDelete,
512
+ tenantField: opts.tenantField
513
+ }));
514
+ for (const action of PUBLISHABLE_ACTIONS) {
515
+ if (opts.actions?.[action]) {
516
+ if (!opts.actions.schema) {
517
+ throw new Error(`Missing schema for action ${action}`);
518
+ }
519
+ if (!factory.hasIdField()) {
520
+ throw new Error(`Missing _id for action ${action}`);
521
+ }
522
+ if (opts.tenantField && !factory.hasTenantIdField()) {
523
+ throw new Error(`Missing ${opts.tenantField} for action ${action}`);
524
+ }
525
+ }
526
+ }
527
+ const schemaName = opts.actions?.schemaName;
528
+ if (opts.actions?.find) {
529
+ actions.find = {
530
+ visibility: opts.actions.find.visibility,
531
+ params: factory.createFindParams(),
532
+ async handler(ctx) {
533
+ const { query, fields, offset, sort, limit, collation } = ctx.params;
534
+ let params = null;
535
+ if (opts.tenantField) {
536
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
537
+ }
538
+ return this._find(query || {}, params, {
539
+ fields,
540
+ sort,
541
+ limit,
542
+ skip: offset,
543
+ collation,
544
+ // @ts-expect-error Scope is not always here and TS doesn't like it
545
+ scope: ctx.params.scope
546
+ });
547
+ }
548
+ };
549
+ }
550
+ if (opts.actions?.findStream) {
551
+ actions.findStream = {
552
+ visibility: opts.actions.findStream.visibility,
553
+ params: factory.createFindParams(),
554
+ async handler(ctx) {
555
+ const { query, fields, offset, sort, limit, collation } = ctx.params;
556
+ let params = null;
557
+ if (opts.tenantField) {
558
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
559
+ }
560
+ return this._findStream(query || {}, params, {
561
+ fields,
562
+ sort,
563
+ limit,
564
+ skip: offset,
565
+ collation,
566
+ // @ts-expect-error Scope is not always here and TS doesn't like it
567
+ scope: ctx.params.scope
568
+ });
569
+ }
570
+ };
571
+ }
572
+ if (opts.actions?.getInternal) {
573
+ actions.getInternal = {
574
+ visibility: opts.actions.getInternal.visibility,
575
+ params: factory.createGetParams({ allowFields: true }),
576
+ async handler(ctx) {
577
+ const { fields, _id } = ctx.params;
578
+ let params = null;
579
+ if (opts.tenantField) {
580
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
581
+ }
582
+ const res = await this._findOne({ _id }, params, {
583
+ fields,
584
+ // @ts-expect-error Scope is not always here and TS doesn't like it
585
+ scope: ctx.params.scope
586
+ });
587
+ if (!res) {
588
+ throw new EntityNotFoundError(_id.toString());
589
+ }
590
+ return res;
591
+ }
592
+ };
593
+ }
594
+ if (opts.actions?.get) {
595
+ actions.get = {
596
+ rest: "GET /:_id",
597
+ openapi: createOpenAPIResponses(factory.createSchemaWithDbFields()),
598
+ visibility: opts.actions.get.visibility,
599
+ params: factory.createGetParams({ allowFields: false }),
600
+ async handler(ctx) {
601
+ const { _id } = ctx.params;
602
+ let params = null;
603
+ if (opts.tenantField) {
604
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
605
+ }
606
+ const res = await this._findOne({ _id }, params, {
607
+ // @ts-expect-error Scope is not always here and TS doesn't like it
608
+ scope: ctx.params.scope
609
+ });
610
+ if (!res) {
611
+ throw new EntityNotFoundError(_id.toString());
612
+ }
613
+ return res;
614
+ }
615
+ };
616
+ }
617
+ if (opts.actions?.countInternal) {
618
+ actions.countInternal = {
619
+ visibility: opts.actions.countInternal.visibility,
620
+ params: factory.createCountParams({ queryType: "object" }),
621
+ async handler(ctx) {
622
+ const { query } = ctx.params;
623
+ let params = null;
624
+ if (opts.tenantField) {
625
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
626
+ }
627
+ return this._countDocuments(query || {}, params, {
628
+ // @ts-expect-error Scope is not always here and TS doesn't like it
629
+ scope: ctx.params.scope
630
+ });
631
+ }
632
+ };
633
+ }
634
+ if (opts.actions?.count) {
635
+ actions.count = {
636
+ rest: "GET /count",
637
+ openapi: createOpenAPIResponses({ type: "integer" }),
638
+ visibility: opts.actions.count.visibility,
639
+ params: factory.createCountParams({ queryType: "stringified" }),
640
+ async handler(ctx) {
641
+ const { sQuery } = ctx.params;
642
+ const query = parseAndValidateQuery(
643
+ this.broker.validator,
644
+ opts.sQuerySchema,
645
+ sQuery
646
+ );
647
+ let params = null;
648
+ if (opts.tenantField) {
649
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
650
+ }
651
+ return this._countDocuments(query, params, {
652
+ // @ts-expect-error Scope is not always here and TS doesn't like it
653
+ scope: ctx.params.scope
654
+ });
655
+ }
656
+ };
657
+ }
658
+ if (opts.actions?.list) {
659
+ actions.list = {
660
+ rest: "GET /",
661
+ openapi: createOpenAPIResponses(factory.createListResponse()),
662
+ visibility: opts.actions.list.visibility,
663
+ params: factory.createListParams({
664
+ queryType: "stringified",
665
+ maxPageSize: opts.actions.list.maxPageSize
666
+ }),
667
+ async handler(ctx) {
668
+ const {
669
+ sQuery,
670
+ sort = opts.actions?.list?.defaultSort,
671
+ collation
672
+ } = ctx.params;
673
+ const query = parseAndValidateQuery(
674
+ this.broker.validator,
675
+ opts.sQuerySchema,
676
+ sQuery
677
+ );
678
+ let params = null;
679
+ if (opts.tenantField) {
680
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
681
+ }
682
+ const page = ctx.params.page || 0;
683
+ const pageSize = ctx.params.pageSize || opts.actions?.list?.defaultPageSize || 10;
684
+ const [rows, total] = await Promise.all([
685
+ this._find(query, params, {
686
+ limit: pageSize,
687
+ skip: page * pageSize,
688
+ sort,
689
+ // @ts-expect-error Scope is not always here and TS doesn't like it
690
+ scope: ctx.params.scope,
691
+ collation
692
+ }),
693
+ this._countDocuments(query, params, {
694
+ // @ts-expect-error Scope is not always here and TS doesn't like it
695
+ scope: ctx.params.scope,
696
+ collation
697
+ })
698
+ ]);
699
+ return {
700
+ rows,
701
+ page,
702
+ pageSize,
703
+ total,
704
+ totalPages: Math.floor((total + pageSize - 1) / pageSize)
705
+ };
706
+ }
707
+ };
708
+ }
709
+ if (opts.actions?.create) {
710
+ actions.create = {
711
+ rest: "POST /",
712
+ openapi: createOpenAPIResponses(factory.createSchemaWithDbFields()),
713
+ visibility: opts.actions.create.visibility,
714
+ bodySchemaRefName: schemaName && `Create${schemaName}`,
715
+ params: factory.createCreateParams({
716
+ allowClientId: opts.actions.create.allowClientId
717
+ }),
718
+ handler(ctx) {
719
+ return this._insertOne(ctx, ctx.params);
720
+ }
721
+ };
722
+ }
723
+ if (opts.actions?.update) {
724
+ actions.update = {
725
+ rest: "PATCH /:_id",
726
+ openapi: createOpenAPIResponses(factory.createSchemaWithDbFields()),
727
+ visibility: opts.actions.update.visibility,
728
+ params: factory.createUpdateParams(),
729
+ handler(ctx) {
730
+ let params = null;
731
+ if (opts.tenantField) {
732
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
733
+ }
734
+ return this._updateOne(ctx, { _id: ctx.params._id }, params, {
735
+ $set: ctx.params
736
+ });
737
+ }
738
+ };
739
+ }
740
+ if (opts.actions?.remove) {
741
+ actions.remove = {
742
+ rest: "DELETE /:_id",
743
+ openapi: createOpenAPIResponses(factory.createSchemaWithDbFields()),
744
+ visibility: opts.actions.remove.visibility,
745
+ params: factory.createRemoveParams(),
746
+ handler(ctx) {
747
+ let params = null;
748
+ if (opts.tenantField) {
749
+ params = { [opts.tenantField]: ctx.params[opts.tenantField] };
750
+ }
751
+ return this._deleteOne(ctx, { _id: ctx.params._id }, params);
752
+ }
753
+ };
754
+ }
755
+ return actions;
756
+ }
757
+
758
+ function DatabaseConnectionMixin(opts) {
759
+ const { databaseName, collectionName, createCollectionOptions } = opts;
760
+ const uri = globalThis.__MONGO_URI__ || opts.uri || process.env.MONGO_URL || process.env.MONGODB_URL || "mongodb://localhost:27017";
761
+ const dbName = globalThis.__MONGO_DB_NAME__ ? `${globalThis.__MONGO_DB_NAME__}-${databaseName}` : databaseName;
762
+ const key = uri;
763
+ return wrapMixin({
764
+ mixins: [GlobalStoreMixin()],
765
+ methods: {
766
+ getMongoClient() {
767
+ return this.mongoClient;
768
+ },
769
+ getCollection(options) {
770
+ return this.getMongoClient().db(dbName).collection(collectionName, options);
771
+ }
772
+ },
773
+ created() {
774
+ let client = this.getFromStore("mongodb", key);
775
+ this.logger.debug(
776
+ "MongoDB mixin starting, loading mongo client from store"
777
+ );
778
+ if (!client) {
779
+ this.logger.info(
780
+ "Didn't find mongo client in store, creating a new one"
781
+ );
782
+ client = new MongoClient(uri);
783
+ this.setClientToStore("mongodb", key, client, async () => {
784
+ this.logger.debug("Closing mongoDB connection");
785
+ await client?.close();
786
+ this.logger.info("MongoDB connection closed");
787
+ });
788
+ client.on("error", (err) => this.logger.error("MongoDB error", err));
789
+ }
790
+ this.mongoClient = client;
791
+ },
792
+ async started() {
793
+ this.logger.debug("Service connecting to mongoDB");
794
+ await this.getMongoClient().connect();
795
+ this.logger.debug("Service connected to mongoDB, creating collection");
796
+ try {
797
+ await this.getMongoClient().db(dbName).createCollection(collectionName, createCollectionOptions);
798
+ } catch (err) {
799
+ if (err?.code !== 48) {
800
+ this.logger.error("Error while creating collection", { err });
801
+ }
802
+ }
803
+ },
804
+ async stopped() {
805
+ await this.removeServiceFromStore("mongodb", key);
806
+ }
807
+ });
808
+ }
809
+
810
+ const {
811
+ MONGO_URL = "",
812
+ MONGODB_URL = "",
813
+ SYNC_MONGO_INDEX,
814
+ SYNC_INDEX_AUTO_CREATE,
815
+ SYNC_INDEX_AUTO_DROP
816
+ } = process.env;
817
+ function getDefaultIndexName(key) {
818
+ return Object.entries(key).flat(1).join("_");
819
+ }
820
+ function isIndexNameEqual(dbIdx, idx) {
821
+ const [keys, opts] = idx;
822
+ return dbIdx.name === (opts?.name || getDefaultIndexName(keys));
823
+ }
824
+ function isIndexEqual(dbIdx, idx) {
825
+ const [keys, opts] = idx;
826
+ if (Object.keys(dbIdx.key).length !== Object.keys(keys).length) {
827
+ return false;
828
+ }
829
+ if (Object.entries(dbIdx.key).some(([field, val]) => keys[field] !== val)) {
830
+ return false;
831
+ }
832
+ if (dbIdx.collation || opts?.collation) {
833
+ if (!dbIdx.collation || !opts?.collation) {
834
+ return false;
835
+ }
836
+ if (!isMatch(dbIdx.collation, opts.collation)) {
837
+ return false;
838
+ }
839
+ }
840
+ if (Boolean(dbIdx.sparse) !== Boolean(opts?.sparse)) {
841
+ return false;
842
+ }
843
+ return dbIdx.expireAfterSeconds === opts?.expireAfterSeconds && dbIdx.unique === opts?.unique && isEqual(dbIdx.partialFilterExpression, opts?.partialFilterExpression);
844
+ }
845
+ function isOnAtlas() {
846
+ return (MONGO_URL || MONGODB_URL).includes(".mongodb.net");
847
+ }
848
+ function shouldAutoCreateIndexes() {
849
+ if (SYNC_INDEX_AUTO_CREATE) {
850
+ return SYNC_INDEX_AUTO_CREATE === "yes";
851
+ }
852
+ if (SYNC_MONGO_INDEX) {
853
+ return SYNC_MONGO_INDEX === "yes";
854
+ }
855
+ return !isOnAtlas();
856
+ }
857
+ function shouldAutoDropIndexes() {
858
+ if (SYNC_INDEX_AUTO_DROP) {
859
+ return SYNC_INDEX_AUTO_DROP === "yes";
860
+ }
861
+ if (SYNC_MONGO_INDEX) {
862
+ return SYNC_MONGO_INDEX === "yes";
863
+ }
864
+ return !isOnAtlas();
865
+ }
866
+ async function getIndexesDifference$1({
867
+ collection,
868
+ declaredIdxs
869
+ }) {
870
+ const dbIdxs = await collection.indexes();
871
+ const idxsToCreate = declaredIdxs.filter((idx) => !dbIdxs.find((dbIdx) => isIndexNameEqual(dbIdx, idx))).map(([key, opts]) => ({ key, ...opts }));
872
+ const idxsToUpdate = declaredIdxs.filter(
873
+ (idx) => dbIdxs.find(
874
+ (dbIdx) => isIndexNameEqual(dbIdx, idx) && !isIndexEqual(dbIdx, idx)
875
+ )
876
+ ).map(([key, opts]) => ({ key, ...opts }));
877
+ const idxsToDelete = dbIdxs.filter((dbIdx) => dbIdx.name !== "_id_").filter((dbIdx) => !declaredIdxs.find((idx) => isIndexNameEqual(dbIdx, idx)));
878
+ return {
879
+ idxsToCreate,
880
+ idxsToDelete,
881
+ idxsToUpdate,
882
+ synced: !(idxsToCreate.length || idxsToUpdate.length || idxsToDelete.length)
883
+ };
884
+ }
885
+
886
+ var IndexStatus = /* @__PURE__ */ ((IndexStatus2) => {
887
+ IndexStatus2["OK"] = "OK";
888
+ IndexStatus2["MISSING"] = "MISSING";
889
+ IndexStatus2["OUTDATED"] = "OUTDATED";
890
+ IndexStatus2["NOT_DECLARED"] = "NOT_DECLARED";
891
+ return IndexStatus2;
892
+ })(IndexStatus || {});
893
+
894
+ async function getIndexesDifference(collection, declaredIndexes = [], declaredSearchIndexes = {}) {
895
+ const states = [];
896
+ const dbIdxs = await collection.listIndexes().toArray();
897
+ for (const idx of declaredIndexes) {
898
+ const dbIdxPos = dbIdxs.findIndex((i) => isIndexNameEqual(i, idx));
899
+ if (dbIdxPos === -1) {
900
+ states.push({
901
+ type: "index",
902
+ status: IndexStatus.MISSING,
903
+ declaredIndex: idx
904
+ });
905
+ } else {
906
+ const [dbIdx] = dbIdxs.splice(dbIdxPos, 1);
907
+ if (!isIndexEqual(dbIdx, idx)) {
908
+ states.push({
909
+ type: "index",
910
+ status: IndexStatus.OUTDATED,
911
+ index: dbIdx,
912
+ declaredIndex: idx
913
+ });
914
+ } else {
915
+ states.push({
916
+ type: "index",
917
+ status: IndexStatus.OK,
918
+ index: dbIdx,
919
+ declaredIndex: idx
920
+ });
921
+ }
922
+ }
923
+ }
924
+ for (const index of dbIdxs) {
925
+ if (index.name !== "_id_") {
926
+ states.push({ type: "index", status: IndexStatus.NOT_DECLARED, index });
927
+ }
928
+ }
929
+ let searchIndexes;
930
+ try {
931
+ searchIndexes = await collection.listSearchIndexes().toArray();
932
+ } catch {
933
+ return states;
934
+ }
935
+ for (const [name, idx] of Object.entries(declaredSearchIndexes)) {
936
+ const dbIdxPos = searchIndexes.findIndex((i) => i.name === name);
937
+ if (dbIdxPos === -1) {
938
+ states.push({
939
+ type: "searchIndex",
940
+ name,
941
+ status: IndexStatus.MISSING,
942
+ declaredSearchIndex: idx
943
+ });
944
+ } else {
945
+ const [dbIdx] = searchIndexes.splice(dbIdxPos, 1);
946
+ if (!isEqual(dbIdx.latestDefinition, idx)) {
947
+ states.push({
948
+ type: "searchIndex",
949
+ name,
950
+ status: IndexStatus.OUTDATED,
951
+ searchIndex: dbIdx.latestDefinition,
952
+ declaredSearchIndex: idx
953
+ });
954
+ } else {
955
+ states.push({
956
+ type: "searchIndex",
957
+ name,
958
+ status: IndexStatus.OK,
959
+ searchIndex: dbIdx.latestDefinition,
960
+ declaredSearchIndex: idx
961
+ });
962
+ }
963
+ }
964
+ }
965
+ for (const index of searchIndexes) {
966
+ states.push({
967
+ type: "searchIndex",
968
+ name: index.name,
969
+ status: IndexStatus.NOT_DECLARED,
970
+ searchIndex: index.latestDefinition
971
+ });
972
+ }
973
+ return states;
974
+ }
975
+ const DATABASE_INDEXES_MIXIN_SYNC_EVENT = "database-indexes-mixin.sync";
976
+ function DatabaseIndexesMixin(opts) {
977
+ return wrapMixin({
978
+ methods: {
979
+ async _syncIndexes({
980
+ dropIndexes,
981
+ createIndexes
982
+ }) {
983
+ if (typeof this.getCollection !== "function") {
984
+ throw new Error(
985
+ "getCollection method not found, did you add the DatabaseConnectionMixin?"
986
+ );
987
+ }
988
+ const collection = this.getCollection();
989
+ const states = await getIndexesDifference(
990
+ collection,
991
+ opts.indexes,
992
+ opts.searchIndexes
993
+ );
994
+ const notOkStates = states.filter((s) => s.status !== IndexStatus.OK);
995
+ if (!notOkStates.length) {
996
+ this.logger.info(
997
+ `Collection ${collection.collectionName} is synced (${states.length} indexes)`
998
+ );
999
+ return;
1000
+ }
1001
+ this.logger.info(
1002
+ `Collection ${collection.collectionName} is not synced (${notOkStates.length}/${states.length} indexes are not OK)`
1003
+ );
1004
+ const syncableStates = notOkStates.filter(
1005
+ (s) => s.status === IndexStatus.NOT_DECLARED && dropIndexes || s.status === IndexStatus.MISSING && createIndexes || s.status === IndexStatus.OUTDATED && createIndexes
1006
+ );
1007
+ for (const state of syncableStates) {
1008
+ try {
1009
+ await this._createIndexFromState(collection, state);
1010
+ } catch (err) {
1011
+ this.logger.warn("Error while syncing indexes", { err, state });
1012
+ }
1013
+ }
1014
+ },
1015
+ async _createIndexFromState(col, state) {
1016
+ if (state.status === IndexStatus.MISSING) {
1017
+ this.logger.info(`Creating missing index`, { state });
1018
+ if (state.type === "index") {
1019
+ const [definition, options] = state.declaredIndex;
1020
+ await col.createIndex(definition, options);
1021
+ } else {
1022
+ await col.createSearchIndex({
1023
+ name: state.name,
1024
+ definition: state.declaredSearchIndex
1025
+ });
1026
+ }
1027
+ } else if (state.status === IndexStatus.NOT_DECLARED) {
1028
+ this.logger.info(`Index is not declared, dropping it`, { state });
1029
+ if (state.type === "index") {
1030
+ if (!state.index.name || state.index.name === "_id_") {
1031
+ throw new Error(
1032
+ `Unable to delete index '${state.index.name}', invalid name`
1033
+ );
1034
+ }
1035
+ await col.dropIndex(state.index.name);
1036
+ } else {
1037
+ await col.dropSearchIndex(state.name);
1038
+ }
1039
+ } else if (state.status === IndexStatus.OUTDATED) {
1040
+ this.logger.info(`Index is outdated, updating it`, { state });
1041
+ if (state.type === "index") {
1042
+ this.logger.warn(
1043
+ "Updating indexes is not supported, it should be done manually"
1044
+ );
1045
+ } else {
1046
+ await col.updateSearchIndex(state.name, state.declaredSearchIndex);
1047
+ }
1048
+ } else {
1049
+ this.logger.info(`Index is OK, doing nothing`, { state });
1050
+ }
1051
+ }
1052
+ },
1053
+ events: {
1054
+ [DATABASE_INDEXES_MIXIN_SYNC_EVENT]: {
1055
+ async handler(ctx) {
1056
+ ctx.logger.info(
1057
+ `Received sync indexes event for service ${this.name}`
1058
+ );
1059
+ await this._syncIndexes({ createIndexes: true, dropIndexes: false });
1060
+ }
1061
+ },
1062
+ "$broker.started": {
1063
+ async handler() {
1064
+ await this._syncIndexes({
1065
+ createIndexes: shouldAutoCreateIndexes(),
1066
+ dropIndexes: shouldAutoDropIndexes()
1067
+ });
1068
+ }
1069
+ }
1070
+ }
1071
+ });
1072
+ }
1073
+
1074
+ function getFieldMode(fields) {
1075
+ const refField = fields?.[0] === "-_id" ? fields?.[1] : fields?.[0];
1076
+ if (refField && !refField.startsWith("-")) {
1077
+ return "allow";
1078
+ }
1079
+ return "deny";
1080
+ }
1081
+ function filterFields(fields, secureFields) {
1082
+ if (!secureFields?.length) {
1083
+ return fields;
1084
+ }
1085
+ const mode = getFieldMode(fields);
1086
+ switch (mode) {
1087
+ case "deny": {
1088
+ if (fields?.some((f) => !f.startsWith("-"))) {
1089
+ throw new Error('All fields must have a "-" prefix in deny mode');
1090
+ }
1091
+ return [
1092
+ .../* @__PURE__ */ new Set([...fields || [], ...secureFields.map((f) => `-${f}`)])
1093
+ ];
1094
+ }
1095
+ case "allow": {
1096
+ if (fields?.some((f) => f.startsWith("-") && f !== "-_id")) {
1097
+ throw new Error('No fields must have a "-" prefix in allow mode');
1098
+ }
1099
+ const filteredFields = fields?.filter(
1100
+ (f) => !secureFields.find((sF) => f === sF || f.startsWith(`${sF}.`))
1101
+ );
1102
+ if (filteredFields?.length === 0) {
1103
+ return ["_id"];
1104
+ }
1105
+ return filteredFields;
1106
+ }
1107
+ default:
1108
+ throw new Error("Should not happen");
1109
+ }
1110
+ }
1111
+ function filterObjectFields(obj, fields) {
1112
+ if (!fields?.length) {
1113
+ return obj;
1114
+ }
1115
+ let res = obj;
1116
+ const [fieldsRemove, fieldsAdd] = partition(
1117
+ fields || [],
1118
+ (f) => f.startsWith("-")
1119
+ );
1120
+ if (fieldsRemove.length) {
1121
+ res = omit(
1122
+ res,
1123
+ fieldsRemove.map((f) => f.substring(1))
1124
+ );
1125
+ }
1126
+ if (fieldsAdd.length) {
1127
+ res = pick(res, fieldsAdd);
1128
+ }
1129
+ return res;
1130
+ }
1131
+ function getQueryFromList(type, list) {
1132
+ if (!list?.length) {
1133
+ return void 0;
1134
+ }
1135
+ const res = {};
1136
+ list.forEach((el) => {
1137
+ if (el.startsWith("-")) {
1138
+ const p = el.slice(1);
1139
+ res[p] = type === "sort" ? -1 : 0;
1140
+ } else {
1141
+ res[el] = 1;
1142
+ }
1143
+ });
1144
+ return res;
1145
+ }
1146
+
1147
+ function DatabaseMethodsMixin(opts) {
1148
+ return wrapMixin({
1149
+ methods: {
1150
+ /**
1151
+ * Helper function that clean an update aggregation pipeline from createdAt changes.
1152
+ * Note: This method mutate the array.
1153
+ */
1154
+ _removeCreatedAtFromUpdateAggregationPipeline(changes) {
1155
+ if (!Array.isArray(changes)) {
1156
+ return;
1157
+ }
1158
+ for (const change of changes) {
1159
+ if (change.$addFields?.createdAt !== void 0) {
1160
+ delete change.$addFields.createdAt;
1161
+ }
1162
+ if (change.$set?.createdAt !== void 0) {
1163
+ delete change.$set.createdAt;
1164
+ }
1165
+ if (typeof change.$unset === "string") {
1166
+ change.$unset = [change.$unset];
1167
+ }
1168
+ if (Array.isArray(change.$unset) && change.$unset.includes("createdAt")) {
1169
+ change.$unset.splice(change.$unset.indexOf("createdAt"), 1);
1170
+ if (change.$unset.length === 0) {
1171
+ changes.splice(changes.indexOf(change), 1);
1172
+ }
1173
+ }
1174
+ if (change.$project) {
1175
+ let isExcludeMode = false;
1176
+ for (const [key, val] of Object.entries(change.$project)) {
1177
+ if (key !== "_id" && (val === false || val === 0)) {
1178
+ isExcludeMode = true;
1179
+ break;
1180
+ }
1181
+ }
1182
+ if (isExcludeMode) {
1183
+ delete change.$project.createdAt;
1184
+ } else {
1185
+ change.$project.createdAt = true;
1186
+ }
1187
+ if (Object.keys(change.$project).length === 0) {
1188
+ changes.splice(changes.indexOf(change), 1);
1189
+ }
1190
+ }
1191
+ }
1192
+ },
1193
+ /**
1194
+ * This method will automatically set the needed operators for our features (timestamps).
1195
+ *
1196
+ * Limitations of this method:
1197
+ * - Dates are generated on the mongo server, except for `update` type with upsert.
1198
+ * - It doesn't support $replaceWith/$replaceRoot in an aggregation pipeline except when using type `replace`.
1199
+ * - Replaces (`replace` type) can only be done with an aggregation pipeline with a single $replaceWith stage.
1200
+ * - Update aggregation pipelines will be modified to let the createdAt field stay the same.
1201
+ */
1202
+ _prepareUpdateFilter(changes, type) {
1203
+ if (!opts.timestamps) {
1204
+ return changes;
1205
+ }
1206
+ if (type === "replace") {
1207
+ if (!Array.isArray(changes) || changes.length !== 1 || !changes[0].$replaceWith) {
1208
+ throw new Error(
1209
+ "Replace with timestamps can only go through an aggregation pipeline with a single $replaceWith stage"
1210
+ );
1211
+ }
1212
+ return [
1213
+ {
1214
+ $replaceWith: {
1215
+ $mergeObjects: [
1216
+ changes[0].$replaceWith,
1217
+ {
1218
+ createdAt: { $ifNull: ["$createdAt", "$$NOW"] },
1219
+ updatedAt: "$$NOW"
1220
+ }
1221
+ ]
1222
+ }
1223
+ }
1224
+ ];
1225
+ }
1226
+ if (Array.isArray(changes)) {
1227
+ if (changes.some((change) => change.$replaceWith || change.$replaceRoot)) {
1228
+ throw new Error(
1229
+ "$replaceWith/$replaceRoot can't be used in an update aggregation pipeline."
1230
+ );
1231
+ }
1232
+ this._removeCreatedAtFromUpdateAggregationPipeline(changes);
1233
+ return [
1234
+ ...changes,
1235
+ {
1236
+ $set: {
1237
+ createdAt: { $ifNull: ["$createdAt", "$$NOW"] },
1238
+ updatedAt: "$$NOW"
1239
+ }
1240
+ }
1241
+ ];
1242
+ }
1243
+ for (const operator of Object.values(changes)) {
1244
+ for (const key of Object.keys(operator)) {
1245
+ if (key === "createdAt" || key === "updatedAt") {
1246
+ delete operator[key];
1247
+ }
1248
+ }
1249
+ }
1250
+ const $currentDate = changes.$currentDate || {};
1251
+ const $setOnInsert = changes.$setOnInsert || {};
1252
+ $currentDate.updatedAt = true;
1253
+ if (type === "create") {
1254
+ $currentDate.createdAt = true;
1255
+ } else {
1256
+ $setOnInsert.createdAt = /* @__PURE__ */ new Date();
1257
+ }
1258
+ return {
1259
+ ...changes,
1260
+ // @ts-expect-error $currentDate is not always here for TS
1261
+ $currentDate,
1262
+ // @ts-expect-error $setOnInsert is not always here for TS
1263
+ $setOnInsert
1264
+ };
1265
+ },
1266
+ /**
1267
+ * Will get the tenant filter from params.
1268
+ * This filter should be used in all read queries to ensure that the user
1269
+ * can only access documents that belong to the same tenant.
1270
+ */
1271
+ _getTenantFilter(params, strict = true) {
1272
+ const { tenantField } = opts;
1273
+ if (!tenantField) {
1274
+ return {};
1275
+ }
1276
+ if (!params?.[tenantField]) {
1277
+ if (!strict) {
1278
+ return {};
1279
+ }
1280
+ throw new Error(`Missing tenant field "${tenantField}" in params`);
1281
+ }
1282
+ return { [tenantField]: params[tenantField] };
1283
+ },
1284
+ /**
1285
+ * Will get the soft delete filter from params.
1286
+ * This filter should be used in all read queries.
1287
+ *
1288
+ * Indexes should also index the deleted field to ensure optimal performance.
1289
+ * Note that in order to support partial indexes, a non deleted field is checked
1290
+ * for `false` and `null` values.
1291
+ */
1292
+ _getSoftDeleteFilter(scope) {
1293
+ if (!opts.softDelete) {
1294
+ return {};
1295
+ }
1296
+ switch (scope) {
1297
+ case "only-deleted":
1298
+ return { deletedAt: { $gte: 0 } };
1299
+ case "no-deleted":
1300
+ case void 0:
1301
+ return { deletedAt: null };
1302
+ case "include-deleted":
1303
+ return {};
1304
+ default:
1305
+ throw new Error(`Unknown soft delete scope ${scope}`);
1306
+ }
1307
+ },
1308
+ /**
1309
+ * Get a query filter optimized ($or problem) with additional filters applied:
1310
+ * - Tenant filter
1311
+ * - Soft delete filter
1312
+ */
1313
+ _getQueryFilter(query, params, scope, strictTenantFilter = true) {
1314
+ return optimizeQuery({
1315
+ ...query,
1316
+ ...this._getTenantFilter(params, strictTenantFilter),
1317
+ ...this._getSoftDeleteFilter(scope)
1318
+ });
1319
+ },
1320
+ /**
1321
+ * INTERNAL, DO NOT USE.
1322
+ * Simple wrapper around the DatabaseConnectionMixin.getCollection method to have typed collection.
1323
+ */
1324
+ _getDatabaseMixinCollection(options) {
1325
+ return this.getCollection(options);
1326
+ },
1327
+ /**
1328
+ * Create a find cursor with database mixin options applied.
1329
+ */
1330
+ _createFindCursor(query, params, options = {}) {
1331
+ const {
1332
+ sort,
1333
+ fields,
1334
+ scope,
1335
+ strictTenantFilter = true,
1336
+ ...driverOptions
1337
+ } = options;
1338
+ return this._getDatabaseMixinCollection().find(
1339
+ this._getQueryFilter(query, params, scope, strictTenantFilter),
1340
+ {
1341
+ ...driverOptions,
1342
+ sort: getQueryFromList("sort", sort),
1343
+ projection: getQueryFromList("projection", fields)
1344
+ }
1345
+ );
1346
+ },
1347
+ async _findOne(query, params, options) {
1348
+ const cursor = this._createFindCursor(query, params, {
1349
+ ...options,
1350
+ limit: -1,
1351
+ batchSize: 1
1352
+ });
1353
+ const res = await cursor.next();
1354
+ await cursor.close();
1355
+ return res;
1356
+ },
1357
+ _find(query, params, options) {
1358
+ return this._createFindCursor(query, params, options).toArray();
1359
+ },
1360
+ _findStream(query, params, options) {
1361
+ return this._createFindCursor(query, params, options).stream();
1362
+ },
1363
+ _countDocuments(query, params, options = {}) {
1364
+ const { scope, strictTenantFilter = true, ...driverOptions } = options;
1365
+ return this._getDatabaseMixinCollection().countDocuments(
1366
+ this._getQueryFilter(query, params, scope, strictTenantFilter),
1367
+ driverOptions
1368
+ );
1369
+ },
1370
+ /**
1371
+ * Insert one document and return it.
1372
+ *
1373
+ * It differs from the driver's insertOne as it returns the inserted document and
1374
+ * send an event with the new document.
1375
+ */
1376
+ async _insertOne(ctx, doc, options = {}) {
1377
+ const { fields, skipCreateEvent, ...driverOptions } = options;
1378
+ if (opts.idGenerator && !doc._id) {
1379
+ doc._id = opts.idGenerator(doc);
1380
+ }
1381
+ const res = await this._getDatabaseMixinCollection().findOneAndUpdate(
1382
+ { _id: { $exists: false } },
1383
+ this._prepareUpdateFilter(
1384
+ { $setOnInsert: doc },
1385
+ "create"
1386
+ ),
1387
+ {
1388
+ ...driverOptions,
1389
+ includeResultMetadata: false,
1390
+ // Document says it's true by default and will be false in a next major
1391
+ upsert: true,
1392
+ returnDocument: "after",
1393
+ projection: getQueryFromList("projection", fields)
1394
+ }
1395
+ );
1396
+ if (!res) {
1397
+ throw new Error("Insert one didn't upsert any document");
1398
+ }
1399
+ if (opts.eventPrefix && !skipCreateEvent) {
1400
+ ctx.emit(
1401
+ `${opts.eventPrefix}.created`,
1402
+ { type: "insert", document: res }
1403
+ );
1404
+ }
1405
+ return res;
1406
+ },
1407
+ /**
1408
+ * Insert many documents and return the list of inserted ids in the same order.
1409
+ */
1410
+ async _insertMany(ctx, docs, options = {}) {
1411
+ const { skipCreateEvent, ...driverOptions } = options;
1412
+ const res = await this._getDatabaseMixinCollection().bulkWrite(
1413
+ docs.map((doc) => {
1414
+ if (opts.idGenerator && !doc._id) {
1415
+ doc._id = opts.idGenerator(doc);
1416
+ }
1417
+ return {
1418
+ updateOne: {
1419
+ upsert: true,
1420
+ filter: { _id: { $exists: false } },
1421
+ update: this._prepareUpdateFilter(
1422
+ { $setOnInsert: doc },
1423
+ "create"
1424
+ )
1425
+ }
1426
+ };
1427
+ }),
1428
+ driverOptions
1429
+ );
1430
+ if (opts.eventPrefix && !skipCreateEvent) {
1431
+ docs.forEach(
1432
+ (doc, i) => ctx.emit(
1433
+ `${opts.eventPrefix}.created`,
1434
+ {
1435
+ type: "insert",
1436
+ document: {
1437
+ _id: doc._id || res.upsertedIds[i],
1438
+ ...doc
1439
+ }
1440
+ }
1441
+ )
1442
+ );
1443
+ }
1444
+ return docs.map((doc, i) => res.upsertedIds[i]);
1445
+ },
1446
+ /**
1447
+ * Update one document and return the after version by default.
1448
+ * To have the before version, use the `returnDocument` option.
1449
+ *
1450
+ * WARNING: Only send an event if returnDocument is 'after'.If returnDocument is 'before',
1451
+ * you MUST pass skipUpdateEvent: true and optionally send the event yourself.
1452
+ */
1453
+ async _updateOne(ctx, query, params, changes, options = {}) {
1454
+ const {
1455
+ fields,
1456
+ sort,
1457
+ strictTenantFilter = true,
1458
+ returnDocument = "after",
1459
+ skipUpdateEvent,
1460
+ ...driverOptions
1461
+ } = options;
1462
+ if (returnDocument === "before" && opts.eventPrefix && !skipUpdateEvent) {
1463
+ throw new Error(
1464
+ "Cannot send update event with returnDocument: before option"
1465
+ );
1466
+ }
1467
+ const res = await this._getDatabaseMixinCollection().findOneAndUpdate(
1468
+ this._getQueryFilter(query, params, "no-deleted", strictTenantFilter),
1469
+ this._prepareUpdateFilter(changes, "update"),
1470
+ {
1471
+ ...driverOptions,
1472
+ returnDocument,
1473
+ includeResultMetadata: false,
1474
+ // Document says it's true by default and will be false in a next major
1475
+ sort: getQueryFromList("sort", sort),
1476
+ projection: getQueryFromList("projection", fields)
1477
+ }
1478
+ );
1479
+ if (res && opts.eventPrefix && !skipUpdateEvent) {
1480
+ ctx.emit(
1481
+ `${opts.eventPrefix}.updated`,
1482
+ { type: "update", document: res }
1483
+ );
1484
+ }
1485
+ return res;
1486
+ },
1487
+ /**
1488
+ * Update many documents and return the number of updated documents.
1489
+ *
1490
+ * WARNING: Do not send any events. You'll have to send an event yourself.
1491
+ */
1492
+ async _updateMany(query, params, changes, options = {}) {
1493
+ const { strictTenantFilter = true, ...driverOptions } = options;
1494
+ return this._getDatabaseMixinCollection().updateMany(
1495
+ this._getQueryFilter(query, params, "no-deleted", strictTenantFilter),
1496
+ this._prepareUpdateFilter(changes, "update"),
1497
+ driverOptions
1498
+ );
1499
+ },
1500
+ /**
1501
+ * Replace one document and return the after version by default.
1502
+ * To have the before version, use the `returnDocument` option.
1503
+ *
1504
+ * WARNING: Only send an event if returnDocument is 'after'.If returnDocument is 'before',
1505
+ * you MUST pass skipUpdateEvent: true and optionally send the event yourself.
1506
+ */
1507
+ async _replaceOne(ctx, query, params, doc, options = {}) {
1508
+ const {
1509
+ fields,
1510
+ sort,
1511
+ strictTenantFilter = true,
1512
+ returnDocument = "after",
1513
+ skipUpdateEvent,
1514
+ ...driverOptions
1515
+ } = options;
1516
+ if (returnDocument === "before" && opts.eventPrefix && !skipUpdateEvent) {
1517
+ throw new Error(
1518
+ "Cannot send update event with returnDocument: before option"
1519
+ );
1520
+ }
1521
+ const res = await this._getDatabaseMixinCollection().findOneAndUpdate(
1522
+ this._getQueryFilter(query, params, "no-deleted", strictTenantFilter),
1523
+ this._prepareUpdateFilter(
1524
+ [{ $replaceWith: { $literal: doc } }],
1525
+ "replace"
1526
+ ),
1527
+ {
1528
+ ...driverOptions,
1529
+ returnDocument,
1530
+ includeResultMetadata: false,
1531
+ // Document says it's true by default and will be false in a next major
1532
+ sort: getQueryFromList("sort", sort),
1533
+ projection: getQueryFromList("projection", fields)
1534
+ }
1535
+ );
1536
+ if (res && opts.eventPrefix && !skipUpdateEvent) {
1537
+ ctx.emit(
1538
+ `${opts.eventPrefix}.updated`,
1539
+ { type: "replace", document: res }
1540
+ );
1541
+ }
1542
+ return res;
1543
+ },
1544
+ /**
1545
+ * Delete one document and return it.
1546
+ * If soft delete is enabled, it will only set the deleted field to true (hiding it from future requests).
1547
+ */
1548
+ async _deleteOne(ctx, query, params, options) {
1549
+ const {
1550
+ sort,
1551
+ fields,
1552
+ strictTenantFilter = true,
1553
+ skipDeleteEvent,
1554
+ ...driverOptions
1555
+ } = options || {};
1556
+ let res;
1557
+ if (opts.softDelete) {
1558
+ res = await this._getDatabaseMixinCollection().findOneAndUpdate(
1559
+ this._getQueryFilter(
1560
+ query,
1561
+ params,
1562
+ "no-deleted",
1563
+ strictTenantFilter
1564
+ ),
1565
+ this._prepareUpdateFilter(
1566
+ // @ts-expect-error deletedAt is not always here for TS
1567
+ { $currentDate: { deletedAt: true } },
1568
+ "update"
1569
+ ),
1570
+ {
1571
+ ...driverOptions,
1572
+ includeResultMetadata: false,
1573
+ // Document says it's true by default and will be false in a next major
1574
+ sort: getQueryFromList("sort", sort),
1575
+ projection: getQueryFromList("projection", fields),
1576
+ returnDocument: "before"
1577
+ }
1578
+ );
1579
+ } else {
1580
+ res = await this._getDatabaseMixinCollection().findOneAndDelete(
1581
+ this._getQueryFilter(
1582
+ query,
1583
+ params,
1584
+ "no-deleted",
1585
+ strictTenantFilter
1586
+ ),
1587
+ {
1588
+ ...driverOptions,
1589
+ sort: getQueryFromList("sort", sort),
1590
+ projection: getQueryFromList("projection", fields)
1591
+ }
1592
+ );
1593
+ }
1594
+ if (res && opts.eventPrefix && !skipDeleteEvent) {
1595
+ ctx.emit(
1596
+ `${opts.eventPrefix}.deleted`,
1597
+ { type: "delete", document: res }
1598
+ );
1599
+ }
1600
+ return res;
1601
+ },
1602
+ /**
1603
+ * Delete many documents and return the number of deleted documents.
1604
+ * If soft delete is enabled, it will only set the deleted field to true (hiding it from future requests).
1605
+ *
1606
+ * WARNING: Do not send any events. You'll have to send an event yourself.
1607
+ */
1608
+ async _deleteMany(query, params, options) {
1609
+ const { strictTenantFilter = true, ...driverOptions } = options || {};
1610
+ if (opts.softDelete) {
1611
+ const res2 = await this._getDatabaseMixinCollection().updateMany(
1612
+ this._getQueryFilter(
1613
+ query,
1614
+ params,
1615
+ "no-deleted",
1616
+ strictTenantFilter
1617
+ ),
1618
+ this._prepareUpdateFilter(
1619
+ // @ts-expect-error deletedAt is not always here for TS
1620
+ { $currentDate: { deletedAt: true } },
1621
+ "update"
1622
+ ),
1623
+ driverOptions
1624
+ );
1625
+ return res2.modifiedCount;
1626
+ }
1627
+ const res = await this._getDatabaseMixinCollection().deleteMany(
1628
+ this._getQueryFilter(query, params, "no-deleted", strictTenantFilter),
1629
+ driverOptions
1630
+ );
1631
+ return res.deletedCount;
1632
+ }
1633
+ },
1634
+ actions: createActions(opts),
1635
+ created() {
1636
+ if (!("getMongoClient" in this)) {
1637
+ throw new Error(
1638
+ "DatabaseConnectionMixin is required to use DatabaseMethodsMixin"
1639
+ );
1640
+ }
1641
+ }
1642
+ });
1643
+ }
1644
+
1645
+ export { AjvActionSchemaFactory, DATABASE_INDEXES_MIXIN_SYNC_EVENT, DatabaseConnectionMixin, DatabaseIndexesMixin, DatabaseMethodsMixin, EntityNotFoundError, IndexStatus, QueryOp, ZodActionSchemaFactory, addQueryOps, addZodQueryOps, createActions, filterFields, filterObjectFields, getDefaultIndexName, getIndexesDifference$1 as getIndexesDifference, getQueryFromList, isIndexEqual, isIndexNameEqual, isOnAtlas, optimizeQuery, optionalMongoId, parseAndValidateQuery, parseStringifiedQuery, removeMongoId, shouldAutoCreateIndexes, shouldAutoDropIndexes };