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