• 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

51.4
/src/validation.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 { Context } = require("moleculer"); // eslint-disable-line no-unused-vars
1✔
10
const { ServiceSchemaError, ValidationError } = require("moleculer").Errors;
1✔
11
const _ = require("lodash");
1✔
12
const { generateValidatorSchemaFromFields } = require("./schema");
1✔
13

14
const Validator = require("fastest-validator");
1✔
15
const validator = new Validator({
1✔
16
        useNewCustomCheckerFunction: true
17
});
18

19
module.exports = function (mixinOpts) {
1✔
20
        return {
85✔
21
                /**
22
                 * Processing the `fields` definition.
23
                 *
24
                 * @private
25
                 */
26
                _processFields() {
27
                        this.$fields = null;
85✔
28
                        this.$primaryField = null;
85✔
29
                        this.$softDelete = false;
85✔
30
                        this.$shouldAuthorizeFields = false;
85✔
31

32
                        if (_.isObject(this.settings.fields)) {
85!
33
                                this.logger.debug(`Process field definitions...`);
85✔
34

35
                                this.$fields = this._processFieldObject(this.settings.fields);
85✔
36

37
                                // Compile validators for basic methods
38
                                this.$validators = {
85✔
39
                                        create: validator.compile(
40
                                                generateValidatorSchemaFromFields(this.settings.fields, {
41
                                                        type: "create",
42
                                                        enableParamsConversion: mixinOpts.enableParamsConversion
43
                                                })
44
                                        ),
45
                                        update: validator.compile(
46
                                                generateValidatorSchemaFromFields(this.settings.fields, {
47
                                                        type: "update",
48
                                                        enableParamsConversion: mixinOpts.enableParamsConversion
49
                                                })
50
                                        ),
51
                                        replace: validator.compile(
52
                                                generateValidatorSchemaFromFields(this.settings.fields, {
53
                                                        type: "replace",
54
                                                        enableParamsConversion: mixinOpts.enableParamsConversion
55
                                                })
56
                                        )
57
                                };
58
                        }
59

60
                        if (!this.$primaryField) this.$primaryField = { name: "_id", columnName: "_id" };
85!
61
                        if (this.$softDelete) this.logger.debug("Soft delete mode: ENABLED");
85!
62
                        this.logger.debug(`Primary key field`, this.$primaryField);
85✔
63
                },
64

65
                _processFieldObject(fields) {
66
                        return _.compact(
85✔
67
                                _.map(fields, (def, name) => {
68
                                        // Disabled field
69
                                        if (def === false) return;
309!
70

71
                                        // Shorthand format { title: true } => { title: {} }
72
                                        if (def === true) def = { type: "any" };
309!
73

74
                                        // Parse shorthand format: { title: "string|min:3" } => { title: { type: "string", min: 3 } }
75
                                        if (_.isString(def)) def = validator.parseShortHand(def);
309!
76

77
                                        // Copy the properties
78
                                        const field = _.cloneDeep(def);
309✔
79

80
                                        // Set name of field
81
                                        field.name = name;
309✔
82

83
                                        if (!field.columnName) field.columnName = field.name;
309✔
84
                                        if (!field.columnType) field.columnType = field.type;
309✔
85

86
                                        if (field.primaryKey === true) this.$primaryField = field;
309✔
87

88
                                        if (field.onRemove) this.$softDelete = true;
309!
89

90
                                        if (field.permission || field.readPermission) {
309!
UNCOV
91
                                                this.$shouldAuthorizeFields = true;
×
92
                                        }
93

94
                                        if (field.required == null) {
309✔
95
                                                if (field.optional != null) field.required = !field.optional;
200!
96
                                                else field.required = false;
200✔
97
                                        }
98

99
                                        if (field.populate) {
309!
UNCOV
100
                                                if (_.isFunction(field.populate)) {
×
UNCOV
101
                                                        field.populate = { handler: field.populate };
×
UNCOV
102
                                                } else if (_.isString(field.populate)) {
×
UNCOV
103
                                                        field.populate = { action: field.populate };
×
UNCOV
104
                                                } else if (_.isObject(field.populate)) {
×
UNCOV
105
                                                        if (!field.populate.action && !field.populate.handler) {
×
106
                                                                throw new ServiceSchemaError(
×
107
                                                                        `Invalid 'populate' definition in '${this.fullName}' service. Missing 'action' or 'handler'.`,
108
                                                                        { populate: field.populate }
109
                                                                );
110
                                                        }
111
                                                } else {
112
                                                        throw new ServiceSchemaError(
×
113
                                                                `Invalid 'populate' definition in '${this.fullName}' service. It should be a 'Function', 'String' or 'Object'.`,
114
                                                                { populate: field.populate }
115
                                                        );
116
                                                }
117
                                        }
118

119
                                        // Handle nested object properties
120
                                        if (field.type == "object" && _.isPlainObject(field.properties)) {
309!
UNCOV
121
                                                field.itemProperties = this._processFieldObject(field.properties);
×
122
                                        }
123

124
                                        // Handle array items
125
                                        if (field.type == "array" && _.isObject(field.items)) {
309!
UNCOV
126
                                                let itemsDef = field.items;
×
UNCOV
127
                                                if (_.isString(field.items)) itemsDef = { type: field.items };
×
128

UNCOV
129
                                                if (itemsDef.type == "object" && itemsDef.properties) {
×
UNCOV
130
                                                        field.itemProperties = this._processFieldObject(itemsDef.properties);
×
131
                                                }
132
                                        }
133

134
                                        return field;
309✔
135
                                })
136
                        );
137
                },
138

139
                /**
140
                 * Check the field authority. Should be implemented in the service.
141
                 *
142
                 * @param {Context} ctx
143
                 * @param {any} permission
144
                 * @param {Object} params
145
                 * @param {Object} field
146
                 */
147
                async checkFieldAuthority(/*ctx, permission, params, field*/) {
UNCOV
148
                        return true;
×
149
                },
150

151
                /**
152
                 * Check the scope authority. Should be implemented in the service.
153
                 *
154
                 * @param {Context} ctx
155
                 * @param {String} name
156
                 * @param {String} operation Values: "add", "remove"
157
                 * @param {Object} scope
158
                 */
159
                async checkScopeAuthority(/*ctx, name, operation, scope*/) {
UNCOV
160
                        return true;
×
161
                },
162

163
                /**
164
                 * Authorize the fields based on logged in user (from ctx).
165
                 *
166
                 * @param {Array<Object>} fields
167
                 * @param {Context} ctx
168
                 * @param {Object} params
169
                 * @param {Object} opts
170
                 * @param {Boolean} opts.isWrite
171
                 * @param {Boolean} opts.permissive
172
                 * @returns {Array<Object>}
173
                 */
174
                async _authorizeFields(fields, ctx, params, opts = {}) {
×
175
                        if (!this.$shouldAuthorizeFields) return fields;
738!
UNCOV
176
                        if (opts.permissive) return fields;
×
177

UNCOV
178
                        const res = [];
×
UNCOV
179
                        await Promise.all(
×
180
                                _.compact(
181
                                        fields.map(field => {
UNCOV
182
                                                if (!opts.isWrite && field.readPermission) {
×
UNCOV
183
                                                        return this.checkFieldAuthority(
×
184
                                                                ctx,
185
                                                                field.readPermission,
186
                                                                params,
187
                                                                field
UNCOV
188
                                                        ).then(has => (has ? res.push(field) : null));
×
UNCOV
189
                                                } else if (field.permission) {
×
UNCOV
190
                                                        return this.checkFieldAuthority(
×
191
                                                                ctx,
192
                                                                field.permission,
193
                                                                params,
194
                                                                field
UNCOV
195
                                                        ).then(has => (has ? res.push(field) : null));
×
196
                                                }
197

UNCOV
198
                                                res.push(field);
×
199
                                        })
200
                                )
201
                        );
UNCOV
202
                        return res;
×
203
                },
204

205
                /**
206
                 * Validate incoming parameters.
207
                 *
208
                 * @param {Context} ctx
209
                 * @param {Object} params
210
                 * @param {Object?} opts
211
                 */
212
                async validateParams(ctx, params, opts = {}) {
×
213
                        const type = opts.type || "create";
318!
214

215
                        // Drop all fields if hard delete
216
                        if (type == "remove" && !this.$softDelete) {
318✔
217
                                return {};
14✔
218
                        }
219

220
                        // Copy all fields if fields in not defined in settings.
221
                        if (!this.$fields) {
304!
UNCOV
222
                                return Object.assign({}, params);
×
223
                        }
224

225
                        const span = this.startSpan(ctx, "Validating", { params });
304✔
226

227
                        const fields = Array.from(this.$fields);
304✔
228
                        const entity = await this._validateObject(ctx, fields, params, opts);
304✔
229

230
                        this.finishSpan(ctx, span);
304✔
231

232
                        return entity;
304✔
233
                },
234

235
                /**
236
                 * Call a custom function what can be a method name or a `Function`.
237
                 *
238
                 * @param {String|Function} fn
239
                 * @param {Array<any>} args
240
                 * @returns {any}
241
                 */
242
                _callCustomFunction(fn, args) {
243
                        fn = typeof fn == "string" ? this[fn] : fn;
1,216!
244
                        return fn.apply(this, args);
1,216✔
245
                },
246

247
                /**
248
                 * Validate an object against field definitions.
249
                 *
250
                 * @param {Context} ctx
251
                 * @param {Array<Object>} fields
252
                 * @param {Object} params
253
                 * @param {Object} opts
254
                 * @returns
255
                 */
256
                async _validateObject(ctx, fields, params, opts) {
257
                        const type = opts.type || "create";
304!
258
                        const oldEntity = opts.entity;
304✔
259

260
                        let entity = {};
304✔
261

262
                        // Removing & Soft delete
263
                        if (type == "remove" && this.$softDelete) {
304!
UNCOV
264
                                fields = fields.filter(field => !!field.onRemove);
×
265
                        }
266

267
                        // Validating (only the root level)
268
                        if (!opts.nested) {
304!
269
                                const check = this.$validators[type];
304✔
270
                                if (check) {
304!
271
                                        const res = check(params);
304✔
272
                                        if (res !== true) {
304!
UNCOV
273
                                                this.logger.debug(`Parameter validation error`, res);
×
274
                                                //console.log(res);
UNCOV
275
                                                throw new ValidationError("Parameters validation error!", null, res);
×
276
                                        }
277
                                }
278
                        }
279

280
                        const sanitizeValue = async (field, value) => {
304✔
281
                                if (value !== undefined) {
1,799✔
282
                                        // Custom validator
283
                                        // Syntax: `validate: (value, entity, field, ctx) => value.length > 6 || "Too short"`
284
                                        if (field.validate) {
1,509!
UNCOV
285
                                                const res = await this._callCustomFunction(field.validate, [
×
286
                                                        {
287
                                                                ctx,
288
                                                                value,
289
                                                                params,
290
                                                                field,
291
                                                                id: opts.id,
292
                                                                operation: type,
293
                                                                entity: oldEntity,
294
                                                                root: opts.root || params
×
295
                                                        }
296
                                                ]);
UNCOV
297
                                                if (res !== true) {
×
UNCOV
298
                                                        this.logger.debug(`Parameter validation error`, { res, field, value });
×
UNCOV
299
                                                        throw new ValidationError(res, "VALIDATION_ERROR", {
×
300
                                                                field: field.name,
301
                                                                value
302
                                                        });
303
                                                }
304
                                        }
305

306
                                        // Nested-object
307
                                        if (field.type == "object" && field.itemProperties) {
1,509!
UNCOV
308
                                                value = await this._validateObject(ctx, field.itemProperties, value, {
×
309
                                                        ...opts,
310
                                                        nested: true,
311
                                                        root: params
312
                                                });
313
                                        }
314

315
                                        // Array
316
                                        if (field.type == "array") {
1,509!
UNCOV
317
                                                if (!Array.isArray(value)) {
×
318
                                                        this.logger.debug(`Parameter validation error`, { field, value });
×
319
                                                        throw new ValidationError(
×
320
                                                                `The field '${field.name}' must be an Array.`,
321
                                                                "VALIDATION_ERROR",
322
                                                                {
323
                                                                        field: field.name,
324
                                                                        value
325
                                                                }
326
                                                        );
327
                                                }
328

UNCOV
329
                                                if (field.items) {
×
UNCOV
330
                                                        if (field.itemProperties) {
×
UNCOV
331
                                                                for (let i = 0; i < value.length; i++) {
×
UNCOV
332
                                                                        value[i] = await this._validateObject(
×
333
                                                                                ctx,
334
                                                                                field.itemProperties,
335
                                                                                value[i],
336
                                                                                {
337
                                                                                        ...opts,
338
                                                                                        nested: true,
339
                                                                                        root: params
340
                                                                                }
341
                                                                        );
342
                                                                }
UNCOV
343
                                                        } else if (field.items.type) {
×
UNCOV
344
                                                                for (let i = 0; i < value.length; i++) {
×
UNCOV
345
                                                                        value[i] = await sanitizeValue(field.items, value[i]);
×
346
                                                                }
347
                                                        }
348
                                                }
349
                                        }
350
                                }
351

352
                                if (["create", "replace"].includes(type)) {
1,799✔
353
                                        // Required/optional
354
                                        if (field.required) {
1,694✔
355
                                                if ((value === null && !field.nullable) || value === undefined) {
992!
UNCOV
356
                                                        this.logger.debug(`Parameter validation error. Field is required`, {
×
357
                                                                field,
358
                                                                value
359
                                                        });
360

UNCOV
361
                                                        throw new ValidationError(
×
362
                                                                "Parameters validation error!",
363
                                                                "VALIDATION_ERROR",
364
                                                                [
365
                                                                        {
366
                                                                                type: "required",
367
                                                                                field: field.name,
368
                                                                                message: `The '${field.name}' field is required.`,
369
                                                                                actual: value
370
                                                                        }
371
                                                                ]
372
                                                        );
373
                                                }
374
                                        }
375
                                }
376

377
                                return value;
1,799✔
378
                        };
379

380
                        const setValue = async (field, value) => {
304✔
381
                                value = await sanitizeValue(field, value);
1,799✔
382

383
                                if (value !== undefined) {
1,799✔
384
                                        if (field.type == "array" || field.type == "object") {
1,509!
UNCOV
385
                                                if (!opts.nestedFieldSupport) {
×
UNCOV
386
                                                        if (Array.isArray(value) || _.isObject(value)) {
×
UNCOV
387
                                                                value = JSON.stringify(value);
×
388
                                                        }
389
                                                }
390
                                        }
391

392
                                        // Set the value to the entity, it's valid.
393
                                        _.set(entity, field.columnName, value);
1,509✔
394
                                }
395
                        };
396

397
                        const authorizedFields = await this._authorizeFields(fields, ctx, params, {
304✔
398
                                permissive: opts.permissive,
399
                                isWrite: true
400
                        });
401

402
                        await Promise.all(
304✔
403
                                authorizedFields.map(async field => {
404
                                        let value = _.get(params, field.name);
1,977✔
405

406
                                        // Virtual field
407
                                        if (field.virtual) return;
1,977✔
408

409
                                        // Custom formatter (can be async)
410
                                        // Syntax: `set: (value, entity, field, ctx) => value.toUpperCase()`
411
                                        if (field.set) {
1,837!
UNCOV
412
                                                value = await this._callCustomFunction(field.set, [
×
413
                                                        {
414
                                                                ctx,
415
                                                                value,
416
                                                                params,
417
                                                                field,
418
                                                                id: opts.id,
419
                                                                operation: type,
420
                                                                entity: oldEntity,
421
                                                                root: opts.root || params
×
422
                                                        }
423
                                                ]);
UNCOV
424
                                                return setValue(field, value);
×
425
                                        }
426

427
                                        const customArgs = [
1,837✔
428
                                                {
429
                                                        ctx,
430
                                                        value,
431
                                                        params,
432
                                                        field,
433
                                                        id: opts.id,
434
                                                        operation: type,
435
                                                        entity: oldEntity,
436
                                                        root: opts.root || params
3,674✔
437
                                                }
438
                                        ];
439

440
                                        // Handlers
441
                                        if (!opts.skipOnHooks) {
1,837!
442
                                                if (type == "create" && field.onCreate) {
1,837✔
443
                                                        if (_.isFunction(field.onCreate)) {
21!
444
                                                                value = await this._callCustomFunction(field.onCreate, customArgs);
21✔
445
                                                        } else {
UNCOV
446
                                                                value = field.onCreate;
×
447
                                                        }
448
                                                        return setValue(field, value);
21✔
449
                                                } else if (type == "update" && field.onUpdate) {
1,816✔
450
                                                        if (_.isFunction(field.onUpdate)) {
7!
451
                                                                value = await this._callCustomFunction(field.onUpdate, customArgs);
7✔
452
                                                        } else {
UNCOV
453
                                                                value = field.onUpdate;
×
454
                                                        }
455
                                                        return setValue(field, value);
7✔
456
                                                } else if (type == "replace" && field.onReplace) {
1,809!
UNCOV
457
                                                        if (_.isFunction(field.onReplace)) {
×
UNCOV
458
                                                                value = await this._callCustomFunction(field.onReplace, customArgs);
×
459
                                                        } else {
UNCOV
460
                                                                value = field.onReplace;
×
461
                                                        }
UNCOV
462
                                                        return setValue(field, value);
×
463
                                                } else if (type == "remove" && field.onRemove) {
1,809!
UNCOV
464
                                                        if (_.isFunction(field.onRemove)) {
×
UNCOV
465
                                                                value = await this._callCustomFunction(field.onRemove, customArgs);
×
466
                                                        } else {
UNCOV
467
                                                                value = field.onRemove;
×
468
                                                        }
UNCOV
469
                                                        return setValue(field, value);
×
470
                                                }
471
                                        }
472

473
                                        if (["create", "replace"].includes(type)) {
1,809✔
474
                                                // Default value
475
                                                if (value === undefined) {
1,704✔
476
                                                        if (field.default !== undefined) {
300!
UNCOV
477
                                                                if (_.isFunction(field.default)) {
×
UNCOV
478
                                                                        value = await this._callCustomFunction(
×
479
                                                                                field.default,
480
                                                                                customArgs
481
                                                                        );
482
                                                                } else {
UNCOV
483
                                                                        value = field.default;
×
484
                                                                }
UNCOV
485
                                                                return setValue(field, value);
×
486
                                                        }
487
                                                }
488
                                        }
489

490
                                        // Readonly
491
                                        if (field.readonly && !opts.permissive) return;
1,809✔
492

493
                                        // Immutable (should check the previous value, if not set yet, we should enable)
494
                                        if (
1,771!
495
                                                ["update", "replace"].includes(type) &&
1,869!
496
                                                field.immutable === true &&
497
                                                !opts.permissive
498
                                        ) {
UNCOV
499
                                                const prevValue = _.get(oldEntity, field.columnName);
×
UNCOV
500
                                                if (prevValue != null) {
×
UNCOV
501
                                                        if (type == "update") {
×
502
                                                                // Skip on update
UNCOV
503
                                                                return;
×
504
                                                        } else {
505
                                                                // Use the previous value on replace
UNCOV
506
                                                                value = prevValue;
×
507
                                                        }
508
                                                }
509
                                        }
510

511
                                        await setValue(field, value);
1,771✔
512
                                })
513
                        );
514

515
                        return entity;
304✔
516
                }
517
        };
518
};
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