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

refactor: updates like, nlike, ilike and nilike operators

e22m4u 1 месяц назад
Родитель
Сommit
bd38947924

+ 116 - 71
dist/cjs/index.cjs

@@ -218,64 +218,6 @@ var init_is_pure_object = __esm({
   }
   }
 });
 });
 
 
-// src/utils/string-to-regexp.js
-function stringToRegexp(pattern, flags = void 0) {
-  if (pattern instanceof RegExp) {
-    return new RegExp(pattern, flags);
-  }
-  let regex = "";
-  for (let i = 0, n = pattern.length; i < n; i++) {
-    const char = pattern.charAt(i);
-    if (char === "%") {
-      regex += ".*";
-    } else {
-      regex += char;
-    }
-  }
-  return new RegExp(regex, flags);
-}
-var init_string_to_regexp = __esm({
-  "src/utils/string-to-regexp.js"() {
-    "use strict";
-    __name(stringToRegexp, "stringToRegexp");
-  }
-});
-
-// src/utils/get-value-by-path.js
-function getValueByPath(obj, path, orElse = void 0) {
-  if (!obj || typeof obj !== "object") return orElse;
-  if (!path || typeof path !== "string") return orElse;
-  const keys = path.split(".");
-  let value = obj;
-  for (const key of keys) {
-    if (typeof value === "object" && value !== null && key in value) {
-      value = value[key];
-    } else {
-      value = orElse;
-      break;
-    }
-  }
-  return value;
-}
-var init_get_value_by_path = __esm({
-  "src/utils/get-value-by-path.js"() {
-    "use strict";
-    __name(getValueByPath, "getValueByPath");
-  }
-});
-
-// src/utils/transform-promise.js
-function transformPromise(valueOrPromise, transformer) {
-  return isPromise(valueOrPromise) ? valueOrPromise.then(transformer) : transformer(valueOrPromise);
-}
-var init_transform_promise = __esm({
-  "src/utils/transform-promise.js"() {
-    "use strict";
-    init_is_promise();
-    __name(transformPromise, "transformPromise");
-  }
-});
-
 // src/errors/not-implemented-error.js
 // src/errors/not-implemented-error.js
 var import_js_format, _NotImplementedError, NotImplementedError;
 var import_js_format, _NotImplementedError, NotImplementedError;
 var init_not_implemented_error = __esm({
 var init_not_implemented_error = __esm({
@@ -342,6 +284,105 @@ var init_errors = __esm({
   }
   }
 });
 });
 
 
+// src/utils/like-to-regexp.js
+function likeToRegexp(pattern, isCaseInsensitive = false) {
+  if (typeof pattern !== "string") {
+    throw new InvalidArgumentError(
+      "The first argument of `likeToRegexp` should be a String, but %v was given.",
+      pattern
+    );
+  }
+  const regexSpecials = "-[]{}()*+?.\\^$|";
+  let regexString = "";
+  let isEscaping = false;
+  for (const char of pattern) {
+    if (isEscaping) {
+      regexString += regexSpecials.includes(char) ? `\\${char}` : char;
+      isEscaping = false;
+    } else if (char === "\\") {
+      isEscaping = true;
+    } else if (char === "%") {
+      regexString += ".*";
+    } else if (char === "_") {
+      regexString += ".";
+    } else if (regexSpecials.includes(char)) {
+      regexString += `\\${char}`;
+    } else {
+      regexString += char;
+    }
+  }
+  if (isEscaping) {
+    regexString += "\\\\";
+  }
+  const flags = isCaseInsensitive ? "i" : "";
+  return new RegExp(`^${regexString}$`, flags);
+}
+var init_like_to_regexp = __esm({
+  "src/utils/like-to-regexp.js"() {
+    "use strict";
+    init_errors();
+    __name(likeToRegexp, "likeToRegexp");
+  }
+});
+
+// src/utils/string-to-regexp.js
+function stringToRegexp(pattern, flags = void 0) {
+  if (pattern instanceof RegExp) {
+    return new RegExp(pattern, flags);
+  }
+  let regex = "";
+  for (let i = 0, n = pattern.length; i < n; i++) {
+    const char = pattern.charAt(i);
+    if (char === "%") {
+      regex += ".*";
+    } else {
+      regex += char;
+    }
+  }
+  return new RegExp(regex, flags);
+}
+var init_string_to_regexp = __esm({
+  "src/utils/string-to-regexp.js"() {
+    "use strict";
+    __name(stringToRegexp, "stringToRegexp");
+  }
+});
+
+// src/utils/get-value-by-path.js
+function getValueByPath(obj, path, orElse = void 0) {
+  if (!obj || typeof obj !== "object") return orElse;
+  if (!path || typeof path !== "string") return orElse;
+  const keys = path.split(".");
+  let value = obj;
+  for (const key of keys) {
+    if (typeof value === "object" && value !== null && key in value) {
+      value = value[key];
+    } else {
+      value = orElse;
+      break;
+    }
+  }
+  return value;
+}
+var init_get_value_by_path = __esm({
+  "src/utils/get-value-by-path.js"() {
+    "use strict";
+    __name(getValueByPath, "getValueByPath");
+  }
+});
+
+// src/utils/transform-promise.js
+function transformPromise(valueOrPromise, transformer) {
+  return isPromise(valueOrPromise) ? valueOrPromise.then(transformer) : transformer(valueOrPromise);
+}
+var init_transform_promise = __esm({
+  "src/utils/transform-promise.js"() {
+    "use strict";
+    init_is_promise();
+    __name(transformPromise, "transformPromise");
+  }
+});
+
 // src/utils/select-object-keys.js
 // src/utils/select-object-keys.js
 function selectObjectKeys(obj, keys) {
 function selectObjectKeys(obj, keys) {
   if (!obj || typeof obj !== "object" || Array.isArray(obj))
   if (!obj || typeof obj !== "object" || Array.isArray(obj))
@@ -466,6 +507,7 @@ var init_utils = __esm({
     init_is_deep_equal();
     init_is_deep_equal();
     init_get_ctor_name();
     init_get_ctor_name();
     init_is_pure_object();
     init_is_pure_object();
+    init_like_to_regexp();
     init_string_to_regexp();
     init_string_to_regexp();
     init_get_value_by_path();
     init_get_value_by_path();
     init_transform_promise();
     init_transform_promise();
@@ -645,6 +687,7 @@ var init_operator_clause_tool = __esm({
     "use strict";
     "use strict";
     import_js_service3 = require("@e22m4u/js-service");
     import_js_service3 = require("@e22m4u/js-service");
     init_utils();
     init_utils();
+    init_utils();
     init_errors();
     init_errors();
     init_errors();
     init_errors();
     _OperatorClauseTool = class _OperatorClauseTool extends import_js_service3.Service {
     _OperatorClauseTool = class _OperatorClauseTool extends import_js_service3.Service {
@@ -939,15 +982,15 @@ var init_operator_clause_tool = __esm({
        * @returns {boolean|undefined}
        * @returns {boolean|undefined}
        */
        */
       testLike(clause, value) {
       testLike(clause, value) {
-        if (!clause || typeof clause !== "object")
+        if (!clause || typeof clause !== "object" || Array.isArray(clause))
           throw new InvalidArgumentError(
           throw new InvalidArgumentError(
             "The first argument of OperatorUtils.testLike should be an Object, but %v was given.",
             "The first argument of OperatorUtils.testLike should be an Object, but %v was given.",
             clause
             clause
           );
           );
         if ("like" in clause && clause.like !== void 0) {
         if ("like" in clause && clause.like !== void 0) {
-          if (typeof clause.like !== "string" && !(clause.like instanceof RegExp))
+          if (typeof clause.like !== "string")
             throw new InvalidOperatorValueError("like", "a String", clause.like);
             throw new InvalidOperatorValueError("like", "a String", clause.like);
-          return stringToRegexp(clause.like).test(value);
+          return likeToRegexp(clause.like).test(value);
         }
         }
       }
       }
       /**
       /**
@@ -965,16 +1008,16 @@ var init_operator_clause_tool = __esm({
        * @returns {boolean|undefined}
        * @returns {boolean|undefined}
        */
        */
       testNlike(clause, value) {
       testNlike(clause, value) {
-        if (!clause || typeof clause !== "object")
+        if (!clause || typeof clause !== "object" || Array.isArray(clause))
           throw new InvalidArgumentError(
           throw new InvalidArgumentError(
             "The first argument of OperatorUtils.testNlike should be an Object, but %v was given.",
             "The first argument of OperatorUtils.testNlike should be an Object, but %v was given.",
             clause
             clause
           );
           );
         if ("nlike" in clause && clause.nlike !== void 0) {
         if ("nlike" in clause && clause.nlike !== void 0) {
-          if (typeof clause.nlike !== "string" && !(clause.nlike instanceof RegExp)) {
+          if (typeof clause.nlike !== "string") {
             throw new InvalidOperatorValueError("nlike", "a String", clause.nlike);
             throw new InvalidOperatorValueError("nlike", "a String", clause.nlike);
           }
           }
-          return !stringToRegexp(clause.nlike).test(value);
+          return !likeToRegexp(clause.nlike).test(value);
         }
         }
       }
       }
       /**
       /**
@@ -992,16 +1035,16 @@ var init_operator_clause_tool = __esm({
        * @returns {boolean|undefined}
        * @returns {boolean|undefined}
        */
        */
       testIlike(clause, value) {
       testIlike(clause, value) {
-        if (!clause || typeof clause !== "object")
+        if (!clause || typeof clause !== "object" || Array.isArray(clause))
           throw new InvalidArgumentError(
           throw new InvalidArgumentError(
             "The first argument of OperatorUtils.testIlike should be an Object, but %v was given.",
             "The first argument of OperatorUtils.testIlike should be an Object, but %v was given.",
             clause
             clause
           );
           );
         if ("ilike" in clause && clause.ilike !== void 0) {
         if ("ilike" in clause && clause.ilike !== void 0) {
-          if (typeof clause.ilike !== "string" && !(clause.ilike instanceof RegExp)) {
+          if (typeof clause.ilike !== "string") {
             throw new InvalidOperatorValueError("ilike", "a String", clause.ilike);
             throw new InvalidOperatorValueError("ilike", "a String", clause.ilike);
           }
           }
-          return stringToRegexp(clause.ilike, "i").test(value);
+          return likeToRegexp(clause.ilike, true).test(value);
         }
         }
       }
       }
       /**
       /**
@@ -1019,20 +1062,20 @@ var init_operator_clause_tool = __esm({
        * @returns {boolean|undefined}
        * @returns {boolean|undefined}
        */
        */
       testNilike(clause, value) {
       testNilike(clause, value) {
-        if (!clause || typeof clause !== "object")
+        if (!clause || typeof clause !== "object" || Array.isArray(clause))
           throw new InvalidArgumentError(
           throw new InvalidArgumentError(
             "The first argument of OperatorUtils.testNilike should be an Object, but %v was given.",
             "The first argument of OperatorUtils.testNilike should be an Object, but %v was given.",
             clause
             clause
           );
           );
         if ("nilike" in clause && clause.nilike !== void 0) {
         if ("nilike" in clause && clause.nilike !== void 0) {
-          if (typeof clause.nilike !== "string" && !(clause.nilike instanceof RegExp)) {
+          if (typeof clause.nilike !== "string") {
             throw new InvalidOperatorValueError(
             throw new InvalidOperatorValueError(
               "nilike",
               "nilike",
               "a String",
               "a String",
               clause.nilike
               clause.nilike
             );
             );
           }
           }
-          return !stringToRegexp(clause.nilike, "i").test(value);
+          return !likeToRegexp(clause.nilike, true).test(value);
         }
         }
       }
       }
       /**
       /**
@@ -1204,7 +1247,7 @@ var init_where_clause_tool = __esm({
           if (typeof value === "string") return !!value.match(example);
           if (typeof value === "string") return !!value.match(example);
           return false;
           return false;
         }
         }
-        if (typeof example === "object") {
+        if (typeof example === "object" && !Array.isArray(example)) {
           const operatorsTest = this.getService(OperatorClauseTool).testAll(
           const operatorsTest = this.getService(OperatorClauseTool).testAll(
             example,
             example,
             value
             value
@@ -6438,6 +6481,7 @@ __export(index_exports, {
   isDeepEqual: () => isDeepEqual,
   isDeepEqual: () => isDeepEqual,
   isPromise: () => isPromise,
   isPromise: () => isPromise,
   isPureObject: () => isPureObject,
   isPureObject: () => isPureObject,
+  likeToRegexp: () => likeToRegexp,
   modelNameToModelKey: () => modelNameToModelKey,
   modelNameToModelKey: () => modelNameToModelKey,
   selectObjectKeys: () => selectObjectKeys,
   selectObjectKeys: () => selectObjectKeys,
   singularize: () => singularize,
   singularize: () => singularize,
@@ -6543,6 +6587,7 @@ init_repository2();
   isDeepEqual,
   isDeepEqual,
   isPromise,
   isPromise,
   isPureObject,
   isPureObject,
+  likeToRegexp,
   modelNameToModelKey,
   modelNameToModelKey,
   selectObjectKeys,
   selectObjectKeys,
   singularize,
   singularize,

+ 2 - 2
src/adapter/builtin/memory-adapter.spec.js

@@ -3409,7 +3409,7 @@ describe('MemoryAdapter', function () {
       await adapter.create('model', input2);
       await adapter.create('model', input2);
       await adapter.create('model', input3);
       await adapter.create('model', input3);
       const filter1 = {where: {foo: 10}};
       const filter1 = {where: {foo: 10}};
-      const filter2 = {where: {foo: {gte: 15}, baz: {like: 'bc'}}};
+      const filter2 = {where: {foo: {gte: 15}, baz: {like: '%bc'}}};
       const filter3 = {where: {bar: true}};
       const filter3 = {where: {bar: true}};
       const result1 = await adapter.find('model', filter1);
       const result1 = await adapter.find('model', filter1);
       const result2 = await adapter.find('model', filter2);
       const result2 = await adapter.find('model', filter2);
@@ -3466,7 +3466,7 @@ describe('MemoryAdapter', function () {
         baz: tableInput2.bazCol,
         baz: tableInput2.bazCol,
       };
       };
       const filter1 = {where: {foo: 10}};
       const filter1 = {where: {foo: 10}};
-      const filter2 = {where: {foo: {gte: 15}, baz: {like: 'bc'}}};
+      const filter2 = {where: {foo: {gte: 15}, baz: {like: '%bc'}}};
       const filter3 = {where: {bar: true}};
       const filter3 = {where: {bar: true}};
       const result1 = await adapter.find('model', filter1);
       const result1 = await adapter.find('model', filter1);
       const result2 = await adapter.find('model', filter2);
       const result2 = await adapter.find('model', filter2);

+ 4 - 4
src/filter/filter-clause.d.ts

@@ -113,10 +113,10 @@ export declare type OperatorClause = {
   nin?: PrimitiveValue[];
   nin?: PrimitiveValue[];
   between?: readonly [string | number, string | number];
   between?: readonly [string | number, string | number];
   exists?: boolean;
   exists?: boolean;
-  like?: string | RegExp;
-  nlike?: string | RegExp;
-  ilike?: string | RegExp;
-  nilike?: string | RegExp;
+  like?: string;
+  nlike?: string;
+  ilike?: string;
+  nilike?: string;
   regexp?: string | RegExp;
   regexp?: string | RegExp;
   flags?: string;
   flags?: string;
 };
 };

+ 13 - 21
src/filter/operator-clause-tool.js

@@ -1,4 +1,5 @@
 import {Service} from '@e22m4u/js-service';
 import {Service} from '@e22m4u/js-service';
+import {likeToRegexp} from '../utils/index.js';
 import {stringToRegexp} from '../utils/index.js';
 import {stringToRegexp} from '../utils/index.js';
 import {InvalidArgumentError} from '../errors/index.js';
 import {InvalidArgumentError} from '../errors/index.js';
 import {InvalidOperatorValueError} from '../errors/index.js';
 import {InvalidOperatorValueError} from '../errors/index.js';
@@ -348,16 +349,16 @@ export class OperatorClauseTool extends Service {
    * @returns {boolean|undefined}
    * @returns {boolean|undefined}
    */
    */
   testLike(clause, value) {
   testLike(clause, value) {
-    if (!clause || typeof clause !== 'object')
+    if (!clause || typeof clause !== 'object' || Array.isArray(clause))
       throw new InvalidArgumentError(
       throw new InvalidArgumentError(
         'The first argument of OperatorUtils.testLike ' +
         'The first argument of OperatorUtils.testLike ' +
           'should be an Object, but %v was given.',
           'should be an Object, but %v was given.',
         clause,
         clause,
       );
       );
     if ('like' in clause && clause.like !== undefined) {
     if ('like' in clause && clause.like !== undefined) {
-      if (typeof clause.like !== 'string' && !(clause.like instanceof RegExp))
+      if (typeof clause.like !== 'string')
         throw new InvalidOperatorValueError('like', 'a String', clause.like);
         throw new InvalidOperatorValueError('like', 'a String', clause.like);
-      return stringToRegexp(clause.like).test(value);
+      return likeToRegexp(clause.like).test(value);
     }
     }
   }
   }
 
 
@@ -376,20 +377,17 @@ export class OperatorClauseTool extends Service {
    * @returns {boolean|undefined}
    * @returns {boolean|undefined}
    */
    */
   testNlike(clause, value) {
   testNlike(clause, value) {
-    if (!clause || typeof clause !== 'object')
+    if (!clause || typeof clause !== 'object' || Array.isArray(clause))
       throw new InvalidArgumentError(
       throw new InvalidArgumentError(
         'The first argument of OperatorUtils.testNlike ' +
         'The first argument of OperatorUtils.testNlike ' +
           'should be an Object, but %v was given.',
           'should be an Object, but %v was given.',
         clause,
         clause,
       );
       );
     if ('nlike' in clause && clause.nlike !== undefined) {
     if ('nlike' in clause && clause.nlike !== undefined) {
-      if (
-        typeof clause.nlike !== 'string' &&
-        !(clause.nlike instanceof RegExp)
-      ) {
+      if (typeof clause.nlike !== 'string') {
         throw new InvalidOperatorValueError('nlike', 'a String', clause.nlike);
         throw new InvalidOperatorValueError('nlike', 'a String', clause.nlike);
       }
       }
-      return !stringToRegexp(clause.nlike).test(value);
+      return !likeToRegexp(clause.nlike).test(value);
     }
     }
   }
   }
 
 
@@ -408,20 +406,17 @@ export class OperatorClauseTool extends Service {
    * @returns {boolean|undefined}
    * @returns {boolean|undefined}
    */
    */
   testIlike(clause, value) {
   testIlike(clause, value) {
-    if (!clause || typeof clause !== 'object')
+    if (!clause || typeof clause !== 'object' || Array.isArray(clause))
       throw new InvalidArgumentError(
       throw new InvalidArgumentError(
         'The first argument of OperatorUtils.testIlike ' +
         'The first argument of OperatorUtils.testIlike ' +
           'should be an Object, but %v was given.',
           'should be an Object, but %v was given.',
         clause,
         clause,
       );
       );
     if ('ilike' in clause && clause.ilike !== undefined) {
     if ('ilike' in clause && clause.ilike !== undefined) {
-      if (
-        typeof clause.ilike !== 'string' &&
-        !(clause.ilike instanceof RegExp)
-      ) {
+      if (typeof clause.ilike !== 'string') {
         throw new InvalidOperatorValueError('ilike', 'a String', clause.ilike);
         throw new InvalidOperatorValueError('ilike', 'a String', clause.ilike);
       }
       }
-      return stringToRegexp(clause.ilike, 'i').test(value);
+      return likeToRegexp(clause.ilike, true).test(value);
     }
     }
   }
   }
 
 
@@ -440,24 +435,21 @@ export class OperatorClauseTool extends Service {
    * @returns {boolean|undefined}
    * @returns {boolean|undefined}
    */
    */
   testNilike(clause, value) {
   testNilike(clause, value) {
-    if (!clause || typeof clause !== 'object')
+    if (!clause || typeof clause !== 'object' || Array.isArray(clause))
       throw new InvalidArgumentError(
       throw new InvalidArgumentError(
         'The first argument of OperatorUtils.testNilike ' +
         'The first argument of OperatorUtils.testNilike ' +
           'should be an Object, but %v was given.',
           'should be an Object, but %v was given.',
         clause,
         clause,
       );
       );
     if ('nilike' in clause && clause.nilike !== undefined) {
     if ('nilike' in clause && clause.nilike !== undefined) {
-      if (
-        typeof clause.nilike !== 'string' &&
-        !(clause.nilike instanceof RegExp)
-      ) {
+      if (typeof clause.nilike !== 'string') {
         throw new InvalidOperatorValueError(
         throw new InvalidOperatorValueError(
           'nilike',
           'nilike',
           'a String',
           'a String',
           clause.nilike,
           clause.nilike,
         );
         );
       }
       }
-      return !stringToRegexp(clause.nilike, 'i').test(value);
+      return !likeToRegexp(clause.nilike, true).test(value);
     }
     }
   }
   }
 
 

+ 328 - 269
src/filter/operator-clause-tool.spec.js

@@ -65,49 +65,49 @@ describe('OperatorClauseTool', function () {
       expect(S.testAll({lte: 5}, 6)).to.be.false;
       expect(S.testAll({lte: 5}, 6)).to.be.false;
     });
     });
 
 
-    it('tests a "inq" operator', function () {
+    it('tests the "inq" operator', function () {
       expect(S.testAll({inq: [1, 2, 3]}, 2)).to.be.true;
       expect(S.testAll({inq: [1, 2, 3]}, 2)).to.be.true;
       expect(S.testAll({inq: [1, 2, 3]}, 'a')).to.be.false;
       expect(S.testAll({inq: [1, 2, 3]}, 'a')).to.be.false;
     });
     });
 
 
-    it('tests a "nin" operator', function () {
+    it('tests the "nin" operator', function () {
       expect(S.testAll({nin: [1, 2, 3]}, 'a')).to.be.true;
       expect(S.testAll({nin: [1, 2, 3]}, 'a')).to.be.true;
       expect(S.testAll({nin: [1, 2, 3]}, 2)).to.be.false;
       expect(S.testAll({nin: [1, 2, 3]}, 2)).to.be.false;
     });
     });
 
 
-    it('tests a "between" operator', function () {
+    it('tests the "between" operator', function () {
       expect(S.testAll({between: [-2, 2]}, 0)).to.be.true;
       expect(S.testAll({between: [-2, 2]}, 0)).to.be.true;
       expect(S.testAll({between: [-2, 2]}, 10)).to.be.false;
       expect(S.testAll({between: [-2, 2]}, 10)).to.be.false;
     });
     });
 
 
-    it('tests an "exists" operator', function () {
+    it('tests the "exists" operator', function () {
       expect(S.testAll({exists: true}, 10)).to.be.true;
       expect(S.testAll({exists: true}, 10)).to.be.true;
       expect(S.testAll({exists: false}, undefined)).to.be.true;
       expect(S.testAll({exists: false}, undefined)).to.be.true;
       expect(S.testAll({exists: true}, undefined)).to.be.false;
       expect(S.testAll({exists: true}, undefined)).to.be.false;
       expect(S.testAll({exists: false}, 10)).to.be.false;
       expect(S.testAll({exists: false}, 10)).to.be.false;
     });
     });
 
 
-    it('tests a "like" operator', function () {
-      expect(S.testAll({like: 'World'}, 'Hello World!')).to.be.true;
-      expect(S.testAll({like: 'world'}, 'Hello World!')).to.be.false;
+    it('tests the "like" operator', function () {
+      expect(S.testAll({like: '%World%'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({like: '%world%'}, 'Hello World!')).to.be.false;
     });
     });
 
 
-    it('tests a "nlike" operator', function () {
-      expect(S.testAll({nlike: 'John'}, 'Hello World!')).to.be.true;
-      expect(S.testAll({nlike: 'World'}, 'Hello World!')).to.be.false;
+    it('tests the "nlike" operator', function () {
+      expect(S.testAll({nlike: '%John%'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({nlike: '%World%'}, 'Hello World!')).to.be.false;
     });
     });
 
 
-    it('tests a "ilike" operator', function () {
-      expect(S.testAll({ilike: 'WORLD'}, 'Hello World!')).to.be.true;
-      expect(S.testAll({ilike: 'John'}, 'Hello World!')).to.be.false;
+    it('tests the "ilike" operator', function () {
+      expect(S.testAll({ilike: '%WORLD%'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({ilike: '%John%'}, 'Hello World!')).to.be.false;
     });
     });
 
 
-    it('tests a "nilike" operator', function () {
-      expect(S.testAll({nilike: 'John'}, 'Hello World!')).to.be.true;
-      expect(S.testAll({nilike: 'world'}, 'Hello World!')).to.be.false;
+    it('tests the "nilike" operator', function () {
+      expect(S.testAll({nilike: '%John%'}, 'Hello World!')).to.be.true;
+      expect(S.testAll({nilike: '%world%'}, 'Hello World!')).to.be.false;
     });
     });
 
 
-    it('tests a "regexp" operator', function () {
+    it('tests the "regexp" operator', function () {
       expect(S.testAll({regexp: 'Wo.+'}, 'Hello World!')).to.be.true;
       expect(S.testAll({regexp: 'Wo.+'}, 'Hello World!')).to.be.true;
       expect(S.testAll({regexp: 'Fo.+'}, 'Hello World!')).to.be.false;
       expect(S.testAll({regexp: 'Fo.+'}, 'Hello World!')).to.be.false;
     });
     });
@@ -640,318 +640,377 @@ describe('OperatorClauseTool', function () {
   });
   });
 
 
   describe('testLike', function () {
   describe('testLike', function () {
-    it('returns undefined if no operator given', function () {
+    it('should return undefined if no operator given', function () {
       const result = S.testLike({}, 'value');
       const result = S.testLike({}, 'value');
       expect(result).to.be.undefined;
       expect(result).to.be.undefined;
     });
     });
 
 
-    it('returns true if a given value matches a substring', function () {
-      expect(S.testLike({like: 'val'}, 'value')).to.be.true;
-      expect(S.testLike({like: 'lue'}, 'value')).to.be.true;
-      expect(S.testLike({like: 'value'}, 'value')).to.be.true;
+    it('should return false when matching by substring', function () {
+      expect(S.testLike({like: 'val'}, 'value')).to.be.false;
+      expect(S.testLike({like: 'lue'}, 'value')).to.be.false;
+      expect(S.testLike({like: 'alu'}, 'value')).to.be.false;
     });
     });
 
 
-    it('returns false if a given value not matches a substring', function () {
+    it('should return false when the value is a substring', function () {
       expect(S.testLike({like: 'value'}, 'val')).to.be.false;
       expect(S.testLike({like: 'value'}, 'val')).to.be.false;
       expect(S.testLike({like: 'value'}, 'lue')).to.be.false;
       expect(S.testLike({like: 'value'}, 'lue')).to.be.false;
-      expect(S.testLike({like: 'value'}, 'foo')).to.be.false;
-    });
-
-    it('uses case-sensitive matching for a substring', function () {
-      expect(S.testLike({like: 'Val'}, 'value')).to.be.false;
-      expect(S.testLike({like: 'Val'}, 'Value')).to.be.true;
-      expect(S.testLike({like: 'val'}, 'Value')).to.be.false;
-    });
-
-    it('returns true if a given value matches a string expression', function () {
-      expect(S.testLike({like: 'val.+'}, 'value')).to.be.true;
-    });
-
-    it('returns false if a given value not matches a string expression', function () {
-      expect(S.testLike({like: 'foo.+'}, 'value')).to.be.false;
-    });
-
-    it('uses case-sensitive matching for a string expression', function () {
-      expect(S.testLike({like: 'Val.+'}, 'value')).to.be.false;
-      expect(S.testLike({like: 'Val.+'}, 'Value')).to.be.true;
-      expect(S.testLike({like: 'val.+'}, 'Value')).to.be.false;
-    });
-
-    it('returns true if a given value matches a RegExp', function () {
-      expect(S.testLike({like: new RegExp(/val.+/)}, 'value')).to.be.true;
-    });
-
-    it('returns false if a given value matches a RegExp', function () {
-      expect(S.testLike({like: new RegExp(/foo.+/)}, 'value')).to.be.false;
+      expect(S.testLike({like: 'value'}, 'alu')).to.be.false;
     });
     });
 
 
-    it('uses case-sensitive matching for a RegExp', function () {
-      expect(S.testLike({like: new RegExp(/Val.+/)}, 'value')).to.be.false;
-      expect(S.testLike({like: new RegExp(/Val.+/)}, 'Value')).to.be.true;
-      expect(S.testLike({like: new RegExp(/val.+/)}, 'Value')).to.be.false;
-    });
-
-    it('throws an error if a first argument is not an object', function () {
-      const throwable = () => S.testLike(10);
-      expect(throwable).to.throw(
-        'The first argument of OperatorUtils.testLike ' +
-          'should be an Object, but 10 was given.',
-      );
-    });
-
-    it('throws an error if an operator value is a number', function () {
-      const like = 10;
-      const throwable = () => S.testLike({like}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
-    });
-
-    it('throws an error if an operator value is an object', function () {
-      const like = {};
-      const throwable = () => S.testLike({like}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+    it('should return true for exact match', function () {
+      expect(S.testLike({like: 'value'}, 'value')).to.be.true;
     });
     });
 
 
-    it('throws an error if an operator value is a null', function () {
-      const like = null;
-      const throwable = () => S.testLike({like}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+    it('should use case-sensitive matching', function () {
+      expect(S.testLike({like: 'VALUE'}, 'VALUE')).to.be.true;
+      expect(S.testLike({like: 'VALUE'}, 'value')).to.be.false;
+      expect(S.testLike({like: 'value'}, 'VALUE')).to.be.false;
+    });
+
+    it('should handle "%" wildcard as zero or more characters', function () {
+      expect(S.testLike({like: 'hello wo%'}, 'hello world today')).to.be.true;
+      expect(S.testLike({like: '%ld today'}, 'hello world today')).to.be.true;
+      expect(S.testLike({like: '%world%'}, 'hello world today')).to.be.true;
+      expect(S.testLike({like: '%hello wo%'}, 'hello world today')).to.be.true;
+      expect(S.testLike({like: '%ld today%'}, 'hello world today')).to.be.true;
+      expect(S.testLike({like: '%wurld%'}, 'hello world today')).to.be.false;
+    });
+
+    it('should handle "_" wildcard as any characters', function () {
+      expect(S.testLike({like: 'h_ll_'}, 'hello')).to.be.true;
+      expect(S.testLike({like: 'hello_world'}, 'hello world')).to.be.true;
+      expect(S.testLike({like: 'hello_'}, 'hello')).to.be.false;
+      expect(S.testLike({like: '_hello'}, 'hello')).to.be.false;
+    });
+
+    it('should throw an error for non-object clause', function () {
+      const throwable = v => () => {
+        S.testLike(v);
+      };
+      const error = s =>
+        format(
+          'The first argument of OperatorUtils.testLike ' +
+            'should be an Object, but %s was given.',
+          s,
+        );
+      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([1, 2, 3])).to.throw(error('Array'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable({})();
+    });
+
+    it('should throw an error for non-string operator value', function () {
+      const throwable = v => () => {
+        S.testLike({like: v});
+      };
+      const error = s =>
+        format(
+          'Condition of {like: ...} should have a String, but %s was given.',
+          s,
+        );
+      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({foo: 'bar'})).to.throw(error('Object'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable([1, 2, 3])).to.throw(error('Array'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      expect(throwable(new RegExp())).to.throw(error('RegExp (instance)'));
+      throwable('str')();
+      throwable(null);
+      throwable(undefined);
     });
     });
   });
   });
 
 
   describe('testNlike', function () {
   describe('testNlike', function () {
-    it('returns undefined if no operator given', function () {
+    it('should return undefined if no operator given', function () {
       const result = S.testNlike({}, 'value');
       const result = S.testNlike({}, 'value');
       expect(result).to.be.undefined;
       expect(result).to.be.undefined;
     });
     });
 
 
-    it('returns false if a given value matches a substring', function () {
-      expect(S.testNlike({nlike: 'val'}, 'value')).to.be.false;
-      expect(S.testNlike({nlike: 'lue'}, 'value')).to.be.false;
-      expect(S.testNlike({nlike: 'value'}, 'value')).to.be.false;
+    it('should return true when matching by substring', function () {
+      expect(S.testNlike({nlike: 'val'}, 'value')).to.be.true;
+      expect(S.testNlike({nlike: 'lue'}, 'value')).to.be.true;
+      expect(S.testNlike({nlike: 'alu'}, 'value')).to.be.true;
     });
     });
 
 
-    it('returns true if a given value not matches a substring', function () {
+    it('should return true when the value is a substring', function () {
       expect(S.testNlike({nlike: 'value'}, 'val')).to.be.true;
       expect(S.testNlike({nlike: 'value'}, 'val')).to.be.true;
       expect(S.testNlike({nlike: 'value'}, 'lue')).to.be.true;
       expect(S.testNlike({nlike: 'value'}, 'lue')).to.be.true;
-      expect(S.testNlike({nlike: 'value'}, 'foo')).to.be.true;
-    });
-
-    it('uses case-sensitive matching for a substring', function () {
-      expect(S.testNlike({nlike: 'Val'}, 'value')).to.be.true;
-      expect(S.testNlike({nlike: 'Val'}, 'Value')).to.be.false;
-      expect(S.testNlike({nlike: 'val'}, 'Value')).to.be.true;
-    });
-
-    it('returns false if a given value matches a string expression', function () {
-      expect(S.testNlike({nlike: 'val.+'}, 'value')).to.be.false;
-    });
-
-    it('returns true if a given value not matches a string expression', function () {
-      expect(S.testNlike({nlike: 'foo.+'}, 'value')).to.be.true;
-    });
-
-    it('uses case-sensitive matching for a string expression', function () {
-      expect(S.testNlike({nlike: 'Val.+'}, 'value')).to.be.true;
-      expect(S.testNlike({nlike: 'Val.+'}, 'Value')).to.be.false;
-      expect(S.testNlike({nlike: 'val.+'}, 'Value')).to.be.true;
+      expect(S.testNlike({nlike: 'value'}, 'alu')).to.be.true;
     });
     });
 
 
-    it('returns false if a given value matches a RegExp', function () {
-      expect(S.testNlike({nlike: new RegExp(/val.+/)}, 'value')).to.be.false;
-    });
-
-    it('returns true if a given value matches a RegExp', function () {
-      expect(S.testNlike({nlike: new RegExp(/foo.+/)}, 'value')).to.be.true;
-    });
-
-    it('uses case-sensitive matching for a RegExp', function () {
-      expect(S.testNlike({nlike: new RegExp(/Val.+/)}, 'value')).to.be.true;
-      expect(S.testNlike({nlike: new RegExp(/Val.+/)}, 'Value')).to.be.false;
-      expect(S.testNlike({nlike: new RegExp(/val.+/)}, 'Value')).to.be.true;
-    });
-
-    it('throws an error if a first argument is not an object', function () {
-      const throwable = () => S.testNlike(10);
-      expect(throwable).to.throw(
-        'The first argument of OperatorUtils.testNlike ' +
-          'should be an Object, but 10 was given.',
-      );
-    });
-
-    it('throws an error if an operator value is a number', function () {
-      const nlike = 10;
-      const throwable = () => S.testNlike({nlike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
-    });
-
-    it('throws an error if an operator value is an object', function () {
-      const nlike = {};
-      const throwable = () => S.testNlike({nlike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+    it('should return true for exact match', function () {
+      expect(S.testNlike({nlike: 'value'}, 'value')).to.be.false;
     });
     });
 
 
-    it('throws an error if an operator value is a null', function () {
-      const nlike = null;
-      const throwable = () => S.testNlike({nlike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+    it('should use case-sensitive matching', function () {
+      expect(S.testNlike({nlike: 'VALUE'}, 'VALUE')).to.be.false;
+      expect(S.testNlike({nlike: 'VALUE'}, 'value')).to.be.true;
+      expect(S.testNlike({nlike: 'value'}, 'VALUE')).to.be.true;
+    });
+
+    it('should handle "%" wildcard as zero or more characters', function () {
+      expect(S.testNlike({nlike: 'hello wo%'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNlike({nlike: '%ld today'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNlike({nlike: '%world%'}, 'hello world today')).to.be.false;
+      expect(S.testNlike({nlike: '%hello wo%'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNlike({nlike: '%ld today%'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNlike({nlike: '%wurld%'}, 'hello world today')).to.be.true;
+    });
+
+    it('should handle "_" wildcard as any characters', function () {
+      expect(S.testNlike({nlike: 'h_ll_'}, 'hello')).to.be.false;
+      expect(S.testNlike({nlike: 'hello_world'}, 'hello world')).to.be.false;
+      expect(S.testNlike({nlike: 'hello_'}, 'hello')).to.be.true;
+      expect(S.testNlike({nlike: '_hello'}, 'hello')).to.be.true;
+    });
+
+    it('should throw an error for non-object clause', function () {
+      const throwable = v => () => {
+        S.testNlike(v);
+      };
+      const error = s =>
+        format(
+          'The first argument of OperatorUtils.testNlike ' +
+            'should be an Object, but %s was given.',
+          s,
+        );
+      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([1, 2, 3])).to.throw(error('Array'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable({})();
+    });
+
+    it('should throw an error for non-string operator value', function () {
+      const throwable = v => () => {
+        S.testNlike({nlike: v});
+      };
+      const error = s =>
+        format(
+          'Condition of {nlike: ...} should have a String, but %s was given.',
+          s,
+        );
+      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({foo: 'bar'})).to.throw(error('Object'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable([1, 2, 3])).to.throw(error('Array'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      expect(throwable(new RegExp())).to.throw(error('RegExp (instance)'));
+      throwable('str')();
+      throwable(null);
+      throwable(undefined);
     });
     });
   });
   });
 
 
   describe('testIlike', function () {
   describe('testIlike', function () {
-    it('returns undefined if no operator given', function () {
+    it('should return undefined if no operator given', function () {
       const result = S.testIlike({}, 'value');
       const result = S.testIlike({}, 'value');
       expect(result).to.be.undefined;
       expect(result).to.be.undefined;
     });
     });
 
 
-    it('returns true if a given value matches a substring', function () {
-      expect(S.testIlike({ilike: 'val'}, 'value')).to.be.true;
-      expect(S.testIlike({ilike: 'lue'}, 'value')).to.be.true;
-      expect(S.testIlike({ilike: 'value'}, 'value')).to.be.true;
+    it('should return false when matching by substring', function () {
+      expect(S.testIlike({ilike: 'val'}, 'value')).to.be.false;
+      expect(S.testIlike({ilike: 'lue'}, 'value')).to.be.false;
+      expect(S.testIlike({ilike: 'alu'}, 'value')).to.be.false;
     });
     });
 
 
-    it('returns false if a given value not matches a substring', function () {
+    it('should return false when the value is a substring', function () {
       expect(S.testIlike({ilike: 'value'}, 'val')).to.be.false;
       expect(S.testIlike({ilike: 'value'}, 'val')).to.be.false;
       expect(S.testIlike({ilike: 'value'}, 'lue')).to.be.false;
       expect(S.testIlike({ilike: 'value'}, 'lue')).to.be.false;
-      expect(S.testIlike({ilike: 'value'}, 'foo')).to.be.false;
-    });
-
-    it('uses case-insensitive matching for a substring', function () {
-      expect(S.testIlike({ilike: 'Val'}, 'value')).to.be.true;
-      expect(S.testIlike({ilike: 'Val'}, 'Value')).to.be.true;
-      expect(S.testIlike({ilike: 'val'}, 'Value')).to.be.true;
-    });
-
-    it('returns true if a given value matches a string expression', function () {
-      expect(S.testIlike({ilike: 'val.+'}, 'value')).to.be.true;
-    });
-
-    it('returns false if a given value not matches a string expression', function () {
-      expect(S.testIlike({ilike: 'foo.+'}, 'value')).to.be.false;
-    });
-
-    it('uses case-insensitive matching for a string expression', function () {
-      expect(S.testIlike({ilike: 'Val.+'}, 'value')).to.be.true;
-      expect(S.testIlike({ilike: 'Val.+'}, 'Value')).to.be.true;
-      expect(S.testIlike({ilike: 'val.+'}, 'Value')).to.be.true;
-    });
-
-    it('returns true if a given value matches a RegExp', function () {
-      expect(S.testIlike({ilike: new RegExp(/val.+/)}, 'value')).to.be.true;
-    });
-
-    it('returns false if a given value matches a RegExp', function () {
-      expect(S.testIlike({ilike: new RegExp(/foo.+/)}, 'value')).to.be.false;
-    });
-
-    it('uses case-insensitive matching for a RegExp', function () {
-      expect(S.testIlike({ilike: new RegExp(/Val.+/)}, 'value')).to.be.true;
-      expect(S.testIlike({ilike: new RegExp(/Val.+/)}, 'Value')).to.be.true;
-      expect(S.testIlike({ilike: new RegExp(/val.+/)}, 'Value')).to.be.true;
-    });
-
-    it('throws an error if a first argument is not an object', function () {
-      const throwable = () => S.testIlike(10);
-      expect(throwable).to.throw(
-        'The first argument of OperatorUtils.testIlike ' +
-          'should be an Object, but 10 was given.',
-      );
-    });
-
-    it('throws an error if an operator value is a number', function () {
-      const ilike = 10;
-      const throwable = () => S.testIlike({ilike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+      expect(S.testIlike({ilike: 'value'}, 'alu')).to.be.false;
     });
     });
 
 
-    it('throws an error if an operator value is an object', function () {
-      const ilike = {};
-      const throwable = () => S.testIlike({ilike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+    it('should return true for exact match', function () {
+      expect(S.testIlike({ilike: 'value'}, 'value')).to.be.true;
     });
     });
 
 
-    it('throws an error if an operator value is a null', function () {
-      const ilike = null;
-      const throwable = () => S.testIlike({ilike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+    it('should use case-insensitive matching', function () {
+      expect(S.testIlike({ilike: 'VALUE'}, 'VALUE')).to.be.true;
+      expect(S.testIlike({ilike: 'VALUE'}, 'value')).to.be.true;
+      expect(S.testIlike({ilike: 'value'}, 'VALUE')).to.be.true;
+    });
+
+    it('should handle "%" wildcard as zero or more characters', function () {
+      expect(S.testIlike({ilike: 'hello wo%'}, 'hello world today')).to.be.true;
+      expect(S.testIlike({ilike: '%ld today'}, 'hello world today')).to.be.true;
+      expect(S.testIlike({ilike: '%world%'}, 'hello world today')).to.be.true;
+      expect(S.testIlike({ilike: '%hello wo%'}, 'hello world today')).to.be
+        .true;
+      expect(S.testIlike({ilike: '%ld today%'}, 'hello world today')).to.be
+        .true;
+      expect(S.testIlike({ilike: '%wurld%'}, 'hello world today')).to.be.false;
+    });
+
+    it('should handle "_" wildcard as any characters', function () {
+      expect(S.testIlike({ilike: 'h_ll_'}, 'hello')).to.be.true;
+      expect(S.testIlike({ilike: 'hello_world'}, 'hello world')).to.be.true;
+      expect(S.testIlike({ilike: 'hello_'}, 'hello')).to.be.false;
+      expect(S.testIlike({ilike: '_hello'}, 'hello')).to.be.false;
+    });
+
+    it('should throw an error for non-object clause', function () {
+      const throwable = v => () => {
+        S.testIlike(v);
+      };
+      const error = s =>
+        format(
+          'The first argument of OperatorUtils.testIlike ' +
+            'should be an Object, but %s was given.',
+          s,
+        );
+      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([1, 2, 3])).to.throw(error('Array'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable({})();
+    });
+
+    it('should throw an error for non-string operator value', function () {
+      const throwable = v => () => {
+        S.testIlike({ilike: v});
+      };
+      const error = s =>
+        format(
+          'Condition of {ilike: ...} should have a String, but %s was given.',
+          s,
+        );
+      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({foo: 'bar'})).to.throw(error('Object'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable([1, 2, 3])).to.throw(error('Array'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      expect(throwable(new RegExp())).to.throw(error('RegExp (instance)'));
+      throwable('str')();
+      throwable(null);
+      throwable(undefined);
     });
     });
   });
   });
 
 
   describe('testNilike', function () {
   describe('testNilike', function () {
-    it('returns undefined if no operator given', function () {
+    it('should return undefined if no operator given', function () {
       const result = S.testNilike({}, 'value');
       const result = S.testNilike({}, 'value');
       expect(result).to.be.undefined;
       expect(result).to.be.undefined;
     });
     });
 
 
-    it('returns false if a given value matches a substring', function () {
-      expect(S.testNilike({nilike: 'val'}, 'value')).to.be.false;
-      expect(S.testNilike({nilike: 'lue'}, 'value')).to.be.false;
-      expect(S.testNilike({nilike: 'value'}, 'value')).to.be.false;
+    it('should return true when matching by substring', function () {
+      expect(S.testNilike({nilike: 'val'}, 'value')).to.be.true;
+      expect(S.testNilike({nilike: 'lue'}, 'value')).to.be.true;
+      expect(S.testNilike({nilike: 'alu'}, 'value')).to.be.true;
     });
     });
 
 
-    it('returns true if a given value not matches a substring', function () {
+    it('should return true when the value is a substring', function () {
       expect(S.testNilike({nilike: 'value'}, 'val')).to.be.true;
       expect(S.testNilike({nilike: 'value'}, 'val')).to.be.true;
       expect(S.testNilike({nilike: 'value'}, 'lue')).to.be.true;
       expect(S.testNilike({nilike: 'value'}, 'lue')).to.be.true;
-      expect(S.testNilike({nilike: 'value'}, 'foo')).to.be.true;
-    });
-
-    it('uses case-insensitive matching for a substring', function () {
-      expect(S.testNilike({nilike: 'Val'}, 'value')).to.be.false;
-      expect(S.testNilike({nilike: 'Val'}, 'Value')).to.be.false;
-      expect(S.testNilike({nilike: 'val'}, 'Value')).to.be.false;
-    });
-
-    it('returns false if a given value matches a string expression', function () {
-      expect(S.testNilike({nilike: 'val.+'}, 'value')).to.be.false;
-    });
-
-    it('returns true if a given value not matches a string expression', function () {
-      expect(S.testNilike({nilike: 'foo.+'}, 'value')).to.be.true;
+      expect(S.testNilike({nilike: 'value'}, 'alu')).to.be.true;
     });
     });
 
 
-    it('uses case-insensitive matching for a string expression', function () {
-      expect(S.testNilike({nilike: 'Val.+'}, 'value')).to.be.false;
-      expect(S.testNilike({nilike: 'Val.+'}, 'Value')).to.be.false;
-      expect(S.testNilike({nilike: 'val.+'}, 'Value')).to.be.false;
-    });
-
-    it('returns false if a given value matches a RegExp', function () {
-      expect(S.testNilike({nilike: new RegExp(/val.+/)}, 'value')).to.be.false;
-    });
-
-    it('returns true if a given value matches a RegExp', function () {
-      expect(S.testNilike({nilike: new RegExp(/foo.+/)}, 'value')).to.be.true;
-    });
-
-    it('uses case-insensitive matching for a RegExp', function () {
-      expect(S.testNilike({nilike: new RegExp(/Val.+/)}, 'value')).to.be.false;
-      expect(S.testNilike({nilike: new RegExp(/Val.+/)}, 'Value')).to.be.false;
-      expect(S.testNilike({nilike: new RegExp(/val.+/)}, 'Value')).to.be.false;
-    });
-
-    it('throws an error if a first argument is not an object', function () {
-      const throwable = () => S.testNilike(10);
-      expect(throwable).to.throw(
-        'The first argument of OperatorUtils.testNilike ' +
-          'should be an Object, but 10 was given.',
-      );
-    });
-
-    it('throws an error if an operator value is a number', function () {
-      const nilike = 10;
-      const throwable = () => S.testNilike({nilike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
-    });
-
-    it('throws an error if an operator value is an object', function () {
-      const nilike = {};
-      const throwable = () => S.testNilike({nilike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+    it('should return false for exact match', function () {
+      expect(S.testNilike({nilike: 'value'}, 'value')).to.be.false;
     });
     });
 
 
-    it('throws an error if an operator value is a null', function () {
-      const nilike = null;
-      const throwable = () => S.testNilike({nilike}, 10);
-      expect(throwable).to.throw(InvalidOperatorValueError);
+    it('should use case-insensitive matching', function () {
+      expect(S.testNilike({nilike: 'VALUE'}, 'VALUE')).to.be.false;
+      expect(S.testNilike({nilike: 'VALUE'}, 'value')).to.be.false;
+      expect(S.testNilike({nilike: 'value'}, 'VALUE')).to.be.false;
+    });
+
+    it('should handle "%" wildcard as zero or more characters', function () {
+      expect(S.testNilike({nilike: 'hello wo%'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNilike({nilike: '%ld today'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNilike({nilike: '%world%'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNilike({nilike: '%hello wo%'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNilike({nilike: '%ld today%'}, 'hello world today')).to.be
+        .false;
+      expect(S.testNilike({nilike: '%wurld%'}, 'hello world today')).to.be.true;
+    });
+
+    it('should handle "_" wildcard as any characters', function () {
+      expect(S.testNilike({nilike: 'h_ll_'}, 'hello')).to.be.false;
+      expect(S.testNilike({nilike: 'hello_world'}, 'hello world')).to.be.false;
+      expect(S.testNilike({nilike: 'hello_'}, 'hello')).to.be.true;
+      expect(S.testNilike({nilike: '_hello'}, 'hello')).to.be.true;
+    });
+
+    it('should throw an error for non-object clause', function () {
+      const throwable = v => () => {
+        S.testNilike(v);
+      };
+      const error = s =>
+        format(
+          'The first argument of OperatorUtils.testNilike ' +
+            'should be an Object, but %s was given.',
+          s,
+        );
+      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([1, 2, 3])).to.throw(error('Array'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      throwable({})();
+    });
+
+    it('should throw an error for non-string operator value', function () {
+      const throwable = v => () => {
+        S.testNilike({nilike: v});
+      };
+      const error = s =>
+        format(
+          'Condition of {nilike: ...} should have a String, but %s was given.',
+          s,
+        );
+      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({foo: 'bar'})).to.throw(error('Object'));
+      expect(throwable({})).to.throw(error('Object'));
+      expect(throwable([1, 2, 3])).to.throw(error('Array'));
+      expect(throwable([])).to.throw(error('Array'));
+      expect(throwable(() => undefined)).to.throw(error('Function'));
+      expect(throwable(new RegExp())).to.throw(error('RegExp (instance)'));
+      throwable('str')();
+      throwable(null);
+      throwable(undefined);
     });
     });
   });
   });
 
 

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

@@ -141,7 +141,7 @@ export class WhereClauseTool extends Service {
       return false;
       return false;
     }
     }
     // Operator clause.
     // Operator clause.
-    if (typeof example === 'object') {
+    if (typeof example === 'object' && !Array.isArray(example)) {
       const operatorsTest = this.getService(OperatorClauseTool).testAll(
       const operatorsTest = this.getService(OperatorClauseTool).testAll(
         example,
         example,
         value,
         value,

+ 4 - 4
src/filter/where-clause-tool.spec.js

@@ -233,13 +233,13 @@ describe('WhereClauseTool', function () {
     });
     });
 
 
     it('uses the "like" operator to match by a substring', function () {
     it('uses the "like" operator to match by a substring', function () {
-      const result = S.filter(OBJECTS, {name: {like: 'liv'}});
+      const result = S.filter(OBJECTS, {name: {like: '%liv%'}});
       expect(result).to.have.length(1);
       expect(result).to.have.length(1);
       expect(result[0]).to.be.eql(OBJECTS[3]);
       expect(result[0]).to.be.eql(OBJECTS[3]);
     });
     });
 
 
     it('uses the "nlike" operator to exclude by a substring', function () {
     it('uses the "nlike" operator to exclude by a substring', function () {
-      const result = S.filter(OBJECTS, {name: {nlike: 'liv'}});
+      const result = S.filter(OBJECTS, {name: {nlike: '%liv%'}});
       expect(result).to.have.length(3);
       expect(result).to.have.length(3);
       expect(result[0]).to.be.eql(OBJECTS[0]);
       expect(result[0]).to.be.eql(OBJECTS[0]);
       expect(result[1]).to.be.eql(OBJECTS[1]);
       expect(result[1]).to.be.eql(OBJECTS[1]);
@@ -247,13 +247,13 @@ describe('WhereClauseTool', function () {
     });
     });
 
 
     it('uses the "ilike" operator to case-insensitively matching by a substring', function () {
     it('uses the "ilike" operator to case-insensitively matching by a substring', function () {
-      const result = S.filter(OBJECTS, {name: {ilike: 'LIV'}});
+      const result = S.filter(OBJECTS, {name: {ilike: '%LIV%'}});
       expect(result).to.have.length(1);
       expect(result).to.have.length(1);
       expect(result[0]).to.be.eql(OBJECTS[3]);
       expect(result[0]).to.be.eql(OBJECTS[3]);
     });
     });
 
 
     it('uses the "nilike" operator to exclude case-insensitively by a substring', function () {
     it('uses the "nilike" operator to exclude case-insensitively by a substring', function () {
-      const result = S.filter(OBJECTS, {name: {nilike: 'LIV'}});
+      const result = S.filter(OBJECTS, {name: {nilike: '%LIV%'}});
       expect(result).to.have.length(3);
       expect(result).to.have.length(3);
       expect(result[0]).to.be.eql(OBJECTS[0]);
       expect(result[0]).to.be.eql(OBJECTS[0]);
       expect(result[1]).to.be.eql(OBJECTS[1]);
       expect(result[1]).to.be.eql(OBJECTS[1]);

+ 1 - 0
src/utils/index.d.ts

@@ -6,6 +6,7 @@ export * from './singularize.js';
 export * from './is-deep-equal.js';
 export * from './is-deep-equal.js';
 export * from './get-ctor-name.js';
 export * from './get-ctor-name.js';
 export * from './is-pure-object.js';
 export * from './is-pure-object.js';
+export * from './like-to-regexp.js';
 export * from './string-to-regexp.js';
 export * from './string-to-regexp.js';
 export * from './get-value-by-path.js';
 export * from './get-value-by-path.js';
 export * from './transform-promise.js';
 export * from './transform-promise.js';

+ 1 - 0
src/utils/index.js

@@ -6,6 +6,7 @@ export * from './singularize.js';
 export * from './is-deep-equal.js';
 export * from './is-deep-equal.js';
 export * from './get-ctor-name.js';
 export * from './get-ctor-name.js';
 export * from './is-pure-object.js';
 export * from './is-pure-object.js';
+export * from './like-to-regexp.js';
 export * from './string-to-regexp.js';
 export * from './string-to-regexp.js';
 export * from './get-value-by-path.js';
 export * from './get-value-by-path.js';
 export * from './transform-promise.js';
 export * from './transform-promise.js';

+ 14 - 0
src/utils/like-to-regexp.d.ts

@@ -0,0 +1,14 @@
+/**
+ * Преобразует SQL LIKE-шаблон в объект RegExp.
+ *
+ * Экранирует специальные символы регулярных выражений,
+ * чтобы они обрабатывались как обычные символы, и преобразует
+ * SQL wildcards (% и _) в их эквиваленты в регулярных выражениях.
+ *
+ * @param pattern
+ * @param isCaseInsensitive
+ */
+export function likeToRegexp(
+  pattern: string,
+  isCaseInsensitive?: boolean,
+): RegExp;

+ 57 - 0
src/utils/like-to-regexp.js

@@ -0,0 +1,57 @@
+import {InvalidArgumentError} from '../errors/index.js';
+
+/**
+ * Преобразует SQL LIKE-шаблон в объект RegExp.
+ *
+ * Экранирует специальные символы регулярных выражений,
+ * чтобы они обрабатывались как обычные символы, и преобразует
+ * SQL wildcards (% и _) в их эквиваленты в регулярных выражениях.
+ *
+ * @param {string} pattern
+ * @param {boolean} isCaseInsensitive
+ * @returns {RegExp}
+ */
+export function likeToRegexp(pattern, isCaseInsensitive = false) {
+  if (typeof pattern !== 'string') {
+    throw new InvalidArgumentError(
+      'The first argument of `likeToRegexp` ' +
+        'should be a String, but %v was given.',
+      pattern,
+    );
+  }
+  // символы, которые имеют специальное значение
+  // в RegExp и должны быть экранированы
+  const regexSpecials = '-[]{}()*+?.\\^$|';
+  let regexString = '';
+  let isEscaping = false;
+  // экранирование
+  for (const char of pattern) {
+    if (isEscaping) {
+      // предыдущий символ был '\', значит текущий символ - литерал
+      regexString += regexSpecials.includes(char) ? `\\${char}` : char;
+      isEscaping = false;
+    } else if (char === '\\') {
+      // символ экранирования, следующий символ будет литералом
+      isEscaping = true;
+    } else if (char === '%') {
+      // SQL wildcard: любое количество любых символов
+      regexString += '.*';
+    } else if (char === '_') {
+      // SQL wildcard: ровно один любой символ
+      regexString += '.';
+    } else if (regexSpecials.includes(char)) {
+      // экранирование других специальных символов RegExp
+      regexString += `\\${char}`;
+    } else {
+      // обычный символ
+      regexString += char;
+    }
+  }
+  // если строка заканчивается на экранирующий символ,
+  // считаем его литералом.
+  if (isEscaping) {
+    regexString += '\\\\';
+  }
+  const flags = isCaseInsensitive ? 'i' : '';
+  return new RegExp(`^${regexString}$`, flags);
+}

+ 143 - 0
src/utils/like-to-regexp.spec.js

@@ -0,0 +1,143 @@
+import {expect} from 'chai';
+import {likeToRegexp} from './like-to-regexp.js';
+
+describe('likeToRegexp', function () {
+  it('throws an error if the pattern is not a string', function () {
+    const error = v =>
+      'The first argument of `likeToRegexp` ' +
+      `should be a String, but ${v} was given.`;
+    expect(() => likeToRegexp(123)).to.throw(error('123'));
+    expect(() => likeToRegexp(null)).to.throw(error('null'));
+    expect(() => likeToRegexp({})).to.throw(error('Object'));
+  });
+
+  describe('basic wildcards', function () {
+    it('should handle "%" as zero or more characters', function () {
+      const re = likeToRegexp('he%o');
+      expect(re.test('hello')).to.be.true;
+      expect(re.test('heo')).to.be.true;
+      expect(re.test('hexxxxo')).to.be.true;
+      expect(re.test('ahello')).to.be.false;
+      expect(re.test('hellob')).to.be.false;
+    });
+
+    it('should handle "_" as exactly one character', function () {
+      const re = likeToRegexp('he_lo');
+      expect(re.test('hello')).to.be.true;
+      expect(re.test('healo')).to.be.true;
+      expect(re.test('he_lo')).to.be.true;
+      expect(re.test('helo')).to.be.false;
+      expect(re.test('helllo')).to.be.false;
+    });
+
+    it('should handle multiple wildcards', function () {
+      const re = likeToRegexp('%_world%');
+      expect(re.test('hello_world_today')).to.be.true;
+      expect(re.test('a_world')).to.be.true;
+      expect(re.test('no_match')).to.be.false;
+    });
+  });
+
+  describe('case sensitivity', function () {
+    it('should be case-sensitive by default', function () {
+      const re = likeToRegexp('Hello%');
+      expect(re.test('Hello World')).to.be.true;
+      expect(re.test('hello World')).to.be.false;
+    });
+
+    it('should be case-insensitive when specified', function () {
+      const re = likeToRegexp('Hello%', true);
+      expect(re.test('Hello World')).to.be.true;
+      expect(re.test('hello World')).to.be.true;
+      expect(re.test('HELLO WORLD')).to.be.true;
+    });
+  });
+
+  describe('escaping', function () {
+    it('should handle escaped "%" as a literal character', function () {
+      const re = likeToRegexp('100\\%');
+      expect(re.test('100%')).to.be.true;
+      expect(re.test('100_')).to.be.false;
+    });
+
+    it('should handle escaped "_" as a literal character', function () {
+      const re = likeToRegexp('file\\_name');
+      expect(re.test('file_name')).to.be.true;
+      expect(re.test('filename')).to.be.false;
+    });
+
+    it('should handle escaped backslash as a literal backslash', function () {
+      const re = likeToRegexp('path\\\\to\\\\file');
+      expect(re.test('path\\to\\file')).to.be.true;
+      expect(re.test('pathtofile')).to.be.false;
+    });
+
+    it('should handle a trailing backslash as a literal backslash', function () {
+      const re = likeToRegexp('path\\');
+      expect(re.test('path\\')).to.be.true;
+      expect(re.test('path')).to.be.false;
+    });
+
+    it('should handle mixed escaping and wildcards', function () {
+      const re = likeToRegexp('%file\\_%.docx');
+      expect(re.test('my_awesome_file_v1.docx')).to.be.true;
+      expect(re.test('my_awesome_file-v1.docx')).to.be.false;
+    });
+  });
+
+  describe('escaping RegExp special character', function () {
+    it('should escape dots "." as literal dots', function () {
+      const re = likeToRegexp('v1.0');
+      expect(re.test('v1.0')).to.be.true;
+      expect(re.test('v1_0')).to.be.false;
+    });
+
+    it('should escape parentheses "()" as literals', function () {
+      const re = likeToRegexp('file(1)');
+      expect(re.test('file(1)')).to.be.true;
+      expect(re.test('file1')).to.be.false;
+    });
+
+    it('should escape plus signs "+"', function () {
+      const re = likeToRegexp('C++');
+      expect(re.test('C++')).to.be.true;
+      expect(re.test('C+')).to.be.false;
+    });
+
+    it('should escape question marks "?"', function () {
+      const re = likeToRegexp('Are you sure?');
+      expect(re.test('Are you sure?')).to.be.true;
+      expect(re.test('Are you sure')).to.be.false;
+    });
+
+    it('should escape curly braces "{}"', function () {
+      const re = likeToRegexp('{id}');
+      expect(re.test('{id}')).to.be.true;
+      expect(re.test('id')).to.be.false;
+    });
+
+    it('should handle a complex string with multiple special characters', function () {
+      const pattern = 'docs\\(v1.0\\)/%.txt+';
+      const re = likeToRegexp(pattern);
+      expect(re.test('docs(v1.0)/document_v2.txt+')).to.be.true;
+      expect(re.test('docs(v1.0)/document_v2.txt')).to.be.false;
+    });
+  });
+
+  describe('full pattern matching', function () {
+    it('should match the entire string, not just a part of it', function () {
+      const re = likeToRegexp('world');
+      expect(re.test('hello world')).to.be.false;
+      expect(re.test('world today')).to.be.false;
+      expect(re.test('world')).to.be.true;
+    });
+
+    it('should match the entire string when using wildcards at boundaries', function () {
+      const re = likeToRegexp('%world%');
+      expect(re.test('hello world')).to.be.true;
+      expect(re.test('world today')).to.be.true;
+      expect(re.test('hello world today')).to.be.true;
+      expect(re.test('world')).to.be.true;
+    });
+  });
+});