@travetto/model-dynamodb 8.0.0-alpha.1 → 8.0.0-alpha.10
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 +204 -94
- package/src/util.ts +30 -27
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#L23)
|
|
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.10",
|
|
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.1019.0",
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.10",
|
|
31
|
+
"@travetto/model": "^8.0.0-alpha.10",
|
|
32
|
+
"@travetto/model-indexed": "^8.0.0-alpha.10"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.15"
|
|
35
36
|
},
|
|
36
37
|
"peerDependenciesMeta": {
|
|
37
38
|
"@travetto/cli": {
|
package/src/service.ts
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import { type AttributeValue, DynamoDB, type PutItemCommandInput, type PutItemCommandOutput } from '@aws-sdk/client-dynamodb';
|
|
1
|
+
import { type AttributeValue, DynamoDB, type PutItemCommandInput, type PutItemCommandOutput, 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
|
-
ModelCrudUtil, ModelExpiryUtil, ModelIndexedUtil, ModelStorageUtil
|
|
7
|
+
type ModelType, NotFoundError, ExistsError, type OptionalId, ModelCrudUtil,
|
|
8
|
+
ModelExpiryUtil, ModelStorageUtil,
|
|
10
9
|
} from '@travetto/model';
|
|
10
|
+
import {
|
|
11
|
+
isModelIndexedIndex, ModelIndexedUtil, type KeyedIndexBody, type KeyedIndexSelection,
|
|
12
|
+
type ListPageOptions, type ListPageResult, type ModelIndexedSupport, type SingleItemIndex,
|
|
13
|
+
type FullKeyedIndexBody, type FullKeyedIndexWithPartialBody, type SortedIndex, type SortedIndexSelection,
|
|
14
|
+
ModelIndexedComputedIndex
|
|
15
|
+
} from '@travetto/model-indexed';
|
|
11
16
|
|
|
12
17
|
import type { DynamoDBModelConfig } from './config.ts';
|
|
13
18
|
import { DynamoDBUtil } from './util.ts';
|
|
14
19
|
|
|
15
|
-
const
|
|
20
|
+
const EXPIRES_ATTRIBUTE = 'expires_at__';
|
|
21
|
+
|
|
22
|
+
const getKey = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue => DynamoDBUtil.toValue(computed.getKey() || 'NULL');
|
|
23
|
+
const getSort = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue => DynamoDBUtil.toValue(computed.getSort());
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* A model service backed by DynamoDB
|
|
@@ -34,6 +42,93 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
34
42
|
return table;
|
|
35
43
|
}
|
|
36
44
|
|
|
45
|
+
async * #scanIndex<
|
|
46
|
+
T extends ModelType,
|
|
47
|
+
K extends KeyedIndexSelection<T>,
|
|
48
|
+
S extends SortedIndexSelection<T>
|
|
49
|
+
>(
|
|
50
|
+
cls: Class,
|
|
51
|
+
idx: SortedIndex<T, K, S>,
|
|
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
|
+
|
|
61
|
+
let startKey = options?.offset ?? undefined;
|
|
62
|
+
let produced = 0;
|
|
63
|
+
|
|
64
|
+
do {
|
|
65
|
+
const remaining = limit - produced;
|
|
66
|
+
const batch = await this.client.query({
|
|
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
|
+
});
|
|
75
|
+
|
|
76
|
+
if (batch.Count && batch.Items) {
|
|
77
|
+
produced += batch.Count;
|
|
78
|
+
|
|
79
|
+
if (produced > limit) {
|
|
80
|
+
const items = batch.Items.slice(0, remaining);
|
|
81
|
+
yield { ...batch, Items: items, };
|
|
82
|
+
} else {
|
|
83
|
+
yield batch;
|
|
84
|
+
}
|
|
85
|
+
startKey = batch.LastEvaluatedKey;
|
|
86
|
+
} else {
|
|
87
|
+
startKey = undefined;
|
|
88
|
+
}
|
|
89
|
+
} while (startKey && produced < limit);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async #getIdByIndex<
|
|
93
|
+
T extends ModelType,
|
|
94
|
+
K extends KeyedIndexSelection<T>,
|
|
95
|
+
S extends SortedIndexSelection<T>
|
|
96
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<string> {
|
|
97
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
98
|
+
|
|
99
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
100
|
+
|
|
101
|
+
const safeName = DynamoDBUtil.toSafeName(idx.name);
|
|
102
|
+
const sorted = idx.type === 'indexed:sorted';
|
|
103
|
+
|
|
104
|
+
const query = {
|
|
105
|
+
TableName: this.#resolveTable(cls),
|
|
106
|
+
IndexName: safeName,
|
|
107
|
+
ProjectionExpression: 'id',
|
|
108
|
+
KeyConditionExpression: [sorted ? `${safeName}_sort__ = :${safeName}_sort` : '', `${safeName}__ = :${safeName}`]
|
|
109
|
+
.filter(expr => !!expr)
|
|
110
|
+
.join(' and '),
|
|
111
|
+
ExpressionAttributeValues: {
|
|
112
|
+
[`:${safeName}`]: getKey(computed),
|
|
113
|
+
...(sorted ? { [`:${safeName}_sort`]: getSort(computed) } : {})
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const result = await this.client.query(query);
|
|
119
|
+
|
|
120
|
+
if (result.Count && result.Items && result.Items[0]) {
|
|
121
|
+
return result.Items[0].id.S!;
|
|
122
|
+
}
|
|
123
|
+
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey({ sort: true }));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error instanceof Error && error.message.includes('The table does not have the specified index')) {
|
|
126
|
+
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey({ sort: true }));
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
37
132
|
async #putItem<T extends ModelType>(cls: Class<T>, id: string, item: T, mode: 'create' | 'update' | 'upsert'): Promise<PutItemCommandOutput> {
|
|
38
133
|
const config = ModelRegistryIndex.getConfig(cls);
|
|
39
134
|
let expiry: number | undefined;
|
|
@@ -48,12 +143,20 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
48
143
|
try {
|
|
49
144
|
if (mode === 'create') {
|
|
50
145
|
const indices: Record<string, unknown> = {};
|
|
51
|
-
for (const idx of
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
146
|
+
for (const idx of ModelRegistryIndex.getIndices(cls)) {
|
|
147
|
+
if (isModelIndexedIndex(idx)) {
|
|
148
|
+
const safeName = DynamoDBUtil.toSafeName(idx.name);
|
|
149
|
+
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
150
|
+
switch (idx.type) {
|
|
151
|
+
case 'indexed:keyed': indices[`${safeName}__`] = getKey(computed); break;
|
|
152
|
+
case 'indexed:sorted': {
|
|
153
|
+
indices[`${safeName}__`] = getKey(computed);
|
|
154
|
+
indices[`${safeName}_sort__`] = getSort(computed);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
console.warn('Unsupported index type on update', { cls: cls.name, idx });
|
|
57
160
|
}
|
|
58
161
|
}
|
|
59
162
|
const query: PutItemCommandInput = {
|
|
@@ -62,7 +165,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
62
165
|
Item: {
|
|
63
166
|
id: DynamoDBUtil.toValue(item.id),
|
|
64
167
|
body: DynamoDBUtil.toValue(JSONUtil.toUTF8(item)),
|
|
65
|
-
...(expiry !== undefined ? { [
|
|
168
|
+
...(expiry !== undefined ? { [EXPIRES_ATTRIBUTE]: DynamoDBUtil.toValue(expiry) } : {}),
|
|
66
169
|
...indices
|
|
67
170
|
},
|
|
68
171
|
ReturnValues: 'NONE'
|
|
@@ -71,14 +174,27 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
71
174
|
} else {
|
|
72
175
|
const indices: Record<string, unknown> = {};
|
|
73
176
|
const expr: string[] = [];
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
177
|
+
|
|
178
|
+
for (const idx of ModelRegistryIndex.getIndices(cls)) {
|
|
179
|
+
if (isModelIndexedIndex(idx)) {
|
|
180
|
+
const safeName = DynamoDBUtil.toSafeName(idx.name);
|
|
181
|
+
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
182
|
+
switch (idx.type) {
|
|
183
|
+
case 'indexed:keyed': {
|
|
184
|
+
indices[`:${safeName}`] = getKey(computed);
|
|
185
|
+
expr.push(`${safeName}__ = :${safeName}`);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case 'indexed:sorted': {
|
|
189
|
+
indices[`:${safeName}`] = getKey(computed);
|
|
190
|
+
indices[`:${safeName}_sort`] = getSort(computed);
|
|
191
|
+
expr.push(`${safeName}__ = :${safeName}`);
|
|
192
|
+
expr.push(`${safeName}_sort__ = :${safeName}_sort`);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
console.warn('Unsupported index type on update', { cls: cls.name, idx });
|
|
82
198
|
}
|
|
83
199
|
}
|
|
84
200
|
|
|
@@ -88,7 +204,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
88
204
|
Key: { id: { S: id } },
|
|
89
205
|
UpdateExpression: `SET ${[
|
|
90
206
|
'body=:body',
|
|
91
|
-
expiry !== undefined ? `${
|
|
207
|
+
expiry !== undefined ? `${EXPIRES_ATTRIBUTE}=:expr` : undefined,
|
|
92
208
|
...expr
|
|
93
209
|
].filter(part => !!part).join(', ')}`,
|
|
94
210
|
ExpressionAttributeValues: {
|
|
@@ -168,7 +284,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
168
284
|
if (ttlEnabled !== ttlRequired) {
|
|
169
285
|
await this.client.updateTimeToLive({
|
|
170
286
|
TableName: table,
|
|
171
|
-
TimeToLiveSpecification: { AttributeName: ttlRequired ?
|
|
287
|
+
TimeToLiveSpecification: { AttributeName: ttlRequired ? EXPIRES_ATTRIBUTE : undefined, Enabled: ttlRequired }
|
|
172
288
|
});
|
|
173
289
|
}
|
|
174
290
|
}
|
|
@@ -288,92 +404,86 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
288
404
|
}
|
|
289
405
|
|
|
290
406
|
// 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> {
|
|
407
|
+
async getByIndex<
|
|
408
|
+
T extends ModelType,
|
|
409
|
+
K extends KeyedIndexSelection<T>,
|
|
410
|
+
S extends SortedIndexSelection<T>
|
|
411
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
|
|
327
412
|
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
328
413
|
}
|
|
329
414
|
|
|
330
|
-
async deleteByIndex<
|
|
415
|
+
async deleteByIndex<
|
|
416
|
+
T extends ModelType,
|
|
417
|
+
K extends KeyedIndexSelection<T>,
|
|
418
|
+
S extends SortedIndexSelection<T>
|
|
419
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
|
|
331
420
|
return this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
332
421
|
}
|
|
333
422
|
|
|
334
|
-
upsertByIndex<
|
|
423
|
+
upsertByIndex<
|
|
424
|
+
T extends ModelType,
|
|
425
|
+
K extends KeyedIndexSelection<T>,
|
|
426
|
+
S extends SortedIndexSelection<T>
|
|
427
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
|
|
335
428
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
336
429
|
}
|
|
337
430
|
|
|
338
|
-
async
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
431
|
+
async updateByIndex<
|
|
432
|
+
T extends ModelType,
|
|
433
|
+
K extends KeyedIndexSelection<T>,
|
|
434
|
+
S extends SortedIndexSelection<T>
|
|
435
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
|
|
436
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
437
|
+
}
|
|
345
438
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
ExpressionAttributeValues: {
|
|
355
|
-
[`:${idxName}`]: DynamoDBUtil.toValue(key)
|
|
356
|
-
},
|
|
357
|
-
ExclusiveStartKey: token
|
|
358
|
-
});
|
|
439
|
+
async updatePartialByIndex<
|
|
440
|
+
T extends ModelType,
|
|
441
|
+
K extends KeyedIndexSelection<T>,
|
|
442
|
+
S extends SortedIndexSelection<T>
|
|
443
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
|
|
444
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
445
|
+
return this.update(cls, item);
|
|
446
|
+
}
|
|
359
447
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
448
|
+
async listByIndex<
|
|
449
|
+
T extends ModelType,
|
|
450
|
+
K extends KeyedIndexSelection<T>,
|
|
451
|
+
S extends SortedIndexSelection<T>
|
|
452
|
+
>(
|
|
453
|
+
cls: Class<T>,
|
|
454
|
+
idx: SortedIndex<T, K, S>,
|
|
455
|
+
body: KeyedIndexBody<T, K>,
|
|
456
|
+
options?: ListPageOptions,
|
|
457
|
+
): Promise<ListPageResult<T>> {
|
|
458
|
+
const items: T[] = [];
|
|
459
|
+
const offset = options?.offset ? JSONUtil.fromBase64<Record<string, AttributeValue>>(options.offset) : undefined;
|
|
460
|
+
for await (const batch of this.#scanIndex(cls, idx, body, { ...options, offset })) {
|
|
461
|
+
for (const item of batch.Items ?? []) {
|
|
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;
|
|
368
470
|
}
|
|
369
471
|
}
|
|
370
472
|
}
|
|
473
|
+
}
|
|
371
474
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
475
|
+
let nextOffset;
|
|
476
|
+
if (items.length) {
|
|
477
|
+
const last: T = items.at(-1)!;
|
|
478
|
+
const computed = ModelIndexedComputedIndex.get(idx, last).validate();
|
|
479
|
+
const safeName = DynamoDBUtil.toSafeName(idx.name);
|
|
480
|
+
nextOffset = JSONUtil.toBase64({
|
|
481
|
+
[`${safeName}__`]: getKey(computed),
|
|
482
|
+
[`${safeName}_sort__`]: getSort(computed),
|
|
483
|
+
id: DynamoDBUtil.toValue(last.id)
|
|
484
|
+
});
|
|
377
485
|
}
|
|
486
|
+
|
|
487
|
+
return { items, nextOffset };
|
|
378
488
|
}
|
|
379
489
|
}
|
package/src/util.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
|
|
6
6
|
import type { Class } from '@travetto/runtime';
|
|
7
7
|
import { ModelCrudUtil, ModelExpiryUtil, ModelRegistryIndex, NotFoundError, type ModelType } from '@travetto/model';
|
|
8
|
+
import { isModelIndexedIndex } from '@travetto/model-indexed';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Configuration for DynamoDB indices
|
|
@@ -19,12 +20,7 @@ type DynamoIndexConfig = {
|
|
|
19
20
|
*/
|
|
20
21
|
export class DynamoDBUtil {
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
* Converts an index name to a simplified format by removing non-alphanumeric characters
|
|
24
|
-
*/
|
|
25
|
-
static simpleName(idx: string): string {
|
|
26
|
-
return idx.replace(/[^A-Za-z0-9]/g, '');
|
|
27
|
-
}
|
|
23
|
+
static toSafeName = (name: string): string => name.toLowerCase().replace(/[^A-Za-z0-9]+/g, '_');
|
|
28
24
|
|
|
29
25
|
/**
|
|
30
26
|
* Converts a JavaScript value to a DynamoDB AttributeValue format
|
|
@@ -51,29 +47,36 @@ export class DynamoDBUtil {
|
|
|
51
47
|
* Generates global secondary indices and attribute definitions based on the model's index configuration.
|
|
52
48
|
*/
|
|
53
49
|
static computeIndexConfig<T extends ModelType>(cls: Class<T>): DynamoIndexConfig {
|
|
54
|
-
const
|
|
50
|
+
const indexes = ModelRegistryIndex.getIndices(cls);
|
|
55
51
|
const attributes: AttributeDefinition[] = [];
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
for (const idx of
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
KeyType: '
|
|
71
|
-
|
|
72
|
-
|
|
52
|
+
const toCreate: GlobalSecondaryIndex[] = [];
|
|
53
|
+
|
|
54
|
+
for (const idx of indexes) {
|
|
55
|
+
if (!isModelIndexedIndex(idx) || ('unique' in idx && idx.unique)) {
|
|
56
|
+
console.warn('Non-indexed indices are not supported in DynamoDB for', { cls: cls.Ⲑid, idx: idx.name });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const keys: KeySchemaElement[] = [];
|
|
61
|
+
|
|
62
|
+
const safeName = this.toSafeName(idx.name);
|
|
63
|
+
|
|
64
|
+
switch (idx.type) {
|
|
65
|
+
case 'indexed:sorted':
|
|
66
|
+
keys.push({ AttributeName: `${safeName}__`, KeyType: 'HASH' });
|
|
67
|
+
keys.push({ AttributeName: `${safeName}_sort__`, KeyType: 'RANGE', });
|
|
68
|
+
attributes.push({ AttributeName: `${safeName}__`, AttributeType: 'S' });
|
|
69
|
+
attributes.push({ AttributeName: `${safeName}_sort__`, AttributeType: 'N' });
|
|
70
|
+
break;
|
|
71
|
+
case 'indexed:keyed': {
|
|
72
|
+
keys.push({ AttributeName: `${safeName}__`, KeyType: 'HASH' });
|
|
73
|
+
attributes.push({ AttributeName: `${safeName}__`, AttributeType: 'S' });
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
IndexName:
|
|
78
|
+
toCreate.push({
|
|
79
|
+
IndexName: safeName,
|
|
77
80
|
// ProvisionedThroughput: '',
|
|
78
81
|
Projection: {
|
|
79
82
|
ProjectionType: 'INCLUDE',
|
|
@@ -83,7 +86,7 @@ export class DynamoDBUtil {
|
|
|
83
86
|
});
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
return { indices:
|
|
89
|
+
return { indices: toCreate.length ? toCreate : undefined, attributes };
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
/**
|