@travetto/model-dynamodb 8.0.0-alpha.2 → 8.0.0-alpha.20
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 -5
- package/src/service.ts +260 -120
- package/src/util.ts +54 -28
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/src/types/
|
|
21
|
+
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L16)
|
|
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.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DynamoDB backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
@@ -26,12 +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.
|
|
29
|
+
"@aws-sdk/client-dynamodb": "^3.1063.0",
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.18",
|
|
31
|
+
"@travetto/model": "^8.0.0-alpha.18",
|
|
32
|
+
"@travetto/model-indexed": "^8.0.0-alpha.20"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.23"
|
|
35
36
|
},
|
|
36
37
|
"peerDependenciesMeta": {
|
|
37
38
|
"@travetto/cli": {
|
package/src/service.ts
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
import { type AttributeValue, DynamoDB, type PutItemCommandInput, type PutItemCommandOutput } 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
|
-
import { JSONUtil, ShutdownManager, TimeUtil, type Class
|
|
3
|
+
import { castTo, JSONUtil, ShutdownManager, TimeUtil, type Class } from '@travetto/runtime';
|
|
4
4
|
import { Injectable, PostConstruct } from '@travetto/di';
|
|
5
5
|
import {
|
|
6
6
|
type ModelCrudSupport, type ModelExpirySupport, ModelRegistryIndex, type ModelStorageSupport,
|
|
7
|
-
type
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
type ModelType, NotFoundError, ExistsError, type OptionalId, ModelCrudUtil,
|
|
8
|
+
ModelExpiryUtil, ModelStorageUtil,
|
|
9
|
+
type ModelListOptions,
|
|
10
10
|
} from '@travetto/model';
|
|
11
|
+
import {
|
|
12
|
+
isModelIndexedIndex, ModelIndexedUtil, type KeyedIndexBody, type KeyedIndexSelection,
|
|
13
|
+
type ModelPageOptions, type ModelPageResult, type ModelIndexedSupport, type SingleItemIndex,
|
|
14
|
+
type FullKeyedIndexBody, type FullKeyedIndexWithPartialBody, type SortedIndex, type SortedIndexSelection,
|
|
15
|
+
ModelIndexedComputedIndex, type ModelIndexedSearchOptions, type SortedIndexSelectionType
|
|
16
|
+
} from '@travetto/model-indexed';
|
|
11
17
|
|
|
12
18
|
import type { DynamoDBModelConfig } from './config.ts';
|
|
13
19
|
import { DynamoDBUtil } from './util.ts';
|
|
14
20
|
|
|
15
|
-
const
|
|
21
|
+
const EXPIRES_ATTRIBUTE = 'expires_at__';
|
|
22
|
+
|
|
23
|
+
const getKey = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue => DynamoDBUtil.toValue(computed.getKey() || 'NULL');
|
|
24
|
+
const getSort = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue => DynamoDBUtil.toValue(computed.getSort());
|
|
16
25
|
|
|
17
26
|
/**
|
|
18
27
|
* A model service backed by DynamoDB
|
|
@@ -34,6 +43,109 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
34
43
|
return table;
|
|
35
44
|
}
|
|
36
45
|
|
|
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;
|
|
53
|
+
let startKey = options?.offset ?? undefined;
|
|
54
|
+
let produced = 0;
|
|
55
|
+
do {
|
|
56
|
+
const remaining = limit - produced;
|
|
57
|
+
const batch = await query(Math.min(remaining, batchSize), startKey);
|
|
58
|
+
|
|
59
|
+
if (batch.Count && batch.Items) {
|
|
60
|
+
produced += batch.Count;
|
|
61
|
+
|
|
62
|
+
const items = (produced > limit) ? batch.Items.slice(0, remaining) : batch.Items;
|
|
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
|
+
};
|
|
69
|
+
} else {
|
|
70
|
+
startKey = undefined;
|
|
71
|
+
}
|
|
72
|
+
} while (startKey && produced < limit && !(options?.abort?.aborted));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async * #scanIndex<T extends ModelType>(
|
|
76
|
+
cls: Class<T>,
|
|
77
|
+
idx: SortedIndex<T>,
|
|
78
|
+
body: KeyedIndexBody<T>,
|
|
79
|
+
options?: ModelPageOptions<Record<string, AttributeValue>> & ModelListOptions,
|
|
80
|
+
transform?: (query: QueryCommandInput) => QueryCommandInput
|
|
81
|
+
): AsyncIterable<{ items: T[], lastKey?: Record<string, AttributeValue> }> {
|
|
82
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
83
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
84
|
+
const { keyIndexName, keyIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
85
|
+
const expression = { [`:${keyIndexName}`]: getKey(computed) };
|
|
86
|
+
|
|
87
|
+
const finalTransform = transform ?? ((query): QueryCommandInput => query);
|
|
88
|
+
|
|
89
|
+
yield* this.#scanCollection(cls, (batchSize, lastKey) => {
|
|
90
|
+
const finalized = finalTransform({
|
|
91
|
+
TableName: this.#resolveTable(cls),
|
|
92
|
+
IndexName: keyIndexName,
|
|
93
|
+
ProjectionExpression: 'body',
|
|
94
|
+
KeyConditionExpression: `${keyIndexAttribute} = :${keyIndexName}`,
|
|
95
|
+
ExpressionAttributeValues: expression,
|
|
96
|
+
Limit: batchSize,
|
|
97
|
+
ExclusiveStartKey: lastKey,
|
|
98
|
+
});
|
|
99
|
+
return this.client.query(finalized);
|
|
100
|
+
}, options);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async #getIdByIndex<
|
|
104
|
+
T extends ModelType,
|
|
105
|
+
K extends KeyedIndexSelection<T>,
|
|
106
|
+
S extends SortedIndexSelection<T>
|
|
107
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<string> {
|
|
108
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
109
|
+
|
|
110
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
111
|
+
|
|
112
|
+
const { keyIndexName, keyIndexAttribute, sortIndexAttribute, sortIndexName } = DynamoDBUtil.indexNames(idx.name);
|
|
113
|
+
const sorted = idx.type === 'indexed:sorted';
|
|
114
|
+
|
|
115
|
+
const query: QueryCommandInput = {
|
|
116
|
+
TableName: this.#resolveTable(cls),
|
|
117
|
+
IndexName: keyIndexName,
|
|
118
|
+
ProjectionExpression: 'id',
|
|
119
|
+
KeyConditionExpression: [
|
|
120
|
+
...(sorted ? [`${sortIndexAttribute} = :${sortIndexName}`] : []),
|
|
121
|
+
`${keyIndexAttribute} = :${keyIndexName}`
|
|
122
|
+
]
|
|
123
|
+
.join(' and '),
|
|
124
|
+
...(computed.idPart ? {
|
|
125
|
+
FilterExpression: 'id = :id'
|
|
126
|
+
} : {}),
|
|
127
|
+
ExpressionAttributeValues: {
|
|
128
|
+
[`:${keyIndexName}`]: getKey(computed),
|
|
129
|
+
...(sorted ? { [`:${sortIndexName}`]: getSort(computed) } : {}),
|
|
130
|
+
...(computed.idPart ? { ':id': DynamoDBUtil.toValue(computed.idPart.value) } : {})
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const result = await this.client.query(query);
|
|
136
|
+
|
|
137
|
+
if (result.Count && result.Items && result.Items[0]) {
|
|
138
|
+
return result.Items[0].id.S!;
|
|
139
|
+
}
|
|
140
|
+
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey({ sort: true }));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof Error && error.message.includes('The table does not have the specified index')) {
|
|
143
|
+
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey({ sort: true }));
|
|
144
|
+
}
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
37
149
|
async #putItem<T extends ModelType>(cls: Class<T>, id: string, item: T, mode: 'create' | 'update' | 'upsert'): Promise<PutItemCommandOutput> {
|
|
38
150
|
const config = ModelRegistryIndex.getConfig(cls);
|
|
39
151
|
let expiry: number | undefined;
|
|
@@ -48,12 +160,20 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
48
160
|
try {
|
|
49
161
|
if (mode === 'create') {
|
|
50
162
|
const indices: Record<string, unknown> = {};
|
|
51
|
-
for (const idx of
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
163
|
+
for (const idx of ModelRegistryIndex.getIndices(cls)) {
|
|
164
|
+
if (isModelIndexedIndex(idx)) {
|
|
165
|
+
const { keyIndexAttribute, sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
166
|
+
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
167
|
+
switch (idx.type) {
|
|
168
|
+
case 'indexed:keyed': indices[keyIndexAttribute] = getKey(computed); break;
|
|
169
|
+
case 'indexed:sorted': {
|
|
170
|
+
indices[keyIndexAttribute] = getKey(computed);
|
|
171
|
+
indices[sortIndexAttribute] = getSort(computed);
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
console.warn('Unsupported index type on update', { cls: cls.name, idx });
|
|
57
177
|
}
|
|
58
178
|
}
|
|
59
179
|
const query: PutItemCommandInput = {
|
|
@@ -62,7 +182,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
62
182
|
Item: {
|
|
63
183
|
id: DynamoDBUtil.toValue(item.id),
|
|
64
184
|
body: DynamoDBUtil.toValue(JSONUtil.toUTF8(item)),
|
|
65
|
-
...(expiry !== undefined ? { [
|
|
185
|
+
...(expiry !== undefined ? { [EXPIRES_ATTRIBUTE]: DynamoDBUtil.toValue(expiry) } : {}),
|
|
66
186
|
...indices
|
|
67
187
|
},
|
|
68
188
|
ReturnValues: 'NONE'
|
|
@@ -71,14 +191,27 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
71
191
|
} else {
|
|
72
192
|
const indices: Record<string, unknown> = {};
|
|
73
193
|
const expr: string[] = [];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
194
|
+
|
|
195
|
+
for (const idx of ModelRegistryIndex.getIndices(cls)) {
|
|
196
|
+
if (isModelIndexedIndex(idx)) {
|
|
197
|
+
const { keyIndexAttribute, sortIndexAttribute, keyIndexName, sortIndexName } = DynamoDBUtil.indexNames(idx.name);
|
|
198
|
+
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
199
|
+
switch (idx.type) {
|
|
200
|
+
case 'indexed:keyed': {
|
|
201
|
+
indices[`:${keyIndexName}`] = getKey(computed);
|
|
202
|
+
expr.push(`${keyIndexAttribute} = :${keyIndexName}`);
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case 'indexed:sorted': {
|
|
206
|
+
indices[`:${keyIndexName}`] = getKey(computed);
|
|
207
|
+
indices[`:${sortIndexName}`] = getSort(computed);
|
|
208
|
+
expr.push(`${keyIndexAttribute} = :${keyIndexName}`);
|
|
209
|
+
expr.push(`${sortIndexAttribute} = :${sortIndexName}`);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
console.warn('Unsupported index type on update', { cls: cls.name, idx });
|
|
82
215
|
}
|
|
83
216
|
}
|
|
84
217
|
|
|
@@ -88,7 +221,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
88
221
|
Key: { id: { S: id } },
|
|
89
222
|
UpdateExpression: `SET ${[
|
|
90
223
|
'body=:body',
|
|
91
|
-
expiry !== undefined ? `${
|
|
224
|
+
expiry !== undefined ? `${EXPIRES_ATTRIBUTE}=:expr` : undefined,
|
|
92
225
|
...expr
|
|
93
226
|
].filter(part => !!part).join(', ')}`,
|
|
94
227
|
ExpressionAttributeValues: {
|
|
@@ -168,7 +301,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
168
301
|
if (ttlEnabled !== ttlRequired) {
|
|
169
302
|
await this.client.updateTimeToLive({
|
|
170
303
|
TableName: table,
|
|
171
|
-
TimeToLiveSpecification: { AttributeName: ttlRequired ?
|
|
304
|
+
TimeToLiveSpecification: { AttributeName: ttlRequired ? EXPIRES_ATTRIBUTE : undefined, Enabled: ttlRequired }
|
|
172
305
|
});
|
|
173
306
|
}
|
|
174
307
|
}
|
|
@@ -253,32 +386,13 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
253
386
|
}
|
|
254
387
|
}
|
|
255
388
|
|
|
256
|
-
async * list<T extends ModelType>(cls: Class<T
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
if (batch.Count && batch.Items) {
|
|
266
|
-
for (const item of batch.Items) {
|
|
267
|
-
try {
|
|
268
|
-
yield await DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!);
|
|
269
|
-
} catch (error) {
|
|
270
|
-
if (!(error instanceof NotFoundError)) {
|
|
271
|
-
throw error;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (!batch.Count || !batch.LastEvaluatedKey) {
|
|
278
|
-
done = true;
|
|
279
|
-
} else {
|
|
280
|
-
token = batch.LastEvaluatedKey;
|
|
281
|
-
}
|
|
389
|
+
async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
390
|
+
for await (const { items } of this.#scanCollection(cls, (batchSize, lastKey) => this.client.scan({
|
|
391
|
+
TableName: this.#resolveTable(cls),
|
|
392
|
+
ExclusiveStartKey: lastKey,
|
|
393
|
+
Limit: batchSize
|
|
394
|
+
}), options)) {
|
|
395
|
+
yield items;
|
|
282
396
|
}
|
|
283
397
|
}
|
|
284
398
|
|
|
@@ -288,92 +402,118 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
288
402
|
}
|
|
289
403
|
|
|
290
404
|
// Indexed
|
|
291
|
-
async
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idxConfig, body);
|
|
297
|
-
|
|
298
|
-
if (idxConfig.type === 'sorted' && sort === undefined) {
|
|
299
|
-
throw new IndexNotSupported(cls, idxConfig, 'Sorted indices require the sort field');
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const idxName = DynamoDBUtil.simpleName(idx);
|
|
303
|
-
|
|
304
|
-
const query = {
|
|
305
|
-
TableName: this.#resolveTable(cls),
|
|
306
|
-
IndexName: idxName,
|
|
307
|
-
ProjectionExpression: 'id',
|
|
308
|
-
KeyConditionExpression: [sort ? `${idxName}_sort__ = :${idxName}_sort` : '', `${idxName}__ = :${idxName}`]
|
|
309
|
-
.filter(expr => !!expr)
|
|
310
|
-
.join(' and '),
|
|
311
|
-
ExpressionAttributeValues: {
|
|
312
|
-
[`:${idxName}`]: DynamoDBUtil.toValue(key),
|
|
313
|
-
...(sort ? { [`:${idxName}_sort`]: DynamoDBUtil.toValue(+sort) } : {})
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
|
|
317
|
-
const result = await this.client.query(query);
|
|
318
|
-
|
|
319
|
-
if (result.Count && result.Items && result.Items[0]) {
|
|
320
|
-
return result.Items[0].id.S!;
|
|
321
|
-
}
|
|
322
|
-
throw new NotFoundError(`${cls.name} Index=${idx}`, key);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Indexed
|
|
326
|
-
async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
|
|
405
|
+
async getByIndex<
|
|
406
|
+
T extends ModelType,
|
|
407
|
+
K extends KeyedIndexSelection<T>,
|
|
408
|
+
S extends SortedIndexSelection<T>
|
|
409
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
|
|
327
410
|
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
328
411
|
}
|
|
329
412
|
|
|
330
|
-
async deleteByIndex<
|
|
413
|
+
async deleteByIndex<
|
|
414
|
+
T extends ModelType,
|
|
415
|
+
K extends KeyedIndexSelection<T>,
|
|
416
|
+
S extends SortedIndexSelection<T>
|
|
417
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
|
|
331
418
|
return this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
332
419
|
}
|
|
333
420
|
|
|
334
|
-
upsertByIndex<
|
|
421
|
+
upsertByIndex<
|
|
422
|
+
T extends ModelType,
|
|
423
|
+
K extends KeyedIndexSelection<T>,
|
|
424
|
+
S extends SortedIndexSelection<T>
|
|
425
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
|
|
335
426
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
336
427
|
}
|
|
337
428
|
|
|
338
|
-
async
|
|
339
|
-
|
|
429
|
+
async updateByIndex<
|
|
430
|
+
T extends ModelType,
|
|
431
|
+
K extends KeyedIndexSelection<T>,
|
|
432
|
+
S extends SortedIndexSelection<T>
|
|
433
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
|
|
434
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
435
|
+
}
|
|
340
436
|
|
|
341
|
-
|
|
342
|
-
|
|
437
|
+
async updatePartialByIndex<
|
|
438
|
+
T extends ModelType,
|
|
439
|
+
K extends KeyedIndexSelection<T>,
|
|
440
|
+
S extends SortedIndexSelection<T>
|
|
441
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
|
|
442
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
443
|
+
return this.update(cls, item);
|
|
444
|
+
}
|
|
343
445
|
|
|
344
|
-
|
|
446
|
+
async pageByIndex<
|
|
447
|
+
T extends ModelType,
|
|
448
|
+
K extends KeyedIndexSelection<T>,
|
|
449
|
+
S extends SortedIndexSelection<T>
|
|
450
|
+
>(
|
|
451
|
+
cls: Class<T>,
|
|
452
|
+
idx: SortedIndex<T, K, S>,
|
|
453
|
+
body: KeyedIndexBody<T, K>,
|
|
454
|
+
options?: ModelPageOptions,
|
|
455
|
+
): Promise<ModelPageResult<T>> {
|
|
456
|
+
const output: T[] = [];
|
|
457
|
+
const offset = options?.offset ? JSONUtil.fromBase64<Record<string, AttributeValue>>(options.offset) : undefined;
|
|
458
|
+
for await (const { items } of this.#scanIndex(cls, idx, body, { limit: 100, ...options, offset })) {
|
|
459
|
+
output.push(...items);
|
|
460
|
+
}
|
|
345
461
|
|
|
346
|
-
let
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
[`:${idxName}`]: DynamoDBUtil.toValue(key)
|
|
356
|
-
},
|
|
357
|
-
ExclusiveStartKey: token
|
|
462
|
+
let nextOffset;
|
|
463
|
+
if (output.length) {
|
|
464
|
+
const last: T = output.at(-1)!;
|
|
465
|
+
const computed = ModelIndexedComputedIndex.get(idx, last).validate();
|
|
466
|
+
const { keyIndexAttribute, sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
467
|
+
nextOffset = JSONUtil.toBase64({
|
|
468
|
+
[keyIndexAttribute]: getKey(computed),
|
|
469
|
+
[sortIndexAttribute]: getSort(computed),
|
|
470
|
+
id: DynamoDBUtil.toValue(last.id)
|
|
358
471
|
});
|
|
472
|
+
}
|
|
359
473
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
try {
|
|
363
|
-
yield await DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!);
|
|
364
|
-
} catch (error) {
|
|
365
|
-
if (!(error instanceof NotFoundError)) {
|
|
366
|
-
throw error;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
474
|
+
return { items: output, nextOffset };
|
|
475
|
+
}
|
|
371
476
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
477
|
+
async * listByIndex<
|
|
478
|
+
T extends ModelType,
|
|
479
|
+
K extends KeyedIndexSelection<T>,
|
|
480
|
+
S extends SortedIndexSelection<T>
|
|
481
|
+
>(
|
|
482
|
+
cls: Class<T>,
|
|
483
|
+
idx: SortedIndex<T, K, S>,
|
|
484
|
+
body: KeyedIndexBody<T, K>,
|
|
485
|
+
options?: ModelListOptions
|
|
486
|
+
): AsyncIterable<T[]> {
|
|
487
|
+
for await (const { items } of this.#scanIndex(cls, idx, body, options)) {
|
|
488
|
+
yield items;
|
|
377
489
|
}
|
|
378
490
|
}
|
|
491
|
+
|
|
492
|
+
async suggestByIndex<
|
|
493
|
+
T extends ModelType,
|
|
494
|
+
S extends SortedIndexSelection<T>,
|
|
495
|
+
K extends KeyedIndexSelection<T>,
|
|
496
|
+
B extends SortedIndexSelectionType<T, S> & string
|
|
497
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
|
|
498
|
+
const results: T[] = [];
|
|
499
|
+
|
|
500
|
+
const { sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
501
|
+
|
|
502
|
+
for await (const { items } of this.#scanIndex(cls, idx, body, { limit: 10, ...options },
|
|
503
|
+
(query) => ({
|
|
504
|
+
...query,
|
|
505
|
+
KeyConditionExpression: [query.KeyConditionExpression, `begins_with(${sortIndexAttribute}, :prefix)`]
|
|
506
|
+
.filter(Boolean)
|
|
507
|
+
.join(' AND '),
|
|
508
|
+
ExpressionAttributeValues: {
|
|
509
|
+
...query.ExpressionAttributeValues,
|
|
510
|
+
':prefix': { S: prefix }
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
)) {
|
|
514
|
+
results.push(...items);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return results;
|
|
518
|
+
}
|
|
379
519
|
}
|
package/src/util.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type {
|
|
|
3
3
|
GlobalSecondaryIndexUpdate, KeySchemaElement
|
|
4
4
|
} from '@aws-sdk/client-dynamodb';
|
|
5
5
|
|
|
6
|
-
import type
|
|
6
|
+
import { type Class, castTo } from '@travetto/runtime';
|
|
7
7
|
import { ModelCrudUtil, ModelExpiryUtil, ModelRegistryIndex, NotFoundError, type ModelType } from '@travetto/model';
|
|
8
|
+
import { isModelIndexedIndex, warnIfIndexedUniqueIndex, warnIfNonIndexedIndex } from '@travetto/model-indexed';
|
|
9
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Configuration for DynamoDB indices
|
|
@@ -19,12 +21,15 @@ type DynamoIndexConfig = {
|
|
|
19
21
|
*/
|
|
20
22
|
export class DynamoDBUtil {
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
static indexNames = (name: string): { keyIndexName: string, sortIndexName: string, keyIndexAttribute: string, sortIndexAttribute: string } => {
|
|
25
|
+
const base = name.toLowerCase().replace(/[^A-Za-z0-9]+/g, '_');
|
|
26
|
+
return {
|
|
27
|
+
keyIndexName: base,
|
|
28
|
+
sortIndexName: `${base}_sort`,
|
|
29
|
+
keyIndexAttribute: `${base}__`,
|
|
30
|
+
sortIndexAttribute: `${base}_sort__`
|
|
31
|
+
};
|
|
32
|
+
};
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* Converts a JavaScript value to a DynamoDB AttributeValue format
|
|
@@ -51,29 +56,50 @@ export class DynamoDBUtil {
|
|
|
51
56
|
* Generates global secondary indices and attribute definitions based on the model's index configuration.
|
|
52
57
|
*/
|
|
53
58
|
static computeIndexConfig<T extends ModelType>(cls: Class<T>): DynamoIndexConfig {
|
|
54
|
-
const
|
|
59
|
+
const indexes = ModelRegistryIndex.getIndices(cls);
|
|
55
60
|
const attributes: AttributeDefinition[] = [];
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
61
|
+
const toCreate: GlobalSecondaryIndex[] = [];
|
|
62
|
+
|
|
63
|
+
const filtered = indexes
|
|
64
|
+
.filter(idx => !warnIfIndexedUniqueIndex(this, cls, [idx]))
|
|
65
|
+
.filter(idx => !warnIfNonIndexedIndex(this, cls, [idx]))
|
|
66
|
+
.filter(isModelIndexedIndex);
|
|
67
|
+
|
|
68
|
+
for (const idx of filtered) {
|
|
69
|
+
const keys: KeySchemaElement[] = [];
|
|
70
|
+
|
|
71
|
+
const { keyIndexName, keyIndexAttribute, sortIndexAttribute } = this.indexNames(idx.name);
|
|
72
|
+
|
|
73
|
+
switch (idx.type) {
|
|
74
|
+
case 'indexed:sorted': {
|
|
75
|
+
const path = idx.sortTemplate[0].path;
|
|
76
|
+
let fieldType = cls;
|
|
77
|
+
for (const field of path) {
|
|
78
|
+
if (SchemaRegistryIndex.has(fieldType)) {
|
|
79
|
+
const schema = SchemaRegistryIndex.getConfig(fieldType);
|
|
80
|
+
if (field in schema.fields) {
|
|
81
|
+
fieldType = schema.fields[field].type;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
keys.push({ AttributeName: keyIndexAttribute, KeyType: 'HASH' });
|
|
89
|
+
keys.push({ AttributeName: sortIndexAttribute, KeyType: 'RANGE', });
|
|
90
|
+
attributes.push({ AttributeName: keyIndexAttribute, AttributeType: 'S' });
|
|
91
|
+
attributes.push({ AttributeName: sortIndexAttribute, AttributeType: castTo(fieldType) === String ? 'S' : 'N' });
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case 'indexed:keyed': {
|
|
95
|
+
keys.push({ AttributeName: keyIndexAttribute, KeyType: 'HASH' });
|
|
96
|
+
attributes.push({ AttributeName: keyIndexAttribute, AttributeType: 'S' });
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
73
99
|
}
|
|
74
100
|
|
|
75
|
-
|
|
76
|
-
IndexName:
|
|
101
|
+
toCreate.push({
|
|
102
|
+
IndexName: keyIndexName,
|
|
77
103
|
// ProvisionedThroughput: '',
|
|
78
104
|
Projection: {
|
|
79
105
|
ProjectionType: 'INCLUDE',
|
|
@@ -83,7 +109,7 @@ export class DynamoDBUtil {
|
|
|
83
109
|
});
|
|
84
110
|
}
|
|
85
111
|
|
|
86
|
-
return { indices:
|
|
112
|
+
return { indices: toCreate.length ? toCreate : undefined, attributes };
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
/**
|