@travetto/model-dynamodb 8.0.0-alpha.10 → 8.0.0-alpha.12
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 -1
- package/package.json +6 -6
- package/src/service.ts +85 -85
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ This module provides an [DynamoDB](https://aws.amazon.com/dynamodb/)-based imple
|
|
|
18
18
|
Supported features:
|
|
19
19
|
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
|
|
20
20
|
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
|
|
21
|
-
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#
|
|
21
|
+
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L15)
|
|
22
22
|
|
|
23
23
|
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-dynamodb",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.12",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DynamoDB backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"directory": "module/model-dynamodb"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@aws-sdk/client-dynamodb": "^3.
|
|
30
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/model-indexed": "^8.0.0-alpha.
|
|
29
|
+
"@aws-sdk/client-dynamodb": "^3.1024.0",
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.11",
|
|
31
|
+
"@travetto/model": "^8.0.0-alpha.11",
|
|
32
|
+
"@travetto/model-indexed": "^8.0.0-alpha.12"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.16"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@travetto/cli": {
|
package/src/service.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AttributeValue, DynamoDB, type PutItemCommandInput, type PutItemCommandOutput, type QueryCommandOutput } from '@aws-sdk/client-dynamodb';
|
|
1
|
+
import { type AttributeValue, DynamoDB, type PutItemCommandInput, type PutItemCommandOutput, type QueryCommandInput, type QueryCommandOutput } from '@aws-sdk/client-dynamodb';
|
|
2
2
|
|
|
3
3
|
import { castTo, JSONUtil, ShutdownManager, TimeUtil, type Class } from '@travetto/runtime';
|
|
4
4
|
import { Injectable, PostConstruct } from '@travetto/di';
|
|
@@ -6,10 +6,11 @@ import {
|
|
|
6
6
|
type ModelCrudSupport, type ModelExpirySupport, ModelRegistryIndex, type ModelStorageSupport,
|
|
7
7
|
type ModelType, NotFoundError, ExistsError, type OptionalId, ModelCrudUtil,
|
|
8
8
|
ModelExpiryUtil, ModelStorageUtil,
|
|
9
|
+
type ModelListOptions,
|
|
9
10
|
} from '@travetto/model';
|
|
10
11
|
import {
|
|
11
12
|
isModelIndexedIndex, ModelIndexedUtil, type KeyedIndexBody, type KeyedIndexSelection,
|
|
12
|
-
type
|
|
13
|
+
type ModelPageOptions, type ModelPageResult, type ModelIndexedSupport, type SingleItemIndex,
|
|
13
14
|
type FullKeyedIndexBody, type FullKeyedIndexWithPartialBody, type SortedIndex, type SortedIndexSelection,
|
|
14
15
|
ModelIndexedComputedIndex
|
|
15
16
|
} from '@travetto/model-indexed';
|
|
@@ -42,51 +43,59 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
42
43
|
return table;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
async * #
|
|
46
|
-
T
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
body: KeyedIndexBody<T, K>,
|
|
53
|
-
options?: ListPageOptions<Record<string, AttributeValue>>
|
|
54
|
-
): AsyncIterable<QueryCommandOutput & { LastEvaluatedOffset?: string }> {
|
|
55
|
-
ModelCrudUtil.ensureNotSubType(cls);
|
|
56
|
-
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
57
|
-
const safeName = DynamoDBUtil.toSafeName(idx.name);
|
|
58
|
-
const expression = { [`:${safeName}`]: getKey(computed) };
|
|
59
|
-
const limit = options?.limit ?? 100;
|
|
60
|
-
|
|
46
|
+
async * #scanCollection<T extends ModelType>(
|
|
47
|
+
cls: Class<T>,
|
|
48
|
+
query: (batchSize: number, lastKey: Record<string, AttributeValue> | undefined) => Promise<QueryCommandOutput>,
|
|
49
|
+
options?: ModelListOptions & ModelPageOptions<Record<string, AttributeValue>>,
|
|
50
|
+
): AsyncIterable<{ items: T[], lastKey?: Record<string, AttributeValue> }> {
|
|
51
|
+
const batchSize = options?.batchSizeHint ?? 100;
|
|
52
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
61
53
|
let startKey = options?.offset ?? undefined;
|
|
62
54
|
let produced = 0;
|
|
63
|
-
|
|
64
55
|
do {
|
|
65
56
|
const remaining = limit - produced;
|
|
66
|
-
const batch = await
|
|
67
|
-
TableName: this.#resolveTable(cls),
|
|
68
|
-
IndexName: safeName,
|
|
69
|
-
ProjectionExpression: 'body',
|
|
70
|
-
KeyConditionExpression: `${safeName}__ = :${safeName}`,
|
|
71
|
-
ExpressionAttributeValues: expression,
|
|
72
|
-
Limit: Math.min(remaining, 100),
|
|
73
|
-
ExclusiveStartKey: startKey,
|
|
74
|
-
});
|
|
57
|
+
const batch = await query(Math.min(remaining, batchSize), startKey);
|
|
75
58
|
|
|
76
59
|
if (batch.Count && batch.Items) {
|
|
77
60
|
produced += batch.Count;
|
|
78
61
|
|
|
79
|
-
|
|
80
|
-
const items = batch.Items.slice(0, remaining);
|
|
81
|
-
yield { ...batch, Items: items, };
|
|
82
|
-
} else {
|
|
83
|
-
yield batch;
|
|
84
|
-
}
|
|
62
|
+
const items = (produced > limit) ? batch.Items.slice(0, remaining) : batch.Items;
|
|
85
63
|
startKey = batch.LastEvaluatedKey;
|
|
64
|
+
yield {
|
|
65
|
+
items: await ModelCrudUtil.filterOutNotFound(
|
|
66
|
+
items.map(item => DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!))),
|
|
67
|
+
lastKey: startKey
|
|
68
|
+
};
|
|
86
69
|
} else {
|
|
87
70
|
startKey = undefined;
|
|
88
71
|
}
|
|
89
|
-
} while (startKey && produced < limit);
|
|
72
|
+
} while (startKey && produced < limit && !(options?.abort?.aborted));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async * #scanIndex<
|
|
76
|
+
T extends ModelType,
|
|
77
|
+
K extends KeyedIndexSelection<T>,
|
|
78
|
+
S extends SortedIndexSelection<T>
|
|
79
|
+
>(
|
|
80
|
+
cls: Class<T>,
|
|
81
|
+
idx: SortedIndex<T, K, S>,
|
|
82
|
+
body: KeyedIndexBody<T, K>,
|
|
83
|
+
options?: ModelPageOptions<Record<string, AttributeValue>> & ModelListOptions
|
|
84
|
+
): AsyncIterable<{ items: T[], lastKey?: Record<string, AttributeValue> }> {
|
|
85
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
86
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
87
|
+
const safeName = DynamoDBUtil.toSafeName(idx.name);
|
|
88
|
+
const expression = { [`:${safeName}`]: getKey(computed) };
|
|
89
|
+
|
|
90
|
+
yield* this.#scanCollection(cls, (batchSize, lastKey) => this.client.query({
|
|
91
|
+
TableName: this.#resolveTable(cls),
|
|
92
|
+
IndexName: safeName,
|
|
93
|
+
ProjectionExpression: 'body',
|
|
94
|
+
KeyConditionExpression: `${safeName}__ = :${safeName}`,
|
|
95
|
+
ExpressionAttributeValues: expression,
|
|
96
|
+
Limit: batchSize,
|
|
97
|
+
ExclusiveStartKey: lastKey,
|
|
98
|
+
}), options);
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
async #getIdByIndex<
|
|
@@ -101,16 +110,22 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
101
110
|
const safeName = DynamoDBUtil.toSafeName(idx.name);
|
|
102
111
|
const sorted = idx.type === 'indexed:sorted';
|
|
103
112
|
|
|
104
|
-
const query = {
|
|
113
|
+
const query: QueryCommandInput = {
|
|
105
114
|
TableName: this.#resolveTable(cls),
|
|
106
115
|
IndexName: safeName,
|
|
107
116
|
ProjectionExpression: 'id',
|
|
108
|
-
KeyConditionExpression: [
|
|
109
|
-
|
|
117
|
+
KeyConditionExpression: [
|
|
118
|
+
...(sorted ? [`${safeName}_sort__ = :${safeName}_sort`] : []),
|
|
119
|
+
`${safeName}__ = :${safeName}`
|
|
120
|
+
]
|
|
110
121
|
.join(' and '),
|
|
122
|
+
...(computed.idPart ? {
|
|
123
|
+
FilterExpression: 'id = :id'
|
|
124
|
+
} : {}),
|
|
111
125
|
ExpressionAttributeValues: {
|
|
112
126
|
[`:${safeName}`]: getKey(computed),
|
|
113
|
-
...(sorted ? { [`:${safeName}_sort`]: getSort(computed) } : {})
|
|
127
|
+
...(sorted ? { [`:${safeName}_sort`]: getSort(computed) } : {}),
|
|
128
|
+
...(computed.idPart ? { ':id': DynamoDBUtil.toValue(computed.idPart.value) } : {})
|
|
114
129
|
}
|
|
115
130
|
};
|
|
116
131
|
|
|
@@ -369,32 +384,13 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
369
384
|
}
|
|
370
385
|
}
|
|
371
386
|
|
|
372
|
-
async * list<T extends ModelType>(cls: Class<T
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
if (batch.Count && batch.Items) {
|
|
382
|
-
for (const item of batch.Items) {
|
|
383
|
-
try {
|
|
384
|
-
yield await DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!);
|
|
385
|
-
} catch (error) {
|
|
386
|
-
if (!(error instanceof NotFoundError)) {
|
|
387
|
-
throw error;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (!batch.Count || !batch.LastEvaluatedKey) {
|
|
394
|
-
done = true;
|
|
395
|
-
} else {
|
|
396
|
-
token = batch.LastEvaluatedKey;
|
|
397
|
-
}
|
|
387
|
+
async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
388
|
+
for await (const { items } of this.#scanCollection(cls, (batchSize, lastKey) => this.client.scan({
|
|
389
|
+
TableName: this.#resolveTable(cls),
|
|
390
|
+
ExclusiveStartKey: lastKey,
|
|
391
|
+
Limit: batchSize
|
|
392
|
+
}), options)) {
|
|
393
|
+
yield items;
|
|
398
394
|
}
|
|
399
395
|
}
|
|
400
396
|
|
|
@@ -445,7 +441,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
445
441
|
return this.update(cls, item);
|
|
446
442
|
}
|
|
447
443
|
|
|
448
|
-
async
|
|
444
|
+
async pageByIndex<
|
|
449
445
|
T extends ModelType,
|
|
450
446
|
K extends KeyedIndexSelection<T>,
|
|
451
447
|
S extends SortedIndexSelection<T>
|
|
@@ -453,28 +449,17 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
453
449
|
cls: Class<T>,
|
|
454
450
|
idx: SortedIndex<T, K, S>,
|
|
455
451
|
body: KeyedIndexBody<T, K>,
|
|
456
|
-
options?:
|
|
457
|
-
): Promise<
|
|
458
|
-
const
|
|
452
|
+
options?: ModelPageOptions,
|
|
453
|
+
): Promise<ModelPageResult<T>> {
|
|
454
|
+
const output: T[] = [];
|
|
459
455
|
const offset = options?.offset ? JSONUtil.fromBase64<Record<string, AttributeValue>>(options.offset) : undefined;
|
|
460
|
-
for await (const
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
items.push(await DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!));
|
|
464
|
-
if (options?.limit && items.length >= options.limit) {
|
|
465
|
-
break;
|
|
466
|
-
}
|
|
467
|
-
} catch (error) {
|
|
468
|
-
if (!(error instanceof NotFoundError)) {
|
|
469
|
-
throw error;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
456
|
+
for await (const { items } of this.#scanIndex(cls, idx, body, { limit: 100, ...options, offset })) {
|
|
457
|
+
output.push(...items);
|
|
473
458
|
}
|
|
474
459
|
|
|
475
460
|
let nextOffset;
|
|
476
|
-
if (
|
|
477
|
-
const last: T =
|
|
461
|
+
if (output.length) {
|
|
462
|
+
const last: T = output.at(-1)!;
|
|
478
463
|
const computed = ModelIndexedComputedIndex.get(idx, last).validate();
|
|
479
464
|
const safeName = DynamoDBUtil.toSafeName(idx.name);
|
|
480
465
|
nextOffset = JSONUtil.toBase64({
|
|
@@ -484,6 +469,21 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
484
469
|
});
|
|
485
470
|
}
|
|
486
471
|
|
|
487
|
-
return { items, nextOffset };
|
|
472
|
+
return { items: output, nextOffset };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async * listByIndex<
|
|
476
|
+
T extends ModelType,
|
|
477
|
+
K extends KeyedIndexSelection<T>,
|
|
478
|
+
S extends SortedIndexSelection<T>
|
|
479
|
+
>(
|
|
480
|
+
cls: Class<T>,
|
|
481
|
+
idx: SortedIndex<T, K, S>,
|
|
482
|
+
body: KeyedIndexBody<T, K>,
|
|
483
|
+
options?: ModelListOptions
|
|
484
|
+
): AsyncIterable<T[]> {
|
|
485
|
+
for await (const { items } of this.#scanIndex(cls, idx, body, options)) {
|
|
486
|
+
yield items;
|
|
487
|
+
}
|
|
488
488
|
}
|
|
489
489
|
}
|