@travetto/model-dynamodb 8.0.0-alpha.24 → 8.0.0-alpha.26
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 +4 -5
- package/__index__.ts +1 -1
- package/package.json +5 -5
- package/src/config.ts +1 -1
- package/src/service.ts +146 -120
- package/src/util.ts +20 -9
- package/support/service.dynamodb.ts +1 -1
package/README.md
CHANGED
|
@@ -13,20 +13,19 @@ npm install @travetto/model-dynamodb
|
|
|
13
13
|
yarn add @travetto/model-dynamodb
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module provides an [DynamoDB](https://aws.amazon.com/dynamodb/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.").
|
|
16
|
+
This module provides an [DynamoDB](https://aws.amazon.com/dynamodb/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [DynamoDB](https://aws.amazon.com/dynamodb/). The entire document is stored as a single value, so nothing is needed to handle schema updates in real time. Indices on the other hand are more complicated, and will not be retroactively computed for new values.
|
|
17
17
|
|
|
18
18
|
Supported features:
|
|
19
|
-
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#
|
|
19
|
+
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
|
|
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#L21)
|
|
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
|
|
|
25
25
|
**Code: Wiring up a custom Model Source**
|
|
26
26
|
```typescript
|
|
27
27
|
import { InjectableFactory } from '@travetto/di';
|
|
28
|
-
|
|
29
|
-
import { DynamoDBModelService, type DynamoDBModelConfig } from '@travetto/model-dynamodb';
|
|
28
|
+
import { type DynamoDBModelConfig, DynamoDBModelService } from '@travetto/model-dynamodb';
|
|
30
29
|
|
|
31
30
|
export class Init {
|
|
32
31
|
@InjectableFactory({ primary: true })
|
package/__index__.ts
CHANGED
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.26",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DynamoDB backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@aws-sdk/client-dynamodb": "^3.1081.0",
|
|
30
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/model-indexed": "^8.0.0-alpha.
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.23",
|
|
31
|
+
"@travetto/model": "^8.0.0-alpha.24",
|
|
32
|
+
"@travetto/model-indexed": "^8.0.0-alpha.26"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.29"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@travetto/cli": {
|
package/src/config.ts
CHANGED
package/src/service.ts
CHANGED
|
@@ -1,26 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AttributeValue,
|
|
3
|
+
DynamoDB,
|
|
4
|
+
type PutItemCommandInput,
|
|
5
|
+
type PutItemCommandOutput,
|
|
6
|
+
type QueryCommandInput,
|
|
7
|
+
type QueryCommandOutput
|
|
8
|
+
} from '@aws-sdk/client-dynamodb';
|
|
2
9
|
|
|
3
|
-
import { castTo, JSONUtil, ShutdownManager, TimeUtil, type Class } from '@travetto/runtime';
|
|
4
10
|
import { Injectable, PostConstruct } from '@travetto/di';
|
|
5
11
|
import {
|
|
6
|
-
|
|
7
|
-
type
|
|
8
|
-
|
|
12
|
+
ExistsError,
|
|
13
|
+
type ModelCrudSupport,
|
|
14
|
+
ModelCrudUtil,
|
|
15
|
+
type ModelExpirySupport,
|
|
16
|
+
ModelExpiryUtil,
|
|
9
17
|
type ModelListOptions,
|
|
18
|
+
ModelRegistryIndex,
|
|
19
|
+
type ModelStorageSupport,
|
|
20
|
+
ModelStorageUtil,
|
|
21
|
+
type ModelType,
|
|
22
|
+
NotFoundError,
|
|
23
|
+
type OptionalId
|
|
10
24
|
} from '@travetto/model';
|
|
11
25
|
import {
|
|
12
|
-
|
|
13
|
-
type
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
type FullKeyedIndexBody,
|
|
27
|
+
type FullKeyedIndexWithPartialBody,
|
|
28
|
+
isModelIndexedIndex,
|
|
29
|
+
type KeyedIndexBody,
|
|
30
|
+
type KeyedIndexSelection,
|
|
31
|
+
ModelIndexedComputedIndex,
|
|
32
|
+
type ModelIndexedSearchOptions,
|
|
33
|
+
type ModelIndexedSupport,
|
|
34
|
+
ModelIndexedUtil,
|
|
35
|
+
type ModelPageOptions,
|
|
36
|
+
type ModelPageResult,
|
|
37
|
+
type SingleItemIndex,
|
|
38
|
+
type SortedIndex,
|
|
39
|
+
type SortedIndexSelection,
|
|
40
|
+
type SortedIndexSelectionType
|
|
16
41
|
} from '@travetto/model-indexed';
|
|
42
|
+
import { type Class, castTo, JSONUtil, ShutdownManager, TimeUtil } from '@travetto/runtime';
|
|
17
43
|
|
|
18
44
|
import type { DynamoDBModelConfig } from './config.ts';
|
|
19
45
|
import { DynamoDBUtil } from './util.ts';
|
|
20
46
|
|
|
21
47
|
const EXPIRES_ATTRIBUTE = 'expires_at__';
|
|
22
48
|
|
|
23
|
-
const getKey = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue =>
|
|
49
|
+
const getKey = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue =>
|
|
50
|
+
DynamoDBUtil.toValue(computed.getKey() || 'NULL');
|
|
24
51
|
const getSort = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue => DynamoDBUtil.toValue(computed.getSort());
|
|
25
52
|
|
|
26
53
|
/**
|
|
@@ -28,12 +55,13 @@ const getSort = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): A
|
|
|
28
55
|
*/
|
|
29
56
|
@Injectable()
|
|
30
57
|
export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
|
|
31
|
-
|
|
32
58
|
idSource = ModelCrudUtil.uuidSource();
|
|
33
59
|
client: DynamoDB;
|
|
34
60
|
config: DynamoDBModelConfig;
|
|
35
61
|
|
|
36
|
-
constructor(config: DynamoDBModelConfig) {
|
|
62
|
+
constructor(config: DynamoDBModelConfig) {
|
|
63
|
+
this.config = config;
|
|
64
|
+
}
|
|
37
65
|
|
|
38
66
|
#resolveTable(cls: Class): string {
|
|
39
67
|
let table = ModelRegistryIndex.getStoreName(cls);
|
|
@@ -43,11 +71,11 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
43
71
|
return table;
|
|
44
72
|
}
|
|
45
73
|
|
|
46
|
-
async
|
|
74
|
+
async *#scanCollection<T extends ModelType>(
|
|
47
75
|
cls: Class<T>,
|
|
48
76
|
query: (batchSize: number, lastKey: Record<string, AttributeValue> | undefined) => Promise<QueryCommandOutput>,
|
|
49
|
-
options?: ModelListOptions & ModelPageOptions<Record<string, AttributeValue
|
|
50
|
-
): AsyncIterable<{ items: T[]
|
|
77
|
+
options?: ModelListOptions & ModelPageOptions<Record<string, AttributeValue>>
|
|
78
|
+
): AsyncIterable<{ items: T[]; lastKey?: Record<string, AttributeValue> }> {
|
|
51
79
|
const batchSize = options?.batchSizeHint ?? 100;
|
|
52
80
|
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
53
81
|
let startKey = options?.offset ?? undefined;
|
|
@@ -59,26 +87,25 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
59
87
|
if (batch.Count && batch.Items) {
|
|
60
88
|
produced += batch.Count;
|
|
61
89
|
|
|
62
|
-
const items =
|
|
90
|
+
const items = produced > limit ? batch.Items.slice(0, remaining) : batch.Items;
|
|
63
91
|
startKey = batch.LastEvaluatedKey;
|
|
64
92
|
yield {
|
|
65
|
-
items: await ModelCrudUtil.filterOutNotFound(
|
|
66
|
-
items.map(item => DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!))),
|
|
93
|
+
items: await ModelCrudUtil.filterOutNotFound(items.map(item => DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!))),
|
|
67
94
|
lastKey: startKey
|
|
68
95
|
};
|
|
69
96
|
} else {
|
|
70
97
|
startKey = undefined;
|
|
71
98
|
}
|
|
72
|
-
} while (startKey && produced < limit && !
|
|
99
|
+
} while (startKey && produced < limit && !options?.abort?.aborted);
|
|
73
100
|
}
|
|
74
101
|
|
|
75
|
-
async
|
|
102
|
+
async *#scanIndex<T extends ModelType>(
|
|
76
103
|
cls: Class<T>,
|
|
77
104
|
idx: SortedIndex<T>,
|
|
78
105
|
body: KeyedIndexBody<T>,
|
|
79
106
|
options?: ModelPageOptions<Record<string, AttributeValue>> & ModelListOptions,
|
|
80
107
|
transform?: (query: QueryCommandInput) => QueryCommandInput
|
|
81
|
-
): AsyncIterable<{ items: T[]
|
|
108
|
+
): AsyncIterable<{ items: T[]; lastKey?: Record<string, AttributeValue> }> {
|
|
82
109
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
83
110
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
84
111
|
const { keyIndexName, keyIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
@@ -86,25 +113,29 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
86
113
|
|
|
87
114
|
const finalTransform = transform ?? ((query): QueryCommandInput => query);
|
|
88
115
|
|
|
89
|
-
yield* this.#scanCollection(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
116
|
+
yield* this.#scanCollection(
|
|
117
|
+
cls,
|
|
118
|
+
(batchSize, lastKey) => {
|
|
119
|
+
const finalized = finalTransform({
|
|
120
|
+
TableName: this.#resolveTable(cls),
|
|
121
|
+
IndexName: keyIndexName,
|
|
122
|
+
ProjectionExpression: 'body',
|
|
123
|
+
KeyConditionExpression: `${keyIndexAttribute} = :${keyIndexName}`,
|
|
124
|
+
ExpressionAttributeValues: expression,
|
|
125
|
+
Limit: batchSize,
|
|
126
|
+
ExclusiveStartKey: lastKey
|
|
127
|
+
});
|
|
128
|
+
return this.client.query(finalized);
|
|
129
|
+
},
|
|
130
|
+
options
|
|
131
|
+
);
|
|
101
132
|
}
|
|
102
133
|
|
|
103
|
-
async #getIdByIndex<
|
|
104
|
-
T
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
async #getIdByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
135
|
+
cls: Class<T>,
|
|
136
|
+
idx: SingleItemIndex<T, K, S>,
|
|
137
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
138
|
+
): Promise<string> {
|
|
108
139
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
109
140
|
|
|
110
141
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
@@ -119,11 +150,12 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
119
150
|
KeyConditionExpression: [
|
|
120
151
|
...(sorted ? [`${sortIndexAttribute} = :${sortIndexName}`] : []),
|
|
121
152
|
`${keyIndexAttribute} = :${keyIndexName}`
|
|
122
|
-
]
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
153
|
+
].join(' and '),
|
|
154
|
+
...(computed.idPart
|
|
155
|
+
? {
|
|
156
|
+
FilterExpression: 'id = :id'
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
127
159
|
ExpressionAttributeValues: {
|
|
128
160
|
[`:${keyIndexName}`]: getKey(computed),
|
|
129
161
|
...(sorted ? { [`:${sortIndexName}`]: getSort(computed) } : {}),
|
|
@@ -134,7 +166,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
134
166
|
try {
|
|
135
167
|
const result = await this.client.query(query);
|
|
136
168
|
|
|
137
|
-
if (result.Count && result.Items
|
|
169
|
+
if (result.Count && result.Items?.[0]) {
|
|
138
170
|
return result.Items[0].id.S!;
|
|
139
171
|
}
|
|
140
172
|
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey({ sort: true }));
|
|
@@ -146,7 +178,12 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
146
178
|
}
|
|
147
179
|
}
|
|
148
180
|
|
|
149
|
-
async #putItem<T extends ModelType>(
|
|
181
|
+
async #putItem<T extends ModelType>(
|
|
182
|
+
cls: Class<T>,
|
|
183
|
+
id: string,
|
|
184
|
+
item: T,
|
|
185
|
+
mode: 'create' | 'update' | 'upsert'
|
|
186
|
+
): Promise<PutItemCommandOutput> {
|
|
150
187
|
const config = ModelRegistryIndex.getConfig(cls);
|
|
151
188
|
let expiry: number | undefined;
|
|
152
189
|
|
|
@@ -165,7 +202,9 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
165
202
|
const { keyIndexAttribute, sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
166
203
|
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
167
204
|
switch (idx.type) {
|
|
168
|
-
case 'indexed:keyed':
|
|
205
|
+
case 'indexed:keyed':
|
|
206
|
+
indices[keyIndexAttribute] = getKey(computed);
|
|
207
|
+
break;
|
|
169
208
|
case 'indexed:sorted': {
|
|
170
209
|
indices[keyIndexAttribute] = getKey(computed);
|
|
171
210
|
indices[sortIndexAttribute] = getSort(computed);
|
|
@@ -219,11 +258,9 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
219
258
|
TableName: this.#resolveTable(cls),
|
|
220
259
|
ConditionExpression: mode === 'update' ? 'attribute_exists(body)' : undefined,
|
|
221
260
|
Key: { id: { S: id } },
|
|
222
|
-
UpdateExpression: `SET ${[
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
...expr
|
|
226
|
-
].filter(part => !!part).join(', ')}`,
|
|
261
|
+
UpdateExpression: `SET ${['body=:body', expiry !== undefined ? `${EXPIRES_ATTRIBUTE}=:expr` : undefined, ...expr]
|
|
262
|
+
.filter(part => !!part)
|
|
263
|
+
.join(', ')}`,
|
|
227
264
|
ExpressionAttributeValues: {
|
|
228
265
|
':body': DynamoDBUtil.toValue(JSONUtil.toUTF8(item)),
|
|
229
266
|
...(expiry !== undefined ? { ':expr': DynamoDBUtil.toValue(expiry) } : {}),
|
|
@@ -272,10 +309,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
272
309
|
TableName: table,
|
|
273
310
|
KeySchema: [{ KeyType: 'HASH', AttributeName: 'id' }],
|
|
274
311
|
BillingMode: 'PAY_PER_REQUEST',
|
|
275
|
-
AttributeDefinitions: [
|
|
276
|
-
{ AttributeName: 'id', AttributeType: 'S' },
|
|
277
|
-
...idx.attributes
|
|
278
|
-
],
|
|
312
|
+
AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }, ...idx.attributes],
|
|
279
313
|
GlobalSecondaryIndexes: idx.indices
|
|
280
314
|
});
|
|
281
315
|
} else {
|
|
@@ -287,10 +321,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
287
321
|
if (changedAttributes.length || indexUpdates?.length) {
|
|
288
322
|
await this.client.updateTable({
|
|
289
323
|
TableName: table,
|
|
290
|
-
AttributeDefinitions: [
|
|
291
|
-
{ AttributeName: 'id', AttributeType: 'S' },
|
|
292
|
-
...idx.attributes
|
|
293
|
-
],
|
|
324
|
+
AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }, ...idx.attributes],
|
|
294
325
|
GlobalSecondaryIndexUpdates: indexUpdates
|
|
295
326
|
});
|
|
296
327
|
}
|
|
@@ -312,7 +343,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
312
343
|
*/
|
|
313
344
|
async deleteModel(cls: Class<ModelType>): Promise<void> {
|
|
314
345
|
const table = this.#resolveTable(cls);
|
|
315
|
-
const { Table: verify } =
|
|
346
|
+
const { Table: verify } = await this.client.describeTable({ TableName: table }).catch(() => ({ Table: undefined }));
|
|
316
347
|
if (verify) {
|
|
317
348
|
await this.client.deleteTable({ TableName: table });
|
|
318
349
|
}
|
|
@@ -324,9 +355,11 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
324
355
|
|
|
325
356
|
async deleteStorage(): Promise<void> {
|
|
326
357
|
for (const model of ModelRegistryIndex.getClasses()) {
|
|
327
|
-
await this.client
|
|
328
|
-
|
|
329
|
-
|
|
358
|
+
await this.client
|
|
359
|
+
.deleteTable({
|
|
360
|
+
TableName: this.#resolveTable(model)
|
|
361
|
+
})
|
|
362
|
+
.catch(() => {});
|
|
330
363
|
}
|
|
331
364
|
}
|
|
332
365
|
|
|
@@ -337,7 +370,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
337
370
|
Key: { id: DynamoDBUtil.toValue(id) }
|
|
338
371
|
});
|
|
339
372
|
|
|
340
|
-
if (result
|
|
373
|
+
if (result?.Item?.body) {
|
|
341
374
|
return DynamoDBUtil.loadAndCheckExpiry(cls, result.Item.body.S!);
|
|
342
375
|
}
|
|
343
376
|
throw new NotFoundError(cls, id);
|
|
@@ -386,12 +419,17 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
386
419
|
}
|
|
387
420
|
}
|
|
388
421
|
|
|
389
|
-
async *
|
|
390
|
-
for await (const { items } of this.#scanCollection(
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
422
|
+
async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
423
|
+
for await (const { items } of this.#scanCollection(
|
|
424
|
+
cls,
|
|
425
|
+
(batchSize, lastKey) =>
|
|
426
|
+
this.client.scan({
|
|
427
|
+
TableName: this.#resolveTable(cls),
|
|
428
|
+
ExclusiveStartKey: lastKey,
|
|
429
|
+
Limit: batchSize
|
|
430
|
+
}),
|
|
431
|
+
options
|
|
432
|
+
)) {
|
|
395
433
|
yield items;
|
|
396
434
|
}
|
|
397
435
|
}
|
|
@@ -402,56 +440,52 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
402
440
|
}
|
|
403
441
|
|
|
404
442
|
// Indexed
|
|
405
|
-
async getByIndex<
|
|
406
|
-
T
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
443
|
+
async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
444
|
+
cls: Class<T>,
|
|
445
|
+
idx: SingleItemIndex<T, K, S>,
|
|
446
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
447
|
+
): Promise<T> {
|
|
410
448
|
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
411
449
|
}
|
|
412
450
|
|
|
413
|
-
async deleteByIndex<
|
|
414
|
-
T
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
451
|
+
async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
452
|
+
cls: Class<T>,
|
|
453
|
+
idx: SingleItemIndex<T, K, S>,
|
|
454
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
455
|
+
): Promise<void> {
|
|
418
456
|
return this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
419
457
|
}
|
|
420
458
|
|
|
421
|
-
upsertByIndex<
|
|
422
|
-
T
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
459
|
+
upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
460
|
+
cls: Class<T>,
|
|
461
|
+
idx: SingleItemIndex<T, K, S>,
|
|
462
|
+
body: OptionalId<T>
|
|
463
|
+
): Promise<T> {
|
|
426
464
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
427
465
|
}
|
|
428
466
|
|
|
429
|
-
async updateByIndex<
|
|
430
|
-
T
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
467
|
+
async updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
468
|
+
cls: Class<T>,
|
|
469
|
+
idx: SingleItemIndex<T, K, S>,
|
|
470
|
+
body: T
|
|
471
|
+
): Promise<T> {
|
|
434
472
|
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
435
473
|
}
|
|
436
474
|
|
|
437
|
-
async updatePartialByIndex<
|
|
438
|
-
T
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
475
|
+
async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
476
|
+
cls: Class<T>,
|
|
477
|
+
idx: SingleItemIndex<T, K, S>,
|
|
478
|
+
body: FullKeyedIndexWithPartialBody<T, K, S>
|
|
479
|
+
): Promise<T> {
|
|
442
480
|
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
443
481
|
return this.update(cls, item);
|
|
444
482
|
}
|
|
445
483
|
|
|
446
|
-
async pageByIndex<
|
|
447
|
-
T extends ModelType,
|
|
448
|
-
K extends KeyedIndexSelection<T>,
|
|
449
|
-
S extends SortedIndexSelection<T>
|
|
450
|
-
>(
|
|
484
|
+
async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
451
485
|
cls: Class<T>,
|
|
452
486
|
idx: SortedIndex<T, K, S>,
|
|
453
487
|
body: KeyedIndexBody<T, K>,
|
|
454
|
-
options?: ModelPageOptions
|
|
488
|
+
options?: ModelPageOptions
|
|
455
489
|
): Promise<ModelPageResult<T>> {
|
|
456
490
|
const output: T[] = [];
|
|
457
491
|
const offset = options?.offset ? JSONUtil.fromBase64<Record<string, AttributeValue>>(options.offset) : undefined;
|
|
@@ -459,7 +493,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
459
493
|
output.push(...items);
|
|
460
494
|
}
|
|
461
495
|
|
|
462
|
-
let nextOffset;
|
|
496
|
+
let nextOffset: string | undefined;
|
|
463
497
|
if (output.length) {
|
|
464
498
|
const last: T = output.at(-1)!;
|
|
465
499
|
const computed = ModelIndexedComputedIndex.get(idx, last).validate();
|
|
@@ -474,11 +508,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
474
508
|
return { items: output, nextOffset };
|
|
475
509
|
}
|
|
476
510
|
|
|
477
|
-
async *
|
|
478
|
-
T extends ModelType,
|
|
479
|
-
K extends KeyedIndexSelection<T>,
|
|
480
|
-
S extends SortedIndexSelection<T>
|
|
481
|
-
>(
|
|
511
|
+
async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
482
512
|
cls: Class<T>,
|
|
483
513
|
idx: SortedIndex<T, K, S>,
|
|
484
514
|
body: KeyedIndexBody<T, K>,
|
|
@@ -499,21 +529,17 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
499
529
|
|
|
500
530
|
const { sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
501
531
|
|
|
502
|
-
for await (const { items } of this.#scanIndex(cls, idx, body, { limit: 10, ...options },
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
':prefix': { S: prefix }
|
|
511
|
-
}
|
|
512
|
-
})
|
|
513
|
-
)) {
|
|
532
|
+
for await (const { items } of this.#scanIndex(cls, idx, body, { limit: 10, ...options }, query => ({
|
|
533
|
+
...query,
|
|
534
|
+
KeyConditionExpression: [query.KeyConditionExpression, `begins_with(${sortIndexAttribute}, :prefix)`].filter(Boolean).join(' AND '),
|
|
535
|
+
ExpressionAttributeValues: {
|
|
536
|
+
...query.ExpressionAttributeValues,
|
|
537
|
+
':prefix': { S: prefix }
|
|
538
|
+
}
|
|
539
|
+
}))) {
|
|
514
540
|
results.push(...items);
|
|
515
541
|
}
|
|
516
542
|
|
|
517
543
|
return results;
|
|
518
544
|
}
|
|
519
|
-
}
|
|
545
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
AttributeDefinition,
|
|
3
|
-
|
|
2
|
+
AttributeDefinition,
|
|
3
|
+
AttributeValue,
|
|
4
|
+
GlobalSecondaryIndex,
|
|
5
|
+
GlobalSecondaryIndexDescription,
|
|
6
|
+
GlobalSecondaryIndexUpdate,
|
|
7
|
+
KeySchemaElement
|
|
4
8
|
} from '@aws-sdk/client-dynamodb';
|
|
5
9
|
|
|
6
|
-
import { type
|
|
7
|
-
import { ModelCrudUtil, ModelExpiryUtil, ModelRegistryIndex, NotFoundError, type ModelType } from '@travetto/model';
|
|
10
|
+
import { ModelCrudUtil, ModelExpiryUtil, ModelRegistryIndex, type ModelType, NotFoundError } from '@travetto/model';
|
|
8
11
|
import { isModelIndexedIndex, warnIfIndexedUniqueIndex, warnIfNonIndexedIndex } from '@travetto/model-indexed';
|
|
12
|
+
import { type Class, castTo } from '@travetto/runtime';
|
|
9
13
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
10
14
|
|
|
11
15
|
/**
|
|
@@ -20,8 +24,9 @@ type DynamoIndexConfig = {
|
|
|
20
24
|
* Utility class for DynamoDB operations and transformations.
|
|
21
25
|
*/
|
|
22
26
|
export class DynamoDBUtil {
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
static indexNames = (
|
|
28
|
+
name: string
|
|
29
|
+
): { keyIndexName: string; sortIndexName: string; keyIndexAttribute: string; sortIndexAttribute: string } => {
|
|
25
30
|
const base = name.toLowerCase().replace(/[^A-Za-z0-9]+/g, '_');
|
|
26
31
|
return {
|
|
27
32
|
keyIndexName: base,
|
|
@@ -86,7 +91,7 @@ export class DynamoDBUtil {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
keys.push({ AttributeName: keyIndexAttribute, KeyType: 'HASH' });
|
|
89
|
-
keys.push({ AttributeName: sortIndexAttribute, KeyType: 'RANGE'
|
|
94
|
+
keys.push({ AttributeName: sortIndexAttribute, KeyType: 'RANGE' });
|
|
90
95
|
attributes.push({ AttributeName: keyIndexAttribute, AttributeType: 'S' });
|
|
91
96
|
attributes.push({ AttributeName: sortIndexAttribute, AttributeType: castTo(fieldType) === String ? 'S' : 'N' });
|
|
92
97
|
break;
|
|
@@ -116,7 +121,10 @@ export class DynamoDBUtil {
|
|
|
116
121
|
* Identifies attribute definitions that have changed between current and requested configurations.
|
|
117
122
|
* Compares attribute names and types to determine additions, removals, or modifications.
|
|
118
123
|
*/
|
|
119
|
-
static findChangedAttributes(
|
|
124
|
+
static findChangedAttributes(
|
|
125
|
+
current: AttributeDefinition[] | undefined,
|
|
126
|
+
requested: AttributeDefinition[] | undefined
|
|
127
|
+
): AttributeDefinition[] {
|
|
120
128
|
const currentMap = Object.fromEntries((current ?? []).map(attr => [attr.AttributeName, attr]));
|
|
121
129
|
const pendingMap = Object.fromEntries((requested ?? []).map(attr => [attr.AttributeName, attr]));
|
|
122
130
|
|
|
@@ -145,7 +153,10 @@ export class DynamoDBUtil {
|
|
|
145
153
|
* Identifies global secondary indices that have changed between current and requested configurations.
|
|
146
154
|
* Generates update operations for creating new indices or deleting removed indices.
|
|
147
155
|
*/
|
|
148
|
-
static findChangedGlobalIndexes(
|
|
156
|
+
static findChangedGlobalIndexes(
|
|
157
|
+
current: GlobalSecondaryIndexDescription[] | undefined,
|
|
158
|
+
requested: GlobalSecondaryIndex[] | undefined
|
|
159
|
+
): GlobalSecondaryIndexUpdate[] {
|
|
149
160
|
const existingMap = Object.fromEntries((current ?? []).map(index => [index.IndexName, index]));
|
|
150
161
|
const pendingMap = Object.fromEntries((requested ?? []).map(index => [index.IndexName, index]));
|
|
151
162
|
|