@opra/mongodb 1.0.0-alpha.8 → 1.0.0-beta.1

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.
@@ -1,6 +1,7 @@
1
1
  import { DATATYPE_METADATA } from '@opra/common';
2
2
  import { ServiceBase } from '@opra/core';
3
3
  import { ObjectId } from 'mongodb';
4
+ import { MongoAdapter } from './mongo-adapter.js';
4
5
  /**
5
6
  * Class representing a MongoDB service for interacting with a collection.
6
7
  * @extends ServiceBase
@@ -20,20 +21,33 @@ export class MongoService extends ServiceBase {
20
21
  this._outputCodecs = {};
21
22
  this._dataType_ = dataType;
22
23
  this.db = options?.db;
23
- this.$documentFilter = this.$documentFilter || options?.documentFilter;
24
- this.$interceptor = this.$interceptor || options?.interceptor;
25
- this.$collectionName = options?.collectionName;
26
- if (!this.$collectionName) {
24
+ this.documentFilter = options?.documentFilter;
25
+ this.interceptor = options?.interceptor;
26
+ if (options?.collectionName)
27
+ this.collectionName = options?.collectionName;
28
+ else {
27
29
  if (typeof dataType === 'string')
28
- this.$collectionName = dataType;
30
+ this.collectionName = dataType;
29
31
  if (typeof dataType === 'function') {
30
32
  const metadata = Reflect.getMetadata(DATATYPE_METADATA, dataType);
31
33
  if (metadata)
32
- this.$collectionName = metadata.name;
34
+ this.collectionName = metadata.name;
33
35
  }
34
36
  }
35
- this.$resourceName = options?.resourceName;
36
- this.$idGenerator = options?.idGenerator;
37
+ this.resourceName = options?.resourceName;
38
+ this.idGenerator = options?.idGenerator;
39
+ this.onError = options?.onError;
40
+ }
41
+ for(context, overwriteProperties, overwriteContext) {
42
+ if (overwriteProperties?.documentFilter && this.documentFilter) {
43
+ overwriteProperties.documentFilter = [
44
+ ...(Array.isArray(this.documentFilter) ? this.documentFilter : [this.documentFilter]),
45
+ ...(Array.isArray(overwriteProperties?.documentFilter)
46
+ ? overwriteProperties?.documentFilter
47
+ : [overwriteProperties?.documentFilter]),
48
+ ];
49
+ }
50
+ return super.for(context, overwriteProperties, overwriteContext);
37
51
  }
38
52
  /**
39
53
  * Retrieves the collection name.
@@ -43,7 +57,7 @@ export class MongoService extends ServiceBase {
43
57
  * @throws {Error} If the collection name is not defined.
44
58
  */
45
59
  getCollectionName() {
46
- const out = typeof this.$collectionName === 'function' ? this.$collectionName(this) : this.$collectionName;
60
+ const out = typeof this.collectionName === 'function' ? this.collectionName(this) : this.collectionName;
47
61
  if (out)
48
62
  return out;
49
63
  throw new Error('collectionName is not defined');
@@ -53,12 +67,10 @@ export class MongoService extends ServiceBase {
53
67
  *
54
68
  * @protected
55
69
  * @returns {string} The resource name.
56
- * @throws {Error} If the collection name is not defined.
70
+ * @throws {Error} If the resource name is not defined.
57
71
  */
58
72
  getResourceName() {
59
- const out = typeof this.$resourceName === 'function'
60
- ? this.$resourceName(this)
61
- : this.$resourceName || this.getCollectionName();
73
+ const out = typeof this.resourceName === 'function' ? this.resourceName(this) : this.resourceName || this.getCollectionName();
62
74
  if (out)
63
75
  return out;
64
76
  throw new Error('resourceName is not defined');
@@ -73,36 +85,6 @@ export class MongoService extends ServiceBase {
73
85
  this._dataType = this.context.document.node.getComplexType(this._dataType_);
74
86
  return this._dataType;
75
87
  }
76
- /**
77
- * Retrieves the codec for the specified operation.
78
- *
79
- * @param operation - The operation to retrieve the encoder for. Valid values are 'create' and 'update'.
80
- */
81
- getInputCodec(operation) {
82
- let validator = this._inputCodecs[operation];
83
- if (validator)
84
- return validator;
85
- const options = { projection: '*' };
86
- if (operation === 'update')
87
- options.partial = 'deep';
88
- const dataType = this.dataType;
89
- validator = dataType.generateCodec('decode', options);
90
- this._inputCodecs[operation] = validator;
91
- return validator;
92
- }
93
- /**
94
- * Retrieves the codec.
95
- */
96
- getOutputCodec(operation) {
97
- let validator = this._outputCodecs[operation];
98
- if (validator)
99
- return validator;
100
- const options = { projection: '*', partial: 'deep' };
101
- const dataType = this.dataType;
102
- validator = dataType.generateCodec('decode', options);
103
- this._outputCodecs[operation] = validator;
104
- return validator;
105
- }
106
88
  /**
107
89
  * Executes the provided function within a transaction.
108
90
  *
@@ -144,191 +126,6 @@ export class MongoService extends ServiceBase {
144
126
  await session.endSession();
145
127
  }
146
128
  }
147
- /**
148
- * Gets the number of documents matching the filter.
149
- *
150
- * @param filter - The filter used to match documents.
151
- * @param options - The options for counting documents.
152
- * @protected
153
- */
154
- async _dbCountDocuments(filter, options) {
155
- const db = this.getDatabase();
156
- const collection = await this.getCollection(db);
157
- options = {
158
- ...options,
159
- limit: undefined,
160
- session: options?.session || this.getSession(),
161
- };
162
- return (await collection.countDocuments(filter || {}, options)) || 0;
163
- }
164
- /**
165
- * Acquires a connection and performs Collection.deleteOne operation
166
- *
167
- * @param filter - The filter used to select the document to remove
168
- * @param options - Optional settings for the command
169
- * @protected
170
- */
171
- async _dbDeleteOne(filter, options) {
172
- const db = this.getDatabase();
173
- const collection = await this.getCollection(db);
174
- options = {
175
- ...options,
176
- session: options?.session || this.getSession(),
177
- };
178
- return await collection.deleteOne(filter || {}, options);
179
- }
180
- /**
181
- * Acquires a connection and performs Collection.deleteMany operation
182
- *
183
- * @param filter - The filter used to select the documents to remove
184
- * @param options - Optional settings for the command
185
- * @protected
186
- */
187
- async _dbDeleteMany(filter, options) {
188
- const db = this.getDatabase();
189
- const collection = await this.getCollection(db);
190
- options = {
191
- ...options,
192
- session: options?.session || this.getSession(),
193
- };
194
- return await collection.deleteMany(filter || {}, options);
195
- }
196
- /**
197
- * Acquires a connection and performs Collection.distinct operation
198
- *
199
- * @param field - Field of the document to find distinct values for
200
- * @param filter - The filter for filtering the set of documents to which we apply the distinct filter.
201
- * @param options - Optional settings for the command
202
- * @protected
203
- */
204
- async _dbDistinct(field, filter, options) {
205
- const db = this.getDatabase();
206
- const collection = await this.getCollection(db);
207
- options = {
208
- ...options,
209
- session: options?.session || this.getSession(),
210
- };
211
- return await collection.distinct(field, filter || {}, options);
212
- }
213
- /**
214
- * Acquires a connection and performs Collection.aggregate operation
215
- *
216
- * @param pipeline - An array of aggregation pipelines to execute
217
- * @param options - Optional settings for the command
218
- * @protected
219
- */
220
- async _dbAggregate(pipeline, options) {
221
- const db = this.getDatabase();
222
- const collection = await this.getCollection(db);
223
- options = {
224
- ...options,
225
- session: options?.session || this.getSession(),
226
- };
227
- return await collection.aggregate(pipeline, options);
228
- }
229
- /**
230
- * Acquires a connection and performs Collection.findOne operation
231
- *
232
- * @param filter - Query for find Operation
233
- * @param options - Optional settings for the command
234
- * @protected
235
- */
236
- async _dbFindOne(filter, options) {
237
- const db = this.getDatabase();
238
- const collection = await this.getCollection(db);
239
- options = {
240
- ...options,
241
- session: options?.session || this.getSession(),
242
- };
243
- return (await collection.findOne(filter || {}, options));
244
- }
245
- /**
246
- * Acquires a connection and performs Collection.find operation
247
- *
248
- * @param filter - The filter predicate. If unspecified,
249
- * then all documents in the collection will match the predicate
250
- * @param options - Optional settings for the command
251
- * @protected
252
- */
253
- async _dbFind(filter, options) {
254
- const db = this.getDatabase();
255
- const collection = await this.getCollection(db);
256
- options = {
257
- ...options,
258
- session: options?.session || this.getSession(),
259
- };
260
- return collection.find(filter || {}, options);
261
- }
262
- /**
263
- * Acquires a connection and performs Collection.insertOne operation
264
- *
265
- * @param doc - The document to insert
266
- * @param options - Optional settings for the command
267
- * @protected
268
- */
269
- async _dbInsertOne(doc, options) {
270
- const db = this.getDatabase();
271
- const collection = await this.getCollection(db);
272
- options = {
273
- ...options,
274
- session: options?.session || this.getSession(),
275
- };
276
- return await collection.insertOne(doc, options);
277
- }
278
- /**
279
- * Acquires a connection and performs Collection.updateOne operation
280
- *
281
- * @param filter - The filter used to select the document to update
282
- * @param update - The update operations to be applied to the document
283
- * @param options - Optional settings for the command
284
- * @protected
285
- */
286
- async _dbUpdateOne(filter, update, options) {
287
- const db = this.getDatabase();
288
- const collection = await this.getCollection(db);
289
- options = {
290
- ...options,
291
- session: options?.session || this.getSession(),
292
- };
293
- return collection.updateOne(filter || {}, update, options);
294
- }
295
- /**
296
- * Acquires a connection and performs Collection.findOneAndUpdate operation
297
- *
298
- * @param filter - The filter used to select the document to update
299
- * @param update - Update operations to be performed on the document
300
- * @param options - Optional settings for the command
301
- * @protected
302
- */
303
- async _dbFindOneAndUpdate(filter, update, options) {
304
- const db = this.getDatabase();
305
- const collection = await this.getCollection(db);
306
- const opts = {
307
- returnDocument: 'after',
308
- includeResultMetadata: false,
309
- ...options,
310
- session: options?.session || this.getSession(),
311
- };
312
- return await collection.findOneAndUpdate(filter || {}, update, opts);
313
- }
314
- /**
315
- * Acquires a connection and performs Collection.updateMany operation
316
- *
317
- * @param filter - The filter used to select the documents to update
318
- * @param update - The update operations to be applied to the documents
319
- * @param options - Optional settings for the command
320
- * @protected
321
- */
322
- async _dbUpdateMany(filter, update, options) {
323
- const db = this.getDatabase();
324
- const collection = await this.getCollection(db);
325
- options = {
326
- ...options,
327
- session: options?.session || this.getSession(),
328
- upsert: false,
329
- };
330
- return await collection.updateMany(filter || {}, update, options);
331
- }
332
129
  /**
333
130
  * Retrieves the database connection.
334
131
  *
@@ -367,8 +164,8 @@ export class MongoService extends ServiceBase {
367
164
  * @protected
368
165
  * @returns {MongoAdapter.AnyId} The generated ID.
369
166
  */
370
- _generateId() {
371
- return typeof this.$idGenerator === 'function' ? this.$idGenerator(this) : new ObjectId();
167
+ _generateId(command) {
168
+ return typeof this.idGenerator === 'function' ? this.idGenerator(command, this) : new ObjectId();
372
169
  }
373
170
  /**
374
171
  * Retrieves the common filter used for querying documents.
@@ -378,19 +175,62 @@ export class MongoService extends ServiceBase {
378
175
  * @returns {FilterInput | Promise<FilterInput> | undefined} The common filter or a Promise
379
176
  * that resolves to the common filter, or undefined if not available.
380
177
  */
381
- _getDocumentFilter(info) {
382
- return typeof this.$documentFilter === 'function' ? this.$documentFilter(info, this) : this.$documentFilter;
383
- }
384
- async _intercept(callback, info) {
178
+ _getDocumentFilter(command) {
179
+ const commonFilter = Array.isArray(this.documentFilter) ? this.documentFilter : [this.documentFilter];
180
+ const mapped = commonFilter.map(f => (typeof f === 'function' ? f(command, this) : f));
181
+ return mapped.length > 1 ? MongoAdapter.prepareFilter(mapped) : mapped[0];
182
+ }
183
+ async _executeCommand(command, commandFn) {
184
+ let proto;
185
+ const next = async () => {
186
+ proto = proto ? Object.getPrototypeOf(proto) : this;
187
+ while (proto) {
188
+ if (proto.interceptor && Object.prototype.hasOwnProperty.call(proto, 'interceptor')) {
189
+ return await proto.interceptor.call(this, next, command, this);
190
+ }
191
+ proto = Object.getPrototypeOf(proto);
192
+ if (!(proto instanceof MongoService))
193
+ break;
194
+ }
195
+ return commandFn();
196
+ };
385
197
  try {
386
- if (this.$interceptor)
387
- return this.$interceptor(callback, info, this);
388
- return callback();
198
+ return await next();
389
199
  }
390
200
  catch (e) {
391
- Error.captureStackTrace(e, this._intercept);
392
- await this.$onError?.(e, this);
201
+ Error.captureStackTrace(e, this._executeCommand);
202
+ await this.onError?.(e, this);
393
203
  throw e;
394
204
  }
395
205
  }
206
+ /**
207
+ * Retrieves the codec for the specified operation.
208
+ *
209
+ * @param operation - The operation to retrieve the encoder for. Valid values are 'create' and 'update'.
210
+ */
211
+ _getInputCodec(operation) {
212
+ let validator = this._inputCodecs[operation];
213
+ if (validator)
214
+ return validator;
215
+ const options = { projection: '*' };
216
+ if (operation === 'update')
217
+ options.partial = 'deep';
218
+ const dataType = this.dataType;
219
+ validator = dataType.generateCodec('decode', options);
220
+ this._inputCodecs[operation] = validator;
221
+ return validator;
222
+ }
223
+ /**
224
+ * Retrieves the codec.
225
+ */
226
+ _getOutputCodec(operation) {
227
+ let validator = this._outputCodecs[operation];
228
+ if (validator)
229
+ return validator;
230
+ const options = { projection: '*', partial: 'deep' };
231
+ const dataType = this.dataType;
232
+ validator = dataType.generateCodec('decode', options);
233
+ this._outputCodecs[operation] = validator;
234
+ return validator;
235
+ }
396
236
  }
@@ -1,4 +1,5 @@
1
1
  import { ResourceNotAvailableError } from '@opra/common';
2
+ import omit from 'lodash.omit';
2
3
  import { ObjectId } from 'mongodb';
3
4
  import { MongoAdapter } from './mongo-adapter.js';
4
5
  import { MongoEntityService } from './mongo-entity-service.js';
@@ -18,12 +19,12 @@ export class MongoSingletonService extends MongoEntityService {
18
19
  */
19
20
  constructor(dataType, options) {
20
21
  super(dataType, options);
21
- this._id = this._id || options?._id || new ObjectId('655608925cad472b75fc6485');
22
+ this._id = options?._id || new ObjectId('655608925cad472b75fc6485');
22
23
  }
23
24
  /**
24
25
  * Asserts the existence of a resource based on the given options.
25
26
  *
26
- * @param {MongoSingletonService.ExistsOptions<T>} [options]
27
+ * @param {MongoEntityService.ExistsOptions<T>} [options]
27
28
  * @returns {Promise<void>} A Promise that resolves when the resource exists.
28
29
  * @throws {ResourceNotAvailableError} If the resource does not exist.
29
30
  */
@@ -31,127 +32,163 @@ export class MongoSingletonService extends MongoEntityService {
31
32
  if (!(await this.exists(options)))
32
33
  throw new ResourceNotAvailableError(this.getResourceName());
33
34
  }
35
+ async create(input, options) {
36
+ const command = {
37
+ crud: 'create',
38
+ method: 'create',
39
+ byId: false,
40
+ input,
41
+ options,
42
+ };
43
+ input._id = this._id;
44
+ return this._executeCommand(command, async () => {
45
+ const r = await this._create(command);
46
+ if (!command.options?.projection)
47
+ return r;
48
+ const findCommand = {
49
+ ...command,
50
+ crud: 'read',
51
+ byId: true,
52
+ documentId: r._id,
53
+ options: omit(options, 'filter'),
54
+ };
55
+ const out = await this._findById(findCommand);
56
+ if (out)
57
+ return out;
58
+ });
59
+ }
34
60
  /**
35
61
  * Creates the document in the database.
36
62
  *
37
- * @param {PartialDTO<T>} input - The partial input to create the document with.
38
- * @param {MongoSingletonService.CreateOptions} [options] - The options for creating the document.
39
- * @return {Promise<PartialDTO<T>>} A promise that resolves to the partial output of the created document.
63
+ * @param {DTO<T>} input - The partial input to create the document with.
64
+ * @param {MongoEntityService.CreateOptions} [options] - The options for creating the document.
65
+ * @returns {Promise<MongoEntityService.CreateResult<T>>} A promise that resolves create operation result
40
66
  * @throws {Error} Throws an error if an unknown error occurs while creating the document.
41
67
  */
42
- async create(input, options) {
43
- input._id = this._id;
44
- const info = {
68
+ async createOnly(input, options) {
69
+ const command = {
45
70
  crud: 'create',
46
- method: 'create',
71
+ method: 'createOnly',
47
72
  byId: false,
48
- documentId: this._id,
49
73
  input,
50
74
  options,
51
75
  };
52
- return this._intercept(() => this._create(input, options), info);
76
+ input._id = this._id;
77
+ return this._executeCommand(command, () => this._create(command));
53
78
  }
54
79
  /**
55
80
  * Deletes a record from the database
56
81
  *
57
- * @param {MongoSingletonService.DeleteOptions<T>} options - The options for deleting the record
82
+ * @param {MongoEntityService.DeleteOptions<T>} options - The options for deleting the record
58
83
  * @returns {Promise<number>} The number of records deleted
59
84
  */
60
85
  async delete(options) {
61
- const info = {
86
+ const command = {
62
87
  crud: 'delete',
63
88
  method: 'delete',
64
89
  byId: true,
65
90
  documentId: this._id,
66
91
  options,
67
92
  };
68
- return this._intercept(async () => {
69
- const filter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info), options?.filter]);
70
- return this._delete(this._id, { ...options, filter });
71
- }, info);
93
+ return this._executeCommand(command, async () => {
94
+ const filter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command), command.options?.filter]);
95
+ command.options = { ...command.options, filter };
96
+ return this._delete(command);
97
+ });
72
98
  }
73
99
  /**
74
100
  * Checks if the document exists in the database.
75
101
  *
102
+ * @param {MongoEntityService.FindOneOptions<T>} [options] - The options for finding the document.
76
103
  * @return {Promise<boolean>} - A promise that resolves to a boolean value indicating if the document exists.
77
104
  */
78
105
  async exists(options) {
79
- return !!(await this.find({ ...options, projection: ['_id'], skip: undefined }));
106
+ const command = {
107
+ crud: 'read',
108
+ method: 'exists',
109
+ byId: true,
110
+ documentId: this._id,
111
+ options,
112
+ };
113
+ return this._executeCommand(command, async () => {
114
+ const documentFilter = await this._getDocumentFilter(command);
115
+ const filter = MongoAdapter.prepareFilter([documentFilter, command.options?.filter]);
116
+ const findCommand = command;
117
+ findCommand.options = { ...command.options, filter, projection: ['_id'] };
118
+ return !!(await this._findById(findCommand));
119
+ });
80
120
  }
81
- /**
82
- * Fetches the document if it exists. Returns undefined if not found.
83
- *
84
- * @param {MongoSingletonService.FindOneOptions<T>} [options] - The options for finding the document.
85
- * @returns {Promise<PartialDTO<T> | undefined>} - A promise that resolves to the found document or undefined if not found.
86
- */
87
121
  async find(options) {
88
- const info = {
122
+ const command = {
89
123
  crud: 'read',
90
- method: 'findOne',
124
+ method: 'findById',
91
125
  byId: true,
92
126
  documentId: this._id,
93
127
  options,
94
128
  };
95
- return this._intercept(async () => {
96
- const filter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info), options?.filter]);
97
- return this._findById(this._id, { ...options, filter });
98
- }, info);
129
+ return this._executeCommand(command, async () => {
130
+ const documentFilter = await this._getDocumentFilter(command);
131
+ const filter = MongoAdapter.prepareFilter([documentFilter, command.options?.filter]);
132
+ command.options = { ...command.options, filter };
133
+ return this._findById(command);
134
+ });
99
135
  }
100
- /**
101
- * Fetches the document from the Mongo collection service. Throws error if not found.
102
- *
103
- * @param {MongoSingletonService.FindOneOptions<T>} options - The options to customize the query.
104
- * @return {Promise<PartialDTO<T>>} - A promise that resolves to the fetched document.
105
- * @throws {ResourceNotAvailableError} - If the document is not found in the collection.
106
- */
107
136
  async get(options) {
108
137
  const out = await this.find(options);
109
138
  if (!out)
110
139
  throw new ResourceNotAvailableError(this.getResourceName());
111
140
  return out;
112
141
  }
113
- /**
114
- * Updates a document in the MongoDB collection.
115
- *
116
- * @param {PatchDTO<T>} input - The partial input to update the document.
117
- * @param {MongoSingletonService.UpdateOptions<T>} [options] - The update options.
118
- *
119
- * @return {Promise<number>} - A promise that resolves to the updated document or undefined if not found.
120
- */
121
- async updateOnly(input, options) {
122
- const info = {
142
+ async update(input, options) {
143
+ const isUpdateFilter = Array.isArray(input) || !!Object.keys(input).find(x => x.startsWith('$'));
144
+ const command = {
123
145
  crud: 'update',
124
146
  method: 'update',
125
- byId: true,
126
147
  documentId: this._id,
127
- input,
148
+ byId: true,
149
+ input: isUpdateFilter ? undefined : input,
150
+ inputRaw: isUpdateFilter ? input : undefined,
128
151
  options,
129
152
  };
130
- return this._intercept(async () => {
131
- const filter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info), options?.filter]);
132
- return this._updateOnly(this._id, input, { ...options, filter });
133
- }, info);
153
+ return this._executeCommand(command, async () => {
154
+ const filter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command), command.options?.filter]);
155
+ command.options = { ...command.options, filter };
156
+ const matchCount = await this._updateOnly(command);
157
+ if (matchCount) {
158
+ const findCommand = {
159
+ ...command,
160
+ crud: 'read',
161
+ byId: true,
162
+ documentId: this._id,
163
+ options: omit(options, ['filter', 'sort']),
164
+ };
165
+ const out = await this._findById(findCommand);
166
+ if (out)
167
+ return out;
168
+ }
169
+ });
134
170
  }
135
171
  /**
136
172
  * Updates a document in the MongoDB collection.
137
173
  *
138
174
  * @param {PatchDTO<T>} input - The partial input to update the document.
139
- * @param {MongoSingletonService.UpdateOptions<T>} [options] - The update options.
175
+ * @param {MongoEntityService.UpdateOneOptions<T>} [options] - The update options.
140
176
  *
141
- * @return {Promise<PartialDTO<T> | undefined>} - A promise that resolves to the updated document or undefined if not found.
177
+ * @return {Promise<number>} - A promise that resolves to the updated document or undefined if not found.
142
178
  */
143
- async update(input, options) {
144
- const info = {
179
+ async updateOnly(input, options) {
180
+ const command = {
145
181
  crud: 'update',
146
- method: 'update',
147
- byId: true,
182
+ method: 'updateOnly',
148
183
  documentId: this._id,
184
+ byId: true,
149
185
  input,
150
186
  options,
151
187
  };
152
- return this._intercept(async () => {
153
- const filter = MongoAdapter.prepareFilter([await this._getDocumentFilter(info), options?.filter]);
154
- return this._update(this._id, input, { ...options, filter });
155
- }, info);
188
+ return this._executeCommand(command, async () => {
189
+ const filter = MongoAdapter.prepareFilter([await this._getDocumentFilter(command), command.options?.filter]);
190
+ command.options = { ...command.options, filter };
191
+ return this._updateOnly(command);
192
+ });
156
193
  }
157
194
  }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }