From a3eb0bb20c59b48cec7923d448dc815d12106900 Mon Sep 17 00:00:00 2001 From: Denis Ferrero Date: Wed, 7 Feb 2024 08:16:19 +0100 Subject: [PATCH 1/2] Case insensitive search --- .../src/index.js | 11 +-- .../test/unit/index.spec.js | 69 +++++++++---------- 2 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/moleculer-db-adapter-sequelize/src/index.js b/packages/moleculer-db-adapter-sequelize/src/index.js index 62b63573..7ec57fc9 100644 --- a/packages/moleculer-db-adapter-sequelize/src/index.js +++ b/packages/moleculer-db-adapter-sequelize/src/index.js @@ -320,12 +320,13 @@ class SequelizeDbAdapter { fields = _.isString(params.searchFields) ? params.searchFields.split(" ") : params.searchFields; } + const lowerCaseSearch = "%" + (params.search).toLowerCase() + "%"; const searchConditions = fields.map(f => { - return { - [f]: { - [Op.like]: "%" + params.search + "%" - } - }; + return Sequelize.where( + Sequelize.fn("lower", Sequelize.col(f)), + Op.like, + lowerCaseSearch + ); }); if (params.query) { diff --git a/packages/moleculer-db-adapter-sequelize/test/unit/index.spec.js b/packages/moleculer-db-adapter-sequelize/test/unit/index.spec.js index 422c8fca..28dbddf8 100644 --- a/packages/moleculer-db-adapter-sequelize/test/unit/index.spec.js +++ b/packages/moleculer-db-adapter-sequelize/test/unit/index.spec.js @@ -229,23 +229,23 @@ describe("Test SequelizeAdapter", () => { it("call with full-text search without query", () => { adapter.model.findAll.mockClear(); adapter.createCursor({ - search: "walter", + search: "WaLtEr", searchFields: ["title", "content"] }); expect(adapter.model.findAll).toHaveBeenCalledTimes(1); expect(adapter.model.findAll).toHaveBeenCalledWith({ where: { [Op.or]: [ - { - title: { - [Op.like]: "%walter%" - } - }, - { - content: { - [Op.like]: "%walter%" - } - } + Sequelize.where( + Sequelize.fn("lower", Sequelize.col("title")), + Op.like, + "%walter%" + ), + Sequelize.where( + Sequelize.fn("lower", Sequelize.col("content")), + Op.like, + "%walter%" + ) ] } }); @@ -255,7 +255,7 @@ describe("Test SequelizeAdapter", () => { adapter.model.findAll.mockClear(); adapter.createCursor({ query: { status: 1 }, - search: "walter", + search: "wAlTeR", searchFields: ["title", "content"] }); expect(adapter.model.findAll).toHaveBeenCalledTimes(1); @@ -264,18 +264,17 @@ describe("Test SequelizeAdapter", () => { [Op.and]: [ { status: 1 }, { [Op.or]: [ - { - title: { - [Op.like]: "%walter%" - } - }, - { - content: { - [Op.like]: "%walter%" - } - } - ] - } + Sequelize.where( + Sequelize.fn("lower", Sequelize.col("title")), + Op.like, + "%walter%" + ), + Sequelize.where( + Sequelize.fn("lower", Sequelize.col("content")), + Op.like, + "%walter%" + ) + ] } ] } }); @@ -290,7 +289,7 @@ describe("Test SequelizeAdapter", () => { { deleted: 0 } ] }, - search: "walter", + search: "WALTER", searchFields: ["title", "content"] }); expect(adapter.model.findAll).toHaveBeenCalledTimes(1); @@ -302,16 +301,16 @@ describe("Test SequelizeAdapter", () => { { deleted: 0 }, ] }, { [Op.or]: [ - { - title: { - [Op.like]: "%walter%" - } - }, - { - content: { - [Op.like]: "%walter%" - } - } + Sequelize.where( + Sequelize.fn("lower", Sequelize.col("title")), + Op.like, + "%walter%" + ), + Sequelize.where( + Sequelize.fn("lower", Sequelize.col("content")), + Op.like, + "%walter%" + ) ] } ] } From 1caaf8f419366ea6ca4a4d9545d11c7d32752e57 Mon Sep 17 00:00:00 2001 From: Denis Ferrero Date: Mon, 29 Apr 2024 17:14:54 +0200 Subject: [PATCH 2/2] Dedicated parameter for case insensitive search #381 --- .../src/index.js | 39 +++-- .../test/unit/index.spec.js | 136 ++++++++++++++++++ 2 files changed, 167 insertions(+), 8 deletions(-) diff --git a/packages/moleculer-db-adapter-sequelize/src/index.js b/packages/moleculer-db-adapter-sequelize/src/index.js index 7ec57fc9..a6404285 100644 --- a/packages/moleculer-db-adapter-sequelize/src/index.js +++ b/packages/moleculer-db-adapter-sequelize/src/index.js @@ -313,6 +313,8 @@ class SequelizeDbAdapter { where: {} }; + const searchConditions = []; + // Text search if (_.isString(params.search) && params.search !== "") { let fields = []; @@ -320,15 +322,38 @@ class SequelizeDbAdapter { fields = _.isString(params.searchFields) ? params.searchFields.split(" ") : params.searchFields; } - const lowerCaseSearch = "%" + (params.search).toLowerCase() + "%"; - const searchConditions = fields.map(f => { - return Sequelize.where( + for (const f of fields) { + searchConditions.push({ + [f]: { + [Op.like]: "%" + params.search + "%" + } + }); + } + } + // Case insensitive search + else if (_.isString(params.iSearch) && params.iSearch !== "") { + let fields = []; + if (params.searchFields) { + fields = _.isString(params.searchFields) ? params.searchFields.split(" ") : params.searchFields; + } + const lowerCaseSearch = "%" + (params.iSearch).toLowerCase() + "%"; + + for (const f of fields) { + searchConditions.push(Sequelize.where( Sequelize.fn("lower", Sequelize.col(f)), Op.like, lowerCaseSearch - ); - }); - + )); + } + } + + // Assign only query params + if (searchConditions.length == 0) { + if (params.query) { + Object.assign(q.where, params.query); + } + // Assign query and search params + } else { if (params.query) { q.where[Op.and] = [ params.query, @@ -337,8 +362,6 @@ class SequelizeDbAdapter { } else { q.where[Op.or] = searchConditions; } - } else if (params.query) { - Object.assign(q.where, params.query); } // Sort diff --git a/packages/moleculer-db-adapter-sequelize/test/unit/index.spec.js b/packages/moleculer-db-adapter-sequelize/test/unit/index.spec.js index 28dbddf8..f40df6d5 100644 --- a/packages/moleculer-db-adapter-sequelize/test/unit/index.spec.js +++ b/packages/moleculer-db-adapter-sequelize/test/unit/index.spec.js @@ -233,6 +233,23 @@ describe("Test SequelizeAdapter", () => { searchFields: ["title", "content"] }); expect(adapter.model.findAll).toHaveBeenCalledTimes(1); + expect(adapter.model.findAll).toHaveBeenCalledWith({ + where: { + [Op.or]: [ + { "title": { [Op.like]: "%WaLtEr%" } }, + { "content": { [Op.like]: "%WaLtEr%" } } + ] + } + }); + }); + + it("call with full-text search (insensitive case) without query", () => { + adapter.model.findAll.mockClear(); + adapter.createCursor({ + iSearch: "WaLtEr", + searchFields: ["title", "content"] + }); + expect(adapter.model.findAll).toHaveBeenCalledTimes(1); expect(adapter.model.findAll).toHaveBeenCalledWith({ where: { [Op.or]: [ @@ -251,6 +268,24 @@ describe("Test SequelizeAdapter", () => { }); }); + it("call with full-text search and full-text insensitive case search without query", () => { + adapter.model.findAll.mockClear(); + adapter.createCursor({ + search: "WaLtEr", + iSearch: "WaLtEr", + searchFields: ["title", "content"] + }); + expect(adapter.model.findAll).toHaveBeenCalledTimes(1); + expect(adapter.model.findAll).toHaveBeenCalledWith({ + where: { + [Op.or]: [ + { "title": { [Op.like]: "%WaLtEr%" } }, + { "content": { [Op.like]: "%WaLtEr%" } } + ] + } + }); + }); + it("call with full-text search with query", () => { adapter.model.findAll.mockClear(); adapter.createCursor({ @@ -259,6 +294,27 @@ describe("Test SequelizeAdapter", () => { searchFields: ["title", "content"] }); expect(adapter.model.findAll).toHaveBeenCalledTimes(1); + expect(adapter.model.findAll).toHaveBeenCalledWith({ + where: { + [Op.and]: [ + { status: 1 }, + { [Op.or]: [ + { "title": { [Op.like]: "%wAlTeR%" } }, + { "content": { [Op.like]: "%wAlTeR%" } } + ] } + ] + } + }); + }); + + it("call with full-text search (insensitive case) with query", () => { + adapter.model.findAll.mockClear(); + adapter.createCursor({ + query: { status: 1 }, + iSearch: "wAlTeR", + searchFields: ["title", "content"] + }); + expect(adapter.model.findAll).toHaveBeenCalledTimes(1); expect(adapter.model.findAll).toHaveBeenCalledWith({ where: { [Op.and]: [ @@ -280,6 +336,28 @@ describe("Test SequelizeAdapter", () => { }); }); + it("call with full-text search and full-text insensitive case search with query", () => { + adapter.model.findAll.mockClear(); + adapter.createCursor({ + query: { status: 1 }, + search: "wAlTeR", + iSearch: "wAlTeR", + searchFields: ["title", "content"] + }); + expect(adapter.model.findAll).toHaveBeenCalledTimes(1); + expect(adapter.model.findAll).toHaveBeenCalledWith({ + where: { + [Op.and]: [ + { status: 1 }, + { [Op.or]: [ + { "title": { [Op.like]: "%wAlTeR%" } }, + { "content": { [Op.like]: "%wAlTeR%" } } + ] } + ] + } + }); + }); + it("call with full-text search & advanced query", () => { adapter.model.findAll.mockClear(); adapter.createCursor({ @@ -293,6 +371,35 @@ describe("Test SequelizeAdapter", () => { searchFields: ["title", "content"] }); expect(adapter.model.findAll).toHaveBeenCalledTimes(1); + expect(adapter.model.findAll).toHaveBeenCalledWith({ + where: { + [Op.and]: [ + { [Op.or]: [ + { status: 1 }, + { deleted: 0 }, + ] }, + { [Op.or]: [ + { "title": { [Op.like]: "%WALTER%" } }, + { "content": { [Op.like]: "%WALTER%" } } + ] } + ] + } + }); + }); + + it("call with full-text search (insensitive case) & advanced query", () => { + adapter.model.findAll.mockClear(); + adapter.createCursor({ + query: { + [Op.or]: [ + { status: 1 }, + { deleted: 0 } + ] + }, + iSearch: "WALTER", + searchFields: ["title", "content"] + }); + expect(adapter.model.findAll).toHaveBeenCalledTimes(1); expect(adapter.model.findAll).toHaveBeenCalledWith({ where: { [Op.and]: [ @@ -317,6 +424,35 @@ describe("Test SequelizeAdapter", () => { }); }); + it("call with full-text search and full-text insensitive case search & advanced query", () => { + adapter.model.findAll.mockClear(); + adapter.createCursor({ + query: { + [Op.or]: [ + { status: 1 }, + { deleted: 0 } + ] + }, + search: "WALTER", + iSearch: "WALTER", + searchFields: ["title", "content"] + }); + expect(adapter.model.findAll).toHaveBeenCalledTimes(1); + expect(adapter.model.findAll).toHaveBeenCalledWith({ + where: { + [Op.and]: [ + { [Op.or]: [ + { status: 1 }, + { deleted: 0 }, + ] }, + { [Op.or]: [ + { "title": { [Op.like]: "%WALTER%" } }, + { "content": { [Op.like]: "%WALTER%" } } + ] } + ] + } + }); + }); });