@salto-io/persistent-pool 0.3.40-main.5ef2e84db → 0.3.40-publishOnlyDist.57b77fb79

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.
Files changed (59) hide show
  1. package/package.json +7 -8
  2. package/dist/test/lib/dynamodb/dynamodb_repo.e2e.test.d.ts +0 -2
  3. package/dist/test/lib/dynamodb/dynamodb_repo.e2e.test.d.ts.map +0 -1
  4. package/dist/test/lib/dynamodb/dynamodb_repo.e2e.test.js +0 -98
  5. package/dist/test/lib/dynamodb/dynamodb_repo.e2e.test.js.map +0 -1
  6. package/dist/test/lib/dynamodb/dynamodb_repo.test.d.ts +0 -2
  7. package/dist/test/lib/dynamodb/dynamodb_repo.test.d.ts.map +0 -1
  8. package/dist/test/lib/dynamodb/dynamodb_repo.test.js +0 -533
  9. package/dist/test/lib/dynamodb/dynamodb_repo.test.js.map +0 -1
  10. package/dist/test/lib/dynamodb/environment/index.d.ts +0 -9
  11. package/dist/test/lib/dynamodb/environment/index.d.ts.map +0 -1
  12. package/dist/test/lib/dynamodb/environment/index.js +0 -70
  13. package/dist/test/lib/dynamodb/environment/index.js.map +0 -1
  14. package/dist/test/lib/dynamodb/environment/types.d.ts +0 -7
  15. package/dist/test/lib/dynamodb/environment/types.d.ts.map +0 -1
  16. package/dist/test/lib/dynamodb/environment/types.js +0 -3
  17. package/dist/test/lib/dynamodb/environment/types.js.map +0 -1
  18. package/dist/test/lib/dynamodb/utils.d.ts +0 -12
  19. package/dist/test/lib/dynamodb/utils.d.ts.map +0 -1
  20. package/dist/test/lib/dynamodb/utils.js +0 -53
  21. package/dist/test/lib/dynamodb/utils.js.map +0 -1
  22. package/dist/test/lib/renewed_lease.test.d.ts +0 -2
  23. package/dist/test/lib/renewed_lease.test.d.ts.map +0 -1
  24. package/dist/test/lib/renewed_lease.test.js +0 -134
  25. package/dist/test/lib/renewed_lease.test.js.map +0 -1
  26. package/dist/test/mock_repo.d.ts +0 -8
  27. package/dist/test/mock_repo.d.ts.map +0 -1
  28. package/dist/test/mock_repo.js +0 -22
  29. package/dist/test/mock_repo.js.map +0 -1
  30. package/dist/test/types.d.ts +0 -7
  31. package/dist/test/types.d.ts.map +0 -1
  32. package/dist/test/types.js +0 -9
  33. package/dist/test/types.js.map +0 -1
  34. package/dist/test/utils/async_to_array.d.ts +0 -3
  35. package/dist/test/utils/async_to_array.d.ts.map +0 -1
  36. package/dist/test/utils/async_to_array.js +0 -32
  37. package/dist/test/utils/async_to_array.js.map +0 -1
  38. package/dist/test/utils/promise_series.d.ts +0 -3
  39. package/dist/test/utils/promise_series.d.ts.map +0 -1
  40. package/dist/test/utils/promise_series.js +0 -28
  41. package/dist/test/utils/promise_series.js.map +0 -1
  42. package/dist/test/utils/random_string.d.ts +0 -6
  43. package/dist/test/utils/random_string.d.ts.map +0 -1
  44. package/dist/test/utils/random_string.js +0 -22
  45. package/dist/test/utils/random_string.js.map +0 -1
  46. package/dist/test/utils/repeat.d.ts +0 -3
  47. package/dist/test/utils/repeat.d.ts.map +0 -1
  48. package/dist/test/utils/repeat.js +0 -20
  49. package/dist/test/utils/repeat.js.map +0 -1
  50. package/dist/test/utils/timing.d.ts +0 -10
  51. package/dist/test/utils/timing.d.ts.map +0 -1
  52. package/dist/test/utils/timing.js +0 -39
  53. package/dist/test/utils/timing.js.map +0 -1
  54. package/dist/tsconfig.tsbuildinfo +0 -14427
  55. package/src/index.ts +0 -21
  56. package/src/lib/dynamodb/dynamodb_repo.ts +0 -487
  57. package/src/lib/dynamodb/utils.ts +0 -168
  58. package/src/lib/renewed_lease.ts +0 -83
  59. package/src/types.ts +0 -106
package/src/index.ts DELETED
@@ -1,21 +0,0 @@
1
- /*
2
- * Copyright 2023 Salto Labs Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with
6
- * the License. You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- export {
17
- repo as dynamoDbRepo,
18
- DynamoRepoOpts as DynamoDbRepoOpts,
19
- } from './lib/dynamodb/dynamodb_repo'
20
- export * from './types'
21
- export { default as RenewedLease, RenewedLeaseOpts } from './lib/renewed_lease'
@@ -1,487 +0,0 @@
1
- /*
2
- * Copyright 2023 Salto Labs Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with
6
- * the License. You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import {
17
- BillingMode,
18
- CreateTableCommandInput,
19
- DynamoDBClient,
20
- DynamoDBClientConfig,
21
- } from '@aws-sdk/client-dynamodb'
22
- import {
23
- DeleteCommand,
24
- DynamoDBDocumentClient,
25
- GetCommand,
26
- PutCommand,
27
- QueryCommand,
28
- QueryCommandInput,
29
- UpdateCommand,
30
- UpdateCommandInput,
31
- } from '@aws-sdk/lib-dynamodb'
32
-
33
- import { functions, retry as retryUtil } from '@salto-io/lowerdash'
34
- import { v4 as uuidv4 } from 'uuid'
35
- import {
36
- InstanceId, Pool, Repo, LeaseWithStatus, Lease,
37
- InstanceNotFoundError, InstanceNotLeasedError, InstanceIdAlreadyRegistered, RetryStrategy,
38
- RepoOpts,
39
- LeaseUpdateOpts,
40
- DEFAULT_LEASE_UPDATE_OPTS,
41
- } from '../../types'
42
- import { Key, dbUtils, dbDocUtils, BatchDeleteOpts, QueryOpts } from './utils'
43
-
44
- const { defaultOpts } = functions
45
- const { withRetry, retryStrategies, RetryError } = retryUtil
46
-
47
- type ItemList = Key[]
48
-
49
- export type DynamoRepoPartialOpts = {
50
- tableName: string
51
- listBatchSize: number | undefined
52
- billingMode: BillingMode
53
- optimisticLockMaxRetries: number
54
- leaseRandomizationRange: number | undefined
55
- batchDelete: BatchDeleteOpts
56
- query: QueryOpts
57
- }
58
-
59
- export type DynamoDbInstances = {
60
- db: DynamoDBClient
61
- dbDoc: DynamoDBDocumentClient
62
- }
63
-
64
- export type DynamoInstancesOrConfig = DynamoDbInstances | {
65
- serviceOpts: DynamoDBClientConfig
66
- }
67
-
68
- export type DynamoRepoRequiredOpts = RepoOpts & DynamoInstancesOrConfig
69
-
70
- export type DynamoRepoOpts = DynamoRepoPartialOpts & DynamoRepoRequiredOpts
71
-
72
- // taken from: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-inference-in-conditional-types
73
- type Unpacked<T> =
74
- T extends (infer U)[] ? U :
75
- T extends (...args: unknown[]) => infer U ? U :
76
- T extends PromiseLike<infer U> ? U :
77
- T;
78
-
79
- const createTableParams = (
80
- { tableName, billingMode }: Pick<DynamoRepoOpts, 'tableName' | 'billingMode'>,
81
- ): CreateTableCommandInput & { TableName: string } => ({
82
- KeySchema: [
83
- { AttributeName: 'type', KeyType: 'HASH' },
84
- { AttributeName: 'id', KeyType: 'RANGE' },
85
- ],
86
- AttributeDefinitions: [
87
- { AttributeName: 'type', AttributeType: 'S' },
88
- { AttributeName: 'id', AttributeType: 'S' },
89
- { AttributeName: 'leaseExpiresBy', AttributeType: 'S' },
90
- ],
91
- LocalSecondaryIndexes: [
92
- {
93
- IndexName: 'leaseExpiresBy',
94
- KeySchema: [
95
- { AttributeName: 'type', KeyType: 'HASH' },
96
- { AttributeName: 'leaseExpiresBy', KeyType: 'RANGE' },
97
- ],
98
- Projection: {
99
- ProjectionType: 'INCLUDE',
100
- NonKeyAttributes: ['version'],
101
- },
102
- },
103
- ],
104
- BillingMode: billingMode,
105
- TableName: tableName,
106
- })
107
-
108
- type LeaseDoc<T> = {
109
- id: string
110
- value: T
111
- version: number
112
- leaseExpiresBy: string
113
- leasingClientId: string
114
- suspensionReason?: string
115
- }
116
-
117
- export const dynamoDbInstances = (opts: DynamoInstancesOrConfig): DynamoDbInstances => {
118
- if ('serviceOpts' in opts) {
119
- const db = new DynamoDBClient(opts.serviceOpts)
120
- const dbDoc = DynamoDBDocumentClient.from(db)
121
- return { db, dbDoc }
122
- }
123
- return { db: opts.db, dbDoc: opts.dbDoc }
124
- }
125
-
126
- const NOT_LEASED = (new Date(0)).toISOString()
127
-
128
- export const repo = defaultOpts.withRequired<
129
- DynamoRepoPartialOpts,
130
- DynamoRepoRequiredOpts,
131
- Promise<Repo>
132
- >(async (opts: DynamoRepoOpts): Promise<Repo> => {
133
- const { db, dbDoc } = dynamoDbInstances(opts)
134
- const utils = dbUtils(db)
135
- const docUtils = dbDocUtils(dbDoc)
136
-
137
- const { clientId } = opts
138
-
139
- const withOptimisticLock = async <TResult>(
140
- f: () => Promise<TResult>,
141
- retryNumber = 0,
142
- ): Promise<TResult> => {
143
- try {
144
- return await f()
145
- } catch (e) {
146
- if (e.toString().includes('ConditionalCheckFailedException')) {
147
- if (retryNumber === opts.optimisticLockMaxRetries) {
148
- throw e
149
- }
150
- return withOptimisticLock(f, retryNumber + 1)
151
- }
152
- throw e
153
- }
154
- }
155
-
156
- const pool = <T>(typeName: string): Pool<T> => {
157
- const queryInput = (
158
- partialQueryInput: Partial<QueryCommandInput> = {},
159
- ): QueryCommandInput => ({
160
- TableName: opts.tableName,
161
- ConsistentRead: true,
162
- Select: 'ALL_ATTRIBUTES',
163
- KeyConditionExpression: '#type = :type',
164
- ExpressionAttributeValues: {
165
- ':type': typeName,
166
- },
167
- ExpressionAttributeNames: {
168
- '#type': 'type',
169
- },
170
- Limit: opts.listBatchSize,
171
- ...partialQueryInput,
172
- })
173
-
174
- const lease = (
175
- returnTimeout: number,
176
- retryStrategy?: () => RetryStrategy,
177
- ): Promise<Lease<T> | null> => withOptimisticLock(async (): Promise<Lease<T> | null> => {
178
- const now = Date.now()
179
- const retry = retryStrategy ?? retryStrategies.none()
180
-
181
- const makeQuery = async (): Promise<ItemList | undefined> => {
182
- const queryResults = await dbDoc.send(new QueryCommand({
183
- TableName: opts.tableName,
184
- IndexName: 'leaseExpiresBy',
185
- ConsistentRead: true,
186
- Select: 'ALL_PROJECTED_ATTRIBUTES',
187
- KeyConditionExpression: '#type = :type and leaseExpiresBy <= :now',
188
- ExpressionAttributeNames: {
189
- '#type': 'type',
190
- },
191
- ExpressionAttributeValues: {
192
- ':type': typeName,
193
- ':now': (new Date(now)).toISOString(),
194
- },
195
- Limit: opts.leaseRandomizationRange,
196
- }))
197
-
198
- const maybeItems = queryResults.Items
199
- const hasItems = maybeItems !== undefined && maybeItems.length !== 0
200
- return hasItems ? maybeItems as ItemList : undefined
201
- }
202
-
203
- let items: ItemList
204
- try {
205
- items = await withRetry(makeQuery, {
206
- strategy: retry,
207
- description: 'waiting for available lease',
208
- }) as ItemList
209
- } catch (e) {
210
- if (e instanceof RetryError && retryStrategy === undefined) {
211
- return null
212
- }
213
- throw e
214
- }
215
-
216
- const itemIndex = Math.floor(Math.random() * items.length)
217
- const { id, version } = items[itemIndex] as Pick<LeaseDoc<T>, 'id' | 'version'>
218
-
219
- const updateResult = await dbDoc.send(new UpdateCommand({
220
- TableName: opts.tableName,
221
- Key: {
222
- type: typeName,
223
- id,
224
- },
225
- UpdateExpression: 'set leaseExpiresBy = :leaseExpiresBy, leasingClientId = :leasingClientId, #version = :newVersion',
226
- ConditionExpression: '#version = :currentVersion',
227
- ExpressionAttributeNames: {
228
- '#version': 'version',
229
- },
230
- ExpressionAttributeValues: {
231
- ':leasingClientId': clientId,
232
- ':currentVersion': version,
233
- ':newVersion': version + 1,
234
- ':leaseExpiresBy': new Date(now + returnTimeout).toISOString(),
235
- },
236
- ReturnValues: 'ALL_NEW',
237
- }))
238
-
239
- return { id, value: (updateResult.Attributes as LeaseDoc<T>).value }
240
- })
241
-
242
- const updateLease = (
243
- id: InstanceId,
244
- updates: Pick<UpdateCommandInput, 'UpdateExpression' | 'ExpressionAttributeValues'>,
245
- updateOpts?: Partial<LeaseUpdateOpts> & { validateLeased: boolean },
246
- ): Promise<void> => {
247
- const { validateClientId, validateLeased } = {
248
- ...DEFAULT_LEASE_UPDATE_OPTS,
249
- ...updateOpts,
250
- }
251
-
252
- return withOptimisticLock(async (): Promise<void> => {
253
- const item = (await dbDoc.send(new GetCommand({
254
- TableName: opts.tableName,
255
- Key: {
256
- type: typeName,
257
- id,
258
- },
259
- }))).Item as LeaseDoc<T> | undefined
260
-
261
- if (item === undefined) {
262
- throw new InstanceNotFoundError({ id, typeName })
263
- }
264
-
265
- if (
266
- (validateLeased && item.leaseExpiresBy === NOT_LEASED)
267
- || (validateClientId && item.leasingClientId !== clientId)
268
- ) {
269
- throw new InstanceNotLeasedError({ id, typeName, clientId })
270
- }
271
-
272
- const { version } = item
273
-
274
- await dbDoc.send(new UpdateCommand({
275
- TableName: opts.tableName,
276
- Key: {
277
- type: typeName,
278
- id,
279
- },
280
- UpdateExpression: updates.UpdateExpression,
281
- ConditionExpression: '#version = :currentVersion',
282
- ExpressionAttributeNames: {
283
- '#version': 'version',
284
- },
285
- ExpressionAttributeValues: {
286
- ':currentVersion': version,
287
- ':newVersion': version + 1,
288
- ...updates.ExpressionAttributeValues,
289
- },
290
- }))
291
- })
292
- }
293
-
294
- return {
295
- async register(value: T, id: InstanceId = uuidv4()): Promise<InstanceId> {
296
- try {
297
- await dbDoc.send(new PutCommand({
298
- TableName: opts.tableName,
299
- Item: {
300
- type: typeName,
301
- id,
302
- version: 0,
303
- value,
304
- leaseExpiresBy: NOT_LEASED,
305
- },
306
- ConditionExpression: 'attribute_not_exists(id)',
307
- }))
308
- return id
309
- } catch (e) {
310
- if (e.toString().includes('ConditionalCheckFailedException')) {
311
- throw new InstanceIdAlreadyRegistered({ id, typeName })
312
- }
313
- throw e
314
- }
315
- },
316
-
317
- async unregister(id: InstanceId): Promise<void> {
318
- try {
319
- await dbDoc.send(new DeleteCommand({
320
- TableName: opts.tableName,
321
- Key: {
322
- type: typeName,
323
- id,
324
- },
325
- ConditionExpression: 'attribute_exists(id)',
326
- }))
327
- } catch (e) {
328
- if (e.toString().includes('ConditionalCheckFailedException')) {
329
- throw new InstanceNotFoundError({ id, typeName })
330
- }
331
- throw e
332
- }
333
- },
334
-
335
- async suspend(
336
- id: InstanceId,
337
- reason: string,
338
- timeout: number,
339
- suspendOpts?: Partial<LeaseUpdateOpts>,
340
- ): Promise<void> {
341
- const now = Date.now()
342
- const leaseExpiresBy = new Date(now + timeout).toISOString()
343
-
344
- return updateLease(
345
- id,
346
- {
347
- UpdateExpression: 'set #version = :newVersion, leaseExpiresBy = :leaseExpiresBy, suspensionReason = :suspensionReason',
348
- ExpressionAttributeValues: {
349
- ':leaseExpiresBy': leaseExpiresBy,
350
- ':suspensionReason': reason,
351
- },
352
- },
353
- { ...suspendOpts, validateLeased: false },
354
- )
355
- },
356
-
357
- async lease(returnTimeout: number): Promise<Lease<T> | null> {
358
- return lease(returnTimeout)
359
- },
360
-
361
- async waitForLease(
362
- returnTimeout: number,
363
- retryStrategy: () => RetryStrategy,
364
- ): Promise<Lease<T>> {
365
- return lease(returnTimeout, retryStrategy) as Promise<Lease<T>>
366
- },
367
-
368
- updateTimeout(
369
- id: InstanceId,
370
- newTimeout: number,
371
- extendOpts?: Partial<LeaseUpdateOpts>,
372
- ): Promise<void> {
373
- const now = Date.now()
374
- const leaseExpiresBy = new Date(now + newTimeout).toISOString()
375
- return updateLease(
376
- id,
377
- {
378
- UpdateExpression: 'set #version = :newVersion, leaseExpiresBy = :leaseExpiresBy',
379
- ExpressionAttributeValues: { ':leaseExpiresBy': leaseExpiresBy },
380
- },
381
- { ...extendOpts, validateLeased: true },
382
- )
383
- },
384
-
385
- return(
386
- id: InstanceId,
387
- returnOpts?: Partial<LeaseUpdateOpts>,
388
- ): Promise<void> {
389
- return updateLease(
390
- id,
391
- {
392
- UpdateExpression: 'set #version = :newVersion, leaseExpiresBy = :NOT_LEASED remove leasingClientId, supensionReason',
393
- ExpressionAttributeValues: { ':NOT_LEASED': NOT_LEASED },
394
- },
395
- { ...returnOpts, validateLeased: true },
396
- )
397
- },
398
-
399
- [Symbol.asyncIterator](): AsyncIterator<LeaseWithStatus<T>> {
400
- const now = Date.now()
401
-
402
- const toLeaseWithStatus = (d: LeaseDoc<T>): LeaseWithStatus<T> => {
403
- const leaseExpiresBy = Date.parse(d.leaseExpiresBy)
404
- const isLeased = leaseExpiresBy > now
405
-
406
- const base = {
407
- id: d.id,
408
- value: d.value,
409
- }
410
-
411
- if (isLeased) {
412
- const unavailableBase = {
413
- ...base,
414
- leaseExpiresBy: new Date(leaseExpiresBy),
415
- clientId: d.leasingClientId,
416
- }
417
- return d.suspensionReason !== undefined
418
- ? { status: 'suspended', ...unavailableBase, suspensionReason: d.suspensionReason }
419
- : { status: 'leased', ...unavailableBase }
420
- }
421
-
422
- return { status: 'available', ...base }
423
- }
424
-
425
- const iter = docUtils.queryIterator<LeaseDoc<T>>(queryInput(), opts.query)
426
-
427
- return {
428
- async next(): Promise<IteratorResult<LeaseWithStatus<T>>> {
429
- const { done, value } = await iter.next()
430
- return {
431
- done,
432
- value: value && toLeaseWithStatus(value),
433
- } as IteratorResult<LeaseWithStatus<T>>
434
- },
435
- }
436
- },
437
-
438
- async clear(): Promise<void> {
439
- type QueryResult = { id: InstanceId }
440
- const batchIter = docUtils.queryBatchIterator<QueryResult>(queryInput({
441
- Select: 'SPECIFIC_ATTRIBUTES',
442
- ProjectionExpression: 'id',
443
- }), opts.query)
444
-
445
- const keysToDelete: Key[] = []
446
-
447
- let batch: Unpacked<ReturnType<typeof batchIter.next>>
448
-
449
- const nextBatch = async (): Promise<void> => {
450
- if (!batch || keysToDelete.length === 0) {
451
- if (batch?.done) {
452
- return undefined
453
- }
454
- batch = await batchIter.next()
455
- if (batch.value) {
456
- keysToDelete.push(
457
- ...batch.value.map(r => ({ type: typeName, id: r.id } as Key))
458
- )
459
- }
460
- return nextBatch()
461
- }
462
-
463
- await docUtils.batchDelete(opts.tableName, keysToDelete, opts.batchDelete)
464
- return nextBatch()
465
- }
466
-
467
- return nextBatch()
468
- },
469
- }
470
- }
471
-
472
- await utils.ensureTableExists(createTableParams(opts))
473
-
474
- return {
475
- pool<T>(typeName: string): Promise<Pool<T>> {
476
- return Promise.resolve(pool<T>(typeName))
477
- },
478
- }
479
- }, {
480
- tableName: 'persistent_pool',
481
- listBatchSize: undefined,
482
- billingMode: 'PAY_PER_REQUEST',
483
- optimisticLockMaxRetries: 10,
484
- leaseRandomizationRange: 10,
485
- batchDelete: { strategy: retryStrategies.exponentialBackoff() },
486
- query: { strategy: retryStrategies.intervals({ interval: 250, maxRetries: 10 }) },
487
- })
@@ -1,168 +0,0 @@
1
- /*
2
- * Copyright 2023 Salto Labs Ltd.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with
6
- * the License. You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import {
17
- CreateTableCommand,
18
- CreateTableCommandInput,
19
- DeleteRequest,
20
- DescribeTableCommand,
21
- DynamoDBClient,
22
- ScalarAttributeType,
23
- } from '@aws-sdk/client-dynamodb'
24
- import {
25
- BatchWriteCommand,
26
- DynamoDBDocumentClient,
27
- QueryCommand,
28
- QueryCommandInput,
29
- } from '@aws-sdk/lib-dynamodb'
30
-
31
- import { retry } from '@salto-io/lowerdash'
32
-
33
- const { withRetry } = retry
34
-
35
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
36
- export const dbUtils = (db: DynamoDBClient) => {
37
- const tableStatus = async (tableName: string): Promise<string | undefined> => {
38
- try {
39
- const description = await db.send(new DescribeTableCommand({ TableName: tableName }))
40
- return description && description.Table && description.Table.TableStatus
41
- } catch (e) {
42
- if (e.toString().includes('ResourceNotFoundException')) {
43
- return undefined
44
- }
45
- throw e
46
- }
47
- }
48
-
49
- const tableExists = async (
50
- tableName: string,
51
- ): Promise<boolean> => await tableStatus(tableName) === 'ACTIVE'
52
-
53
- const ensureTableExists = async (
54
- tableParams: CreateTableCommandInput & { TableName: string},
55
- waitOpts?: retry.RetryOpts,
56
- ): Promise<void> => {
57
- try {
58
- await db.send(new CreateTableCommand(tableParams))
59
- } catch (e) {
60
- if (!e.toString().includes('ResourceInUseException')) {
61
- throw e
62
- }
63
- }
64
- await withRetry(() => tableExists(tableParams.TableName), waitOpts)
65
- }
66
- return {
67
- tableStatus,
68
- tableExists,
69
- ensureTableExists,
70
- }
71
- }
72
-
73
- export type QueryOpts = Omit<retry.RetryOpts, 'description'>
74
- export type BatchDeleteOpts = Omit<retry.RetryOpts, 'description'>
75
-
76
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
- export type Key = Record<string, any> | undefined
78
- type AttributeMap = Record<string, ScalarAttributeType>
79
-
80
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
81
- export const dbDocUtils = (dbDoc: DynamoDBDocumentClient) => {
82
- const queryBatchIterator = <TReturn extends {} = AttributeMap>(
83
- input: QueryCommandInput,
84
- opts?: QueryOpts,
85
- ): AsyncIterator<TReturn[], void> => {
86
- let done = false
87
- let items: TReturn[]
88
- let startKey: Key
89
-
90
- return {
91
- next: async () => {
92
- if (done) {
93
- return { done: true, value: undefined }
94
- }
95
-
96
- await withRetry(async (): Promise<boolean> => {
97
- const queryResults = await dbDoc.send(new QueryCommand({
98
- ...input,
99
- ExclusiveStartKey: startKey,
100
- }))
101
-
102
- startKey = queryResults.LastEvaluatedKey
103
-
104
- done = startKey === undefined
105
- items = queryResults.Items as TReturn[]
106
-
107
- return done || Boolean(items?.length)
108
- }, { ...opts, description: 'query' })
109
-
110
- return (items && items.length !== 0)
111
- ? { done: false, value: items }
112
- : { done: true, value: undefined }
113
- },
114
- }
115
- }
116
-
117
- const queryIterator = <TReturn extends {} = AttributeMap>(
118
- input: QueryCommandInput,
119
- opts?: QueryOpts,
120
- ): AsyncIterator<TReturn, void> => {
121
- const batchIter = queryBatchIterator<TReturn>(input, opts)
122
- let batch: IteratorResult<TReturn[]>
123
-
124
- return {
125
- async next(): Promise<IteratorResult<TReturn>> {
126
- if (!batch || (!batch.done && batch.value.length === 0)) {
127
- batch = await batchIter.next()
128
- return this.next()
129
- }
130
-
131
- return batch.done
132
- ? { done: true, value: undefined }
133
- : { done: false, value: batch.value.shift() as TReturn }
134
- },
135
- }
136
- }
137
-
138
- const MAX_ITEMS_PER_BATCH_WRITE = 25
139
-
140
- const batchDelete = async (
141
- tableName: string,
142
- keysToDelete: Key[],
143
- opts: BatchDeleteOpts,
144
- ): Promise<void> => {
145
- await withRetry(async () => {
146
- const keysToSend = keysToDelete.splice(0, MAX_ITEMS_PER_BATCH_WRITE)
147
-
148
- const deleteResult = await dbDoc.send(new BatchWriteCommand({
149
- RequestItems: {
150
- [tableName]: keysToSend.map(r => ({ DeleteRequest: { Key: r } })),
151
- },
152
- }))
153
-
154
- const unprocessed = (deleteResult.UnprocessedItems?.[tableName] ?? [])
155
- .map(r => (r.DeleteRequest as DeleteRequest).Key)
156
-
157
- keysToDelete.push(...unprocessed)
158
-
159
- return keysToDelete.length === 0
160
- }, { ...opts, description: 'batchDelete' })
161
- }
162
-
163
- return {
164
- queryBatchIterator,
165
- queryIterator,
166
- batchDelete,
167
- }
168
- }