Просмотр исходного кода

fix: the filter option "fields"

e22m4u 2 лет назад
Родитель
Сommit
ea93bb87df

+ 1 - 1
src/adapter/adapter-loader.js

@@ -16,7 +16,7 @@ export class AdapterLoader extends Service {
   async loadByName(adapterName, settings = undefined) {
     if (!adapterName || typeof adapterName !== 'string')
       throw new InvalidArgumentError(
-        'The adapter name must be a non-empty String, but %v given.',
+        'The adapter name should be a non-empty String, but %v given.',
         adapterName,
       );
     let adapterCtor;

+ 1 - 1
src/adapter/adapter-loader.spec.js

@@ -9,7 +9,7 @@ describe('AdapterLoader', function () {
     it('requires an adapter name as a non-empty string', async function () {
       const promise = S.loadByName('');
       await expect(promise).to.be.rejectedWith(
-        'The adapter name must be a non-empty String, but "" given.',
+        'The adapter name should be a non-empty String, but "" given.',
       );
     });
 

+ 1 - 1
src/adapter/decorator/data-sanitizing-decorator.js

@@ -15,7 +15,7 @@ export class DataSanitizingDecorator extends Service {
   decorate(adapter) {
     if (!adapter || !(adapter instanceof Adapter))
       throw new InvalidArgumentError(
-        'A first argument of DataSanitizingDecorator.decorate must be ' +
+        'A first argument of DataSanitizingDecorator.decorate should be ' +
           'an Adapter instance, but %v given.',
         adapter,
       );

+ 1 - 1
src/adapter/decorator/data-validation-decorator.js

@@ -15,7 +15,7 @@ export class DataValidationDecorator extends Service {
   decorate(adapter) {
     if (!adapter || !(adapter instanceof Adapter))
       throw new InvalidArgumentError(
-        'A first argument of DataValidationDecorator.decorate must be ' +
+        'A first argument of DataValidationDecorator.decorate should be ' +
           'an Adapter instance, but %v given.',
         adapter,
       );

+ 1 - 1
src/adapter/decorator/default-values-decorator.js

@@ -15,7 +15,7 @@ export class DefaultValuesDecorator extends Service {
   decorate(adapter) {
     if (!adapter || !(adapter instanceof Adapter))
       throw new InvalidArgumentError(
-        'A first argument of DefaultValuesDecorator.decorate must be ' +
+        'A first argument of DefaultValuesDecorator.decorate should be ' +
           'an Adapter instance, but %v given.',
         adapter,
       );

+ 1 - 1
src/adapter/decorator/fields-filtering-decorator.js

@@ -15,7 +15,7 @@ export class FieldsFilteringDecorator extends Service {
   decorate(adapter) {
     if (!adapter || !(adapter instanceof Adapter))
       throw new InvalidArgumentError(
-        'A first argument of FieldsFilteringDecorator.decorate must be ' +
+        'A first argument of FieldsFilteringDecorator.decorate should be ' +
           'an Adapter instance, but %v given.',
         adapter,
       );

+ 1 - 1
src/adapter/decorator/inclusion-decorator.js

@@ -15,7 +15,7 @@ export class InclusionDecorator extends Service {
   decorate(adapter) {
     if (!adapter || !(adapter instanceof Adapter))
       throw new InvalidArgumentError(
-        'A first argument of InclusionDecorator.decorate must be ' +
+        'A first argument of InclusionDecorator.decorate should be ' +
           'an Adapter instance, but %v given.',
         adapter,
       );

+ 2 - 2
src/definition/model/model-data-sanitizer.js

@@ -17,13 +17,13 @@ export class ModelDataSanitizer extends Service {
     if (!modelName || typeof modelName !== 'string')
       throw new InvalidArgumentError(
         'The first argument of ModelDataSanitizer.sanitize ' +
-          'must be a string, but %v given.',
+          'should be a string, but %v given.',
         modelName,
       );
     if (!modelData || typeof modelData !== 'object')
       throw new InvalidArgumentError(
         'The second argument of ModelDataSanitizer.sanitize ' +
-          'must be an Object, but %v given.',
+          'should be an Object, but %v given.',
         modelData,
       );
     return this.getService(

+ 1 - 1
src/definition/model/model-data-validator.js

@@ -19,7 +19,7 @@ export class ModelDataValidator extends Service {
   validate(modelName, modelData, isPartial = false) {
     if (!isPureObject(modelData))
       throw new InvalidArgumentError(
-        'The data of the model %v must be an Object, but %v given.',
+        'The data of the model %v should be an Object, but %v given.',
         modelName,
         modelData,
       );

+ 1 - 1
src/definition/model/model-data-validator.spec.js

@@ -24,7 +24,7 @@ describe('ModelDataValidator', function () {
       };
       const error = given =>
         format(
-          'The data of the model "model" must be an Object, but %s given.',
+          'The data of the model "model" should be an Object, but %s given.',
           given,
         );
       expect(throwable('str')).to.throw(error('"str"'));

+ 1 - 1
src/definition/model/model-definition-utils.js

@@ -361,7 +361,7 @@ export class ModelDefinitionUtils extends Service {
     if (!modelData || typeof modelData !== 'object' || Array.isArray(modelData))
       throw new InvalidArgumentError(
         'The second argument of ModelDefinitionUtils.excludeObjectKeysByRelationNames ' +
-          'must be an Object, but %v given.',
+          'should be an Object, but %v given.',
         modelData,
       );
     const relDefs = this.getRelationsDefinitionInBaseModelHierarchy(modelName);

+ 1 - 1
src/definition/model/model-definition-utils.spec.js

@@ -1456,7 +1456,7 @@ describe('ModelDefinitionUtils', function () {
       const error = v =>
         format(
           'The second argument of ModelDefinitionUtils.excludeObjectKeysByRelationNames ' +
-            'must be an Object, but %s given.',
+            'should be an Object, but %s given.',
           v,
         );
       expect(throwable('')).to.throw(error('""'));

+ 32 - 21
src/filter/fields-clause-tool.js

@@ -10,30 +10,39 @@ export class FieldsClauseTool extends Service {
   /**
    * Filter.
    *
-   * @param {object|object[]} entities
+   * @param {object|object[]} input
    * @param {string} modelName
    * @param {string|string[]|undefined} clause
    * @returns {object|object[]}
    */
-  filter(entities, modelName, clause) {
-    const isArray = Array.isArray(entities);
-    entities = isArray ? entities : [entities];
+  filter(input, modelName, clause) {
+    const isArray = Array.isArray(input);
+    let entities = isArray ? input : [input];
     entities.forEach(entity => {
       if (!entity || typeof entity !== 'object' || Array.isArray(entity))
         throw new InvalidArgumentError(
-          'A first argument of FieldClauseTool.filter should be an Object or ' +
+          'The first argument of FieldsClauseTool.filter should be an Object or ' +
             'an Array of Object, but %v given.',
           entity,
         );
     });
 
-    if (!clause) return entities;
+    if (!modelName || typeof modelName !== 'string')
+      throw new InvalidArgumentError(
+        'The second argument of FieldsClauseTool.filter should be ' +
+          'a non-empty String, but %v given.',
+        modelName,
+      );
+
+    if (clause == null) return input;
     const fields = Array.isArray(clause) ? clause.slice() : [clause];
+    if (!fields.length) return input;
+
     fields.forEach(field => {
       if (!field || typeof field !== 'string')
         throw new InvalidArgumentError(
-          'The provided option "fields" should be a String ' +
-            'or an Array of String, but %v given.',
+          'The provided option "fields" should be a non-empty String ' +
+            'or an Array of non-empty String, but %v given.',
           field,
         );
     });
@@ -54,14 +63,15 @@ export class FieldsClauseTool extends Service {
    * @param {string|string[]|undefined} clause
    */
   static validateFieldsClause(clause) {
-    if (!clause) return;
-    const tempClause = Array.isArray(clause) ? clause : [clause];
-    tempClause.forEach(key => {
-      if (!key || typeof key !== 'string')
+    if (clause == null) return;
+    const fields = Array.isArray(clause) ? clause : [clause];
+    if (!fields.length) return;
+    fields.forEach(field => {
+      if (!field || typeof field !== 'string')
         throw new InvalidArgumentError(
           'The provided option "fields" should be a non-empty String ' +
-            'or an Array of String, but %v given.',
-          key,
+            'or an Array of non-empty String, but %v given.',
+          field,
         );
     });
   }
@@ -73,16 +83,17 @@ export class FieldsClauseTool extends Service {
    * @returns {string[]|undefined}
    */
   static normalizeFieldsClause(clause) {
-    if (!clause) return;
-    clause = Array.isArray(clause) ? clause : [clause];
-    clause.forEach(key => {
-      if (!key || typeof key !== 'string')
+    if (clause == null) return;
+    const fields = Array.isArray(clause) ? clause : [clause];
+    if (!fields.length) return;
+    fields.forEach(field => {
+      if (!field || typeof field !== 'string')
         throw new InvalidArgumentError(
           'The provided option "fields" should be a non-empty String ' +
-            'or an Array of String, but %v given.',
-          key,
+            'or an Array of non-empty String, but %v given.',
+          field,
         );
     });
-    return clause;
+    return fields;
   }
 }

+ 445 - 100
src/filter/fields-clause-tool.spec.js

@@ -11,123 +11,468 @@ const T = S.getService(FieldsClauseTool);
 
 describe('FieldsClauseTool', function () {
   describe('filter', function () {
-    it('returns an object with selected fields', function () {
-      const value = {foo: 'fooVal', bar: 'barVal', baz: 'bazVal'};
-      const fields = ['bar', 'baz'];
-      const result = T.filter(value, MODEL_NAME, fields);
-      expect(result).to.be.eql({bar: 'barVal', baz: 'bazVal'});
-    });
+    describe('object', function () {
+      describe('single field', function () {
+        it('requires the first argument to be an object', function () {
+          const throwable = v => () => T.filter(v, MODEL_NAME, 'bar');
+          const error = v =>
+            format(
+              'The first argument of FieldsClauseTool.filter should be an Object or ' +
+                'an Array of Object, but %s given.',
+              v,
+            );
+          expect(throwable('str')).to.throw(error('"str"'));
+          expect(throwable('')).to.throw(error('""'));
+          expect(throwable(10)).to.throw(error('10'));
+          expect(throwable(0)).to.throw(error('0'));
+          expect(throwable(true)).to.throw(error('true'));
+          expect(throwable(false)).to.throw(error('false'));
+          expect(throwable(undefined)).to.throw(error('undefined'));
+          expect(throwable(null)).to.throw(error('null'));
+          expect(throwable({foo: 'a1', bar: 'a2'})()).to.be.eql({bar: 'a2'});
+          expect(throwable({})()).to.be.eql({});
+        });
 
-    it('returns an array with selected fields', function () {
-      const value = [
-        {foo: 'fooVal', bar: 'barVal', baz: 'bazVal'},
-        {foo: 'fooVal', bar: 'barVal', baz: 'bazVal'},
-        {foo: 'fooVal', bar: 'barVal', baz: 'bazVal'},
-      ];
-      const fields = ['bar', 'baz'];
-      const result = T.filter(value, MODEL_NAME, fields);
-      expect(result[0]).to.be.eql({bar: 'barVal', baz: 'bazVal'});
-      expect(result[1]).to.be.eql({bar: 'barVal', baz: 'bazVal'});
-      expect(result[2]).to.be.eql({bar: 'barVal', baz: 'bazVal'});
-    });
+        it('requires the second argument to be a non-empty string', function () {
+          const entity = {foo: 'a1', bar: 'a2', baz: 'a3'};
+          const throwable = v => () => T.filter(entity, v, 'bar');
+          const error = v =>
+            format(
+              'The second argument of FieldsClauseTool.filter should be ' +
+                'a non-empty String, but %s given.',
+              v,
+            );
+          expect(throwable('')).to.throw(error('""'));
+          expect(throwable(10)).to.throw(error('10'));
+          expect(throwable(0)).to.throw(error('0'));
+          expect(throwable(true)).to.throw(error('true'));
+          expect(throwable(false)).to.throw(error('false'));
+          expect(throwable(undefined)).to.throw(error('undefined'));
+          expect(throwable(null)).to.throw(error('null'));
+          expect(throwable('model')()).to.be.eql({bar: 'a2'});
+        });
 
-    it('includes a primary key', function () {
-      const value = {[DEF_PK]: 10, foo: 'fooVal', bar: 'barVal', baz: 'bazVal'};
-      const fields = ['bar', 'baz'];
-      const result = T.filter(value, MODEL_NAME, fields);
-      expect(result).to.be.eql({[DEF_PK]: 10, bar: 'barVal', baz: 'bazVal'});
-    });
+        it('requires the third argument to be a non-empty string', function () {
+          const entity = {foo: 'a1', bar: 'a2', baz: 'a3'};
+          const throwable = v => () => T.filter(entity, MODEL_NAME, v);
+          const error = v =>
+            format(
+              'The provided option "fields" should be a non-empty String ' +
+                'or an Array of non-empty String, but %s given.',
+              v,
+            );
+          expect(throwable('')).to.throw(error('""'));
+          expect(throwable(10)).to.throw(error('10'));
+          expect(throwable(0)).to.throw(error('0'));
+          expect(throwable(true)).to.throw(error('true'));
+          expect(throwable(false)).to.throw(error('false'));
+          expect(throwable({})).to.throw(error('Object'));
+          expect(throwable('bar')()).to.be.eql({bar: 'a2'});
+          expect(throwable(undefined)()).to.be.eq(entity);
+          expect(throwable(null)()).to.be.eq(entity);
+        });
 
-    it('throws an error if a first argument is not an object', function () {
-      const throwable = () => T.filter(10, MODEL_NAME, ['bar']);
-      expect(throwable).to.throw(
-        'A first argument of FieldClauseTool.filter should be an Object or ' +
-          'an Array of Object, but 10 given.',
-      );
-    });
+        it('picks field of the given object', function () {
+          const value = {foo: 'a1', bar: 'a2', baz: 'a3'};
+          const result = T.filter(value, MODEL_NAME, 'bar');
+          expect(result).to.be.eql({bar: 'a2'});
+        });
 
-    it('throws an error if elements of a first argument is not an object', function () {
-      const throwable = () => T.filter([10], MODEL_NAME, ['bar']);
-      expect(throwable).to.throw(
-        'A first argument of FieldClauseTool.filter should be an Object or ' +
-          'an Array of Object, but 10 given.',
-      );
-    });
+        it('includes the primary key of the given object', function () {
+          const value = {[DEF_PK]: 10, foo: 'a1', bar: 'a2', baz: 'a3'};
+          const result = T.filter(value, MODEL_NAME, 'bar');
+          expect(result).to.be.eql({[DEF_PK]: 10, bar: 'a2'});
+        });
+      });
+
+      describe('multiple fields', function () {
+        it('requires the first argument to be an object', function () {
+          const throwable = v => () => T.filter(v, MODEL_NAME, ['bar', 'baz']);
+          const error = v =>
+            format(
+              'The first argument of FieldsClauseTool.filter should be an Object or ' +
+                'an Array of Object, but %s given.',
+              v,
+            );
+          expect(throwable('str')).to.throw(error('"str"'));
+          expect(throwable('')).to.throw(error('""'));
+          expect(throwable(10)).to.throw(error('10'));
+          expect(throwable(0)).to.throw(error('0'));
+          expect(throwable(true)).to.throw(error('true'));
+          expect(throwable(false)).to.throw(error('false'));
+          expect(throwable(undefined)).to.throw(error('undefined'));
+          expect(throwable(null)).to.throw(error('null'));
+          expect(throwable({foo: 'a1', bar: 'a2'})()).to.be.eql({bar: 'a2'});
+          expect(throwable({})()).to.be.eql({});
+        });
 
-    it('throws an error if a second argument is not a string', function () {
-      const throwable = () => T.filter({}, MODEL_NAME, 10);
-      expect(throwable).to.throw(
-        'The provided option "fields" should be a String ' +
-          'or an Array of String, but 10 given.',
-      );
+        it('requires the second argument to be a non-empty string', function () {
+          const entity = {foo: 'a1', bar: 'a2', baz: 'a3'};
+          const throwable = v => () => T.filter(entity, v, ['bar', 'baz']);
+          const error = v =>
+            format(
+              'The second argument of FieldsClauseTool.filter should be ' +
+                'a non-empty String, but %s given.',
+              v,
+            );
+          expect(throwable('')).to.throw(error('""'));
+          expect(throwable(10)).to.throw(error('10'));
+          expect(throwable(0)).to.throw(error('0'));
+          expect(throwable(true)).to.throw(error('true'));
+          expect(throwable(false)).to.throw(error('false'));
+          expect(throwable(undefined)).to.throw(error('undefined'));
+          expect(throwable(null)).to.throw(error('null'));
+          expect(throwable('model')()).to.be.eql({bar: 'a2', baz: 'a3'});
+        });
+
+        it('requires the third argument to be an array of non-empty strings', function () {
+          const entity = {foo: 'a1', bar: 'a2', baz: 'a3'};
+          const throwable = v => () => T.filter(entity, MODEL_NAME, v);
+          const error = v =>
+            format(
+              'The provided option "fields" should be a non-empty String ' +
+                'or an Array of non-empty String, but %s given.',
+              v,
+            );
+          expect(throwable([''])).to.throw(error('""'));
+          expect(throwable([10])).to.throw(error('10'));
+          expect(throwable([0])).to.throw(error('0'));
+          expect(throwable([true])).to.throw(error('true'));
+          expect(throwable([false])).to.throw(error('false'));
+          expect(throwable([{}])).to.throw(error('Object'));
+          expect(throwable([undefined])).to.throw(error('undefined'));
+          expect(throwable([null])).to.throw(error('null'));
+          expect(throwable(['bar', 'baz'])()).to.be.eql({bar: 'a2', baz: 'a3'});
+          expect(throwable([])()).to.be.eq(entity);
+        });
+
+        it('picks fields of the given object', function () {
+          const value = {foo: 'a1', bar: 'a2', baz: 'a3'};
+          const result = T.filter(value, MODEL_NAME, ['bar', 'baz']);
+          expect(result).to.be.eql({bar: 'a2', baz: 'a3'});
+        });
+
+        it('includes the primary key of the given object', function () {
+          const value = {[DEF_PK]: 10, foo: 'a1', bar: 'a2', baz: 'a3'};
+          const result = T.filter(value, MODEL_NAME, ['bar', 'baz']);
+          expect(result).to.be.eql({[DEF_PK]: 10, bar: 'a2', baz: 'a3'});
+        });
+      });
     });
 
-    it('throws an error if elements of a second argument is not a string', function () {
-      const throwable = () => T.filter({}, MODEL_NAME, [10]);
-      expect(throwable).to.throw(
-        'The provided option "fields" should be a String ' +
-          'or an Array of String, but 10 given.',
-      );
+    describe('array', function () {
+      describe('single field', function () {
+        it('requires the first argument to be an array of objects', function () {
+          const throwable = v => () => T.filter(v, MODEL_NAME, 'bar');
+          const error = v =>
+            format(
+              'The first argument of FieldsClauseTool.filter should be an Object or ' +
+                'an Array of Object, but %s given.',
+              v,
+            );
+          expect(throwable(['str'])).to.throw(error('"str"'));
+          expect(throwable([''])).to.throw(error('""'));
+          expect(throwable([10])).to.throw(error('10'));
+          expect(throwable([0])).to.throw(error('0'));
+          expect(throwable([true])).to.throw(error('true'));
+          expect(throwable([false])).to.throw(error('false'));
+          expect(throwable([undefined])).to.throw(error('undefined'));
+          expect(throwable([null])).to.throw(error('null'));
+          expect(throwable([{foo: 'a1', bar: 'a2'}])()).to.be.eql([
+            {bar: 'a2'},
+          ]);
+          expect(throwable([{}])()).to.be.eql([{}]);
+        });
+
+        it('requires the second argument to be a non-empty string', function () {
+          const entities = [
+            {foo: 'a1', bar: 'a2', baz: 'a3'},
+            {foo: 'b1', bar: 'b2', baz: 'b3'},
+            {foo: 'c1', bar: 'c2', baz: 'c3'},
+          ];
+          const throwable = v => () => T.filter(entities, v, 'bar');
+          const error = v =>
+            format(
+              'The second argument of FieldsClauseTool.filter should be ' +
+                'a non-empty String, but %s given.',
+              v,
+            );
+          expect(throwable('')).to.throw(error('""'));
+          expect(throwable(10)).to.throw(error('10'));
+          expect(throwable(0)).to.throw(error('0'));
+          expect(throwable(true)).to.throw(error('true'));
+          expect(throwable(false)).to.throw(error('false'));
+          expect(throwable(undefined)).to.throw(error('undefined'));
+          expect(throwable(null)).to.throw(error('null'));
+          expect(throwable('model')()).to.be.eql([
+            {bar: 'a2'},
+            {bar: 'b2'},
+            {bar: 'c2'},
+          ]);
+        });
+
+        it('requires the third argument to be a non-empty string', function () {
+          const entities = [
+            {foo: 'a1', bar: 'a2', baz: 'a3'},
+            {foo: 'b1', bar: 'b2', baz: 'b3'},
+            {foo: 'c1', bar: 'c2', baz: 'c3'},
+          ];
+          const throwable = v => () => T.filter(entities, MODEL_NAME, v);
+          const error = v =>
+            format(
+              'The provided option "fields" should be a non-empty String ' +
+                'or an Array of non-empty String, but %s given.',
+              v,
+            );
+          expect(throwable('')).to.throw(error('""'));
+          expect(throwable(10)).to.throw(error('10'));
+          expect(throwable(0)).to.throw(error('0'));
+          expect(throwable(true)).to.throw(error('true'));
+          expect(throwable(false)).to.throw(error('false'));
+          expect(throwable({})).to.throw(error('Object'));
+          expect(throwable('bar')()).to.be.eql([
+            {bar: 'a2'},
+            {bar: 'b2'},
+            {bar: 'c2'},
+          ]);
+          expect(throwable(undefined)()).to.be.eq(entities);
+          expect(throwable(null)()).to.be.eq(entities);
+        });
+
+        it('picks field of the given object', function () {
+          const entities = [
+            {foo: 'a1', bar: 'a2', baz: 'a3'},
+            {foo: 'b1', bar: 'b2', baz: 'b3'},
+            {foo: 'c1', bar: 'c2', baz: 'c3'},
+          ];
+          const result = T.filter(entities, MODEL_NAME, 'bar');
+          expect(result).to.have.lengthOf(3);
+          expect(result[0]).to.be.eql({bar: 'a2'});
+          expect(result[1]).to.be.eql({bar: 'b2'});
+          expect(result[2]).to.be.eql({bar: 'c2'});
+        });
+
+        it('includes the primary key of the given object', function () {
+          const entities = [
+            {[DEF_PK]: 1, foo: 'a1', bar: 'a2', baz: 'a3'},
+            {[DEF_PK]: 2, foo: 'b1', bar: 'b2', baz: 'b3'},
+            {[DEF_PK]: 3, foo: 'c1', bar: 'c2', baz: 'c3'},
+          ];
+          const result = T.filter(entities, MODEL_NAME, 'bar');
+          expect(result).to.have.lengthOf(3);
+          expect(result[0]).to.be.eql({[DEF_PK]: 1, bar: 'a2'});
+          expect(result[1]).to.be.eql({[DEF_PK]: 2, bar: 'b2'});
+          expect(result[2]).to.be.eql({[DEF_PK]: 3, bar: 'c2'});
+        });
+      });
+
+      describe('multiple fields', function () {
+        it('requires the first argument to be an array of objects', function () {
+          const throwable = v => () => T.filter(v, MODEL_NAME, ['bar', 'baz']);
+          const error = v =>
+            format(
+              'The first argument of FieldsClauseTool.filter should be an Object or ' +
+                'an Array of Object, but %s given.',
+              v,
+            );
+          expect(throwable(['str'])).to.throw(error('"str"'));
+          expect(throwable([''])).to.throw(error('""'));
+          expect(throwable([10])).to.throw(error('10'));
+          expect(throwable([0])).to.throw(error('0'));
+          expect(throwable([true])).to.throw(error('true'));
+          expect(throwable([false])).to.throw(error('false'));
+          expect(throwable([undefined])).to.throw(error('undefined'));
+          expect(throwable([null])).to.throw(error('null'));
+          expect(throwable([{foo: 'a1', bar: 'a2'}])()).to.be.eql([
+            {bar: 'a2'},
+          ]);
+          expect(throwable([{}])()).to.be.eql([{}]);
+        });
+
+        it('requires the second argument to be a non-empty string', function () {
+          const entities = [
+            {foo: 'a1', bar: 'a2', baz: 'a3'},
+            {foo: 'b1', bar: 'b2', baz: 'b3'},
+            {foo: 'c1', bar: 'c2', baz: 'c3'},
+          ];
+          const throwable = v => () => T.filter(entities, v, ['bar', 'baz']);
+          const error = v =>
+            format(
+              'The second argument of FieldsClauseTool.filter should be ' +
+                'a non-empty String, but %s given.',
+              v,
+            );
+          expect(throwable('')).to.throw(error('""'));
+          expect(throwable(10)).to.throw(error('10'));
+          expect(throwable(0)).to.throw(error('0'));
+          expect(throwable(true)).to.throw(error('true'));
+          expect(throwable(false)).to.throw(error('false'));
+          expect(throwable(undefined)).to.throw(error('undefined'));
+          expect(throwable(null)).to.throw(error('null'));
+          expect(throwable('model')()).to.be.eql([
+            {bar: 'a2', baz: 'a3'},
+            {bar: 'b2', baz: 'b3'},
+            {bar: 'c2', baz: 'c3'},
+          ]);
+        });
+
+        it('requires the third argument to be a non-empty string', function () {
+          const entities = [
+            {foo: 'a1', bar: 'a2', baz: 'a3'},
+            {foo: 'b1', bar: 'b2', baz: 'b3'},
+            {foo: 'c1', bar: 'c2', baz: 'c3'},
+          ];
+          const throwable = v => () => T.filter(entities, MODEL_NAME, v);
+          const error = v =>
+            format(
+              'The provided option "fields" should be a non-empty String ' +
+                'or an Array of non-empty String, but %s given.',
+              v,
+            );
+          expect(throwable([''])).to.throw(error('""'));
+          expect(throwable([10])).to.throw(error('10'));
+          expect(throwable([0])).to.throw(error('0'));
+          expect(throwable([true])).to.throw(error('true'));
+          expect(throwable([false])).to.throw(error('false'));
+          expect(throwable([{}])).to.throw(error('Object'));
+          expect(throwable([undefined])).to.throw(error('undefined'));
+          expect(throwable([null])).to.throw(error('null'));
+          expect(throwable(['bar', 'baz'])()).to.be.eql([
+            {bar: 'a2', baz: 'a3'},
+            {bar: 'b2', baz: 'b3'},
+            {bar: 'c2', baz: 'c3'},
+          ]);
+        });
+
+        it('picks field of the given object', function () {
+          const entities = [
+            {foo: 'a1', bar: 'a2', baz: 'a3'},
+            {foo: 'b1', bar: 'b2', baz: 'b3'},
+            {foo: 'c1', bar: 'c2', baz: 'c3'},
+          ];
+          const result = T.filter(entities, MODEL_NAME, ['bar', 'baz']);
+          expect(result).to.have.lengthOf(3);
+          expect(result[0]).to.be.eql({bar: 'a2', baz: 'a3'});
+          expect(result[1]).to.be.eql({bar: 'b2', baz: 'b3'});
+          expect(result[2]).to.be.eql({bar: 'c2', baz: 'c3'});
+        });
+
+        it('includes the primary key of the given object', function () {
+          const entities = [
+            {[DEF_PK]: 1, foo: 'a1', bar: 'a2', baz: 'a3'},
+            {[DEF_PK]: 2, foo: 'b1', bar: 'b2', baz: 'b3'},
+            {[DEF_PK]: 3, foo: 'c1', bar: 'c2', baz: 'c3'},
+          ];
+          const result = T.filter(entities, MODEL_NAME, ['bar', 'baz']);
+          expect(result).to.have.lengthOf(3);
+          expect(result[0]).to.be.eql({[DEF_PK]: 1, bar: 'a2', baz: 'a3'});
+          expect(result[1]).to.be.eql({[DEF_PK]: 2, bar: 'b2', baz: 'b3'});
+          expect(result[2]).to.be.eql({[DEF_PK]: 3, bar: 'c2', baz: 'c3'});
+        });
+      });
     });
   });
 
   describe('validateFieldsClause', function () {
-    it('requires a non-empty string or an array of non-empty strings', function () {
-      const validate = v => () => FieldsClauseTool.validateFieldsClause(v);
-      const error = v =>
-        format(
-          'The provided option "fields" should be a non-empty String ' +
-            'or an Array of String, but %s given.',
-          v,
-        );
-      expect(validate(10)).to.throw(error('10'));
-      expect(validate(true)).to.throw(error('true'));
-      expect(validate({})).to.throw(error('Object'));
-      expect(validate([''])).to.throw(error('""'));
-      expect(validate([10])).to.throw(error('10'));
-      expect(validate([true])).to.throw(error('true'));
-      expect(validate([false])).to.throw(error('false'));
-      expect(validate([undefined])).to.throw(error('undefined'));
-      expect(validate([null])).to.throw(error('null'));
-      validate('')();
-      validate(false)();
-      validate(undefined)();
-      validate(null)();
-      validate('foo')();
-      validate(['foo'])();
+    describe('single field', function () {
+      it('requires the first argument to be a non-empty string', function () {
+        const throwable = v => () => FieldsClauseTool.validateFieldsClause(v);
+        const error = v =>
+          format(
+            'The provided option "fields" should be a non-empty String ' +
+              'or an Array of non-empty String, but %s given.',
+            v,
+          );
+        expect(throwable('')).to.throw(error('""'));
+        expect(throwable(10)).to.throw(error('10'));
+        expect(throwable(0)).to.throw(error('0'));
+        expect(throwable(true)).to.throw(error('true'));
+        expect(throwable(false)).to.throw(error('false'));
+        expect(throwable({})).to.throw(error('Object'));
+        throwable('field')();
+        throwable(undefined)();
+        throwable(null)();
+      });
+    });
+
+    describe('multiple fields', function () {
+      it('requires the first argument to be an array of non-empty strings', function () {
+        const throwable = v => () => FieldsClauseTool.validateFieldsClause(v);
+        const error = v =>
+          format(
+            'The provided option "fields" should be a non-empty String ' +
+              'or an Array of non-empty String, but %s given.',
+            v,
+          );
+        expect(throwable([''])).to.throw(error('""'));
+        expect(throwable([10])).to.throw(error('10'));
+        expect(throwable([0])).to.throw(error('0'));
+        expect(throwable([true])).to.throw(error('true'));
+        expect(throwable([false])).to.throw(error('false'));
+        expect(throwable([{}])).to.throw(error('Object'));
+        expect(throwable([undefined])).to.throw(error('undefined'));
+        expect(throwable([null])).to.throw(error('null'));
+        throwable(['field'])();
+        throwable([])();
+      });
     });
   });
 
   describe('normalizeFieldsClause', function () {
-    it('returns an array of strings', function () {
-      const fn = FieldsClauseTool.normalizeFieldsClause;
-      expect(fn('foo')).to.be.eql(['foo']);
-      expect(fn(['foo'])).to.be.eql(['foo']);
+    describe('single field', function () {
+      it('requires the first argument to be a non-empty string', function () {
+        const throwable = clause => () =>
+          FieldsClauseTool.normalizeFieldsClause(clause);
+        const error = v =>
+          format(
+            'The provided option "fields" should be a non-empty String ' +
+              'or an Array of non-empty String, but %s given.',
+            v,
+          );
+        expect(throwable('')).to.throw(error('""'));
+        expect(throwable(10)).to.throw(error('10'));
+        expect(throwable(0)).to.throw(error('0'));
+        expect(throwable(true)).to.throw(error('true'));
+        expect(throwable(false)).to.throw(error('false'));
+        expect(throwable({})).to.throw(error('Object'));
+        expect(throwable('field')()).to.be.eql(['field']);
+        expect(throwable(undefined)()).to.be.undefined;
+        expect(throwable(null)()).to.be.undefined;
+      });
+
+      it('returns an array of strings', function () {
+        const fn = FieldsClauseTool.normalizeFieldsClause;
+        expect(fn('foo')).to.be.eql(['foo']);
+      });
     });
 
-    it('requires a non-empty string or an array of non-empty strings', function () {
-      const fn = clause => () => FieldsClauseTool.normalizeFieldsClause(clause);
-      const error = v =>
-        format(
-          'The provided option "fields" should be a non-empty String ' +
-            'or an Array of String, but %s given.',
-          v,
-        );
-      expect(fn(10)).to.throw(error('10'));
-      expect(fn(true)).to.throw(error('true'));
-      expect(fn({})).to.throw(error('Object'));
-      expect(fn([''])).to.throw(error('""'));
-      expect(fn([10])).to.throw(error('10'));
-      expect(fn([true])).to.throw(error('true'));
-      expect(fn([false])).to.throw(error('false'));
-      expect(fn([undefined])).to.throw(error('undefined'));
-      expect(fn([null])).to.throw(error('null'));
-      expect(fn('')()).to.be.undefined;
-      expect(fn(false)()).to.be.undefined;
-      expect(fn(undefined)()).to.be.undefined;
-      expect(fn(null)()).to.be.undefined;
-      expect(fn('foo')()).to.be.eql(['foo']);
-      expect(fn(['foo'])()).to.be.eql(['foo']);
+    describe('multiple fields', function () {
+      it('requires the first argument to be an array of non-empty strings', function () {
+        const throwable = clause => () =>
+          FieldsClauseTool.normalizeFieldsClause(clause);
+        const error = v =>
+          format(
+            'The provided option "fields" should be a non-empty String ' +
+              'or an Array of non-empty String, but %s given.',
+            v,
+          );
+        expect(throwable([''])).to.throw(error('""'));
+        expect(throwable([10])).to.throw(error('10'));
+        expect(throwable([0])).to.throw(error('0'));
+        expect(throwable([true])).to.throw(error('true'));
+        expect(throwable([false])).to.throw(error('false'));
+        expect(throwable([{}])).to.throw(error('Object'));
+        expect(throwable([undefined])).to.throw(error('undefined'));
+        expect(throwable([null])).to.throw(error('null'));
+        expect(throwable(['field'])()).to.be.eql(['field']);
+        expect(throwable([])()).to.be.undefined;
+      });
+
+      it('returns an array of strings', function () {
+        const fn = FieldsClauseTool.normalizeFieldsClause;
+        expect(fn(['foo'])).to.be.eql(['foo']);
+        expect(fn(['foo', 'bar'])).to.be.eql(['foo', 'bar']);
+      });
     });
   });
 });

+ 1 - 1
src/filter/operator-clause-tool.js

@@ -504,7 +504,7 @@ export class OperatorClauseTool extends Service {
       const flags = clause.flags || undefined;
       if (flags && typeof flags !== 'string')
         throw new InvalidArgumentError(
-          'RegExp flags must be a String, but %v given.',
+          'RegExp flags should be a String, but %v given.',
           clause.flags,
         );
       if (!value || typeof value !== 'string') return false;

+ 1 - 1
src/filter/operator-clause-tool.spec.js

@@ -1049,7 +1049,7 @@ describe('OperatorClauseTool', function () {
       const throwable = v => () =>
         S.testRegexp({regexp: 'Val.+', flags: v}, 'val');
       const error = v =>
-        format('RegExp flags must be a String, but %s given.', v);
+        format('RegExp flags should be a String, but %s given.', v);
       expect(throwable(10)).to.throw(error('10'));
       expect(throwable(true)).to.throw(error('true'));
       expect(throwable([])).to.throw(error('Array'));