From fcbb8ecb30ab9331a20b6105ae24467fe65f659f Mon Sep 17 00:00:00 2001 From: dzruyk Date: Fri, 6 Jan 2023 21:45:56 +0300 Subject: [PATCH 1/4] feat: add sqlite3 full text search module --- server/modules/search/sqlite3/definition.yml | 8 ++ server/modules/search/sqlite3/engine.js | 142 +++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 server/modules/search/sqlite3/definition.yml create mode 100644 server/modules/search/sqlite3/engine.js diff --git a/server/modules/search/sqlite3/definition.yml b/server/modules/search/sqlite3/definition.yml new file mode 100644 index 00000000..810f6e81 --- /dev/null +++ b/server/modules/search/sqlite3/definition.yml @@ -0,0 +1,8 @@ +key: sqlite3 +title: Database - sqlite3 +description: Advanced Sqlite3-based search engine. +author: dzruyk +logo: https://static.requarks.io/logo/database.svg +website: https://www.requarks.io/ +isAvailable: true +props: {} diff --git a/server/modules/search/sqlite3/engine.js b/server/modules/search/sqlite3/engine.js new file mode 100644 index 00000000..e2660722 --- /dev/null +++ b/server/modules/search/sqlite3/engine.js @@ -0,0 +1,142 @@ +const stream = require('stream') +const Promise = require('bluebird') +const pipeline = Promise.promisify(stream.pipeline) +const _ = require('lodash') + +/* global WIKI */ + +module.exports = { + async activate() { + if (WIKI.config.db.type !== 'sqlite') { + throw new WIKI.Error.SearchActivationFailed('Must use Sqlite3 database to activate this engine!') + } + let opts = await WIKI.models.knex.schema.raw('PRAGMA compile_options') + if (!_.find(opts, { compile_options: 'ENABLE_FTS5' })) { + throw new WIKI.Error.SearchActivationFailed('Sqlite3 must have FTS5 module!') + } + }, + async deactivate() { + WIKI.logger.info(`(SEARCH/SQLITE3) Dropping index tables...`) + await WIKI.models.knex.schema.dropTable('fts5_pages_vector') + WIKI.logger.info(`(SEARCH/SQLITE3) Index tables have been dropped.`) + }, + /** + * INIT + */ + async init() { + WIKI.logger.info(`(SEARCH/SQLITE3) Initializing...`) + + // -> Create Search Index + const indexExists = await WIKI.models.knex.schema.hasTable('fts5_pages_vector') + if (!indexExists) { + WIKI.logger.info(`(SEARCH/SQLITE3) Creating Pages Vector table...`) + await WIKI.models.knex.raw('create virtual table fts5_pages_vector using fts5(tokenize=unicode61, path, locale, title,description,content)') + } + WIKI.logger.info(`(SEARCH/SQLITE3) Initialization completed.`) + }, + /** + * QUERY + * + * @param {String} q Query + * @param {Object} opts Additional options + */ + async query(q, opts) { + try { + let qry = ` + SELECT rowid as id, path, locale, title, description + FROM "fts5_pages_vector" + WHERE content MATCH ? + ORDER BY rank + ` + let qryParams = [ q ] + + const results = await WIKI.models.knex.raw(qry, qryParams) + return { + results, + suggestions: [], + totalHits: results.length, + } + } catch (err) { + WIKI.logger.warn('Search Engine Error:') + WIKI.logger.warn(err) + } + }, + /** + * CREATE + * + * @param {Object} page Page to create + */ + async created(page) { + await WIKI.models.knex.raw(` + INSERT INTO "fts5_pages_vector" (path, locale, title, description, content) VALUES ( + ?, ?, ?, ?, ? + ) + `, [page.path, page.localeCode, page.title, page.description, page.safeContent]) + }, + /** + * UPDATE + * + * @param {Object} page Page to update + */ + async updated(page) { + await WIKI.models.knex.raw(` + UPDATE "fts5_pages_vector" SET + title = ?, + description = ?, + content = ? + WHERE path = ? AND locale = ? + `, [page.title, page.description, page.safeContent, page.path, page.localeCode]) + }, + /** + * DELETE + * + * @param {Object} page Page to delete + */ + async deleted(page) { + await WIKI.models.knex('fts5_pages_vector').where({ + locale: page.localeCode, + path: page.path + }).del().limit(1) + }, + /** + * RENAME + * + * @param {Object} page Page to rename + */ + async renamed(page) { + await WIKI.models.knex('fts5_pages_vector').where({ + locale: page.localeCode, + path: page.path + }).update({ + locale: page.destinationLocaleCode, + path: page.destinationPath + }) + }, + /** + * REBUILD INDEX + */ + async rebuild() { + WIKI.logger.info(`(SEARCH/SQLITE3) Rebuilding Index...`) + await WIKI.models.knex('fts5_pages_vector').truncate() + + await pipeline( + WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'render').select().from('pages').where({ + isPublished: true, + isPrivate: false + }).stream(), + new stream.Transform({ + objectMode: true, + transform: async (page, enc, cb) => { + const content = WIKI.models.pages.cleanHTML(page.render) + await WIKI.models.knex.raw(` + INSERT INTO "fts5_pages_vector" (path, locale, title, description, content) VALUES ( + ?, ?, ?, ?, ? + ) + `, [page.path, page.localeCode, page.title, page.description, content]) + cb() + } + }) + ) + WIKI.logger.info(`(SEARCH/SQLITE3) Index rebuilt successfully.`) + } +} From eea6ee229aa1bec93076de55d3a155e157a11e63 Mon Sep 17 00:00:00 2001 From: dzruyk Date: Wed, 11 Jan 2023 03:07:02 +0300 Subject: [PATCH 2/4] feat: add sqlite query preprocessor --- server/modules/search/sqlite3/engine.js | 45 ++- server/modules/search/sqlite3/match-query.js | 349 +++++++++++++++++++ 2 files changed, 369 insertions(+), 25 deletions(-) create mode 100644 server/modules/search/sqlite3/match-query.js diff --git a/server/modules/search/sqlite3/engine.js b/server/modules/search/sqlite3/engine.js index e2660722..0c4f5a4d 100644 --- a/server/modules/search/sqlite3/engine.js +++ b/server/modules/search/sqlite3/engine.js @@ -3,6 +3,8 @@ const Promise = require('bluebird') const pipeline = Promise.promisify(stream.pipeline) const _ = require('lodash') +const matchquery = require('./match-query') + /* global WIKI */ module.exports = { @@ -44,13 +46,20 @@ module.exports = { try { let qry = ` SELECT rowid as id, path, locale, title, description - FROM "fts5_pages_vector" - WHERE content MATCH ? - ORDER BY rank - ` - let qryParams = [ q ] + FROM "fts5_pages_vector"` + let qryEnd = 'ORDER BY rank' + let qryWhere = 'WHERE fts5_pages_vector MATCH ?' + + let o = matchquery.parse(q) + if (o.negated) + qryWhere = 'WHERE rowid not in (select rowid from fts5_pages_vector where fts5_pages_vector MATCH ?)' + let qryParams = [ o.str ] - const results = await WIKI.models.knex.raw(qry, qryParams) + const results = await WIKI.models.knex.raw(` + ${qry} + ${qryWhere} + ${qryEnd} + ` , qryParams) return { results, suggestions: [], @@ -119,24 +128,10 @@ module.exports = { WIKI.logger.info(`(SEARCH/SQLITE3) Rebuilding Index...`) await WIKI.models.knex('fts5_pages_vector').truncate() - await pipeline( - WIKI.models.knex.column('path', 'localeCode', 'title', 'description', 'render').select().from('pages').where({ - isPublished: true, - isPrivate: false - }).stream(), - new stream.Transform({ - objectMode: true, - transform: async (page, enc, cb) => { - const content = WIKI.models.pages.cleanHTML(page.render) - await WIKI.models.knex.raw(` - INSERT INTO "fts5_pages_vector" (path, locale, title, description, content) VALUES ( - ?, ?, ?, ?, ? - ) - `, [page.path, page.localeCode, page.title, page.description, content]) - cb() - } - }) - ) - WIKI.logger.info(`(SEARCH/SQLITE3) Index rebuilt successfully.`) + await WIKI.models.knex.raw(` + INSERT INTO "fts5_pages_vector" (path, locale, title, description, content) + SELECT path, localeCode, title, description, content from pages`) + } + } diff --git a/server/modules/search/sqlite3/match-query.js b/server/modules/search/sqlite3/match-query.js new file mode 100644 index 00000000..e4f21e43 --- /dev/null +++ b/server/modules/search/sqlite3/match-query.js @@ -0,0 +1,349 @@ +const _ = require('lodash') + +/* + * Full text query preprocessor for sqlite3 FTS similar to pg-tsquery. + * Converts input string into internal sqlite match query + * FTS info: https://www.sqlite.org/fts5.html#full_text_query_syntax + */ + +/* +| input | output | +| --- | --- | +| `foo bar` | `foo bar` | +| `foo -bar`, `foo !bar`, `foo + !bar` | `foo NOT bar` | +| `foo bar,bip`, `foo+bar | bip` | `(foo bar) OR bip` | +| `foo (bar,bip)`, `foo+(bar|bip)` | `foo (bar OR bip)` | +| `foo*,bar* bana*` | `(foo *) or (bar * bana*)` | +*/ + + +module.exports = { + parse(input) { + let p = new MatchQueryParser() + let v = p.parse(input) + + let negated = v.negated + /* + * Since sqlite does not support top level negated MATCH queries + * calling function need to create negated sql query like + * select * not in (select ... match) + */ + if (negated) + v.negated = false + return { + negated, + str: v.toString() + } + } +} + +class Token +{ + constructor(type, value) { + this.type = type + this.value = value + } +} + +class Node +{ + constructor({type, value, negated = false, args, parNode = undefined, star = false}) { + this.type = type + this.value = value + this.negated = negated + this.star = star + this.args = args + if (this.args) { + this.args.forEach(item => { item.parNode = this }) + } + this.parNode = parNode + } + + toString() { + let s = '' + if (this.type == 'id') { + s = `"${this.value}"` + if (this.star) + s += '*' + } else { + let separator = '' + + if (this.type == 'and') { + separator = ' AND ' + } else if (this.type == 'or') { + separator = ' OR ' + } else { + throw new Error('should not reach') + } + + if (this.args && this.args.length > 0) + this.args.forEach(item => { + if (s != '') { + if (item.negated && this.type == 'and') + s += ' ' + else + s += separator + } + s += item + }) + if (this.parNode !== undefined || this.negated) + s = `(${s})` + } + if (this.negated) + s = 'NOT ' + s + return s + } +} + +function negateNodeType(node) +{ + if (node.type == 'or') + return 'and' + else if (node.type == 'and') + return 'or' + else + throw new Error('should not reach') +} + +function negateNodes(lst) +{ + lst.forEach(item => { + if (!(item instanceof Node)) + throw new Error('should not reach') + item.negated = !item.negated + }) +} + +class MatchQueryParser +{ + constructor() { + this.tokenRegex = /^([",!*\(\)-])/ + this.phraseSeparator = ' ' + this.terms = /[ \t,!*\(\)-]/ + this.knownLexemes = { + '-' : 'not', '!' : 'not', 'not' : 'not', + '&' : 'and', 'and' : 'and', + ',' : 'or', 'or' : 'or', '|' : 'or', + } + } + + asKeywordToken(s) { + const k = s.toLowerCase() + if (!this.knownLexemes.hasOwnProperty(k)) + return undefined + return new Token(this.knownLexemes[k], s) + } + + intNextToken() { + let tail = this.input.substring(this.idx).trimStart() + if (!tail) + return undefined + + tail = tail.trimStart() + this.idx = this.input.length - tail.length + + let m = tail.match(this.tokenRegex) + if (m) { + if (m[0] == '"') { + let idx = tail.indexOf('"', 1) + if (idx == -1) { + tail = tail.substring(1) + this.idx = this.input.length + } else { + tail = tail.substring(1, idx) + this.idx += idx + 1 + } + return new Token('id', tail) + } + this.idx += m[0].length + let keyword = this.asKeywordToken(m[0]) + return keyword || new Token(m[0], m[0]) + } + + // this is literal string, find next valid token start + let idx = tail.search(this.terms) + if (idx > 0) + tail = tail.substring(0, idx) + this.idx += tail.length + + let keyword = this.asKeywordToken(tail) + return keyword || new Token('id', tail) + } + + nextToken() { + this.tok = this.intNextToken() + return this.tok + } + + match(v) { + if (this.tok === undefined) + return false + return this.tok.type == v + } + + eat(v) { + if (!this.match(v)) + return false + this.nextToken() + return true + } + + setParent(node, par) { + if (node === undefined) + return undefined + + node.parNode = par + if (!node.args) + return + + node.args.forEach(item => { + if (item instanceof Node) + this.setParent(item, node) + }) + } + /* + * Sqlite3 `NOT` operator is binary but our input search string + * have unary not ('!', '-') operators so we need to preprocess request + * and rearange some items to generate valid queries + */ + preprocess(node) { + if (node === undefined || node.args === undefined) + return node + + node.args.forEach(item => { + if (item instanceof Node) + this.preprocess(item) + }) + + //try to rearrange items + let l = [], nl = [] + node.args.forEach(item => { + if (item.negated) + nl.push(item) + else + l.push(item) + }) + + if (l.length == 0 && nl.length > 1) { + /* invert node type if all children are negated */ + node.negated = !node.negated + node.type = negateNodeType(node) + negateNodes(node.args) + return node + } else if (nl.length > 1) { + // merge multiple negated nodes into one, since NOT(A & B) = NOT(A) | NOT(B) + negateNodes(nl) + nl = [ + new Node({ + type: negateNodeType(node), + parNode: node, + negated: true, + args: nl + + }) + ] + } + node.args = l.concat(nl) + + return node + } + + parse(str) { + this.input = str + this.tok = undefined + this.idx = 0 + + this.nextToken() + let o = this.parseOr() + this.setParent(o, undefined) + return this.preprocess(o) + } + + parseOr() { + let o = this.parseAnd() + if (!o) + return undefined + if (!this.match('or')) + return o + + let l = [o] + + while (this.eat('or')) { + o = this.parseAnd() + if (!o) + break + l.push(o) + } + return new Node({ + type: 'or', + args: l + }) + } + + parseAnd() { + let o = this.parseLit() + if (!o) + return undefined + + let l = [o] + while (true) { + this.eat('and') //optional + o = this.parseLit() + if (!o) + break + l.push(o) + } + if (l.length == 1) + return l[0] + + return new Node({ + type: 'and', + args: l, + }) + } + + parseLit() { + let o = this.tok + let negated = false + let star = false + + if (o == undefined) + return o + + if (this.eat('not')) { + if (this.tok == undefined) { + return new Node({ + type: 'id', + negated: false, + value: o.value, + }) + } + negated = true + o = this.tok + } + + if (this.eat('(')) { + let tail = this.input + let n = this.parseOr() + if (!this.eat(')') || n === undefined) + return undefined + n.negated = negated + return n + } + if (['and', 'or', '(', ')'].indexOf(o.type) >= 0) { + return undefined + } + + this.nextToken() + if (this.eat('*')) + star = true + + return new Node({ + type: 'id', + negated, + star, + value: o.value, + }) + } +} + From cf41b97d2d8b407fcc1b119fe11e57d68ee60c46 Mon Sep 17 00:00:00 2001 From: Nicolas Giard Date: Sun, 29 Jan 2023 18:24:55 -0500 Subject: [PATCH 3/4] fix: code cleanup --- server/modules/search/sqlite3/engine.js | 27 ++++++++++++------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/server/modules/search/sqlite3/engine.js b/server/modules/search/sqlite3/engine.js index 0c4f5a4d..3b4e71d1 100644 --- a/server/modules/search/sqlite3/engine.js +++ b/server/modules/search/sqlite3/engine.js @@ -12,7 +12,7 @@ module.exports = { if (WIKI.config.db.type !== 'sqlite') { throw new WIKI.Error.SearchActivationFailed('Must use Sqlite3 database to activate this engine!') } - let opts = await WIKI.models.knex.schema.raw('PRAGMA compile_options') + const opts = await WIKI.models.knex.schema.raw('PRAGMA compile_options') if (!_.find(opts, { compile_options: 'ENABLE_FTS5' })) { throw new WIKI.Error.SearchActivationFailed('Sqlite3 must have FTS5 module!') } @@ -32,7 +32,7 @@ module.exports = { const indexExists = await WIKI.models.knex.schema.hasTable('fts5_pages_vector') if (!indexExists) { WIKI.logger.info(`(SEARCH/SQLITE3) Creating Pages Vector table...`) - await WIKI.models.knex.raw('create virtual table fts5_pages_vector using fts5(tokenize=unicode61, path, locale, title,description,content)') + await WIKI.models.knex.raw('CREATE VIRTUAL TABLE fts5_pages_vector USING fts5(tokenize=unicode61, path, locale, title, description, content)') } WIKI.logger.info(`(SEARCH/SQLITE3) Initialization completed.`) }, @@ -44,26 +44,26 @@ module.exports = { */ async query(q, opts) { try { - let qry = ` - SELECT rowid as id, path, locale, title, description + const qry = ` + SELECT rowid AS id, path, locale, title, description FROM "fts5_pages_vector"` - let qryEnd = 'ORDER BY rank' + const qryEnd = 'ORDER BY rank' let qryWhere = 'WHERE fts5_pages_vector MATCH ?' - let o = matchquery.parse(q) - if (o.negated) - qryWhere = 'WHERE rowid not in (select rowid from fts5_pages_vector where fts5_pages_vector MATCH ?)' - let qryParams = [ o.str ] + const o = matchquery.parse(q) + if (o.negated) { + qryWhere = 'WHERE rowid NOT IN (SELECT rowid FROM fts5_pages_vector WHERE fts5_pages_vector MATCH ?)' + } const results = await WIKI.models.knex.raw(` ${qry} ${qryWhere} ${qryEnd} - ` , qryParams) + `, [o.str]) return { results, suggestions: [], - totalHits: results.length, + totalHits: results.length } } catch (err) { WIKI.logger.warn('Search Engine Error:') @@ -130,8 +130,7 @@ module.exports = { await WIKI.models.knex.raw(` INSERT INTO "fts5_pages_vector" (path, locale, title, description, content) - SELECT path, localeCode, title, description, content from pages`) - + SELECT path, localeCode, title, description, content FROM pages` + ) } - } From c47e63383e3ec5b026ebde370d09de5fb240fb03 Mon Sep 17 00:00:00 2001 From: dzruyk Date: Tue, 31 Jan 2023 02:51:35 +0300 Subject: [PATCH 4/4] fix: standard style fixes --- server/modules/search/sqlite3/engine.js | 3 +- server/modules/search/sqlite3/match-query.js | 200 ++++++++++--------- 2 files changed, 111 insertions(+), 92 deletions(-) diff --git a/server/modules/search/sqlite3/engine.js b/server/modules/search/sqlite3/engine.js index 3b4e71d1..34bce709 100644 --- a/server/modules/search/sqlite3/engine.js +++ b/server/modules/search/sqlite3/engine.js @@ -1,6 +1,5 @@ const stream = require('stream') const Promise = require('bluebird') -const pipeline = Promise.promisify(stream.pipeline) const _ = require('lodash') const matchquery = require('./match-query') @@ -94,7 +93,7 @@ module.exports = { description = ?, content = ? WHERE path = ? AND locale = ? - `, [page.title, page.description, page.safeContent, page.path, page.localeCode]) + `, [page.title, page.description, page.safeContent, page.path, page.localeCode]) }, /** * DELETE diff --git a/server/modules/search/sqlite3/match-query.js b/server/modules/search/sqlite3/match-query.js index e4f21e43..84685b05 100644 --- a/server/modules/search/sqlite3/match-query.js +++ b/server/modules/search/sqlite3/match-query.js @@ -1,5 +1,4 @@ const _ = require('lodash') - /* * Full text query preprocessor for sqlite3 FTS similar to pg-tsquery. * Converts input string into internal sqlite match query @@ -16,20 +15,20 @@ const _ = require('lodash') | `foo*,bar* bana*` | `(foo *) or (bar * bana*)` | */ - module.exports = { parse(input) { - let p = new MatchQueryParser() - let v = p.parse(input) + const p = new MatchQueryParser() + const v = p.parse(input) - let negated = v.negated + const negated = v.negated /* * Since sqlite does not support top level negated MATCH queries * calling function need to create negated sql query like * select * not in (select ... match) */ - if (negated) + if (negated) { v.negated = false + } return { negated, str: v.toString() @@ -37,17 +36,15 @@ module.exports = { } } -class Token -{ +class Token { constructor(type, value) { this.type = type this.value = value } } -class Node -{ - constructor({type, value, negated = false, args, parNode = undefined, star = false}) { +class Node { + constructor({ type, value, negated = false, args, parNode = undefined, star = false }) { this.type = type this.value = value this.negated = negated @@ -61,92 +58,98 @@ class Node toString() { let s = '' - if (this.type == 'id') { + if (this.type === 'id') { s = `"${this.value}"` - if (this.star) + if (this.star) { s += '*' + } } else { let separator = '' - if (this.type == 'and') { + if (this.type === 'and') { separator = ' AND ' - } else if (this.type == 'or') { + } else if (this.type === 'or') { separator = ' OR ' } else { throw new Error('should not reach') } - if (this.args && this.args.length > 0) + if (this.args && this.args.length > 0) { this.args.forEach(item => { - if (s != '') { - if (item.negated && this.type == 'and') + if (s !== '') { + if (item.negated && this.type === 'and') { s += ' ' - else + } else { s += separator + } } s += item }) - if (this.parNode !== undefined || this.negated) + } + if (this.parNode !== undefined || this.negated) { s = `(${s})` + } } - if (this.negated) + if (this.negated) { s = 'NOT ' + s + } return s } } -function negateNodeType(node) -{ - if (node.type == 'or') - return 'and' - else if (node.type == 'and') - return 'or' - else - throw new Error('should not reach') +function negateNodeType(node) { + if (node.type === 'or') { + return 'and' + } else if (node.type === 'and') { + return 'or' + } else { + throw new Error('should not reach') + } } -function negateNodes(lst) -{ - lst.forEach(item => { - if (!(item instanceof Node)) - throw new Error('should not reach') - item.negated = !item.negated - }) +function negateNodes(lst) { + lst.forEach(item => { + if (!(item instanceof Node)) { + throw new Error('should not reach') + } + item.negated = !item.negated + }) } -class MatchQueryParser -{ +class MatchQueryParser { constructor() { - this.tokenRegex = /^([",!*\(\)-])/ + this.tokenRegex = /^([",!*()-])/ this.phraseSeparator = ' ' - this.terms = /[ \t,!*\(\)-]/ + this.terms = /[ \t,!*()-]/ this.knownLexemes = { - '-' : 'not', '!' : 'not', 'not' : 'not', - '&' : 'and', 'and' : 'and', - ',' : 'or', 'or' : 'or', '|' : 'or', + '-': 'not', '!': 'not', 'not': 'not', + '&': 'and', 'and': 'and', + ',': 'or', 'or': 'or', '|': 'or' } } asKeywordToken(s) { const k = s.toLowerCase() - if (!this.knownLexemes.hasOwnProperty(k)) + if (!_.has(this.knownLexemes, k)) { return undefined + } return new Token(this.knownLexemes[k], s) } intNextToken() { let tail = this.input.substring(this.idx).trimStart() - if (!tail) + if (!tail) { return undefined + } tail = tail.trimStart() this.idx = this.input.length - tail.length - let m = tail.match(this.tokenRegex) + const m = tail.match(this.tokenRegex) if (m) { - if (m[0] == '"') { - let idx = tail.indexOf('"', 1) - if (idx == -1) { + if (m[0] === '"') { + const idx = tail.indexOf('"', 1) + if (idx === -1) { tail = tail.substring(1) this.idx = this.input.length } else { @@ -156,17 +159,18 @@ class MatchQueryParser return new Token('id', tail) } this.idx += m[0].length - let keyword = this.asKeywordToken(m[0]) + const keyword = this.asKeywordToken(m[0]) return keyword || new Token(m[0], m[0]) } // this is literal string, find next valid token start - let idx = tail.search(this.terms) - if (idx > 0) + const idx = tail.search(this.terms) + if (idx > 0) { tail = tail.substring(0, idx) + } this.idx += tail.length - let keyword = this.asKeywordToken(tail) + const keyword = this.asKeywordToken(tail) return keyword || new Token('id', tail) } @@ -176,55 +180,65 @@ class MatchQueryParser } match(v) { - if (this.tok === undefined) + if (this.tok === undefined) { return false - return this.tok.type == v + } + return this.tok.type === v } eat(v) { - if (!this.match(v)) + if (!this.match(v)) { return false + } this.nextToken() return true } setParent(node, par) { - if (node === undefined) + if (node === undefined) { return undefined + } node.parNode = par - if (!node.args) + if (!node.args) { return + } node.args.forEach(item => { - if (item instanceof Node) + if (item instanceof Node) { this.setParent(item, node) + } }) } + /* * Sqlite3 `NOT` operator is binary but our input search string * have unary not ('!', '-') operators so we need to preprocess request * and rearange some items to generate valid queries */ preprocess(node) { - if (node === undefined || node.args === undefined) + if (node === undefined || node.args === undefined) { return node + } node.args.forEach(item => { - if (item instanceof Node) + if (item instanceof Node) { this.preprocess(item) + } }) - //try to rearrange items - let l = [], nl = [] + // try to rearrange items + const l = [] + let nl = [] node.args.forEach(item => { - if (item.negated) + if (item.negated) { nl.push(item) - else + } else { l.push(item) + } }) - if (l.length == 0 && nl.length > 1) { + if (l.length === 0 && nl.length > 1) { /* invert node type if all children are negated */ node.negated = !node.negated node.type = negateNodeType(node) @@ -254,24 +268,26 @@ class MatchQueryParser this.idx = 0 this.nextToken() - let o = this.parseOr() + const o = this.parseOr() this.setParent(o, undefined) return this.preprocess(o) } parseOr() { let o = this.parseAnd() - if (!o) + if (!o) { return undefined - if (!this.match('or')) + } else if (!this.match('or')) { return o + } - let l = [o] + const l = [o] while (this.eat('or')) { o = this.parseAnd() - if (!o) + if (!o) { break + } l.push(o) } return new Node({ @@ -282,23 +298,26 @@ class MatchQueryParser parseAnd() { let o = this.parseLit() - if (!o) + if (!o) { return undefined + } - let l = [o] + const l = [o] while (true) { - this.eat('and') //optional + this.eat('and') // optional 'and' keyword o = this.parseLit() - if (!o) + if (!o) { break + } l.push(o) } - if (l.length == 1) + if (l.length === 1) { return l[0] + } return new Node({ type: 'and', - args: l, + args: l }) } @@ -307,26 +326,27 @@ class MatchQueryParser let negated = false let star = false - if (o == undefined) + if (o === undefined) { return o + } if (this.eat('not')) { - if (this.tok == undefined) { - return new Node({ - type: 'id', - negated: false, - value: o.value, - }) + if (this.tok === undefined) { + return new Node({ + type: 'id', + negated: false, + value: o.value + }) } negated = true o = this.tok } if (this.eat('(')) { - let tail = this.input - let n = this.parseOr() - if (!this.eat(')') || n === undefined) + const n = this.parseOr() + if (!this.eat(')') || n === undefined) { return undefined + } n.negated = negated return n } @@ -335,15 +355,15 @@ class MatchQueryParser } this.nextToken() - if (this.eat('*')) + if (this.eat('*')) { star = true + } return new Node({ type: 'id', negated, star, - value: o.value, + value: o.value }) } } -