@backstage/plugin-search-backend-module-elasticsearch 0.0.3 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @backstage/plugin-search-backend-module-elasticsearch
2
2
 
3
+ ## 0.0.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 68512f5178: Add `newClient()` method to re-use the configuration of the elastic search
8
+ engine with custom clients
9
+
10
+ ## 0.0.6
11
+
12
+ ### Patch Changes
13
+
14
+ - dcd1a0c3f4: Minor improvement to the API reports, by not unpacking arguments directly
15
+
16
+ ## 0.0.5
17
+
18
+ ### Patch Changes
19
+
20
+ - 36350bf8b3: Pinning version of elastic search client to 7.13.0 to prevent breaking change towards third party ElasticSearch clusters on 7.14.0.
21
+
22
+ ## 0.0.4
23
+
24
+ ### Patch Changes
25
+
26
+ - f0c2c81676: Added rejectUnauthorized config option
27
+
3
28
  ## 0.0.3
4
29
 
5
30
  ### Patch Changes
package/config.d.ts CHANGED
@@ -20,90 +20,103 @@ export interface Config {
20
20
  /**
21
21
  * Options for ElasticSearch
22
22
  */
23
- elasticsearch?:
24
- | // elastic = Elastic.co ElasticSearch provider
25
- {
26
- provider: 'elastic';
27
-
23
+ elasticsearch?: {
24
+ /** Miscellaneous options for the client */
25
+ clientOptions?: {
26
+ ssl?: {
28
27
  /**
29
- * Elastic.co CloudID
30
- * See: https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-connecting.html#authentication
28
+ * If true the server will reject any connection which is not
29
+ * authorized with the list of supplied CAs.
30
+ * @default true
31
31
  */
32
- cloudId: string;
33
-
34
- auth: {
35
- username: string;
32
+ rejectUnauthorized?: boolean;
33
+ };
34
+ } & (
35
+ | {
36
+ // elastic = Elastic.co ElasticSearch provider
37
+ provider: 'elastic';
36
38
 
37
39
  /**
38
- * @visibility secret
40
+ * Elastic.co CloudID
41
+ * See: https://www.elastic.co/guide/en/elasticsearch/client/javascript-api/current/client-connecting.html#authentication
39
42
  */
40
- password: string;
41
- };
42
- }
43
+ cloudId: string;
43
44
 
44
- /**
45
- * AWS = Amazon Elasticsearch Service provider
46
- *
47
- * Authentication is handled using the default AWS credentials provider chain
48
- */
49
- | {
50
- provider: 'aws';
45
+ auth: {
46
+ username: string;
51
47
 
52
- /**
53
- * Node configuration.
54
- * URL AWS ES endpoint to connect to.
55
- * Eg. https://my-es-cluster.eu-west-1.es.amazonaws.com
56
- */
57
- node: string;
58
- }
48
+ /**
49
+ * @visibility secret
50
+ */
51
+ password: string;
52
+ };
53
+ }
59
54
 
60
- /**
61
- * Standard ElasticSearch
62
- *
63
- * Includes self-hosted clusters and others that provide direct connection via an endpoint
64
- * and authentication method (see possible authentication options below)
65
- */
66
- | {
67
- /**
68
- * Node configuration.
69
- * URL/URLS to ElasticSearch node to connect to.
70
- * Either direct URL like 'https://localhost:9200' or with credentials like 'https://username:password@localhost:9200'
71
- */
72
- node: string | string[];
55
+ /**
56
+ * AWS = Amazon Elasticsearch Service provider
57
+ *
58
+ * Authentication is handled using the default AWS credentials provider chain
59
+ */
60
+ | {
61
+ provider: 'aws';
73
62
 
74
- /**
75
- * Authentication credentials for ElasticSearch
76
- * If both ApiKey/Bearer token and username+password is provided, tokens take precedence
77
- */
78
- auth?:
79
- | {
80
- username: string;
63
+ /**
64
+ * Node configuration.
65
+ * URL AWS ES endpoint to connect to.
66
+ * Eg. https://my-es-cluster.eu-west-1.es.amazonaws.com
67
+ */
68
+ node: string;
69
+ }
81
70
 
82
- /**
83
- * @visibility secret
84
- */
85
- password: string;
86
- }
87
- | {
88
- /**
89
- * Base64 Encoded API key to be used to connect to the cluster.
90
- * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html
91
- *
92
- * @visibility secret
93
- */
94
- apiKey: string;
95
- };
96
- /* TODO(kuangp): unsupported until @elastic/elasticsearch@7.14 is released
71
+ /**
72
+ * Standard ElasticSearch
73
+ *
74
+ * Includes self-hosted clusters and others that provide direct connection via an endpoint
75
+ * and authentication method (see possible authentication options below)
76
+ */
97
77
  | {
78
+ /**
79
+ * Node configuration.
80
+ * URL/URLS to ElasticSearch node to connect to.
81
+ * Either direct URL like 'https://localhost:9200' or with credentials like 'https://username:password@localhost:9200'
82
+ */
83
+ node: string | string[];
98
84
 
99
- /**
100
- * Bearer authentication token to connect to the cluster.
101
- * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html
102
- *
103
- * @visibility secret
104
- *
105
- bearer: string;
106
- };*/
107
- };
85
+ /**
86
+ * Authentication credentials for ElasticSearch
87
+ * If both ApiKey/Bearer token and username+password is provided, tokens take precedence
88
+ */
89
+ auth?:
90
+ | {
91
+ username: string;
92
+
93
+ /**
94
+ * @visibility secret
95
+ */
96
+ password: string;
97
+ }
98
+ | {
99
+ /**
100
+ * Base64 Encoded API key to be used to connect to the cluster.
101
+ * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-api-key.html
102
+ *
103
+ * @visibility secret
104
+ */
105
+ apiKey: string;
106
+ };
107
+ /* TODO(kuangp): unsupported until @elastic/elasticsearch@7.14 is released
108
+ | {
109
+
110
+ /**
111
+ * Bearer authentication token to connect to the cluster.
112
+ * See: https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-create-service-token.html
113
+ *
114
+ * @visibility secret
115
+ *
116
+ bearer: string;
117
+ };*/
118
+ }
119
+ );
120
+ };
108
121
  };
109
122
  }
package/dist/index.cjs.js CHANGED
@@ -20,12 +20,13 @@ function isBlank(str) {
20
20
  return lodash.isEmpty(str) && !lodash.isNumber(str) || lodash.isNaN(str);
21
21
  }
22
22
  class ElasticSearchSearchEngine {
23
- constructor(elasticSearchClient, aliasPostfix, indexPrefix, logger) {
24
- this.elasticSearchClient = elasticSearchClient;
23
+ constructor(elasticSearchClientOptions, aliasPostfix, indexPrefix, logger) {
24
+ this.elasticSearchClientOptions = elasticSearchClientOptions;
25
25
  this.aliasPostfix = aliasPostfix;
26
26
  this.indexPrefix = indexPrefix;
27
27
  this.logger = logger;
28
28
  this.indexSeparator = "-index__";
29
+ this.elasticSearchClient = this.newClient((options) => new elasticsearch.Client(options));
29
30
  }
30
31
  static async fromConfig({
31
32
  logger,
@@ -33,68 +34,36 @@ class ElasticSearchSearchEngine {
33
34
  aliasPostfix = `search`,
34
35
  indexPrefix = ``
35
36
  }) {
36
- return new ElasticSearchSearchEngine(await ElasticSearchSearchEngine.constructElasticSearchClient(logger, config.getConfig("search.elasticsearch")), aliasPostfix, indexPrefix, logger);
37
- }
38
- static async constructElasticSearchClient(logger, config) {
39
- if (!config) {
40
- throw new Error("No elastic search config found");
41
- }
42
- if (config.getOptionalString("provider") === "elastic") {
37
+ const options = await createElasticSearchClientOptions(config.getConfig("search.elasticsearch"));
38
+ if (options.provider === "elastic") {
43
39
  logger.info("Initializing Elastic.co ElasticSearch search engine.");
44
- const authConfig2 = config.getConfig("auth");
45
- return new elasticsearch.Client({
46
- cloud: {
47
- id: config.getString("cloudId")
48
- },
49
- auth: {
50
- username: authConfig2.getString("username"),
51
- password: authConfig2.getString("password")
52
- }
53
- });
54
- }
55
- if (config.getOptionalString("provider") === "aws") {
40
+ } else if (options.provider === "aws") {
56
41
  logger.info("Initializing AWS ElasticSearch search engine.");
57
- const awsCredentials = await awsEsConnection.awsGetCredentials();
58
- const AWSConnection = awsEsConnection.createAWSConnection(awsCredentials);
59
- return new elasticsearch.Client({
60
- node: config.getString("node"),
61
- ...AWSConnection
62
- });
42
+ } else {
43
+ logger.info("Initializing ElasticSearch search engine.");
63
44
  }
64
- logger.info("Initializing ElasticSearch search engine.");
65
- const authConfig = config.getOptionalConfig("auth");
66
- const auth = authConfig && (authConfig.has("apiKey") ? {
67
- apiKey: authConfig.getString("apiKey")
68
- } : {
69
- username: authConfig.getString("username"),
70
- password: authConfig.getString("password")
71
- });
72
- return new elasticsearch.Client({
73
- node: config.getString("node"),
74
- auth
75
- });
45
+ return new ElasticSearchSearchEngine(options, aliasPostfix, indexPrefix, logger);
76
46
  }
77
- translator({
78
- term,
79
- filters = {},
80
- types,
81
- pageCursor
82
- }) {
47
+ newClient(create) {
48
+ return create(this.elasticSearchClientOptions);
49
+ }
50
+ translator(query) {
51
+ const { term, filters = {}, types, pageCursor } = query;
83
52
  const filter = Object.entries(filters).filter(([_, value]) => Boolean(value)).map(([key, value]) => {
84
53
  if (["string", "number", "boolean"].includes(typeof value)) {
85
- return esb__default['default'].matchQuery(key, value.toString());
54
+ return esb__default["default"].matchQuery(key, value.toString());
86
55
  }
87
56
  if (Array.isArray(value)) {
88
- return esb__default['default'].boolQuery().should(value.map((it) => esb__default['default'].matchQuery(key, it.toString())));
57
+ return esb__default["default"].boolQuery().should(value.map((it) => esb__default["default"].matchQuery(key, it.toString())));
89
58
  }
90
59
  this.logger.error("Failed to query, unrecognized filter type", key, value);
91
60
  throw new Error("Failed to add filters to query. Unrecognized filter type");
92
61
  });
93
- const query = isBlank(term) ? esb__default['default'].matchAllQuery() : esb__default['default'].multiMatchQuery(["*"], term).fuzziness("auto").minimumShouldMatch(1);
62
+ const esbQuery = isBlank(term) ? esb__default["default"].matchAllQuery() : esb__default["default"].multiMatchQuery(["*"], term).fuzziness("auto").minimumShouldMatch(1);
94
63
  const pageSize = 25;
95
- const {page} = decodePageCursor(pageCursor);
64
+ const { page } = decodePageCursor(pageCursor);
96
65
  return {
97
- elasticSearchQuery: esb__default['default'].requestBodySearch().query(esb__default['default'].boolQuery().filter(filter).must([query])).from(page * pageSize).size(pageSize).toJSON(),
66
+ elasticSearchQuery: esb__default["default"].requestBodySearch().query(esb__default["default"].boolQuery().filter(filter).must([esbQuery])).from(page * pageSize).size(pageSize).toJSON(),
98
67
  documentTypes: types,
99
68
  pageSize
100
69
  };
@@ -120,7 +89,7 @@ class ElasticSearchSearchEngine {
120
89
  datasource: documents,
121
90
  onDocument() {
122
91
  return {
123
- index: {_index: index}
92
+ index: { _index: index }
124
93
  };
125
94
  },
126
95
  refreshOnCompletion: index
@@ -129,8 +98,8 @@ class ElasticSearchSearchEngine {
129
98
  await this.elasticSearchClient.indices.updateAliases({
130
99
  body: {
131
100
  actions: [
132
- {remove: {index: this.constructIndexName(type, "*"), alias}},
133
- {add: {index, alias}}
101
+ { remove: { index: this.constructIndexName(type, "*"), alias } },
102
+ { add: { index, alias } }
134
103
  ]
135
104
  }
136
105
  });
@@ -155,18 +124,18 @@ class ElasticSearchSearchEngine {
155
124
  }
156
125
  }
157
126
  async query(query) {
158
- const {elasticSearchQuery, documentTypes, pageSize} = this.translator(query);
127
+ const { elasticSearchQuery, documentTypes, pageSize } = this.translator(query);
159
128
  const queryIndices = documentTypes ? documentTypes.map((it) => this.constructSearchAlias(it)) : this.constructSearchAlias("*");
160
129
  try {
161
130
  const result = await this.elasticSearchClient.search({
162
131
  index: queryIndices,
163
132
  body: elasticSearchQuery
164
133
  });
165
- const {page} = decodePageCursor(query.pageCursor);
134
+ const { page } = decodePageCursor(query.pageCursor);
166
135
  const hasNextPage = result.body.hits.total.value > page * pageSize;
167
136
  const hasPreviousPage = page > 0;
168
- const nextPageCursor = hasNextPage ? encodePageCursor({page: page + 1}) : void 0;
169
- const previousPageCursor = hasPreviousPage ? encodePageCursor({page: page - 1}) : void 0;
137
+ const nextPageCursor = hasNextPage ? encodePageCursor({ page: page + 1 }) : void 0;
138
+ const previousPageCursor = hasPreviousPage ? encodePageCursor({ page: page - 1 }) : void 0;
170
139
  return {
171
140
  results: result.body.hits.hits.map((d) => ({
172
141
  type: this.getTypeFromIndex(d._index),
@@ -177,7 +146,7 @@ class ElasticSearchSearchEngine {
177
146
  };
178
147
  } catch (e) {
179
148
  this.logger.error(`Failed to query documents for indices ${queryIndices}`, e);
180
- return Promise.reject({results: []});
149
+ return Promise.reject({ results: [] });
181
150
  }
182
151
  }
183
152
  constructIndexName(type, postFix) {
@@ -193,15 +162,70 @@ class ElasticSearchSearchEngine {
193
162
  }
194
163
  function decodePageCursor(pageCursor) {
195
164
  if (!pageCursor) {
196
- return {page: 0};
165
+ return { page: 0 };
197
166
  }
198
167
  return {
199
168
  page: Number(Buffer.from(pageCursor, "base64").toString("utf-8"))
200
169
  };
201
170
  }
202
- function encodePageCursor({page}) {
171
+ function encodePageCursor({ page }) {
203
172
  return Buffer.from(`${page}`, "utf-8").toString("base64");
204
173
  }
174
+ async function createElasticSearchClientOptions(config) {
175
+ if (!config) {
176
+ throw new Error("No elastic search config found");
177
+ }
178
+ const clientOptionsConfig = config.getOptionalConfig("clientOptions");
179
+ const sslConfig = clientOptionsConfig == null ? void 0 : clientOptionsConfig.getOptionalConfig("ssl");
180
+ if (config.getOptionalString("provider") === "elastic") {
181
+ const authConfig2 = config.getConfig("auth");
182
+ return {
183
+ provider: "elastic",
184
+ cloud: {
185
+ id: config.getString("cloudId")
186
+ },
187
+ auth: {
188
+ username: authConfig2.getString("username"),
189
+ password: authConfig2.getString("password")
190
+ },
191
+ ...sslConfig ? {
192
+ ssl: {
193
+ rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
194
+ }
195
+ } : {}
196
+ };
197
+ }
198
+ if (config.getOptionalString("provider") === "aws") {
199
+ const awsCredentials = await awsEsConnection.awsGetCredentials();
200
+ const AWSConnection = awsEsConnection.createAWSConnection(awsCredentials);
201
+ return {
202
+ provider: "aws",
203
+ node: config.getString("node"),
204
+ ...AWSConnection,
205
+ ...sslConfig ? {
206
+ ssl: {
207
+ rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
208
+ }
209
+ } : {}
210
+ };
211
+ }
212
+ const authConfig = config.getOptionalConfig("auth");
213
+ const auth = authConfig && (authConfig.has("apiKey") ? {
214
+ apiKey: authConfig.getString("apiKey")
215
+ } : {
216
+ username: authConfig.getString("username"),
217
+ password: authConfig.getString("password")
218
+ });
219
+ return {
220
+ node: config.getString("node"),
221
+ auth,
222
+ ...sslConfig ? {
223
+ ssl: {
224
+ rejectUnauthorized: sslConfig == null ? void 0 : sslConfig.getOptionalBoolean("rejectUnauthorized")
225
+ }
226
+ } : {}
227
+ };
228
+ }
205
229
 
206
230
  exports.ElasticSearchSearchEngine = ElasticSearchSearchEngine;
207
231
  //# sourceMappingURL=index.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs.js","sources":["../src/engines/ElasticSearchSearchEngine.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n awsGetCredentials,\n createAWSConnection,\n} from '@acuris/aws-es-connection';\nimport { Config } from '@backstage/config';\nimport {\n IndexableDocument,\n SearchEngine,\n SearchQuery,\n SearchResultSet,\n} from '@backstage/search-common';\nimport { Client } from '@elastic/elasticsearch';\nimport esb from 'elastic-builder';\nimport { isEmpty, isNaN as nan, isNumber } from 'lodash';\nimport { Logger } from 'winston';\n\nexport type ConcreteElasticSearchQuery = {\n documentTypes?: string[];\n elasticSearchQuery: Object;\n pageSize: number;\n};\n\ntype ElasticSearchQueryTranslator = (\n query: SearchQuery,\n) => ConcreteElasticSearchQuery;\n\ntype ElasticSearchOptions = {\n logger: Logger;\n config: Config;\n aliasPostfix?: string;\n indexPrefix?: string;\n};\n\ntype ElasticSearchResult = {\n _index: string;\n _type: string;\n _score: number;\n _source: IndexableDocument;\n};\n\nfunction duration(startTimestamp: [number, number]): string {\n const delta = process.hrtime(startTimestamp);\n const seconds = delta[0] + delta[1] / 1e9;\n return `${seconds.toFixed(1)}s`;\n}\n\nfunction isBlank(str: string) {\n return (isEmpty(str) && !isNumber(str)) || nan(str);\n}\n\nexport class ElasticSearchSearchEngine implements SearchEngine {\n constructor(\n private readonly elasticSearchClient: Client,\n private readonly aliasPostfix: string,\n private readonly indexPrefix: string,\n private readonly logger: Logger,\n ) {}\n\n static async fromConfig({\n logger,\n config,\n aliasPostfix = `search`,\n indexPrefix = ``,\n }: ElasticSearchOptions) {\n return new ElasticSearchSearchEngine(\n await ElasticSearchSearchEngine.constructElasticSearchClient(\n logger,\n config.getConfig('search.elasticsearch'),\n ),\n aliasPostfix,\n indexPrefix,\n logger,\n );\n }\n\n private static async constructElasticSearchClient(\n logger: Logger,\n config?: Config,\n ) {\n if (!config) {\n throw new Error('No elastic search config found');\n }\n\n if (config.getOptionalString('provider') === 'elastic') {\n logger.info('Initializing Elastic.co ElasticSearch search engine.');\n const authConfig = config.getConfig('auth');\n return new Client({\n cloud: {\n id: config.getString('cloudId'),\n },\n auth: {\n username: authConfig.getString('username'),\n password: authConfig.getString('password'),\n },\n });\n }\n if (config.getOptionalString('provider') === 'aws') {\n logger.info('Initializing AWS ElasticSearch search engine.');\n const awsCredentials = await awsGetCredentials();\n const AWSConnection = createAWSConnection(awsCredentials);\n return new Client({\n node: config.getString('node'),\n ...AWSConnection,\n });\n }\n logger.info('Initializing ElasticSearch search engine.');\n const authConfig = config.getOptionalConfig('auth');\n const auth =\n authConfig &&\n (authConfig.has('apiKey')\n ? {\n apiKey: authConfig.getString('apiKey'),\n }\n : {\n username: authConfig.getString('username'),\n password: authConfig.getString('password'),\n });\n return new Client({\n node: config.getString('node'),\n auth,\n });\n }\n\n protected translator({\n term,\n filters = {},\n types,\n pageCursor,\n }: SearchQuery): ConcreteElasticSearchQuery {\n const filter = Object.entries(filters)\n .filter(([_, value]) => Boolean(value))\n .map(([key, value]: [key: string, value: any]) => {\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n return esb.matchQuery(key, value.toString());\n }\n if (Array.isArray(value)) {\n return esb\n .boolQuery()\n .should(value.map(it => esb.matchQuery(key, it.toString())));\n }\n this.logger.error(\n 'Failed to query, unrecognized filter type',\n key,\n value,\n );\n throw new Error(\n 'Failed to add filters to query. Unrecognized filter type',\n );\n });\n const query = isBlank(term)\n ? esb.matchAllQuery()\n : esb\n .multiMatchQuery(['*'], term)\n .fuzziness('auto')\n .minimumShouldMatch(1);\n const pageSize = 25;\n const { page } = decodePageCursor(pageCursor);\n\n return {\n elasticSearchQuery: esb\n .requestBodySearch()\n .query(esb.boolQuery().filter(filter).must([query]))\n .from(page * pageSize)\n .size(pageSize)\n .toJSON(),\n documentTypes: types,\n pageSize,\n };\n }\n\n setTranslator(translator: ElasticSearchQueryTranslator) {\n this.translator = translator;\n }\n\n async index(type: string, documents: IndexableDocument[]): Promise<void> {\n this.logger.info(\n `Started indexing ${documents.length} documents for index ${type}`,\n );\n const startTimestamp = process.hrtime();\n const alias = this.constructSearchAlias(type);\n const index = this.constructIndexName(type, `${Date.now()}`);\n try {\n const aliases = await this.elasticSearchClient.cat.aliases({\n format: 'json',\n name: alias,\n });\n const removableIndices = aliases.body.map(\n (r: Record<string, any>) => r.index,\n );\n\n await this.elasticSearchClient.indices.create({\n index,\n });\n const result = await this.elasticSearchClient.helpers.bulk({\n datasource: documents,\n onDocument() {\n return {\n index: { _index: index },\n };\n },\n refreshOnCompletion: index,\n });\n\n this.logger.info(\n `Indexing completed for index ${type} in ${duration(startTimestamp)}`,\n result,\n );\n await this.elasticSearchClient.indices.updateAliases({\n body: {\n actions: [\n { remove: { index: this.constructIndexName(type, '*'), alias } },\n { add: { index, alias } },\n ],\n },\n });\n\n this.logger.info('Removing stale search indices', removableIndices);\n if (removableIndices.length) {\n await this.elasticSearchClient.indices.delete({\n index: removableIndices,\n });\n }\n } catch (e) {\n this.logger.error(`Failed to index documents for type ${type}`, e);\n const response = await this.elasticSearchClient.indices.exists({\n index,\n });\n const indexCreated = response.body;\n if (indexCreated) {\n this.logger.info(`Removing created index ${index}`);\n await this.elasticSearchClient.indices.delete({\n index,\n });\n }\n }\n }\n\n async query(query: SearchQuery): Promise<SearchResultSet> {\n const { elasticSearchQuery, documentTypes, pageSize } =\n this.translator(query);\n const queryIndices = documentTypes\n ? documentTypes.map(it => this.constructSearchAlias(it))\n : this.constructSearchAlias('*');\n try {\n const result = await this.elasticSearchClient.search({\n index: queryIndices,\n body: elasticSearchQuery,\n });\n const { page } = decodePageCursor(query.pageCursor);\n const hasNextPage = result.body.hits.total.value > page * pageSize;\n const hasPreviousPage = page > 0;\n const nextPageCursor = hasNextPage\n ? encodePageCursor({ page: page + 1 })\n : undefined;\n const previousPageCursor = hasPreviousPage\n ? encodePageCursor({ page: page - 1 })\n : undefined;\n\n return {\n results: result.body.hits.hits.map((d: ElasticSearchResult) => ({\n type: this.getTypeFromIndex(d._index),\n document: d._source,\n })),\n nextPageCursor,\n previousPageCursor,\n };\n } catch (e) {\n this.logger.error(\n `Failed to query documents for indices ${queryIndices}`,\n e,\n );\n return Promise.reject({ results: [] });\n }\n }\n\n private readonly indexSeparator = '-index__';\n\n private constructIndexName(type: string, postFix: string) {\n return `${this.indexPrefix}${type}${this.indexSeparator}${postFix}`;\n }\n\n private getTypeFromIndex(index: string) {\n return index\n .substring(this.indexPrefix.length)\n .split(this.indexSeparator)[0];\n }\n\n private constructSearchAlias(type: string) {\n const postFix = this.aliasPostfix ? `__${this.aliasPostfix}` : '';\n return `${this.indexPrefix}${type}${postFix}`;\n }\n}\n\nexport function decodePageCursor(pageCursor?: string): { page: number } {\n if (!pageCursor) {\n return { page: 0 };\n }\n\n return {\n page: Number(Buffer.from(pageCursor, 'base64').toString('utf-8')),\n };\n}\n\nexport function encodePageCursor({ page }: { page: number }): string {\n return Buffer.from(`${page}`, 'utf-8').toString('base64');\n}\n"],"names":["isEmpty","isNumber","nan","Client","awsGetCredentials","createAWSConnection","esb"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs.js","sources":["../src/engines/ElasticSearchSearchEngine.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n awsGetCredentials,\n createAWSConnection,\n} from '@acuris/aws-es-connection';\nimport { Config } from '@backstage/config';\nimport {\n IndexableDocument,\n SearchEngine,\n SearchQuery,\n SearchResultSet,\n} from '@backstage/search-common';\nimport { Client } from '@elastic/elasticsearch';\nimport esb from 'elastic-builder';\nimport { isEmpty, isNaN as nan, isNumber } from 'lodash';\nimport { Logger } from 'winston';\n\nimport type { ElasticSearchClientOptions } from './ElasticSearchClientOptions';\n\nexport type { ElasticSearchClientOptions };\n\nexport type ConcreteElasticSearchQuery = {\n documentTypes?: string[];\n elasticSearchQuery: Object;\n pageSize: number;\n};\n\ntype ElasticSearchQueryTranslator = (\n query: SearchQuery,\n) => ConcreteElasticSearchQuery;\n\ntype ElasticSearchOptions = {\n logger: Logger;\n config: Config;\n aliasPostfix?: string;\n indexPrefix?: string;\n};\n\ntype ElasticSearchResult = {\n _index: string;\n _type: string;\n _score: number;\n _source: IndexableDocument;\n};\n\nfunction duration(startTimestamp: [number, number]): string {\n const delta = process.hrtime(startTimestamp);\n const seconds = delta[0] + delta[1] / 1e9;\n return `${seconds.toFixed(1)}s`;\n}\n\nfunction isBlank(str: string) {\n return (isEmpty(str) && !isNumber(str)) || nan(str);\n}\n\n/**\n * @public\n */\nexport class ElasticSearchSearchEngine implements SearchEngine {\n private readonly elasticSearchClient: Client;\n\n constructor(\n private readonly elasticSearchClientOptions: ElasticSearchClientOptions,\n private readonly aliasPostfix: string,\n private readonly indexPrefix: string,\n private readonly logger: Logger,\n ) {\n this.elasticSearchClient = this.newClient(options => new Client(options));\n }\n\n static async fromConfig({\n logger,\n config,\n aliasPostfix = `search`,\n indexPrefix = ``,\n }: ElasticSearchOptions) {\n const options = await createElasticSearchClientOptions(\n config.getConfig('search.elasticsearch'),\n );\n if (options.provider === 'elastic') {\n logger.info('Initializing Elastic.co ElasticSearch search engine.');\n } else if (options.provider === 'aws') {\n logger.info('Initializing AWS ElasticSearch search engine.');\n } else {\n logger.info('Initializing ElasticSearch search engine.');\n }\n\n return new ElasticSearchSearchEngine(\n options,\n aliasPostfix,\n indexPrefix,\n logger,\n );\n }\n\n /**\n * Create a custom search client from the derived elastic search\n * configuration. This need not be the same client that the engine uses\n * internally.\n */\n newClient<T>(create: (options: ElasticSearchClientOptions) => T): T {\n return create(this.elasticSearchClientOptions);\n }\n\n protected translator(query: SearchQuery): ConcreteElasticSearchQuery {\n const { term, filters = {}, types, pageCursor } = query;\n\n const filter = Object.entries(filters)\n .filter(([_, value]) => Boolean(value))\n .map(([key, value]: [key: string, value: any]) => {\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n return esb.matchQuery(key, value.toString());\n }\n if (Array.isArray(value)) {\n return esb\n .boolQuery()\n .should(value.map(it => esb.matchQuery(key, it.toString())));\n }\n this.logger.error(\n 'Failed to query, unrecognized filter type',\n key,\n value,\n );\n throw new Error(\n 'Failed to add filters to query. Unrecognized filter type',\n );\n });\n const esbQuery = isBlank(term)\n ? esb.matchAllQuery()\n : esb\n .multiMatchQuery(['*'], term)\n .fuzziness('auto')\n .minimumShouldMatch(1);\n const pageSize = 25;\n const { page } = decodePageCursor(pageCursor);\n\n return {\n elasticSearchQuery: esb\n .requestBodySearch()\n .query(esb.boolQuery().filter(filter).must([esbQuery]))\n .from(page * pageSize)\n .size(pageSize)\n .toJSON(),\n documentTypes: types,\n pageSize,\n };\n }\n\n setTranslator(translator: ElasticSearchQueryTranslator) {\n this.translator = translator;\n }\n\n async index(type: string, documents: IndexableDocument[]): Promise<void> {\n this.logger.info(\n `Started indexing ${documents.length} documents for index ${type}`,\n );\n const startTimestamp = process.hrtime();\n const alias = this.constructSearchAlias(type);\n const index = this.constructIndexName(type, `${Date.now()}`);\n try {\n const aliases = await this.elasticSearchClient.cat.aliases({\n format: 'json',\n name: alias,\n });\n const removableIndices = aliases.body.map(\n (r: Record<string, any>) => r.index,\n );\n\n await this.elasticSearchClient.indices.create({\n index,\n });\n const result = await this.elasticSearchClient.helpers.bulk({\n datasource: documents,\n onDocument() {\n return {\n index: { _index: index },\n };\n },\n refreshOnCompletion: index,\n });\n\n this.logger.info(\n `Indexing completed for index ${type} in ${duration(startTimestamp)}`,\n result,\n );\n await this.elasticSearchClient.indices.updateAliases({\n body: {\n actions: [\n { remove: { index: this.constructIndexName(type, '*'), alias } },\n { add: { index, alias } },\n ],\n },\n });\n\n this.logger.info('Removing stale search indices', removableIndices);\n if (removableIndices.length) {\n await this.elasticSearchClient.indices.delete({\n index: removableIndices,\n });\n }\n } catch (e) {\n this.logger.error(`Failed to index documents for type ${type}`, e);\n const response = await this.elasticSearchClient.indices.exists({\n index,\n });\n const indexCreated = response.body;\n if (indexCreated) {\n this.logger.info(`Removing created index ${index}`);\n await this.elasticSearchClient.indices.delete({\n index,\n });\n }\n }\n }\n\n async query(query: SearchQuery): Promise<SearchResultSet> {\n const { elasticSearchQuery, documentTypes, pageSize } =\n this.translator(query);\n const queryIndices = documentTypes\n ? documentTypes.map(it => this.constructSearchAlias(it))\n : this.constructSearchAlias('*');\n try {\n const result = await this.elasticSearchClient.search({\n index: queryIndices,\n body: elasticSearchQuery,\n });\n const { page } = decodePageCursor(query.pageCursor);\n const hasNextPage = result.body.hits.total.value > page * pageSize;\n const hasPreviousPage = page > 0;\n const nextPageCursor = hasNextPage\n ? encodePageCursor({ page: page + 1 })\n : undefined;\n const previousPageCursor = hasPreviousPage\n ? encodePageCursor({ page: page - 1 })\n : undefined;\n\n return {\n results: result.body.hits.hits.map((d: ElasticSearchResult) => ({\n type: this.getTypeFromIndex(d._index),\n document: d._source,\n })),\n nextPageCursor,\n previousPageCursor,\n };\n } catch (e) {\n this.logger.error(\n `Failed to query documents for indices ${queryIndices}`,\n e,\n );\n return Promise.reject({ results: [] });\n }\n }\n\n private readonly indexSeparator = '-index__';\n\n private constructIndexName(type: string, postFix: string) {\n return `${this.indexPrefix}${type}${this.indexSeparator}${postFix}`;\n }\n\n private getTypeFromIndex(index: string) {\n return index\n .substring(this.indexPrefix.length)\n .split(this.indexSeparator)[0];\n }\n\n private constructSearchAlias(type: string) {\n const postFix = this.aliasPostfix ? `__${this.aliasPostfix}` : '';\n return `${this.indexPrefix}${type}${postFix}`;\n }\n}\n\nexport function decodePageCursor(pageCursor?: string): { page: number } {\n if (!pageCursor) {\n return { page: 0 };\n }\n\n return {\n page: Number(Buffer.from(pageCursor, 'base64').toString('utf-8')),\n };\n}\n\nexport function encodePageCursor({ page }: { page: number }): string {\n return Buffer.from(`${page}`, 'utf-8').toString('base64');\n}\n\nasync function createElasticSearchClientOptions(\n config?: Config,\n): Promise<ElasticSearchClientOptions> {\n if (!config) {\n throw new Error('No elastic search config found');\n }\n const clientOptionsConfig = config.getOptionalConfig('clientOptions');\n const sslConfig = clientOptionsConfig?.getOptionalConfig('ssl');\n\n if (config.getOptionalString('provider') === 'elastic') {\n const authConfig = config.getConfig('auth');\n return {\n provider: 'elastic',\n cloud: {\n id: config.getString('cloudId'),\n },\n auth: {\n username: authConfig.getString('username'),\n password: authConfig.getString('password'),\n },\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n }\n if (config.getOptionalString('provider') === 'aws') {\n const awsCredentials = await awsGetCredentials();\n const AWSConnection = createAWSConnection(awsCredentials);\n return {\n provider: 'aws',\n node: config.getString('node'),\n ...AWSConnection,\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n }\n const authConfig = config.getOptionalConfig('auth');\n const auth =\n authConfig &&\n (authConfig.has('apiKey')\n ? {\n apiKey: authConfig.getString('apiKey'),\n }\n : {\n username: authConfig.getString('username'),\n password: authConfig.getString('password'),\n });\n return {\n node: config.getString('node'),\n auth,\n ...(sslConfig\n ? {\n ssl: {\n rejectUnauthorized:\n sslConfig?.getOptionalBoolean('rejectUnauthorized'),\n },\n }\n : {}),\n };\n}\n"],"names":["isEmpty","isNumber","nan","Client","esb","awsGetCredentials","createAWSConnection"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,100 @@
1
+ /// <reference types="node" />
1
2
  import { Config } from '@backstage/config';
2
3
  import { SearchEngine, SearchQuery, IndexableDocument, SearchResultSet } from '@backstage/search-common';
3
- import { Client } from '@elastic/elasticsearch';
4
4
  import { Logger } from 'winston';
5
+ import { ConnectionOptions } from 'tls';
6
+
7
+ /**
8
+ * Options used to configure the `@elastic/elasticsearch` client and
9
+ * are what will be passed as an argument to the
10
+ * {@link ElasticSearchEngine.newClient} method
11
+ *
12
+ * They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`,
13
+ * but are maintained separately so that this interface is not coupled to
14
+ */
15
+ interface ElasticSearchClientOptions {
16
+ provider?: 'aws' | 'elastic';
17
+ node?: string | string[] | ElasticSearchNodeOptions | ElasticSearchNodeOptions[];
18
+ nodes?: string | string[] | ElasticSearchNodeOptions | ElasticSearchNodeOptions[];
19
+ Transport?: ElasticSearchTransportConstructor;
20
+ Connection?: ElasticSearchConnectionConstructor;
21
+ maxRetries?: number;
22
+ requestTimeout?: number;
23
+ pingTimeout?: number;
24
+ sniffInterval?: number | boolean;
25
+ sniffOnStart?: boolean;
26
+ sniffEndpoint?: string;
27
+ sniffOnConnectionFault?: boolean;
28
+ resurrectStrategy?: 'ping' | 'optimistic' | 'none';
29
+ suggestCompression?: boolean;
30
+ compression?: 'gzip';
31
+ ssl?: ConnectionOptions;
32
+ agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false;
33
+ nodeFilter?: (connection: any) => boolean;
34
+ nodeSelector?: ((connections: any[]) => any) | string;
35
+ headers?: Record<string, any>;
36
+ opaqueIdPrefix?: string;
37
+ name?: string | symbol;
38
+ auth?: ElasticSearchAuth;
39
+ proxy?: string | URL;
40
+ enableMetaHeader?: boolean;
41
+ cloud?: {
42
+ id: string;
43
+ username?: string;
44
+ password?: string;
45
+ };
46
+ disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor';
47
+ }
48
+ declare type ElasticSearchAuth = {
49
+ username: string;
50
+ password: string;
51
+ } | {
52
+ apiKey: string | {
53
+ id: string;
54
+ api_key: string;
55
+ };
56
+ };
57
+ interface ElasticSearchNodeOptions {
58
+ url: URL;
59
+ id?: string;
60
+ agent?: ElasticSearchAgentOptions;
61
+ ssl?: ConnectionOptions;
62
+ headers?: Record<string, any>;
63
+ roles?: {
64
+ master: boolean;
65
+ data: boolean;
66
+ ingest: boolean;
67
+ ml: boolean;
68
+ };
69
+ }
70
+ interface ElasticSearchAgentOptions {
71
+ keepAlive?: boolean;
72
+ keepAliveMsecs?: number;
73
+ maxSockets?: number;
74
+ maxFreeSockets?: number;
75
+ }
76
+ interface ElasticSearchConnectionConstructor {
77
+ new (opts?: any): any;
78
+ statuses: {
79
+ ALIVE: string;
80
+ DEAD: string;
81
+ };
82
+ roles: {
83
+ MASTER: string;
84
+ DATA: string;
85
+ INGEST: string;
86
+ ML: string;
87
+ };
88
+ }
89
+ interface ElasticSearchTransportConstructor {
90
+ new (opts?: any): any;
91
+ sniffReasons: {
92
+ SNIFF_ON_START: string;
93
+ SNIFF_INTERVAL: string;
94
+ SNIFF_ON_CONNECTION_FAULT: string;
95
+ DEFAULT: string;
96
+ };
97
+ }
5
98
 
6
99
  declare type ConcreteElasticSearchQuery = {
7
100
  documentTypes?: string[];
@@ -15,15 +108,24 @@ declare type ElasticSearchOptions = {
15
108
  aliasPostfix?: string;
16
109
  indexPrefix?: string;
17
110
  };
111
+ /**
112
+ * @public
113
+ */
18
114
  declare class ElasticSearchSearchEngine implements SearchEngine {
19
- private readonly elasticSearchClient;
115
+ private readonly elasticSearchClientOptions;
20
116
  private readonly aliasPostfix;
21
117
  private readonly indexPrefix;
22
118
  private readonly logger;
23
- constructor(elasticSearchClient: Client, aliasPostfix: string, indexPrefix: string, logger: Logger);
119
+ private readonly elasticSearchClient;
120
+ constructor(elasticSearchClientOptions: ElasticSearchClientOptions, aliasPostfix: string, indexPrefix: string, logger: Logger);
24
121
  static fromConfig({ logger, config, aliasPostfix, indexPrefix, }: ElasticSearchOptions): Promise<ElasticSearchSearchEngine>;
25
- private static constructElasticSearchClient;
26
- protected translator({ term, filters, types, pageCursor, }: SearchQuery): ConcreteElasticSearchQuery;
122
+ /**
123
+ * Create a custom search client from the derived elastic search
124
+ * configuration. This need not be the same client that the engine uses
125
+ * internally.
126
+ */
127
+ newClient<T>(create: (options: ElasticSearchClientOptions) => T): T;
128
+ protected translator(query: SearchQuery): ConcreteElasticSearchQuery;
27
129
  setTranslator(translator: ElasticSearchQueryTranslator): void;
28
130
  index(type: string, documents: IndexableDocument[]): Promise<void>;
29
131
  query(query: SearchQuery): Promise<SearchResultSet>;
@@ -33,4 +135,4 @@ declare class ElasticSearchSearchEngine implements SearchEngine {
33
135
  private constructSearchAlias;
34
136
  }
35
137
 
36
- export { ElasticSearchSearchEngine };
138
+ export { ElasticSearchClientOptions, ElasticSearchSearchEngine };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@backstage/plugin-search-backend-module-elasticsearch",
3
- "version": "0.0.3",
3
+ "description": "A module for the search backend that implements search using ElasticSearch",
4
+ "version": "0.0.7",
4
5
  "main": "dist/index.cjs.js",
5
6
  "types": "dist/index.d.ts",
6
7
  "license": "Apache-2.0",
@@ -22,15 +23,15 @@
22
23
  "@acuris/aws-es-connection": "^2.2.0",
23
24
  "@backstage/config": "^0.1.8",
24
25
  "@backstage/search-common": "^0.2.0",
25
- "@elastic/elasticsearch": "^7.13.0",
26
+ "@elastic/elasticsearch": "7.13.0",
26
27
  "aws-sdk": "^2.948.0",
27
28
  "elastic-builder": "^2.16.0",
28
29
  "lodash": "^4.17.21",
29
30
  "winston": "^3.2.1"
30
31
  },
31
32
  "devDependencies": {
32
- "@backstage/backend-common": "^0.9.1",
33
- "@backstage/cli": "^0.7.10",
33
+ "@backstage/backend-common": "^0.9.14",
34
+ "@backstage/cli": "^0.10.2",
34
35
  "@elastic/elasticsearch-mock": "^0.3.0"
35
36
  },
36
37
  "files": [
@@ -41,5 +42,5 @@
41
42
  "testEnvironment": "node"
42
43
  },
43
44
  "configSchema": "config.d.ts",
44
- "gitHead": "174a8b532e8b6dda0e733ec943d1aa7187a3821a"
45
+ "gitHead": "9ff0f1e76d4510edda2f1b1b3e58cba168a76190"
45
46
  }