@vida-global/core 2.5.0 → 3.0.1
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/README.md +1 -0
- package/index.js +4 -0
- package/lib/activeRecord/baseRecord.js +2 -2
- package/lib/search/README.md +232 -0
- package/lib/search/abstractSearchClient.js +188 -0
- package/lib/search/errors.js +31 -0
- package/lib/search/index.js +16 -0
- package/lib/search/openSearchClient.js +319 -0
- package/lib/search/query.js +163 -0
- package/lib/server/authorizer.js +42 -0
- package/lib/server/index.js +4 -2
- package/lib/server/serverController.js +37 -3
- package/package.json +2 -1
- package/test/activeRecord/baseRecord.test.js +22 -0
- package/test/activeRecord/db/schema.test.js +15 -0
- package/test/search/abstractSearchClient.test.js +354 -0
- package/test/search/errors.test.js +41 -0
- package/test/search/helpers/searchClient.js +137 -0
- package/test/search/openSearchClient.test.js +514 -0
- package/test/search/query.test.js +286 -0
- package/test/search/serializer.test.js +202 -0
- package/test/server/authorizer.test.js +105 -0
- package/test/server/helpers/authorizer.js +19 -0
- package/test/server/serverController.test.js +77 -0
package/README.md
CHANGED
|
@@ -9,3 +9,4 @@ This package contains core elements used across all Vida Apps. It is included in
|
|
|
9
9
|
- [Logger](lib/logger) — scoped, child-aware logger with environment-driven levels.
|
|
10
10
|
- [VidaServer](lib/server) — Express-based server with auto-loaded controllers, validation, rendering, callbacks, cookies, and SSE streaming.
|
|
11
11
|
- [JobQueue](lib/jobQueue) — BullMQ-backed queue with workers, retries/backoff, progress reporting, and queue inspection helpers.
|
|
12
|
+
- [Search](lib/search) — backend-agnostic search client with a chainable query vocabulary, an OpenSearch Serverless implementation, and index mapping management.
|
package/index.js
CHANGED
|
@@ -6,12 +6,14 @@ const JobQueue = require('./lib/jobQueue');
|
|
|
6
6
|
const { logger } = require('./lib/logger');
|
|
7
7
|
const redisLibs = require('./lib/redis');
|
|
8
8
|
const cacheLibs = require('./lib/cache');
|
|
9
|
+
const searchLibs = require('./lib/search');
|
|
9
10
|
const serverLibs = require('./lib/server');
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
const {
|
|
13
14
|
AbstractServerError,
|
|
14
15
|
ApiDocsGenerator,
|
|
16
|
+
Authorizer,
|
|
15
17
|
VidaServer,
|
|
16
18
|
VidaServerController,
|
|
17
19
|
} = serverLibs;
|
|
@@ -21,11 +23,13 @@ const serverErrorsToExport = Object.values(serverLibs).filter(obj => obj.prototy
|
|
|
21
23
|
module.exports = {
|
|
22
24
|
ActiveRecord,
|
|
23
25
|
APM,
|
|
26
|
+
Authorizer,
|
|
24
27
|
...httpLibs,
|
|
25
28
|
JobQueue,
|
|
26
29
|
logger,
|
|
27
30
|
...cacheLibs,
|
|
28
31
|
...redisLibs,
|
|
32
|
+
...searchLibs,
|
|
29
33
|
...serverErrorsToExport,
|
|
30
34
|
ApiDocsGenerator,
|
|
31
35
|
AppDependencies,
|
|
@@ -379,7 +379,7 @@ class BaseRecord extends Model {
|
|
|
379
379
|
/***********************************************************************************************
|
|
380
380
|
* CACHING
|
|
381
381
|
***********************************************************************************************/
|
|
382
|
-
static async _cachedFind(ids, { clear=false }={}) {
|
|
382
|
+
static async _cachedFind(ids, { clear=false, expireSeconds=null }={}) {
|
|
383
383
|
const multiFind = Array.isArray(ids)
|
|
384
384
|
const idsToFind = multiFind ? ids : [ids];
|
|
385
385
|
const keyPairs = idsToFind.map(id => [this._recordCacheKey(id), id]);
|
|
@@ -387,7 +387,7 @@ class BaseRecord extends Model {
|
|
|
387
387
|
const idsToKeys = Object.fromEntries(keyPairs.map(p => p.reverse()));
|
|
388
388
|
const keys = Object.keys(keysToIds);
|
|
389
389
|
|
|
390
|
-
let records = await this.cachedFetch(keys, { clear }, async (missedKeys) => {
|
|
390
|
+
let records = await this.cachedFetch(keys, { clear, expireSeconds }, async (missedKeys) => {
|
|
391
391
|
const idsToFind = missedKeys.map(key => keysToIds[key]);
|
|
392
392
|
const pk = this.primaryKeyAttribute
|
|
393
393
|
const records = await this.where({[pk]: idsToFind});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# Search
|
|
2
|
+
`Search` is a backend-agnostic search client. A caller chains a query in plain vocabulary
|
|
3
|
+
(`equals`, `range`, `search`) and a backend subclass turns it into whatever its engine speaks.
|
|
4
|
+
`OpenSearchClient` is the only backend shipped today; the vocabulary carries no OpenSearch syntax,
|
|
5
|
+
so another engine can be added without changing a single call site.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Setup
|
|
9
|
+
|
|
10
|
+
An OpenSearch client signs its requests with AWS SigV4 and reads four environment variables:
|
|
11
|
+
|
|
12
|
+
| Variable | Purpose |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `OPENSEARCH_REGION` | AWS region of the collection. Falls back to `DEFAULT_AWS_REGION`. |
|
|
15
|
+
| `DEFAULT_AWS_REGION` | Region used when `OPENSEARCH_REGION` is unset. |
|
|
16
|
+
| `OPENSEARCH_AWS_KEY` | Access key id. |
|
|
17
|
+
| `OPENSEARCH_AWS_SECRET` | Secret access key. |
|
|
18
|
+
|
|
19
|
+
The collection endpoint is not an environment variable the library reads. Each subclass declares
|
|
20
|
+
its own, so one application can talk to several collections. Environments are kept apart by
|
|
21
|
+
pointing that endpoint at a different collection per deploy — index names are not namespaced.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Defining a client
|
|
25
|
+
|
|
26
|
+
One subclass per index. It declares where the index lives, what it is called, and what its mapping
|
|
27
|
+
should be.
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
const { OpenSearchClient } = require('@vida-global/core');
|
|
31
|
+
|
|
32
|
+
class MessageSearchClient extends OpenSearchClient {
|
|
33
|
+
get endpoint() { return process.env.OPENSEARCH_MESSAGE_NODE; }
|
|
34
|
+
get index() { return 'messages'; }
|
|
35
|
+
|
|
36
|
+
get mapping() {
|
|
37
|
+
return {
|
|
38
|
+
properties: {
|
|
39
|
+
roomId: { type: 'keyword' },
|
|
40
|
+
message: { type: 'text' },
|
|
41
|
+
timestamp: { type: 'date' }
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
## Searching
|
|
50
|
+
|
|
51
|
+
`query()` returns a `Query`. Every method on it returns a **new** `Query`, so a part-built query is
|
|
52
|
+
safe to hold onto and reuse. `fetch()` runs it.
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
const client = new MessageSearchClient();
|
|
56
|
+
|
|
57
|
+
const { total, results } = await client.query()
|
|
58
|
+
.equals('status', 'success')
|
|
59
|
+
.range('timestamp', { gte: startAt, lte: endAt })
|
|
60
|
+
.sort('timestamp', 'desc')
|
|
61
|
+
.limit(50)
|
|
62
|
+
.fetch();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`fetch` resolves to `{ total, results }`. `total` is the exact number of matching documents, not
|
|
66
|
+
the number on this page. Each entry in `results` is `{ id, score, document }`, where `document` is
|
|
67
|
+
what was stored. Override `_buildResult(hit)` on a subclass to reshape a single result.
|
|
68
|
+
|
|
69
|
+
`AbstractSearchClient` assembles that envelope from five small hooks the backend implements:
|
|
70
|
+
`_hitsFromResponse`, `_totalFromResponse`, `_idFromHit`, `_scoreFromHit`, and `_documentFromHit`.
|
|
71
|
+
Override whichever one differs rather than rewriting the whole response loop.
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
### Conditions
|
|
75
|
+
|
|
76
|
+
| Method | Matches |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `equals(field, value)` | The field holds exactly this value |
|
|
79
|
+
| `in(field, values)` | The field holds any one of these values |
|
|
80
|
+
| `range(field, bounds)` | `bounds` is any of `gte`, `gt`, `lte`, `lt` |
|
|
81
|
+
| `exists(field)` | The field is present |
|
|
82
|
+
| `missing(field)` | The field is absent |
|
|
83
|
+
| `matches(field, text, options)` | Text match, fuzzy by default. `options.fuzziness` overrides it |
|
|
84
|
+
| `phrase(field, text)` | The words appear together, in order |
|
|
85
|
+
| `fullText(text, options)` | Text match across several fields at once, with per-field weights |
|
|
86
|
+
| `nested(path, query)` | A sub-query against an array of objects |
|
|
87
|
+
|
|
88
|
+
Field names are passed through untouched. OpenSearch stores a string field twice — analyzed for
|
|
89
|
+
text search, and exact under a `.keyword` suffix — so an exact match on a text field asks for it
|
|
90
|
+
by name:
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
client.query().equals('roomId.keyword', roomId)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
### Weighting fields in a text search
|
|
98
|
+
|
|
99
|
+
`fullText` looks in several fields at once. `boost` makes a hit in one field count for more than a
|
|
100
|
+
hit in another. Anything left out of `boost` carries a weight of 1.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
client.query().fullText(queryText, {
|
|
104
|
+
fields: ['message', 'fromUser', 'summary'],
|
|
105
|
+
boost: { fromUser: 5, summary: 5 }
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Pass `wildcard: true` to match partial words. It finds `invoice` when the user types `voic`, but it
|
|
110
|
+
is slow on a large index and it weakens relevance scoring, so leave it off unless partial matching
|
|
111
|
+
is what you want.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
### Combining conditions
|
|
115
|
+
|
|
116
|
+
Chained conditions are combined with AND. `or` and `not` take either a condition or a whole
|
|
117
|
+
sub-query, and both accept several at once:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
client.query()
|
|
121
|
+
.equals('status', 'success')
|
|
122
|
+
.or(
|
|
123
|
+
client.query().equals('from', userId),
|
|
124
|
+
client.query().equals('to', userId)
|
|
125
|
+
)
|
|
126
|
+
.not(client.query().exists('deletedAt'))
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
One `or(a, b)` call builds one group: the record must match `a` or `b`. Calling `or` twice adds to
|
|
130
|
+
that same group. For two independent groups, nest a sub-query in each.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
### Paging and ordering
|
|
134
|
+
|
|
135
|
+
`limit`, `offset`, `sort`, and `minScore` chain like conditions and travel with the query.
|
|
136
|
+
|
|
137
|
+
`minScore` drops weak matches. The engine applies it, so `total` counts only what survived.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
### Reading across date-partitioned indexes
|
|
141
|
+
|
|
142
|
+
Pass a suffix to the constructor to write into a dated index. Reads then target that one index
|
|
143
|
+
unless you ask for the wildcard.
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const client = new ApiLogSearchClient('2026-10');
|
|
147
|
+
|
|
148
|
+
// writes api-logs-2026-10
|
|
149
|
+
await client.append(logObject);
|
|
150
|
+
|
|
151
|
+
// reads api-logs-2026-10
|
|
152
|
+
await client.query().equals('accountId', id).fetch();
|
|
153
|
+
|
|
154
|
+
// reads api-logs-*
|
|
155
|
+
await client.query().equals('accountId', id).fetch({ indexWildcard: true });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Writing
|
|
160
|
+
|
|
161
|
+
| Method | What it does |
|
|
162
|
+
|---|---|
|
|
163
|
+
| `upsert(id, document)` | Creates the document, or merges into it if the id already exists |
|
|
164
|
+
| `append(document)` | Writes a new document and lets the engine assign the id |
|
|
165
|
+
| `bulkUpsert(documents)` | One request for many. Each entry is `{ id, document }` |
|
|
166
|
+
| `delete(id)` | Removes one document |
|
|
167
|
+
| `deleteWhere(query)` | Removes everything the query matches. Returns how many |
|
|
168
|
+
|
|
169
|
+
Every write throws on failure. `bulkUpsert` throws `BulkWriteError`, which carries a `failures`
|
|
170
|
+
list of `{ id, reason }` — a bulk request answers `200` even when individual documents fail, so
|
|
171
|
+
the error is built from the response body, not the status code.
|
|
172
|
+
|
|
173
|
+
`deleteWhere` searches a page, deletes each id, and repeats until nothing matches. OpenSearch
|
|
174
|
+
Serverless has no delete-by-query API, so there is no single-request version. A failure stops the
|
|
175
|
+
loop and throws; what was already deleted stays deleted.
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
### Collection types
|
|
179
|
+
|
|
180
|
+
An OpenSearch Serverless collection has a type, and only a `SEARCH` collection accepts a write
|
|
181
|
+
addressed to a specific id. Nothing is declared up front: when the collection refuses such a
|
|
182
|
+
write, `upsert` and `bulkUpsert` catch the refusal and raise `UnsupportedOperationError` with the
|
|
183
|
+
original error attached as `cause`.
|
|
184
|
+
|
|
185
|
+
`_isUnsupportedOperation(error)` is what reads a refusal out of an OpenSearch error. Override it
|
|
186
|
+
on a subclass if the message you see does not match.
|
|
187
|
+
|
|
188
|
+
`append` works on every collection type.
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
## Index mappings
|
|
192
|
+
|
|
193
|
+
A mapping is the index's schema. `syncIndex` makes the live index match what the subclass
|
|
194
|
+
declares: it creates the index if it is missing, then pushes the mapping.
|
|
195
|
+
|
|
196
|
+
```js
|
|
197
|
+
await MessageSearchClient.syncIndex();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
OpenSearch accepts a new field on an existing index but rejects a changed type on an existing
|
|
201
|
+
field. `syncIndex` does not diff anything — it sends the mapping and lets a conflict throw.
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
## Retries
|
|
205
|
+
|
|
206
|
+
Nothing retries by default. A subclass opts in by raising `numRetries`:
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
class MessageSearchClient extends OpenSearchClient {
|
|
210
|
+
get numRetries() { return 3; }
|
|
211
|
+
get retryBackoffDelay() { return 1000; }
|
|
212
|
+
get retryMaxDelay() { return 8000; }
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`OpenSearchClient` only retries server errors — status 500 through 599. Override `shouldRetry`
|
|
217
|
+
to change that.
|
|
218
|
+
|
|
219
|
+
Retries cover `fetch`, `upsert`, `delete`, and `syncIndex`. They never cover `append` or
|
|
220
|
+
`bulkUpsert`: those carry no caller-supplied id, so re-sending a request that actually succeeded
|
|
221
|
+
would write the document twice.
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
## Adding a backend
|
|
225
|
+
|
|
226
|
+
Subclass `AbstractSearchClient` and implement the hooks it declares: `_serializeConditions`,
|
|
227
|
+
`_execute`, the five response hooks, the four write hooks, and the three index hooks.
|
|
228
|
+
`AbstractSearchClient` owns the parts that are not engine-specific — the retry loop, the
|
|
229
|
+
`deleteWhere` loop, the `syncIndex` order, and the result envelope.
|
|
230
|
+
|
|
231
|
+
Collaborators are reached through seams, so tests inject doubles by subclassing and overriding
|
|
232
|
+
`openSearchClientClass`, `awsSignerFactory`, or `loggerLibrary`.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const { Query } = require('./query');
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AbstractSearchClient {
|
|
5
|
+
#indexSuffix;
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
constructor(indexSuffix) {
|
|
9
|
+
if (this.constructor === AbstractSearchClient) {
|
|
10
|
+
throw new Error('SearchClients must be subclasses of AbstractSearchClient');
|
|
11
|
+
}
|
|
12
|
+
this.#indexSuffix = indexSuffix;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
get indexSuffix() { return this.#indexSuffix; }
|
|
17
|
+
get deleteBatchSize() { return 500; }
|
|
18
|
+
get numRetries() { return 0; }
|
|
19
|
+
get retryBackoffDelay() { return 1000; }
|
|
20
|
+
get retryMaxDelay() { return 8000; }
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
query() {
|
|
24
|
+
return new Query(this);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
/***********************************************************************************************
|
|
29
|
+
* READ
|
|
30
|
+
***********************************************************************************************/
|
|
31
|
+
async fetch(conditions, options = {}) {
|
|
32
|
+
const serializedConditions = this._serializeConditions(conditions);
|
|
33
|
+
const response = await this._withRetry(() => this._execute(serializedConditions, options));
|
|
34
|
+
return this._processResponse(response);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_processResponse(response) {
|
|
39
|
+
const hits = this._hitsFromResponse(response)
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
total: this._totalFromResponse(response),
|
|
43
|
+
results: hits.hits.map(hit => this._buildResult(hit))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_buildResult(hit) {
|
|
49
|
+
const id = this._idFromHit(hit);
|
|
50
|
+
const score = this._scoreFromHit(hit);
|
|
51
|
+
const document = this._documentFromHit(hit);
|
|
52
|
+
return { id, score, document };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
_hitsFromResponse(response) { throw new Error('Not Implemented') }
|
|
57
|
+
_totalFromResponse(response) { throw new Error('Not Implemented') }
|
|
58
|
+
_idFromHit(hit) { throw new Error('Not Implemented') }
|
|
59
|
+
_scoreFromHit(hit) { throw new Error('Not Implemented') }
|
|
60
|
+
_documentFromHit(hit) { throw new Error('Not Implemented') }
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
/***********************************************************************************************
|
|
64
|
+
* WRITE
|
|
65
|
+
***********************************************************************************************/
|
|
66
|
+
async upsert(id, document) {
|
|
67
|
+
return await this._withRetry(() => this._upsert(id, document));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async delete(id) {
|
|
72
|
+
return await this._withRetry(() => this._delete(id));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async append(document) {
|
|
77
|
+
return await this._append(document);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async bulkUpsert(documents) {
|
|
82
|
+
return await this._bulkUpsert(documents);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async deleteWhere(query) {
|
|
87
|
+
const deletedIds = new Set();
|
|
88
|
+
let batch = await this.#fetchDeletionBatch(query, deletedIds);
|
|
89
|
+
|
|
90
|
+
while (batch.length) {
|
|
91
|
+
await this.#deleteBatch(batch, deletedIds);
|
|
92
|
+
batch = await this.#fetchDeletionBatch(query, deletedIds);
|
|
93
|
+
}
|
|
94
|
+
return deletedIds.size;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async #fetchDeletionBatch(query, deletedIds) {
|
|
99
|
+
const { results } = await query.limit(this.deleteBatchSize).fetch();
|
|
100
|
+
return results.map(result => result.id).filter(id => !deletedIds.has(id));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async #deleteBatch(ids, deletedIds) {
|
|
105
|
+
for (const id of ids) {
|
|
106
|
+
await this.delete(id);
|
|
107
|
+
deletedIds.add(id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/***********************************************************************************************
|
|
113
|
+
* ADMIN
|
|
114
|
+
***********************************************************************************************/
|
|
115
|
+
static async syncIndex(indexSuffix) {
|
|
116
|
+
return await new this(indexSuffix).syncIndex();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
async syncIndex() {
|
|
121
|
+
return await this._withRetry(() => this._syncIndex());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async _syncIndex() {
|
|
126
|
+
if (!await this._indexExists()) await this._createIndex();
|
|
127
|
+
return await this._putMapping();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
/***********************************************************************************************
|
|
132
|
+
* RETRIES
|
|
133
|
+
***********************************************************************************************/
|
|
134
|
+
shouldRetry(attemptCount, _error) {
|
|
135
|
+
return attemptCount - 1 < this.numRetries;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async _withRetry(operation) {
|
|
140
|
+
let attemptCount = 0;
|
|
141
|
+
|
|
142
|
+
while (true) {
|
|
143
|
+
attemptCount += 1;
|
|
144
|
+
try {
|
|
145
|
+
return await operation();
|
|
146
|
+
} catch (error) {
|
|
147
|
+
await this.#handleFailedAttempt(error, attemptCount);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async #handleFailedAttempt(error, attemptCount) {
|
|
154
|
+
if (!this.shouldRetry(attemptCount, error)) {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
await this.#waitBeforeRetry(attemptCount);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async #waitBeforeRetry(attemptCount) {
|
|
162
|
+
const backoff = 2 ** (attemptCount - 1) * this.retryBackoffDelay;
|
|
163
|
+
const delay = Math.min(backoff, this.retryMaxDelay);
|
|
164
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
/***********************************************************************************************
|
|
169
|
+
* BACKEND HOOKS
|
|
170
|
+
***********************************************************************************************/
|
|
171
|
+
get index() { throw new Error('Not Implemented') }
|
|
172
|
+
get mapping() { throw new Error('Not Implemented') }
|
|
173
|
+
|
|
174
|
+
_serializeConditions(conditions) { throw new Error('Not Implemented') }
|
|
175
|
+
_execute(serializedConditions, options) { throw new Error('Not Implemented') }
|
|
176
|
+
_upsert(id, document) { throw new Error('Not Implemented') }
|
|
177
|
+
_append(document) { throw new Error('Not Implemented') }
|
|
178
|
+
_bulkUpsert(documents) { throw new Error('Not Implemented') }
|
|
179
|
+
_delete(id) { throw new Error('Not Implemented') }
|
|
180
|
+
_indexExists() { throw new Error('Not Implemented') }
|
|
181
|
+
_createIndex() { throw new Error('Not Implemented') }
|
|
182
|
+
_putMapping() { throw new Error('Not Implemented') }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
module.exports = {
|
|
187
|
+
AbstractSearchClient
|
|
188
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class SearchError extends Error {
|
|
2
|
+
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = this.constructor.name;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnsupportedOperationError extends SearchError {}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BulkWriteError extends SearchError {
|
|
14
|
+
#failures;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
constructor(message, failures) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.#failures = failures;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
get failures() { return this.#failures; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
BulkWriteError,
|
|
29
|
+
SearchError,
|
|
30
|
+
UnsupportedOperationError
|
|
31
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { AbstractSearchClient } = require('./abstractSearchClient');
|
|
2
|
+
const { OpenSearchClient } = require('./openSearchClient');
|
|
3
|
+
const { Query } = require('./query');
|
|
4
|
+
const { SearchError,
|
|
5
|
+
BulkWriteError,
|
|
6
|
+
UnsupportedOperationError } = require('./errors');
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
AbstractSearchClient,
|
|
11
|
+
SearchError,
|
|
12
|
+
BulkWriteError,
|
|
13
|
+
OpenSearchClient,
|
|
14
|
+
Query,
|
|
15
|
+
UnsupportedOperationError
|
|
16
|
+
};
|