diff --git a/config/config.json.ctmpl b/config/config.json.ctmpl index 292cf83dd6..2958e3f282 100644 --- a/config/config.json.ctmpl +++ b/config/config.json.ctmpl @@ -54,10 +54,6 @@ {{if service "bookbrainz-elasticsearch-test"}} {{with index (service "bookbrainz-elasticsearch-test") 0}} "node": "http://{{.Address}}:{{.Port}}", - "auth": { - "username": "elastic", - "password": "changeme" - }, "requestTimeout": 30000 {{end}} {{end}} @@ -65,10 +61,6 @@ {{if service "bookbrainz-elasticsearch"}} {{with index (service "bookbrainz-elasticsearch") 0}} "node": "http://{{.Address}}:{{.Port}}", - "auth": { - "username": "elastic", - "password": "changeme" - }, "requestTimeout": 30000 {{end}} {{end}} diff --git a/config/config.json.example b/config/config.json.example index d467dd7b26..ae0ea80505 100644 --- a/config/config.json.example +++ b/config/config.json.example @@ -34,10 +34,6 @@ }, "search": { "node": "http://elasticsearch:9200", - "auth": { - "username": "elastic", - "password": "changeme" - }, "requestTimeout": 30000 }, "mailConfig" :{ diff --git a/config/config.local.json.example b/config/config.local.json.example index 4af1228803..8f5b67b8d4 100644 --- a/config/config.local.json.example +++ b/config/config.local.json.example @@ -34,10 +34,6 @@ }, "search": { "node": "http://localhost:9200", - "auth": { - "username": "elastic", - "password": "changeme" - }, "requestTimeout": 60000 } } diff --git a/src/api/app.js b/src/api/app.js index c1407a7714..b8d04197a0 100644 --- a/src/api/app.js +++ b/src/api/app.js @@ -69,7 +69,6 @@ mainRouter.use((req, res) => { // https://github.com/elastic/elasticsearch-js/issues/33 search.init(app.locals.orm, Object.assign({}, config.search)); - const DEFAULT_API_PORT = 9098; app.set('port', process.env.PORT || DEFAULT_API_PORT); diff --git a/src/client/components/pages/entities/cbReviewModal.tsx b/src/client/components/pages/entities/cbReviewModal.tsx index 73e4154c4f..b7c7d398e3 100644 --- a/src/client/components/pages/entities/cbReviewModal.tsx +++ b/src/client/components/pages/entities/cbReviewModal.tsx @@ -503,7 +503,7 @@ class CBReviewModal extends React.Component< /* executes getAccessToken() only in a browser to avoid unnecessary server-side calls during component mounting */ componentDidMount = async () => { - if (typeof window !== "undefined") { + if (typeof window !== 'undefined') { this.accessToken = await this.getAccessToken(); } }; diff --git a/src/client/entity-editor/entity-editor.tsx b/src/client/entity-editor/entity-editor.tsx index 317b6e32ba..0f0a83366a 100644 --- a/src/client/entity-editor/entity-editor.tsx +++ b/src/client/entity-editor/entity-editor.tsx @@ -84,10 +84,10 @@ const EntityEditor = (props: Props) => { window.onbeforeunload = handleUrlChange; }, [handleUrlChange]); - if(entity){ - entityURL = getEntityUrl(entity); + if (entity) { + entityURL = getEntityUrl(entity); } - + return (
diff --git a/src/client/entity-editor/identifier-editor/identifier-row.tsx b/src/client/entity-editor/identifier-editor/identifier-row.tsx index 285d66ba3a..e81c91aaa1 100644 --- a/src/client/entity-editor/identifier-editor/identifier-row.tsx +++ b/src/client/entity-editor/identifier-editor/identifier-row.tsx @@ -31,12 +31,12 @@ import { } from '../validators/common'; import type {Dispatch} from 'redux'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; +import IdentifierLink from '../../components/pages/entities/identifiers-links.js'; import Select from 'react-select'; import ValueField from './value-field'; import {collapseWhiteSpaces} from '../../../common/helpers/utils'; import {connect} from 'react-redux'; import {faTimes} from '@fortawesome/free-solid-svg-icons'; -import IdentifierLink from "../../components/pages/entities/identifiers-links.js" type OwnProps = { @@ -131,11 +131,11 @@ function IdentifierRow({ {typeValue && valueValue && ( - + Preview Link: - - - + + + )}
diff --git a/src/client/helpers/utils.tsx b/src/client/helpers/utils.tsx index cb70e02a2a..a7b2e078a6 100644 --- a/src/client/helpers/utils.tsx +++ b/src/client/helpers/utils.tsx @@ -213,7 +213,7 @@ export function stringToHTMLWithLinks(content: string) { cleanUrl = url.substring(0, firstUnbalancedParanthesis); suffix = url.substring(firstUnbalancedParanthesis); } - let link = `${cleanUrl}`; + const link = `${cleanUrl}`; return link + suffix; } ); diff --git a/src/common/helpers/search.ts b/src/common/helpers/search.ts index e1092311eb..0c2a3ab1cd 100644 --- a/src/common/helpers/search.ts +++ b/src/common/helpers/search.ts @@ -19,9 +19,9 @@ import * as commonUtils from './utils'; import {camelCase, isString, snakeCase, upperFirst} from 'lodash'; - -import ElasticSearch from '@elastic/elasticsearch'; +import ElasticSearch, {ApiResponse, type Client, type ClientOptions} from '@elastic/elasticsearch'; import type {EntityTypeString} from 'bookbrainz-data/lib/types/entity'; +import {type ORM} from 'bookbrainz-data'; import httpStatus from 'http-status'; import log from 'log'; @@ -33,7 +33,7 @@ const _bulkIndexSize = 10000; const _retryDelay = 10; const _maxJitter = 75; -let _client = null; +let _client:Client = null; function sanitizeEntityType(type) { if (!type) { @@ -189,39 +189,45 @@ export async function _bulkIndexEntities(entities) { operationSucceeded = true; - // eslint-disable-next-line no-await-in-loop - const response = await _client.bulk({ - body: bulkOperations - }).catch(error => { log.error('error bulk indexing entities for search:', error); }); - - /* - * In case of failed index operations, the promise won't be rejected; - * instead, we have to inspect the response and respond to any failures - * individually. - */ - if (response?.errors === true) { - entitiesToIndex = response.items.reduce((accumulator, item) => { - // We currently only handle queue overrun - if (item.index.status === httpStatus.TOO_MANY_REQUESTS) { - const failedEntity = entities.find( - (element) => (element.bbid ?? element.id) === item.index._id - ); - - accumulator.push(failedEntity); - } + try { + // eslint-disable-next-line no-await-in-loop + const {body: bulkResponse} = await _client.bulk({ + body: bulkOperations + }); + + /* + * In case of failed index operations, the promise won't be rejected; + * instead, we have to inspect the response and respond to any failures + * individually. + */ + if (bulkResponse?.errors === true) { + entitiesToIndex = bulkResponse.items.reduce((accumulator, item) => { + // We currently only handle queue overrun + if (item.index.status === httpStatus.TOO_MANY_REQUESTS) { + const failedEntity = entities.find( + (element) => (element.bbid ?? element.id) === item.index._id + ); + + accumulator.push(failedEntity); + } - return accumulator; - }, []); + return accumulator; + }, []); - if (entitiesToIndex.length) { - operationSucceeded = false; + if (entitiesToIndex.length) { + operationSucceeded = false; - const jitter = Math.random() * _maxJitter; - // eslint-disable-next-line no-await-in-loop - await new Promise(resolve => setTimeout(resolve, _retryDelay + jitter)); + const jitter = Math.random() * _maxJitter; + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => setTimeout(resolve, _retryDelay + jitter)); + } } } + catch (error) { + log.error('error bulk indexing entities for search:', error); + operationSucceeded = false; + } } } @@ -587,22 +593,37 @@ export function searchByName(orm, name, type, size, from) { return _searchForEntities(orm, dslQuery); } -export async function init(orm, options) { - if (!isString(options.host)) { - options.host = 'localhost:9200'; +/** + * Search init + * @description Sets up the search server connection with defaults, + * and returns a connection status boolean + * @param {ORM} orm the BookBrainz ORM + * @param {ClientOptions} [options] Optional (but recommended) connection settings, will provide defaults if missing + * @returns {Promise} A Promise which resolves to the connection status boolean + */ +export async function init(orm: ORM, options:ClientOptions) { + if (!isString(options.node)) { + const defaultOptions:ClientOptions = { + node: 'http://localhost:9200', + requestTimeout: 60000 + }; + log.warning('ElasticSearch configuration not provided. Using default settings.'); + _client = new ElasticSearch.Client(defaultOptions); + } + else { + _client = new ElasticSearch.Client(options); } - - _client = new ElasticSearch.Client(options); - - // Automatically index on app startup if we haven't already try { - const mainIndexExists = await _client.indices.exists({index: _index}); - if (mainIndexExists) { - return null; - } - return generateIndex(orm); + await _client.ping(); } catch (error) { - return null; + log.warning('Could not connect to ElasticSearch:', error.toString()); + return false; + } + const mainIndexExists = await _client.indices.exists({index: _index}); + if (!mainIndexExists) { + // Automatically index on app startup if we haven't already, but don't block app setup + generateIndex(orm).catch(log.error); } + return true; } diff --git a/src/server/app.js b/src/server/app.js index 52a957f6a8..db9684bff7 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -95,10 +95,13 @@ if (config.influx) { // Authentication code depends on session, so init session first const authInitiated = auth.init(app); +let searchInitiated; // Clone search config to prevent error if starting webserver and api // https://github.com/elastic/elasticsearch-js/issues/33 -search.init(app.locals.orm, Object.assign({}, config.search)); +(async function initializeSearch() { + searchInitiated = await search.init(app.locals.orm, Object.assign({}, config.search)); +})(); // Set up constants that will remain valid for the life of the app debug(`Git revision: ${siteRevision}`); @@ -124,6 +127,14 @@ app.use((req, res, next) => { }); } + if (!searchInitiated) { + const msg = 'We could not connect to our search server, all search functionality is unavailable.'; + res.locals.alerts.push({ + level: 'danger', + message: `${msg}` + }); + } + if (!req.session || !authInitiated) { res.locals.alerts.push({ level: 'danger',