Add jsdoc to search utils

This commit is contained in:
Andrew Kingston 2021-05-13 13:11:16 +01:00
parent 8e2aff1716
commit f54096447d
1 changed files with 74 additions and 5 deletions

View File

@ -3,10 +3,20 @@ const { checkSlashesInUrl } = require("../../../utilities")
const env = require("../../../environment") const env = require("../../../environment")
const fetch = require("node-fetch") const fetch = require("node-fetch")
/**
* Escapes any characters in a string which lucene searches require to be
* escaped.
* @param value The value to escape
* @returns {string}
*/
const luceneEscape = value => { const luceneEscape = value => {
return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&") return `${value}`.replace(/[ #+\-&|!(){}\[\]^"~*?:\\]/g, "\\$&")
} }
/**
* Class to build lucene query URLs.
* Optionally takes a base lucene query object.
*/
class QueryBuilder { class QueryBuilder {
constructor(appId, base) { constructor(appId, base) {
this.appId = appId this.appId = appId
@ -105,6 +115,7 @@ class QueryBuilder {
} }
} }
// Construct the actual lucene search query string from JSON structure
if (this.query.string) { if (this.query.string) {
build(this.query.string, (key, value) => { build(this.query.string, (key, value) => {
return value ? `${key}:${luceneEscape(value.toLowerCase())}*` : null return value ? `${key}:${luceneEscape(value.toLowerCase())}*` : null
@ -146,6 +157,7 @@ class QueryBuilder {
build(this.query.notEmpty, key => `${key}:["" TO *]`) build(this.query.notEmpty, key => `${key}:["" TO *]`)
} }
// Build the full search URL
let url = `${env.COUCH_DB_URL}/${this.appId}/_design/database/_search` let url = `${env.COUCH_DB_URL}/${this.appId}/_design/database/_search`
url += `/${SearchIndexes.ROWS}?q=${output}` url += `/${SearchIndexes.ROWS}?q=${output}`
url += `&limit=${Math.min(this.limit, 200)}` url += `&limit=${Math.min(this.limit, 200)}`
@ -161,11 +173,18 @@ class QueryBuilder {
if (this.bookmark) { if (this.bookmark) {
url += `&bookmark=${this.bookmark}` url += `&bookmark=${this.bookmark}`
} }
console.log(url) console.log(url)
// Fix any double slashes in the URL
return checkSlashesInUrl(url) return checkSlashesInUrl(url)
} }
} }
/**
* Executes a lucene search query.
* @param query The query URL
* @returns {Promise<{rows: []}>}
*/
const runQuery = async query => { const runQuery = async query => {
const response = await fetch(query, { const response = await fetch(query, {
method: "GET", method: "GET",
@ -183,6 +202,22 @@ const runQuery = async query => {
return output return output
} }
/**
* Gets round the fixed limit of 200 results from a query by fetching as many
* pages as required and concatenating the results. This recursively operates
* until enough results have been found.
* @param appId {string} The app ID to search
* @param query {object} The JSON query structure
* @param tableId {string} The table ID to search
* @param sort {string} The sort column
* @param sortOrder {string} The sort order ("ascending" or "descending")
* @param sortType {string} Whether to treat sortable values as strings or
* numbers. ("string" or "number")
* @param limit {number} The number of results to fetch
* @param bookmark {string|null} Current bookmark in the recursive search
* @param rows {array|null} Current results in the recursive search
* @returns {Promise<*[]|*>}
*/
const recursiveSearch = async ( const recursiveSearch = async (
appId, appId,
query, query,
@ -191,8 +226,8 @@ const recursiveSearch = async (
sortOrder, sortOrder,
sortType, sortType,
limit, limit,
bookmark, bookmark = null,
rows rows = []
) => { ) => {
if (rows.length >= limit) { if (rows.length >= limit) {
return rows return rows
@ -226,6 +261,21 @@ const recursiveSearch = async (
) )
} }
/**
* Performs a paginated search. A bookmark will be returned to allow the next
* page to be fetched. There is a max limit off 200 results per page in a
* paginated search.
* @param appId {string} The app ID to search
* @param query {object} The JSON query structure
* @param tableId {string} The table ID to search
* @param sort {string} The sort column
* @param sortOrder {string} The sort order ("ascending" or "descending")
* @param sortType {string} Whether to treat sortable values as strings or
* numbers. ("string" or "number")
* @param limit {number} The desired page size
* @param bookmark {string} The bookmark to resume from
* @returns {Promise<{hasNextPage: boolean, rows: *[]}>}
*/
exports.paginatedSearch = async ( exports.paginatedSearch = async (
appId, appId,
query, query,
@ -239,6 +289,7 @@ exports.paginatedSearch = async (
if (limit == null || isNaN(limit) || limit < 0) { if (limit == null || isNaN(limit) || limit < 0) {
limit = 50 limit = 50
} }
limit = Math.min(limit, 200)
const builder = new QueryBuilder(appId, query) const builder = new QueryBuilder(appId, query)
.setTable(tableId) .setTable(tableId)
.setSort(sort) .setSort(sort)
@ -249,17 +300,36 @@ exports.paginatedSearch = async (
.setLimit(limit) .setLimit(limit)
.buildSearchURL() .buildSearchURL()
const searchResults = await runQuery(searchUrl) const searchResults = await runQuery(searchUrl)
// Try fetching 1 row in the next page to see if another page of results
// exists or not
const nextUrl = builder const nextUrl = builder
.setBookmark(searchResults.bookmark) .setBookmark(searchResults.bookmark)
.setLimit(1) .setLimit(1)
.buildSearchURL() .buildSearchURL()
const nextResults = await runQuery(nextUrl) const nextResults = await runQuery(nextUrl)
return { return {
...searchResults, ...searchResults,
hasNextPage: nextResults.rows && nextResults.rows.length > 0, hasNextPage: nextResults.rows && nextResults.rows.length > 0,
} }
} }
/**
* Performs a full search, fetching multiple pages if required to return the
* desired amount of results. There is a limit of 1000 results to avoid
* heavy performance hits, and to avoid client components breaking from
* handling too much data.
* @param appId {string} The app ID to search
* @param query {object} The JSON query structure
* @param tableId {string} The table ID to search
* @param sort {string} The sort column
* @param sortOrder {string} The sort order ("ascending" or "descending")
* @param sortType {string} Whether to treat sortable values as strings or
* numbers. ("string" or "number")
* @param limit {number} The desired number of results
* @returns {Promise<{rows: *}>}
*/
exports.fullSearch = async ( exports.fullSearch = async (
appId, appId,
query, query,
@ -272,6 +342,7 @@ exports.fullSearch = async (
if (limit == null || isNaN(limit) || limit < 0) { if (limit == null || isNaN(limit) || limit < 0) {
limit = 1000 limit = 1000
} }
limit = Math.min(limit, 1000)
const rows = await recursiveSearch( const rows = await recursiveSearch(
appId, appId,
query, query,
@ -279,9 +350,7 @@ exports.fullSearch = async (
sort, sort,
sortOrder, sortOrder,
sortType, sortType,
Math.min(limit, 1000), limit
null,
[]
) )
return { rows } return { rows }
} }