import axios from 'axios'
import lunr from 'lunr'
import {freshdesk} from './plugin'
import * as util from './util'
import {version as packageVersion} from '../package.json'
import flatten from 'array-flatten'
let documentStorage = {}
/**
* @typedef Plugin
* @memberOf LunrSearch
* @property {Function} plugin a lunr builder plugin
* @property {Function} fetch a function to fetch documents
* @property {Function} isSupported checks whether the plugin is supported in the current environment
*/
/**
* @typedef Result
* @memberOf LunrSearch
* @property {Object} document the document that was matched
* @property {Object} source describes the source of the document
* @property {string} source.type the type of the document source, examples are 'forum' or 'solution'
* @property {string} source.url the URL that the document was fetched from
* @property {lunr.Index~Result} result the lunr search result
*/
/**
* A class to handle building, importing, exporting, and searching a lunr index.
* It contains static and instance methods of each function so you can choose how to use it.
*
* @example <caption>Instance usage</caption>
* const builder = new lunr.Builder() // if you need to,
* builder.metadataWhitelist = ['tags'] // you can customize the builder that will be used internally
*
* const lunrSearch = new LunrSearch('myfreshdeskdomain', 'freshdeskusername', 'freshdeskpassword', {
* builder, // pass the custom builder if needed
* plugins: [myCustomBuilderPlugin],
* editDistance: 2 // in case your users can't type very well :)
* })
*
* lunrSearch.buildIndex().then(() => mySearchBar.addEventListener('input', () => lunrSeach.search(mySearchBar.value))
*
* @example <caption>Static usage</caption>
* const opts = {domain: 'freshdeskdomain', user: 'username', pass: 'password'}
* const builder = new lunr.Builder() // custom builder, etc.
* const indexPromise = LunrSearch.buildIndex({builder, ...opts}) // you can pass a custom builder into the static methods too
*
* indexPromise.then(() => mySearchBar.addEventListener('input', () => lunrSeach.search(mySearchBar.value))
*
* @param {string} domain the domain to use for all fetch requests
* @param {string} user the username to use for all fetch requests
* @param {string} pass the password to use for all fetch requests
* @param {object} [options={}]
* @param {lunr.Builder} [options.builder] a custom builder to use instead of creating a new instance
* @param {Plugin[]} [options.plugins] a list of plugins to use
* @param {number} [options.editDistance=1] a default edit distance to use in search queries
* @class LunrSearch
*/
export default class LunrSearch {
constructor (domain, user, pass, {
builder = new lunr.Builder(),
plugins = [freshdesk],
editDistance = 1
} = {}) {
/**
* The package version.
* @memberOf LunrSearch
* @member {string} VERSION
* @static
*/
this.constructor.VERSION = packageVersion
/**
* the fetched documents
* the keys are the document ID and the value is the document.
* useful for getting info about a search result.
*
* @memberOf LunrSearch
* @member {Object} docs
* @instance
*/
this.docs = {}
/**
* the domain to use for all fetch requests
*
* @member {string} domain
* @memberOf LunrSearch
* @instance
*/
this.domain = domain
/**
* the username to use for all fetch requests
*
* @member {string} user
* @memberOf LunrSearch
* @instance
*/
this.user = user
/**
* the password to use for all fetch requests
*
* @member {string} pass
* @memberOf LunrSearch
* @instance
*/
this.pass = pass
/**
* a custom builder to use
*
* @member {lunr.Builder} builder
* @memberOf LunrSearch
* @instance
*/
this.builder = builder
/**
* a list of plugins to use
*
* @member {Plugin[]} plugins
* @memberOf LunrSearch
* @instance
*/
this.plugins = plugins
/**
* a default edit distance to use in search queries
*
* @member {number} editDistance
* @memberOf LunrSearch
* @default 0
* @instance
*/
this.editDistance = editDistance
/**
* The built lunr index.
*
* @member {lunr.Index} index
* @memberOf LunrSearch
* @instance
*/
this.index = null
}
/**
* Searches the index with the given query.
*
* @param {string} query the query to search with
* @param {Object} [options={}]
* @param {number} [options.editDistance] the edit distance to use for the search
* @return {Result[]} an array of search results
* @memberOf LunrSearch
* @instance
*/
search (query, {editDistance} = {}) {
return query ? util.buildResults(this.index.search(`${query}~${editDistance || this.editDistance}`), this.docs) : []
}
/**
* Gets and imports an index fram a URL.
*
* @async
* @param {string} url the URL of the endpoint
* @param {Object} [auth={}] authentication details for the API
* @param {string} [auth.user] the username for the API
* @param {string} [auth.pass] the password for the API
* @return {lunr.Index}
* @memberOf LunrSearch
* @instance
*/
async getIndex (url, {user, pass} = {}) {
this.index = await this.constructor.getIndex(url, {user, pass})
return this.index
}
/**
* Imports an exported lunr index and stores it.
*
* @param {string} json the index to import as a json string
* @return {void}
* @memberOf LunrSearch
* @instance
*/
importIndex (json) {
this.index = this.constructor.importIndex(json)
}
/**
* Posts index to a URL.
* If the index exists locally, it will not be built. Call {@link LunrSearch#buildIndex} to build a fresh index.
* If the index does not already exist, it will be built asynchronously.
*
* @async
* @param {string} url the URL of the endpoint
* @param {Object} [auth={}] authentication details for the API
* @param {string} [auth.user] the username for the API
* @param {string} [auth.pass] the password for the API
* @return {axios.Response}
* @memberOf LunrSearch
* @instance
*/
async postIndex (url, {user, pass} = {}) {
return this.constructor.postIndex(await this.exportIndex(), url, {user, pass})
}
/**
* Serializes and exports a lunr index.
* If the index exists locally, it will not be built. Call #buildIndex to build a fresh index.
* If the index does not already exist, it will be built asynchronously.
*
* @async
* @return {Promise<string>} the serialized index
* @memberOf LunrSearch
* @instance
*/
async exportIndex () {
return JSON.stringify(this.index || await this.buildIndex())
}
/**
* Builds an index and stores it.
*
* @async
* @return {lunr.Index} the built index
* @memberOf LunrSearch
* @instance
*/
async buildIndex () {
const docs = Object.values(this.docs)
this.index = await this.constructor.buildIndex({
docs: docs.length > 0 ? docs : await this.fetch(),
builder: this.builder,
plugins: this.plugins
})
return this.index
}
/**
* Fetches some documents that are ready to be indexed.
*
* @async
* @return {Object[]} the array of Objects to be indexed
* @memberOf LunrSearch
* @instance
*/
async fetch () {
const docs = await this.constructor.fetch({
domain: this.domain,
user: this.user,
pass: this.pass,
plugins: this.plugins
})
this.docs = documentStorage
return docs
}
/**
* Searches the given index with the given query.
* If query is falsey no search is performed.
*
* @param {string} query the query to search with
* @param {lunr.Index} index an index to use for the search
* @param {Object} [options={}]
* @param {number} [options.editDistance=1] the edit distance to use for the search
* @return {Result[]} an array of search results
* @memberOf LunrSearch
* @static
*/
static search (query, index, {editDistance = 1}) {
return query ? util.buildResults(index.search(`${query}~${editDistance}`), documentStorage) : []
}
/**
* Gets and imports an index fram a URL.
*
* @async
* @param {string} url the URL of the endpoint
* @param {Object} [auth={}] authentication details for the API
* @param {string} [auth.user] the username for the API
* @param {string} [auth.pass] the password for the API
* @return {lunr.Index}
* @memberOf LunrSearch
* @static
*/
static async getIndex (url, {user, pass} = {}) {
return axios.get(url, {auth: {username: user, password: pass}}).then(({data}) => this.importIndex(data))
}
/**
* Imports an exported lunr index and returns it.
*
* @param {string} json the stringified index to import
* @return {lunr.Index}
* @memberOf LunrSearch
* @static
*/
static importIndex (json) {
return lunr.Index.load(JSON.parse(json))
}
/**
* Posts an exported index to a URL.
*
* @async
* @param {lunr.Index} index the index to be posted
* @param {string} url the URL of the endpoint
* @param {Object} [auth={}] authentication details for the API
* @param {string} [auth.user] the username for the API
* @param {string} [auth.pass] the password for the API
* @return {axios.Response}
* @memberOf LunrSearch
* @static
*/
static async postIndex (index, url, {user, pass} = {}) {
return axios.post(url, index, {auth: {username: user, password: pass}})
}
/**
* Builds then serializes and exports a lunr index.
*
* @async
* @param {Object} [options={}]
* @param {string} [options.domain] the domain of the API to make the request to
* @param {string} [options.user] the username for the API
* @param {string} [options.pass] the password for the API
* @param {lunr.Builder} [options.builder] the builder to use for building the index
* @param {Function[]} [options.plugins] a list of plugins for the builder to use
* @return {string} the serialized index
* @memberOf LunrSearch
* @static
*/
static async exportIndex ({
domain,
user,
pass,
builder = new lunr.Builder(),
plugins = [freshdesk]
}) {
return JSON.stringify(await this.buildIndex({domain, user, pass, builder, plugins}))
}
/**
* Builds an index and returns it.
* If no docs are supplied, they will be fetched.
*
* @async
* @param {Object} [options={}]
* @param {string} [options.domain] the domain of the API to make the request to
* @param {string} [options.user] the username for the API
* @param {string} [options.pass] the password for the API
* @param {Object[]} [options.docs] list of docs to build the index with
* @param {lunr.Builder} [options.builder] the builder to use for building the index
* @param {Plugin[]} [options.plugins] a list of plugins
* @return {lunr.Index} the built index
* @memberOf LunrSearch
* @static
*/
static async buildIndex ({
domain,
user,
pass,
docs: _docs,
builder = new lunr.Builder(),
plugins = [freshdesk]
}) {
plugins.forEach(p => builder.use(p.plugin))
const docs = _docs || await this.fetch({domain, user, pass})
docs.forEach(doc => builder.add(doc))
return builder.build()
}
/**
* Fetches some documents that are ready to be indexed.
*
* @async
* @param {Object} [options={}]
* @param {string} [options.domain] the domain of the API to make the request to
* @param {string} [options.user] the username for the API
* @param {string} [options.pass] the password for the API
* @param {Plugin[]} [options.plugins] a list of plugins
* @return {Object[]} the array of Objects to be indexed
* @memberOf LunrSearch
* @static
*/
static async fetch ({
domain,
user,
pass,
plugins = [freshdesk]
}) {
return new Promise((resolve, reject) => {
const docs = []
Promise.all(plugins.map(plugin => plugin.fetch(domain, user, pass).then(d => d.forEach(doc => {
docs.push(doc)
documentStorage[doc.id] = doc
})))).then(() => resolve(docs))
})
}
/**
* Checks to see if certain functions are defined.
*
* @param {Plugin[]} [plugins] a list of plugins
* @return {Boolean}
* @memberOf LunrSearch
* @static
*/
static isSupported (plugins = [freshdesk]) {
let supported = true
plugins.forEach(plugin => { supported = supported && plugin.isSupported() })
supported = supported && util.definedFunction([].forEach)
return supported
}
}