@opra/mongodb 0.31.13 → 0.32.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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MongoCollectionService = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const lodash_omit_1 = tslib_1.__importDefault(require("lodash.omit"));
6
+ const mongodb_1 = require("mongodb");
6
7
  const common_1 = require("@opra/common");
7
8
  const mongo_adapter_js_1 = require("./mongo-adapter.js");
8
9
  const mongo_service_js_1 = require("./mongo-service.js");
@@ -13,112 +14,232 @@ const mongo_service_js_1 = require("./mongo-service.js");
13
14
  class MongoCollectionService extends mongo_service_js_1.MongoService {
14
15
  constructor(dataType, options) {
15
16
  super(dataType, options);
17
+ this._encoders = {};
16
18
  this.defaultLimit = options?.defaultLimit || 10;
17
- this.keyFields = options?.keyField ?
18
- (Array.isArray(options.keyField) ? options.keyField : [options.keyField]) : undefined;
19
- }
20
- async create(doc, options) {
21
- const r = await this._rawInsertOne(doc, options);
22
- if (r.insertedId) {
23
- const out = await this.get(r.insertedId, options);
24
- if (out)
25
- return out;
26
- }
19
+ this.collectionKey = options?.collectionKey || '_id';
20
+ }
21
+ /**
22
+ * Checks if document exists. Throws error if not.
23
+ *
24
+ * @param id
25
+ */
26
+ async assert(id) {
27
+ if (!(await this.exists(id)))
28
+ throw new common_1.ResourceNotFoundError(this.resourceName || this.getCollectionName(), id);
29
+ }
30
+ /**
31
+ * Inserts a single document into MongoDB
32
+ *
33
+ * @param input
34
+ * @param options
35
+ */
36
+ async create(input, options) {
37
+ const encode = this._getEncoder('create');
38
+ const doc = encode(input);
39
+ doc._id = doc._id || this._generateId();
40
+ const r = await this.__insertOne(doc, options);
41
+ if (r.insertedId)
42
+ return doc;
27
43
  /* istanbul ignore next */
28
44
  throw new Error('Unknown error while creating document');
29
45
  }
46
+ /**
47
+ * Gets the number of documents matching the filter.
48
+ *
49
+ * @param options
50
+ */
30
51
  async count(options) {
31
- const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter) || {};
32
- return this._rawCountDocuments(filter, (0, lodash_omit_1.default)(options, 'filter'));
52
+ const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter);
53
+ return this.__countDocuments(filter, (0, lodash_omit_1.default)(options, 'filter'));
33
54
  }
55
+ /**
56
+ * Delete a document from a collection
57
+ *
58
+ * @param id
59
+ * @param options
60
+ */
34
61
  async delete(id, options) {
35
- const filter = mongo_adapter_js_1.MongoAdapter.prepareKeyValues(id, this.keyFields);
36
- const optionsFilter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter);
37
- if (optionsFilter)
38
- filter.$and = [...(Array.isArray(optionsFilter) ? optionsFilter : [optionsFilter])];
39
- const r = await this._rawDeleteOne(filter, options);
62
+ const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter([
63
+ mongo_adapter_js_1.MongoAdapter.prepareKeyValues(id, [this.collectionKey]),
64
+ options?.filter
65
+ ]);
66
+ const r = await this.__deleteOne(filter, options);
40
67
  return r.deletedCount;
41
68
  }
69
+ /**
70
+ * Delete multiple documents from a collection
71
+ *
72
+ * @param options
73
+ */
42
74
  async deleteMany(options) {
43
- const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter) || {};
44
- const r = await this._rawDeleteMany(filter, (0, lodash_omit_1.default)(options, 'filter'));
75
+ const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter);
76
+ const r = await this.__deleteMany(filter, (0, lodash_omit_1.default)(options, 'filter'));
45
77
  return r.deletedCount;
46
78
  }
79
+ /**
80
+ * Checks if document exists.
81
+ * Returns true if document exists, false otherwise
82
+ *
83
+ * @param id
84
+ */
85
+ async exists(id) {
86
+ return !!(await this.findById(id, { pick: ['_id'] }));
87
+ }
88
+ /**
89
+ * Fetches the first document matches by id.
90
+ *
91
+ * @param id
92
+ * @param options
93
+ */
94
+ async findById(id, options) {
95
+ const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter([
96
+ mongo_adapter_js_1.MongoAdapter.prepareKeyValues(id, [this.collectionKey]),
97
+ options?.filter
98
+ ]);
99
+ return await this.findOne({ ...options, filter });
100
+ }
101
+ /**
102
+ * Fetches the first document that matches the filter.
103
+ * Returns undefined if not found.
104
+ *
105
+ * @param options
106
+ */
107
+ async findOne(options) {
108
+ const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter);
109
+ const mongoOptions = {
110
+ ...options,
111
+ sort: options?.sort ? mongo_adapter_js_1.MongoAdapter.prepareSort(options.sort) : undefined,
112
+ projection: mongo_adapter_js_1.MongoAdapter.prepareProjection(this.getDataType(), options),
113
+ limit: undefined
114
+ };
115
+ const out = await this.__findOne(filter, mongoOptions);
116
+ return out || undefined;
117
+ }
118
+ /**
119
+ * Fetches all document that matches the filter
120
+ *
121
+ * @param options
122
+ */
123
+ async findMany(options) {
124
+ const mongoOptions = {
125
+ ...(0, lodash_omit_1.default)(options, ['pick', 'include', 'omit', 'sort', 'skip', 'limit', 'filter', 'count'])
126
+ };
127
+ const limit = options?.limit || this.defaultLimit;
128
+ const stages = [];
129
+ let dataStages = stages;
130
+ if (options?.count) {
131
+ dataStages = [];
132
+ stages.push({
133
+ $facet: {
134
+ data: dataStages,
135
+ count: [{ $count: 'totalMatches' }]
136
+ }
137
+ });
138
+ }
139
+ if (options?.filter) {
140
+ const optionsFilter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter);
141
+ dataStages.push({ $match: optionsFilter });
142
+ }
143
+ if (options?.skip)
144
+ dataStages.push({ $skip: options.skip });
145
+ if (options?.sort) {
146
+ const sort = mongo_adapter_js_1.MongoAdapter.prepareSort(options.sort);
147
+ if (sort)
148
+ dataStages.push({ $sort: sort });
149
+ }
150
+ dataStages.push({ $limit: limit });
151
+ const dataType = this.getDataType();
152
+ const projection = mongo_adapter_js_1.MongoAdapter.prepareProjection(dataType, options);
153
+ if (projection)
154
+ dataStages.push({ $project: projection });
155
+ const cursor = await this.__aggregate(stages, {
156
+ ...mongoOptions
157
+ });
158
+ try {
159
+ if (options?.count) {
160
+ const facetResult = await cursor.toArray();
161
+ this.context.response.totalMatches = facetResult[0].count[0].totalMatches || 0;
162
+ return facetResult[0].data;
163
+ }
164
+ else
165
+ return await cursor.toArray();
166
+ }
167
+ finally {
168
+ if (!cursor.closed)
169
+ await cursor.close();
170
+ }
171
+ }
172
+ /**
173
+ * Fetches the first Document that matches the id. Throws error undefined if not found.
174
+ *
175
+ * @param id
176
+ * @param options
177
+ */
47
178
  async get(id, options) {
48
- const filter = mongo_adapter_js_1.MongoAdapter.prepareKeyValues(id, this.keyFields);
49
- const optionsFilter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter);
50
- if (optionsFilter)
51
- filter.$and = [...(Array.isArray(optionsFilter) ? optionsFilter : [optionsFilter])];
52
- const out = await this.findOne({ ...options, filter });
179
+ const out = await this.findById(id, options);
53
180
  if (!out)
54
181
  throw new common_1.ResourceNotFoundError(this.resourceName || this.getCollectionName(), id);
55
182
  return out;
56
183
  }
57
- async findOne(options) {
58
- const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter) || {};
184
+ /**
185
+ * Updates a single document.
186
+ * Returns updated document
187
+ *
188
+ * @param id
189
+ * @param input
190
+ * @param options
191
+ */
192
+ async update(id, input, options) {
193
+ const encode = this._getEncoder('update');
194
+ const doc = encode(input);
195
+ const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter([
196
+ mongo_adapter_js_1.MongoAdapter.prepareKeyValues(id, [this.collectionKey]),
197
+ options?.filter
198
+ ]);
199
+ const patch = mongo_adapter_js_1.MongoAdapter.preparePatch(doc);
59
200
  const mongoOptions = {
60
- ...(0, lodash_omit_1.default)(options, ['sort', 'skip', 'limit', 'filter']),
201
+ ...options,
202
+ includeResultMetadata: false,
203
+ upsert: undefined,
61
204
  projection: mongo_adapter_js_1.MongoAdapter.prepareProjection(this.getDataType(), options),
62
205
  };
63
- const out = await this._rawFindOne(filter, mongoOptions);
64
- if (out) {
65
- if (this.transformData)
66
- return this.transformData(out);
67
- return out;
68
- }
206
+ const out = await this.__findOneAndUpdate(filter, patch, mongoOptions);
207
+ return out || undefined;
69
208
  }
70
- async findMany(options) {
71
- const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter) || {};
72
- const [items, count] = await Promise.all([
73
- // Promise that returns items
74
- Promise.resolve().then(async () => {
75
- const cursor = await this._rawFind(filter, {
76
- ...(0, lodash_omit_1.default)(options, 'filter'),
77
- sort: options?.sort ? mongo_adapter_js_1.MongoAdapter.prepareSort(options.sort) : undefined,
78
- projection: mongo_adapter_js_1.MongoAdapter.prepareProjection(this.getDataType(), options)
79
- });
80
- try {
81
- const out = [];
82
- let obj;
83
- while (out.length < this.defaultLimit && (obj = await cursor.next())) {
84
- const v = this.transformData ? this.transformData(obj) : obj;
85
- if (v)
86
- out.push(obj);
87
- }
88
- return out;
89
- }
90
- finally {
91
- await cursor.close();
92
- }
93
- }),
94
- // Promise that returns count
95
- (options?.count ? this.count(options) : Promise.resolve()),
209
+ /**
210
+ * Updates a single document
211
+ * Returns number of updated documents
212
+ *
213
+ * @param id
214
+ * @param input
215
+ * @param options
216
+ */
217
+ async updateOnly(id, input, options) {
218
+ const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter([
219
+ mongo_adapter_js_1.MongoAdapter.prepareKeyValues(id, [this.collectionKey]),
220
+ options?.filter
96
221
  ]);
97
- if (options?.count)
98
- this.context.response.totalMatches = count || 0;
99
- return items;
100
- }
101
- async update(id, doc, options) {
102
- const filter = mongo_adapter_js_1.MongoAdapter.prepareKeyValues(id, this.keyFields);
103
- const optionsFilter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter);
104
- if (optionsFilter)
105
- filter.$and = [...(Array.isArray(optionsFilter) ? optionsFilter : [optionsFilter])];
106
- // Prevent updating _id field
107
- delete doc._id;
222
+ const encode = this._getEncoder('update');
223
+ const doc = encode(input);
108
224
  const patch = mongo_adapter_js_1.MongoAdapter.preparePatch(doc);
109
- patch.$set = patch.$set || {};
110
225
  const mongoOptions = {
111
226
  ...options,
112
- upsert: undefined
227
+ includeResultMetadata: false,
228
+ upsert: undefined,
229
+ projection: mongo_adapter_js_1.MongoAdapter.prepareProjection(this.getDataType(), options),
113
230
  };
114
- const r = await this._rawUpdateOne(filter, patch, mongoOptions);
115
- if (r.upsertedCount)
116
- return await this.get(r.upsertedId, options);
117
- return await this.get(id, options);
118
- }
119
- async updateMany(doc, options) {
120
- // Prevent updating _id field
121
- delete doc._id;
231
+ const out = await this.__updateOne(filter, patch, mongoOptions);
232
+ return out.modifiedCount;
233
+ }
234
+ /**
235
+ * Update multiple documents in a collection
236
+ *
237
+ * @param input
238
+ * @param options
239
+ */
240
+ async updateMany(input, options) {
241
+ const encode = this._getEncoder('update');
242
+ const doc = encode(input);
122
243
  const patch = mongo_adapter_js_1.MongoAdapter.preparePatch(doc);
123
244
  patch.$set = patch.$set || {};
124
245
  const mongoOptions = {
@@ -126,8 +247,49 @@ class MongoCollectionService extends mongo_service_js_1.MongoService {
126
247
  upsert: undefined
127
248
  };
128
249
  const filter = mongo_adapter_js_1.MongoAdapter.prepareFilter(options?.filter) || {};
129
- const r = await this._rawUpdateMany(filter, patch, mongoOptions);
250
+ const r = await this.__updateMany(filter, patch, mongoOptions);
130
251
  return r.matchedCount;
131
252
  }
253
+ /**
254
+ * Generates Id value
255
+ *
256
+ * @protected
257
+ */
258
+ _generateId() {
259
+ return new mongodb_1.ObjectId();
260
+ }
261
+ /**
262
+ * Generates a new Validator for encoding or returns from cache if already generated before
263
+ * @param operation
264
+ * @protected
265
+ */
266
+ _getEncoder(operation) {
267
+ let encoder = this._encoders[operation];
268
+ if (encoder)
269
+ return encoder;
270
+ encoder = this._generateEncoder(operation);
271
+ this._encoders[operation] = encoder;
272
+ return encoder;
273
+ }
274
+ /**
275
+ * Generates a new Valgen Validator for encode operation
276
+ *
277
+ * @param operation
278
+ * @protected
279
+ */
280
+ _generateEncoder(operation) {
281
+ let encoder = this._encoders[operation];
282
+ if (encoder)
283
+ return encoder;
284
+ const dataType = this.getDataType();
285
+ const options = {};
286
+ if (operation === 'update') {
287
+ options.omit = ['_id'];
288
+ options.partial = true;
289
+ }
290
+ encoder = dataType.generateCodec('encode', options);
291
+ this._encoders[operation] = encoder;
292
+ return encoder;
293
+ }
132
294
  }
133
295
  exports.MongoCollectionService = MongoCollectionService;
@@ -20,10 +20,22 @@ class MongoService extends core_1.ApiService {
20
20
  }
21
21
  this.resourceName = options?.resourceName || this.collectionName;
22
22
  }
23
+ getDataType() {
24
+ return this.context.api.getComplexType(this._dataType);
25
+ }
23
26
  forContext(arg0, attributes) {
24
27
  return super.forContext(arg0, attributes);
25
28
  }
26
- async _rawInsertOne(doc, options) {
29
+ /**
30
+ * Inserts a single document into MongoDB. If documents passed in do not contain the **_id** field,
31
+ * one will be added to each of the documents missing it by the driver, mutating the document. This behavior
32
+ * can be overridden by setting the **forceServerObjectId** flag.
33
+ *
34
+ * @param doc
35
+ * @param options
36
+ * @protected
37
+ */
38
+ async __insertOne(doc, options) {
27
39
  const db = await this.getDatabase();
28
40
  const collection = await this.getCollection(db);
29
41
  options = {
@@ -38,7 +50,14 @@ class MongoService extends core_1.ApiService {
38
50
  throw e;
39
51
  }
40
52
  }
41
- async _rawCountDocuments(filter, options) {
53
+ /**
54
+ * Gets the number of documents matching the filter.
55
+ *
56
+ * @param filter
57
+ * @param options
58
+ * @protected
59
+ */
60
+ async __countDocuments(filter, options) {
42
61
  const db = await this.getDatabase();
43
62
  const collection = await this.getCollection(db);
44
63
  options = {
@@ -54,7 +73,13 @@ class MongoService extends core_1.ApiService {
54
73
  throw e;
55
74
  }
56
75
  }
57
- async _rawDeleteOne(filter, options) {
76
+ /**
77
+ * Delete a document from a collection
78
+ *
79
+ * @param filter - The filter used to select the document to remove
80
+ * @param options - Optional settings for the command
81
+ */
82
+ async __deleteOne(filter, options) {
58
83
  const db = await this.getDatabase();
59
84
  const collection = await this.getCollection(db);
60
85
  options = {
@@ -69,7 +94,14 @@ class MongoService extends core_1.ApiService {
69
94
  throw e;
70
95
  }
71
96
  }
72
- async _rawDeleteMany(filter, options) {
97
+ /**
98
+ * Delete multiple documents from a collection
99
+ *
100
+ * @param filter
101
+ * @param options
102
+ * @protected
103
+ */
104
+ async __deleteMany(filter, options) {
73
105
  const db = await this.getDatabase();
74
106
  const collection = await this.getCollection(db);
75
107
  options = {
@@ -84,7 +116,36 @@ class MongoService extends core_1.ApiService {
84
116
  throw e;
85
117
  }
86
118
  }
87
- async _rawFindOne(filter, options) {
119
+ /**
120
+ * Create a new Change Stream, watching for new changes (insertions, updates, replacements, deletions, and invalidations) in this collection.
121
+ *
122
+ * @param pipeline
123
+ * @param options
124
+ * @protected
125
+ */
126
+ async __aggregate(pipeline, options) {
127
+ const db = await this.getDatabase();
128
+ const collection = await this.getCollection(db);
129
+ options = {
130
+ ...options,
131
+ session: options?.session || this.session
132
+ };
133
+ try {
134
+ return await collection.aggregate(pipeline, options);
135
+ }
136
+ catch (e) {
137
+ await this._onError(e);
138
+ throw e;
139
+ }
140
+ }
141
+ /**
142
+ * Fetches the first document that matches the filter
143
+ *
144
+ * @param filter
145
+ * @param options
146
+ * @protected
147
+ */
148
+ async __findOne(filter, options) {
88
149
  const db = await this.getDatabase();
89
150
  const collection = await this.getCollection(db);
90
151
  options = {
@@ -99,7 +160,14 @@ class MongoService extends core_1.ApiService {
99
160
  throw e;
100
161
  }
101
162
  }
102
- async _rawFind(filter, options) {
163
+ /**
164
+ * Creates a cursor for a filter that can be used to iterate over results from MongoDB
165
+ *
166
+ * @param filter
167
+ * @param options
168
+ * @protected
169
+ */
170
+ async __find(filter, options) {
103
171
  const db = await this.getDatabase();
104
172
  const collection = await this.getCollection(db);
105
173
  options = {
@@ -114,22 +182,63 @@ class MongoService extends core_1.ApiService {
114
182
  throw e;
115
183
  }
116
184
  }
117
- async _rawUpdateOne(filter, doc, options) {
185
+ /**
186
+ * Update a single document in a collection
187
+ *
188
+ * @param filter
189
+ * @param update
190
+ * @param options
191
+ * @protected
192
+ */
193
+ async __updateOne(filter, update, options) {
118
194
  const db = await this.getDatabase();
119
195
  const collection = await this.getCollection(db);
120
196
  options = {
197
+ session: this.session,
198
+ ...options
199
+ };
200
+ try {
201
+ return collection.updateOne(filter, update, options);
202
+ }
203
+ catch (e) {
204
+ await this._onError(e);
205
+ throw e;
206
+ }
207
+ }
208
+ /**
209
+ * Find a document and update it in one atomic operation. Requires a write lock for the duration of the operation.
210
+ *
211
+ * @param filter
212
+ * @param doc
213
+ * @param options
214
+ * @protected
215
+ */
216
+ async __findOneAndUpdate(filter, doc, options) {
217
+ const db = await this.getDatabase();
218
+ const collection = await this.getCollection(db);
219
+ const opts = {
220
+ returnDocument: 'after',
221
+ session: this.session,
222
+ includeResultMetadata: false,
121
223
  ...options,
122
- session: options?.session || this.session
123
224
  };
124
225
  try {
125
- return await collection.updateOne(filter, doc, options);
226
+ return await collection.findOneAndUpdate(filter, doc, opts);
126
227
  }
127
228
  catch (e) {
128
229
  await this._onError(e);
129
230
  throw e;
130
231
  }
131
232
  }
132
- async _rawUpdateMany(filter, doc, options) {
233
+ /**
234
+ * Update multiple documents in a collection
235
+ *
236
+ * @param filter
237
+ * @param doc
238
+ * @param options
239
+ * @protected
240
+ */
241
+ async __updateMany(filter, doc, options) {
133
242
  const db = await this.getDatabase();
134
243
  const collection = await this.getCollection(db);
135
244
  options = {
@@ -156,9 +265,6 @@ class MongoService extends core_1.ApiService {
156
265
  throw new Error(`Database not set!`);
157
266
  return this.db;
158
267
  }
159
- getDataType() {
160
- return this.context.api.getComplexType(this._dataType);
161
- }
162
268
  async getCollection(db) {
163
269
  return db.collection(this.getCollectionName());
164
270
  }