• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

moleculerjs / database / #178

12 Nov 2023 04:51PM UTC coverage: 38.137% (-56.5%) from 94.594%
#178

push

icebob
Fix #53

690 of 1512 branches covered (0.0%)

Branch coverage included in aggregate %.

44 of 45 new or added lines in 2 files covered. (97.78%)

2662 existing lines in 19 files now uncovered.

1517 of 4275 relevant lines covered (35.49%)

50.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.86
/src/methods.js
1
/*
2
 * @moleculer/database
3
 * Copyright (c) 2022 MoleculerJS (https://github.com/moleculerjs/database)
4
 * MIT Licensed
5
 */
6

7
"use strict";
8

9
const Adapters = require("./adapters");
1✔
10
const { Context } = require("moleculer"); // eslint-disable-line no-unused-vars
1✔
11
const { EntityNotFoundError } = require("./errors");
1✔
12
const { MoleculerClientError } = require("moleculer").Errors;
1✔
13
const { Transform } = require("stream");
1✔
14
const _ = require("lodash");
1✔
15
const C = require("./constants");
1✔
16

17
module.exports = function (mixinOpts) {
1✔
18
        const cacheOpts = mixinOpts.cache && mixinOpts.cache.enabled ? mixinOpts.cache : null;
85!
19

20
        const cacheColumnName = new Map();
85✔
21

22
        return {
85✔
23
                /**
24
                 * Get or create an adapter. Multi-tenant support method.
25
                 * @param {Context?} ctx
26
                 */
27
                async getAdapter(ctx) {
28
                        const [hash, adapterOpts] = await this.getAdapterByContext(ctx, mixinOpts.adapter);
706✔
29
                        const item = this.adapters.get(hash);
706✔
30
                        if (item) {
706✔
31
                                item.touched = Date.now();
621✔
32
                                if (!item.connectPromise) return item.adapter;
621!
33
                                // This adapter is connecting now. Wait for it.
34
                                this.logger.debug(`Adapter '${hash}' is connecting, right now. Wait for it...`);
×
35
                                await item.connectPromise;
×
36
                                return item.adapter;
×
37
                        }
38

39
                        this.logger.debug(`Adapter not found for '${hash}'. Create a new adapter instance...`);
85✔
40
                        const adapter = Adapters.resolve(adapterOpts);
85✔
41
                        adapter.init(this);
85✔
42
                        // We store the promise of connect, because we don't want to call the connect method twice.
43
                        const connectPromise = this._connect(adapter, hash, adapterOpts);
85✔
44
                        const storedAdapterItem = { hash, adapter, connectPromise, touched: Date.now() };
85✔
45
                        // Store the adapter
46
                        this.adapters.set(hash, storedAdapterItem);
85✔
47
                        await this.maintenanceAdapters();
85✔
48
                        // Wait for real connect
49
                        await connectPromise;
85✔
50
                        this.logger.info(
85✔
51
                                `Adapter '${hash}' connected. Number of adapters:`,
52
                                this.adapters.size
53
                        );
54
                        // Clean the connect promise
55
                        delete storedAdapterItem.connectPromise;
85✔
56

57
                        return adapter;
85✔
58
                },
59

60
                /**
61
                 * For multi-tenant support this method generates a cache key
62
                 * hash value from `ctx`. By default, it returns "default" hash key.
63
                 * It can be overwritten to implement custom multi-tenant solution.
64
                 *
65
                 * @param {Context?} ctx
66
                 * @param {Object|any?} adapterDef
67
                 */
68
                getAdapterByContext(ctx, adapterDef) {
69
                        return ["default", adapterDef];
706✔
70
                },
71

72
                async maintenanceAdapters() {
73
                        if (mixinOpts.maximumAdapters == null || mixinOpts.maximumAdapters < 1) return;
85!
74

UNCOV
75
                        const surplus = this.adapters.size - mixinOpts.maximumAdapters;
×
UNCOV
76
                        if (surplus > 0) {
×
UNCOV
77
                                let adapters = Array.from(this.adapters.values());
×
UNCOV
78
                                adapters.sort((a, b) => a.touched - b.touched);
×
UNCOV
79
                                const closeable = adapters.slice(0, surplus);
×
UNCOV
80
                                this.logger.info(
×
81
                                        `Close ${closeable.length} old adapter(s). Limit: ${mixinOpts.maximumAdapters}, Current: ${this.adapters.size}`
82
                                );
UNCOV
83
                                for (const { adapter, hash } of closeable) {
×
UNCOV
84
                                        await this._disconnect(adapter, hash);
×
85
                                }
86
                        }
87
                },
88

89
                /**
90
                 * Connect to the DB
91
                 *
92
                 * @param {Adapter} adapter
93
                 * @param {String} hash
94
                 * @param {Object} adapterOpts
95
                 */
96
                _connect(adapter, hash, adapterOpts) {
97
                        return new this.Promise((resolve, reject) => {
85✔
98
                                const connecting = async () => {
85✔
99
                                        try {
85✔
100
                                                await adapter.connect();
85✔
101
                                                if (this.$hooks["adapterConnected"])
85!
UNCOV
102
                                                        await this.$hooks["adapterConnected"](adapter, hash, adapterOpts);
×
103

104
                                                this._metricInc(C.METRIC_ADAPTER_TOTAL);
85✔
105
                                                this._metricInc(C.METRIC_ADAPTER_ACTIVE);
85✔
106

107
                                                resolve();
85✔
108
                                        } catch (err) {
109
                                                this.logger.error("Connection error!", err);
×
110
                                                if (mixinOpts.autoReconnect) {
×
111
                                                        setTimeout(() => {
×
112
                                                                this.logger.warn("Reconnecting...");
×
113
                                                                connecting();
×
114
                                                        }, 1000);
115
                                                } else {
116
                                                        reject(err);
×
117
                                                }
118
                                        }
119
                                };
120
                                connecting();
85✔
121
                        });
122
                },
123

124
                /**
125
                 * Disconnect an adapter
126
                 *
127
                 * @param {Adapter} adapter
128
                 * @param {String} hash
129
                 */
130
                async _disconnect(adapter, hash) {
131
                        // Remove from cache
132
                        const item = Array.from(this.adapters.values()).find(item => item.adapter == adapter);
85✔
133
                        if (item) {
85!
UNCOV
134
                                this.adapters.delete(item.hash);
×
135
                        }
136

137
                        // Close the connection
138
                        if (_.isFunction(adapter.disconnect)) await adapter.disconnect();
85!
139
                        this.logger.info(
85✔
140
                                `Adapter '${hash || "unknown"}' disconnected. Number of adapters:`,
85!
141
                                this.adapters.size
142
                        );
143

144
                        this._metricDec(C.METRIC_ADAPTER_ACTIVE);
85✔
145

146
                        if (this.$hooks["adapterDisconnected"])
85!
UNCOV
147
                                await this.$hooks["adapterDisconnected"](adapter, hash);
×
148
                },
149

150
                /**
151
                 * Disconnect all adapters
152
                 */
153
                _disconnectAll() {
154
                        const adapters = Array.from(this.adapters.values());
85✔
155
                        this.adapters.clear();
85✔
156

157
                        this.logger.info(`Disconnect ${adapters.length} adapters...`);
85✔
158
                        return Promise.all(
85✔
159
                                adapters.map(({ adapter, hash }) => this._disconnect(adapter, hash))
85✔
160
                        );
161
                },
162

163
                /**
164
                 * Apply scopes for the params query.
165
                 *
166
                 * @param {Object} params
167
                 * @param {Context?} ctx
168
                 */
169
                async _applyScopes(params, ctx) {
170
                        let scopes = this.settings.defaultScopes ? Array.from(this.settings.defaultScopes) : [];
380!
171
                        if (params.scope && params.scope !== true) {
380!
UNCOV
172
                                (Array.isArray(params.scope) ? params.scope : [params.scope]).forEach(scope => {
×
UNCOV
173
                                        if (scope.startsWith("-")) {
×
UNCOV
174
                                                scopes = scopes.filter(s => s !== scope.substring(1));
×
175
                                        }
UNCOV
176
                                        scopes.push(scope);
×
177
                                });
178
                        } else if (params.scope === false) {
380!
179
                                // Disable default scopes
UNCOV
180
                                scopes = scopes.map(s => `-${s}`);
×
181
                        }
182

183
                        if (scopes && scopes.length > 0) {
380!
UNCOV
184
                                scopes = await this._filterScopeNamesByPermission(ctx, scopes);
×
UNCOV
185
                                if (scopes && scopes.length > 0) {
×
UNCOV
186
                                        this.logger.debug(`Applying scopes...`, scopes);
×
187

UNCOV
188
                                        let q = _.cloneDeep(params.query || {});
×
UNCOV
189
                                        for (const scopeName of scopes) {
×
UNCOV
190
                                                const scope = this.settings.scopes[scopeName];
×
UNCOV
191
                                                if (!scope) continue;
×
192

UNCOV
193
                                                if (_.isFunction(scope)) q = await scope.call(this, q, ctx, params);
×
UNCOV
194
                                                else q = _.merge(q, scope);
×
195
                                        }
UNCOV
196
                                        params.query = q;
×
197

UNCOV
198
                                        this.logger.debug(`Applied scopes into the query...`, params.query);
×
199
                                }
200
                        }
201

202
                        return params;
380✔
203
                },
204

205
                /**
206
                 * Filter the scopes according to permissions.
207
                 *
208
                 * @param {Context?} ctx
209
                 * @param {Array<String>} scopeNames
210
                 * @returns {Array<String>}
211
                 */
212
                async _filterScopeNamesByPermission(ctx, scopeNames) {
UNCOV
213
                        const res = [];
×
UNCOV
214
                        for (let scopeName of scopeNames) {
×
UNCOV
215
                                let operation = "add";
×
UNCOV
216
                                if (scopeName.startsWith("-")) {
×
UNCOV
217
                                        operation = "remove";
×
UNCOV
218
                                        scopeName = scopeName.substring(1);
×
219
                                }
UNCOV
220
                                const scope = this.settings.scopes[scopeName];
×
UNCOV
221
                                if (!scope) continue;
×
222

UNCOV
223
                                const has = await this.checkScopeAuthority(ctx, scopeName, operation, scope);
×
UNCOV
224
                                if ((operation == "add" && has) || (operation == "remove" && !has)) {
×
UNCOV
225
                                        res.push(scopeName);
×
226
                                }
227
                        }
UNCOV
228
                        return res;
×
229
                },
230

231
                /**
232
                 * Sanitize incoming parameters for `find`, `list`, `count` actions.
233
                 *
234
                 * @param {Object} params
235
                 * @param {Object?} opts
236
                 * @returns {Object}
237
                 */
238
                sanitizeParams(params, opts) {
239
                        const p = Object.assign({}, params);
443✔
240
                        if (typeof p.limit === "string") p.limit = Number(p.limit);
443!
241
                        if (typeof p.offset === "string") p.offset = Number(p.offset);
443!
242
                        if (typeof p.page === "string") p.page = Number(p.page);
443!
243
                        if (typeof p.pageSize === "string") p.pageSize = Number(p.pageSize);
443!
244

245
                        if (typeof p.query === "string") p.query = JSON.parse(p.query);
443!
246

247
                        if (typeof p.sort === "string") p.sort = p.sort.replace(/,/g, " ").split(" ");
443✔
248
                        if (typeof p.fields === "string") p.fields = p.fields.replace(/,/g, " ").split(" ");
443!
249
                        if (typeof p.populate === "string")
443!
UNCOV
250
                                p.populate = p.populate.replace(/,/g, " ").split(" ");
×
251
                        if (typeof p.searchFields === "string")
443!
252
                                p.searchFields = p.searchFields.replace(/,/g, " ").split(" ");
×
253
                        if (typeof p.scope === "string") {
443!
UNCOV
254
                                if (p.scope === "true") p.scope = true;
×
UNCOV
255
                                else if (p.scope === "false") p.scope = false;
×
UNCOV
256
                                else p.scope = p.scope.replace(/,/g, " ").split(" ");
×
257
                        }
258

259
                        if (opts && opts.removeLimit) {
443✔
260
                                if (p.limit) delete p.limit;
77✔
261
                                if (p.offset) delete p.offset;
77✔
262
                                if (p.page) delete p.page;
77✔
263
                                if (p.pageSize) delete p.pageSize;
77✔
264

265
                                return p;
77✔
266
                        }
267

268
                        if (opts && opts.list) {
366✔
269
                                // Default `pageSize`
270
                                if (!p.pageSize) p.pageSize = mixinOpts.defaultPageSize;
63✔
271

272
                                // Default `page`
273
                                if (!p.page) p.page = 1;
63✔
274

275
                                // Limit the `pageSize`
276
                                if (mixinOpts.maxLimit > 0 && p.pageSize > mixinOpts.maxLimit)
63!
277
                                        p.pageSize = mixinOpts.maxLimit;
×
278

279
                                // Calculate the limit & offset from page & pageSize
280
                                p.limit = p.pageSize;
63✔
281
                                p.offset = (p.page - 1) * p.pageSize;
63✔
282
                        }
283
                        // Limit the `limit`
284
                        if (mixinOpts.maxLimit > 0 && p.limit > mixinOpts.maxLimit)
366!
285
                                p.limit = mixinOpts.maxLimit;
×
286

287
                        return p;
366✔
288
                },
289

290
                /**
291
                 * Find all entities by query & limit.
292
                 *
293
                 * @param {Context} ctx
294
                 * @param {Object?} params
295
                 * @param {Object?} opts
296
                 */
297
                async findEntities(ctx, params = ctx.params, opts = {}) {
179✔
298
                        this._metricInc(C.METRIC_ENTITIES_FIND_TOTAL);
121✔
299
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_FIND_TIME);
121✔
300
                        const span = this.startSpan(ctx, "Find entities", { params, opts });
121✔
301

302
                        params = this.sanitizeParams(params);
121✔
303
                        params = await this._applyScopes(params, ctx);
121✔
304
                        params = this.paramsFieldNameConversion(params);
121✔
305

306
                        const adapter = await this.getAdapter(ctx);
121✔
307

308
                        this.logger.debug(`Find entities`, params);
121✔
309
                        let result = await adapter.find(params);
121✔
310
                        if (opts.transform !== false) {
121!
311
                                result = await this.transformResult(adapter, result, params, ctx);
121✔
312
                        }
313
                        timeEnd();
121✔
314
                        this.finishSpan(ctx, span);
121✔
315

316
                        return result;
121✔
317
                },
318

319
                /**
320
                 * Find all entities by query & limit.
321
                 *
322
                 * @param {Context} ctx
323
                 * @param {Object?} params
324
                 * @param {Object?} opts
325
                 * @returns {Promise<Stream>}
326
                 */
327
                async streamEntities(ctx, params = ctx.params, opts = {}) {
1!
328
                        this._metricInc(C.METRIC_ENTITIES_STREAM_TOTAL);
1✔
329
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_STREAM_TIME);
1✔
330
                        const span = this.startSpan(ctx, "Stream entities", { params, opts });
1✔
331

332
                        params = this.sanitizeParams(params);
1✔
333
                        params = await this._applyScopes(params, ctx);
1✔
334
                        params = this.paramsFieldNameConversion(params);
1✔
335

336
                        this.logger.debug(`Stream entities`, params);
1✔
337
                        const adapter = await this.getAdapter(ctx);
1✔
338
                        const stream = await adapter.findStream(params);
1✔
339

340
                        if (opts.transform !== false) {
1!
341
                                const self = this;
1✔
342
                                const transform = new Transform({
1✔
343
                                        objectMode: true,
344
                                        transform: async function (doc, encoding, done) {
345
                                                const res = await self.transformResult(adapter, doc, params, ctx);
10✔
346
                                                this.push(res);
10✔
347
                                                return done();
10✔
348
                                        }
349
                                });
350
                                stream.pipe(transform);
1✔
351
                                return transform;
1✔
352
                        }
353
                        timeEnd();
×
354
                        this.finishSpan(ctx, span);
×
355

356
                        return stream;
×
357
                },
358

359
                /**
360
                 * Count entities by query & limit.
361
                 *
362
                 * @param {Context} ctx
363
                 * @param {Object?} params
364
                 */
365
                async countEntities(ctx, params = ctx.params) {
14✔
366
                        this._metricInc(C.METRIC_ENTITIES_COUNT_TOTAL);
77✔
367
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_COUNT_TIME);
77✔
368
                        const span = this.startSpan(ctx, "Count entities", { params });
77✔
369

370
                        params = this.sanitizeParams(params, { removeLimit: true });
77✔
371
                        params = await this._applyScopes(params, ctx);
77✔
372
                        params = this.paramsFieldNameConversion(params);
77✔
373

374
                        this.logger.debug(`Count entities`, params);
77✔
375
                        const adapter = await this.getAdapter(ctx);
77✔
376
                        const result = await adapter.count(params);
77✔
377
                        timeEnd();
77✔
378
                        this.finishSpan(ctx, span);
77✔
379

380
                        return result;
77✔
381
                },
382

383
                /**
384
                 * Find only one entity by query.
385
                 *
386
                 * @param {Context} ctx
387
                 * @param {Object?} params
388
                 * @param {Object?} opts
389
                 */
390
                async findEntity(ctx, params = ctx.params, opts = {}) {
×
UNCOV
391
                        this._metricInc(C.METRIC_ENTITIES_FINDONE_TOTAL);
×
UNCOV
392
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_FINDONE_TIME);
×
UNCOV
393
                        const span = this.startSpan(ctx, "Find entity", { params, opts });
×
394

UNCOV
395
                        params = this.sanitizeParams(params, { removeLimit: true });
×
UNCOV
396
                        params = await this._applyScopes(params, ctx);
×
UNCOV
397
                        params = this.paramsFieldNameConversion(params);
×
UNCOV
398
                        params.limit = 1;
×
399

UNCOV
400
                        this.logger.debug(`Find an entity`, params);
×
UNCOV
401
                        const adapter = await this.getAdapter(ctx);
×
UNCOV
402
                        let result = await adapter.findOne(params);
×
UNCOV
403
                        if (opts.transform !== false) {
×
UNCOV
404
                                result = await this.transformResult(adapter, result, params, ctx);
×
405
                        }
UNCOV
406
                        timeEnd();
×
UNCOV
407
                        this.finishSpan(ctx, span);
×
408

UNCOV
409
                        return result;
×
410
                },
411

412
                /**
413
                 * Get ID value from `params`.
414
                 *
415
                 * @param {Object} params
416
                 */
417
                _getIDFromParams(params, throwIfNotExist = true) {
225✔
418
                        let id = params[this.$primaryField.name];
225✔
419

420
                        if (throwIfNotExist && id == null) {
225!
UNCOV
421
                                throw new MoleculerClientError("Missing id field.", 400, "MISSING_ID", { params });
×
422
                        }
423

424
                        return id;
225✔
425
                },
426

427
                /**
428
                 * Resolve entities by IDs with mapping.
429
                 *
430
                 * @param {Context} ctx
431
                 * @param {Object?} params
432
                 * @param {Object?} opts
433
                 */
434
                async resolveEntities(ctx, params = ctx.params, opts = {}) {
×
435
                        this._metricInc(C.METRIC_ENTITIES_RESOLVE_TOTAL);
181✔
436
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_RESOLVE_TIME);
181✔
437
                        const span = this.startSpan(ctx, "Resolve entities", { params, opts });
181✔
438

439
                        // Get ID value from params
440
                        let id = this._getIDFromParams(params);
181✔
441
                        const origID = id;
181✔
442
                        const origParams = params;
181✔
443
                        const multi = Array.isArray(id);
181✔
444
                        if (!multi) id = [id];
181!
445

446
                        // Decode ID if need
447
                        id = id.map(id => this._sanitizeID(id, opts));
181✔
448

449
                        params = this.sanitizeParams(params);
181✔
450

451
                        // Apply scopes & set ID filtering
452
                        params = Object.assign({}, params);
181✔
453
                        if (!params.query) params.query = {};
181!
454
                        params = await this._applyScopes(params, ctx);
181✔
455
                        params = this.paramsFieldNameConversion(params);
181✔
456

457
                        let idField = this.$primaryField.columnName;
181✔
458

459
                        if (multi) {
181!
UNCOV
460
                                params.query[idField] = { $in: id };
×
461
                        } else {
462
                                params.query[idField] = id[0];
181✔
463
                        }
464

465
                        this.logger.debug(`Resolve entities`, id);
181✔
466
                        const adapter = await this.getAdapter(ctx);
181✔
467

468
                        // Find the entities
469
                        let result = await adapter.find(params);
181✔
470
                        if (!result || result.length == 0) {
181✔
471
                                timeEnd();
35✔
472
                                this.finishSpan(ctx, span);
35✔
473
                                if (opts.throwIfNotExist) throw new EntityNotFoundError(origID);
35✔
474
                                return params.mapping === true ? {} : multi ? [] : null;
7!
475
                        }
476

477
                        if (this.$hooks["afterResolveEntities"]) {
146!
UNCOV
478
                                await this.$hooks["afterResolveEntities"](
×
479
                                        ctx,
480
                                        multi ? id : id[0],
×
481
                                        multi ? result : result[0],
×
482
                                        origParams,
483
                                        opts
484
                                );
485
                        }
486

487
                        // For mapping
488
                        const unTransformedRes = Array.from(result);
146✔
489

490
                        // Transforming
491
                        if (opts.transform !== false) {
146✔
492
                                result = await this.transformResult(adapter, result, params, ctx);
102✔
493
                        }
494

495
                        // Mapping
496
                        if (params.mapping === true) {
146✔
497
                                result = result.reduce((map, doc, i) => {
16✔
498
                                        let id = unTransformedRes[i][idField];
16✔
499
                                        if (this.$primaryField.secure) id = this.encodeID(id);
16✔
500

501
                                        map[id] = doc;
16✔
502
                                        return map;
16✔
503
                                }, {});
504
                        } else if (multi && opts.reorderResult) {
130!
505
                                // Reorder result to the same as ID array (it needs for DataLoader)
UNCOV
506
                                const tmp = [];
×
UNCOV
507
                                id.forEach(id => {
×
UNCOV
508
                                        const idx = unTransformedRes.findIndex(doc => doc[idField] == id);
×
UNCOV
509
                                        tmp.push(idx != -1 ? result[idx] : null);
×
510
                                });
UNCOV
511
                                result = tmp;
×
512
                        } else if (!multi) {
130!
513
                                result = result[0];
130✔
514
                        }
515
                        timeEnd();
146✔
516
                        this.finishSpan(ctx, span);
146✔
517

518
                        return result;
146✔
519
                },
520

521
                /**
522
                 * Create an entity.
523
                 *
524
                 * @param {Context} ctx
525
                 * @param {Object?} params
526
                 * @param {Object?} opts
527
                 */
528
                async createEntity(ctx, params = ctx.params, opts = {}) {
224✔
529
                        this._metricInc(C.METRIC_ENTITIES_CREATEONE_TOTAL);
112✔
530
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_CREATEONE_TIME);
112✔
531
                        const span = this.startSpan(ctx, "Create entity", { params, opts });
112✔
532

533
                        const adapter = await this.getAdapter(ctx);
112✔
534

535
                        params = await this.validateParams(ctx, params, {
112✔
536
                                ...opts,
537
                                type: "create",
538
                                nestedFieldSupport: adapter.hasNestedFieldSupport
539
                        });
540

541
                        this.logger.debug(`Create an entity`, params);
112✔
542
                        let result = await adapter.insert(params);
112✔
543
                        if (opts.transform !== false) {
112!
544
                                result = await this.transformResult(adapter, result, {}, ctx);
112✔
545
                        }
546

547
                        await this._entityChanged("create", result, null, ctx, opts);
112✔
548
                        timeEnd();
112✔
549
                        this.finishSpan(ctx, span);
112✔
550

551
                        return result;
112✔
552
                },
553

554
                /**
555
                 * Insert multiple entities.
556
                 *
557
                 * @param {Context} ctx
558
                 * @param {Array<Object>?} params
559
                 * @param {Object?} opts
560
                 */
561
                async createEntities(ctx, params = ctx.params, opts = {}) {
×
562
                        this._metricInc(C.METRIC_ENTITIES_CREATEMANY_TOTAL);
15✔
563
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_CREATEMANY_TIME);
15✔
564
                        const span = this.startSpan(ctx, "Create entities", { params, opts });
15✔
565

566
                        const adapter = await this.getAdapter(ctx);
15✔
567
                        const entities = await Promise.all(
15✔
568
                                params.map(
569
                                        async entity =>
570
                                                await this.validateParams(ctx, entity, {
164✔
571
                                                        ...opts,
572
                                                        type: "create",
573
                                                        nestedFieldSupport: adapter.hasNestedFieldSupport
574
                                                })
575
                                )
576
                        );
577

578
                        this.logger.debug(`Create multiple entities`, entities);
15✔
579
                        let result = await adapter.insertMany(entities, {
15✔
580
                                returnEntities: opts.returnEntities
581
                        });
582
                        if (opts.returnEntities && opts.transform !== false) {
15!
583
                                result = await this.transformResult(adapter, result, {}, ctx);
15✔
584
                        }
585

586
                        await this._entityChanged("create", result, null, ctx, { ...opts, batch: true });
15✔
587

588
                        timeEnd();
15✔
589
                        this.finishSpan(ctx, span);
15✔
590

591
                        return result;
15✔
592
                },
593

594
                /**
595
                 * Update an entity (patch).
596
                 *
597
                 * @param {Context} ctx
598
                 * @param {Object?} params
599
                 * @param {Object?} opts
600
                 */
601
                async updateEntity(ctx, params = ctx.params, opts = {}) {
56✔
602
                        this._metricInc(C.METRIC_ENTITIES_UPDATEONE_TOTAL);
30✔
603
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_UPDATEONE_TIME);
30✔
604
                        const span = this.startSpan(ctx, "Update entity", { params, opts });
30✔
605

606
                        params = _.cloneDeep(params);
30✔
607
                        const adapter = await this.getAdapter(ctx);
30✔
608
                        let id = this._getIDFromParams(params);
30✔
609

610
                        // Call because it throws error if entity is not exist
611
                        let entity = await this.resolveEntities(
30✔
612
                                ctx,
613
                                {
614
                                        [this.$primaryField.name]: id,
615
                                        scope: opts.scope
616
                                },
617
                                {
618
                                        transform: false,
619
                                        throwIfNotExist: true
620
                                }
621
                        );
622

623
                        const rawUpdate = opts.raw === true;
30✔
624
                        if (!rawUpdate) {
30✔
625
                                params = await this.validateParams(ctx, params, {
28✔
626
                                        ...opts,
627
                                        type: "update",
628
                                        entity,
629
                                        id,
630
                                        nestedFieldSupport: adapter.hasNestedFieldSupport
631
                                });
632
                        }
633

634
                        id = this._sanitizeID(id, opts);
30✔
635

636
                        delete params[this.$primaryField.columnName];
30✔
637
                        //if (this.$primaryField.columnName != this.$primaryField.name)
638
                        delete params[this.$primaryField.name];
30✔
639

640
                        this.logger.debug(`Update an entity`, id, params);
30✔
641
                        let result;
642
                        const hasChanges = Object.keys(params).length > 0;
30✔
643
                        if (hasChanges) {
30!
644
                                result = await adapter.updateById(id, params, { raw: rawUpdate });
30✔
645
                        } else {
646
                                // Nothing to update
UNCOV
647
                                result = entity;
×
648
                        }
649

650
                        if (opts.transform !== false) {
30!
651
                                result = await this.transformResult(adapter, result, {}, ctx);
30✔
652
                                entity = await this.transformResult(adapter, entity, {}, ctx);
30✔
653
                        }
654

655
                        if (hasChanges) {
30!
656
                                await this._entityChanged("update", result, entity, ctx, opts);
30✔
657
                        }
658
                        timeEnd();
30✔
659
                        this.finishSpan(ctx, span);
30✔
660

661
                        return result;
30✔
662
                },
663

664
                /**
665
                 * Update multiple entities (patch).
666
                 *
667
                 * @param {Context} ctx
668
                 * @param {Object} params
669
                 * @param {Object} params.query
670
                 * @param {Object} params.changes
671
                 * @param {String|Array<String>|Boolean} params.scope
672
                 * @param {Object?} opts
673
                 */
674
                async updateEntities(ctx, params = ctx.params, opts = {}) {
×
UNCOV
675
                        this._metricInc(C.METRIC_ENTITIES_UPDATEMANY_TOTAL);
×
UNCOV
676
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_UPDATEMANY_TIME);
×
UNCOV
677
                        const span = this.startSpan(ctx, "Update entities", { params, opts });
×
678

UNCOV
679
                        const adapter = await this.getAdapter(ctx);
×
680

UNCOV
681
                        const _entities = await this.findEntities(
×
682
                                ctx,
683
                                { query: params.query, scope: params.scope },
684
                                { transform: false }
685
                        );
686

UNCOV
687
                        const res = await this.Promise.all(
×
688
                                _entities.map(async _entity => {
UNCOV
689
                                        let entity = adapter.entityToJSON(_entity);
×
UNCOV
690
                                        let id = entity[this.$primaryField.columnName];
×
UNCOV
691
                                        id = this.$primaryField.secure ? this.encodeID(id) : id;
×
692

UNCOV
693
                                        return await this.updateEntity(
×
694
                                                ctx,
695
                                                {
696
                                                        ...params.changes,
697
                                                        [this.$primaryField.name]: id
698
                                                },
699
                                                {
700
                                                        scope: params.scope,
701
                                                        ...opts
702
                                                }
703
                                        );
704
                                })
705
                        );
UNCOV
706
                        timeEnd();
×
UNCOV
707
                        this.finishSpan(ctx, span);
×
708

UNCOV
709
                        return res;
×
710
                },
711

712
                /**
713
                 * Replace an entity.
714
                 *
715
                 * @param {Context} ctx
716
                 * @param {Object?} params
717
                 * @param {Object?} opts
718
                 */
719
                async replaceEntity(ctx, params = ctx.params, opts = {}) {
×
UNCOV
720
                        this._metricInc(C.METRIC_ENTITIES_REPLACEONE_TOTAL);
×
UNCOV
721
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_REPLACEONE_TIME);
×
UNCOV
722
                        const span = this.startSpan(ctx, "Replace entity", { params, opts });
×
723

UNCOV
724
                        let id = this._getIDFromParams(params);
×
725

726
                        // Call because it throws error if entity is not exist
UNCOV
727
                        let entity = await this.resolveEntities(
×
728
                                ctx,
729
                                {
730
                                        [this.$primaryField.name]: id,
731
                                        scope: opts.scope
732
                                },
733
                                {
734
                                        transform: false,
735
                                        throwIfNotExist: true
736
                                }
737
                        );
UNCOV
738
                        const adapter = await this.getAdapter(ctx);
×
739

UNCOV
740
                        params = await this.validateParams(ctx, params, {
×
741
                                ...opts,
742
                                type: "replace",
743
                                entity,
744
                                id,
745
                                nestedFieldSupport: adapter.hasNestedFieldSupport
746
                        });
747

UNCOV
748
                        id = this._sanitizeID(id, opts);
×
749

UNCOV
750
                        delete params[this.$primaryField.columnName];
×
UNCOV
751
                        if (this.$primaryField.columnName != this.$primaryField.name) {
×
UNCOV
752
                                delete params[this.$primaryField.name];
×
753
                        }
754

UNCOV
755
                        this.logger.debug(`Replace an entity`, id, params);
×
UNCOV
756
                        let result = await adapter.replaceById(id, params);
×
757

UNCOV
758
                        if (opts.transform !== false) {
×
UNCOV
759
                                result = await this.transformResult(adapter, result, {}, ctx);
×
UNCOV
760
                                entity = await this.transformResult(adapter, entity, {}, ctx);
×
761
                        }
762

UNCOV
763
                        await this._entityChanged("replace", result, entity, ctx, opts);
×
764

UNCOV
765
                        timeEnd();
×
UNCOV
766
                        this.finishSpan(ctx, span);
×
767

UNCOV
768
                        return result;
×
769
                },
770

771
                /**
772
                 * Delete an entity.
773
                 *
774
                 * @param {Context} ctx
775
                 * @param {Object?} params
776
                 * @param {Object?} opts
777
                 */
778
                async removeEntity(ctx, params = ctx.params, opts = {}) {
28✔
779
                        this._metricInc(C.METRIC_ENTITIES_REMOVEONE_TOTAL);
14✔
780
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_REMOVEONE_TIME);
14✔
781
                        const span = this.startSpan(ctx, "Remove entity", { params, opts });
14✔
782

783
                        let id = this._getIDFromParams(params);
14✔
784
                        const origID = id;
14✔
785

786
                        let entity = await this.resolveEntities(
14✔
787
                                ctx,
788
                                {
789
                                        [this.$primaryField.name]: id,
790
                                        scope: opts.scope
791
                                },
792
                                {
793
                                        transform: false,
794
                                        throwIfNotExist: true
795
                                }
796
                        );
797

798
                        const adapter = await this.getAdapter(ctx);
14✔
799

800
                        params = await this.validateParams(ctx, params, {
14✔
801
                                ...opts,
802
                                type: "remove",
803
                                entity,
804
                                id,
805
                                nestedFieldSupport: adapter.hasNestedFieldSupport
806
                        });
807

808
                        id = this._sanitizeID(id, opts);
14✔
809

810
                        let softDelete = this.$softDelete;
14✔
811
                        if (opts.softDelete === false) softDelete = false;
14!
812

813
                        if (softDelete) {
14!
UNCOV
814
                                this.logger.debug(`Soft delete an entity`, id, params);
×
815
                                // Soft delete
UNCOV
816
                                entity = await adapter.updateById(id, params);
×
817
                        } else {
818
                                // Real delete
819
                                this.logger.debug(`Delete an entity`, id);
14✔
820
                                await adapter.removeById(id);
14✔
821
                        }
822

823
                        if (opts.transform !== false) {
14!
824
                                entity = await this.transformResult(adapter, entity, params, ctx);
14✔
825
                        }
826

827
                        await this._entityChanged("remove", entity, null, ctx, {
14✔
828
                                ...opts,
829
                                softDelete: !!softDelete
830
                        });
831

832
                        timeEnd();
14✔
833
                        this.finishSpan(ctx, span);
14✔
834

835
                        return origID;
14✔
836
                },
837

838
                /**
839
                 * Delete multiple entities.
840
                 *
841
                 * @param {Context} ctx
842
                 * @param {Object?} params
843
                 * @param {Object?} params.query
844
                 * @param {String|Array<String>|Boolean} params.scope
845
                 * @param {Object?} opts
846
                 */
847
                async removeEntities(ctx, params = ctx.params, opts = {}) {
×
UNCOV
848
                        this._metricInc(C.METRIC_ENTITIES_REMOVEMANY_TOTAL);
×
UNCOV
849
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_REMOVEMANY_TIME);
×
UNCOV
850
                        const span = this.startSpan(ctx, "Remove entities", { params, opts });
×
851

UNCOV
852
                        const adapter = await this.getAdapter(ctx);
×
853

UNCOV
854
                        const _entities = await this.findEntities(
×
855
                                ctx,
856
                                { query: params.query, scope: params.scope },
857
                                { transform: false }
858
                        );
859

UNCOV
860
                        const res = await this.Promise.all(
×
861
                                _entities.map(async _entity => {
UNCOV
862
                                        let entity = adapter.entityToJSON(_entity);
×
UNCOV
863
                                        let id = entity[this.$primaryField.columnName];
×
UNCOV
864
                                        id = this.$primaryField.secure ? this.encodeID(id) : id;
×
865

UNCOV
866
                                        return await this.removeEntity(
×
867
                                                ctx,
868
                                                {
869
                                                        [this.$primaryField.name]: id
870
                                                },
871
                                                {
872
                                                        scope: params.scope,
873
                                                        ...opts
874
                                                }
875
                                        );
876
                                })
877
                        );
UNCOV
878
                        timeEnd();
×
UNCOV
879
                        this.finishSpan(ctx, span);
×
880

UNCOV
881
                        return res;
×
882
                },
883

884
                /**
885
                 * Clear all entities.
886
                 *
887
                 * @param {Context} ctx
888
                 * @param {Object?} params
889
                 */
890
                async clearEntities(ctx, params) {
891
                        this._metricInc(C.METRIC_ENTITIES_CLEAR_TOTAL);
71✔
892
                        const timeEnd = this._metricTime(C.METRIC_ENTITIES_CLEAR_TIME);
71✔
893
                        const span = this.startSpan(ctx, "Clear all entities", { params });
71✔
894

895
                        this.logger.debug(`Clear all entities`, params);
71✔
896
                        const adapter = await this.getAdapter(ctx);
71✔
897
                        const result = await adapter.clear(params);
71✔
898

899
                        await this._entityChanged("clear", null, null, ctx);
71✔
900

901
                        timeEnd();
71✔
902
                        this.finishSpan(ctx, span);
71✔
903

904
                        return result;
71✔
905
                },
906

907
                /**
908
                 * Create indexes.
909
                 * @param {Adapter?} adapter If null, it gets adapter with `getAdapter()`
910
                 * @param {Array<Object>?} indexes If null, it uses the `settings.indexes`
911
                 */
912
                async createIndexes(adapter, indexes) {
UNCOV
913
                        adapter = await (adapter || this.getAdapter());
×
UNCOV
914
                        if (!indexes) indexes = this.settings.indexes;
×
UNCOV
915
                        if (Array.isArray(indexes)) {
×
UNCOV
916
                                await Promise.all(indexes.map(def => this.createIndex(adapter, def)));
×
917
                        }
918
                },
919

920
                /**
921
                 * Create an index.
922
                 *
923
                 * @param {Object} def
924
                 */
925
                createIndex(adapter, def) {
UNCOV
926
                        const newDef = _.cloneDeep(def);
×
UNCOV
927
                        this.logger.debug(`Create an index`, def);
×
UNCOV
928
                        if (_.isString(def.fields))
×
929
                                newDef.fields = this._getColumnNameFromFieldName(def.fields);
×
UNCOV
930
                        else if (Array.isArray(def.fields))
×
931
                                newDef.fields = def.fields.map(f => this._getColumnNameFromFieldName(f));
×
UNCOV
932
                        else if (_.isPlainObject(def.fields))
×
UNCOV
933
                                newDef.fields = this._queryFieldNameConversion(def.fields, false);
×
UNCOV
934
                        return adapter.createIndex(newDef);
×
935
                },
936

937
                /**
938
                 * Called when an entity changed.
939
                 *
940
                 * @param {String} type
941
                 * @param {any} data
942
                 * @param {any} oldData
943
                 * @param {Context?} ctx
944
                 * @param {Object?} opts
945
                 */
946
                async _entityChanged(type, data, oldData, ctx, opts = {}) {
71✔
947
                        if (cacheOpts && cacheOpts.eventType) {
242✔
948
                                const eventName = cacheOpts.eventName || `cache.clean.${this.name}`;
221✔
949
                                if (eventName) {
221!
950
                                        const payload = {
221✔
951
                                                type,
952
                                                data,
953
                                                opts
954
                                        };
955

956
                                        // Cache cleaning event
957
                                        (ctx || this.broker)[cacheOpts.eventType](eventName, payload);
221✔
958
                                }
959
                        }
960

961
                        await this.entityChanged(type, data, oldData, ctx, opts);
242✔
962
                },
963

964
                /**
965
                 * Send entity lifecycle events
966
                 *
967
                 * @param {String} type
968
                 * @param {any} data
969
                 * @param {any} oldData
970
                 * @param {Context?} ctx
971
                 * @param {Object?} opts
972
                 */
973
                async entityChanged(type, data, oldData, ctx, opts) {
974
                        if (mixinOpts.entityChangedEventType) {
205!
975
                                const op = type + (type == "clear" ? "ed" : "d");
205✔
976
                                const eventName = `${this.name}.${op}`;
205✔
977

978
                                const payload = {
205✔
979
                                        type,
980
                                        data,
981
                                        opts
982
                                };
983

984
                                if (mixinOpts.entityChangedOldEntity) {
205!
UNCOV
985
                                        payload.oldData = oldData;
×
986
                                }
987

988
                                (ctx || this.broker)[mixinOpts.entityChangedEventType](eventName, payload);
205✔
989
                        }
990
                },
991

992
                /**
993
                 * Encode ID of entity.
994
                 *
995
                 * @methods
996
                 * @param {any} id
997
                 * @returns {any}
998
                 */
999
                encodeID(id) {
1000
                        return id;
×
1001
                },
1002

1003
                /**
1004
                 * Decode ID of entity.
1005
                 *
1006
                 * @methods
1007
                 * @param {any} id
1008
                 * @returns {any}
1009
                 */
1010
                decodeID(id) {
1011
                        return id;
×
1012
                },
1013

1014
                /**
1015
                 * Sanitize the input ID. Decode if it's secured.
1016
                 * @param {any} id
1017
                 * @param {Object?} opts
1018
                 * @returns {any}
1019
                 */
1020
                _sanitizeID(id, opts) {
1021
                        if (opts.secureID != null) {
225!
1022
                                return opts.secureID ? this.decodeID(id) : id;
×
1023
                        } else if (this.$primaryField.secure) {
225✔
1024
                                return this.decodeID(id);
70✔
1025
                        }
1026
                        return id;
155✔
1027
                },
1028

1029
                /**
1030
                 * Get the columnName from field name.
1031
                 *
1032
                 * @param {String} fieldName
1033
                 * @returns {String} columnName
1034
                 */
1035
                _getColumnNameFromFieldName(fieldName) {
1036
                        const res = cacheColumnName.get(fieldName);
70✔
1037
                        if (res) return res;
70✔
1038

1039
                        const field = this.$fields.find(f => f.name == fieldName);
112✔
1040
                        if (field) {
14!
1041
                                cacheColumnName.set(fieldName, field.columnName);
14✔
1042
                                return field.columnName;
14✔
1043
                        }
1044
                        return fieldName;
×
1045
                },
1046

1047
                /**
1048
                 * Convert fieldName to columnName in `sort`
1049
                 * @param {Array<String>} sort
1050
                 * @returns {Array<String>}
1051
                 */
1052
                _sortFieldNameConversion(sort) {
1053
                        return sort.map(fieldName => {
56✔
1054
                                if (fieldName.startsWith("-")) {
56✔
1055
                                        return "-" + this._getColumnNameFromFieldName(fieldName.slice(1));
14✔
1056
                                } else {
1057
                                        return this._getColumnNameFromFieldName(fieldName);
42✔
1058
                                }
1059
                        });
1060
                },
1061

1062
                /**
1063
                 * Convert fieldName to columnName in `query`
1064
                 * @param {Object} query
1065
                 * @param {Boolean} recursive
1066
                 * @returns {Object}
1067
                 */
1068
                _queryFieldNameConversion(query, recursive) {
1069
                        return Object.keys(query).reduce((res, fieldName) => {
195✔
1070
                                const columnName = this._getColumnNameFromFieldName(fieldName);
14✔
1071
                                if (_.isPlainObject(res[columnName]) && recursive) {
14!
1072
                                        res[columnName] = this._queryFieldNameConversion(query[fieldName], recursive);
×
1073
                                } else {
1074
                                        res[columnName] = query[fieldName];
14✔
1075
                                }
1076
                                return res;
14✔
1077
                        }, {});
1078
                },
1079

1080
                /**
1081
                 * Convert field names to column names in `params`
1082
                 * @param {Object} p
1083
                 * @returns {Object}
1084
                 */
1085
                paramsFieldNameConversion(p) {
1086
                        // Fieldname -> columnName conversions
1087
                        if (p.sort) {
380✔
1088
                                p.sort = this._sortFieldNameConversion(p.sort);
56✔
1089
                        }
1090
                        if (p.searchFields) {
380!
UNCOV
1091
                                p.searchFields = this._sortFieldNameConversion(p.searchFields);
×
1092
                        }
1093
                        if (p.query) {
380✔
1094
                                p.query = this._queryFieldNameConversion(p.query, true);
195✔
1095
                        }
1096

1097
                        return p;
380✔
1098
                }
1099
        };
1100
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc