@travetto/model-dynamodb 8.0.0-alpha.16 → 8.0.0-alpha.17
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 +71 -41
- package/src/util.ts +37 -13
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#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.17",
|
|
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.1031.0",
|
|
30
|
+
"@travetto/config": "^8.0.0-alpha.15",
|
|
31
|
+
"@travetto/model": "^8.0.0-alpha.15",
|
|
32
|
+
"@travetto/model-indexed": "^8.0.0-alpha.17"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.20"
|
|
36
36
|
},
|
|
37
37
|
"peerDependenciesMeta": {
|
|
38
38
|
"@travetto/cli": {
|
package/src/service.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
isModelIndexedIndex, ModelIndexedUtil, type KeyedIndexBody, type KeyedIndexSelection,
|
|
13
13
|
type ModelPageOptions, type ModelPageResult, type ModelIndexedSupport, type SingleItemIndex,
|
|
14
14
|
type FullKeyedIndexBody, type FullKeyedIndexWithPartialBody, type SortedIndex, type SortedIndexSelection,
|
|
15
|
-
ModelIndexedComputedIndex
|
|
15
|
+
ModelIndexedComputedIndex, type ModelIndexedSearchOptions, type SortedIndexSelectionType
|
|
16
16
|
} from '@travetto/model-indexed';
|
|
17
17
|
|
|
18
18
|
import type { DynamoDBModelConfig } from './config.ts';
|
|
@@ -72,30 +72,32 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
72
72
|
} while (startKey && produced < limit && !(options?.abort?.aborted));
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
async * #scanIndex<
|
|
76
|
-
T extends ModelType,
|
|
77
|
-
K extends KeyedIndexSelection<T>,
|
|
78
|
-
S extends SortedIndexSelection<T>
|
|
79
|
-
>(
|
|
75
|
+
async * #scanIndex<T extends ModelType>(
|
|
80
76
|
cls: Class<T>,
|
|
81
|
-
idx: SortedIndex<T
|
|
82
|
-
body: KeyedIndexBody<T
|
|
83
|
-
options?: ModelPageOptions<Record<string, AttributeValue>> & ModelListOptions
|
|
77
|
+
idx: SortedIndex<T>,
|
|
78
|
+
body: KeyedIndexBody<T>,
|
|
79
|
+
options?: ModelPageOptions<Record<string, AttributeValue>> & ModelListOptions,
|
|
80
|
+
transform?: (query: QueryCommandInput) => QueryCommandInput
|
|
84
81
|
): AsyncIterable<{ items: T[], lastKey?: Record<string, AttributeValue> }> {
|
|
85
82
|
ModelCrudUtil.ensureNotSubType(cls);
|
|
86
83
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
87
|
-
const
|
|
88
|
-
const expression = { [`:${
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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);
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
async #getIdByIndex<
|
|
@@ -107,24 +109,24 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
107
109
|
|
|
108
110
|
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
109
111
|
|
|
110
|
-
const
|
|
112
|
+
const { keyIndexName, keyIndexAttribute, sortIndexAttribute, sortIndexName } = DynamoDBUtil.indexNames(idx.name);
|
|
111
113
|
const sorted = idx.type === 'indexed:sorted';
|
|
112
114
|
|
|
113
115
|
const query: QueryCommandInput = {
|
|
114
116
|
TableName: this.#resolveTable(cls),
|
|
115
|
-
IndexName:
|
|
117
|
+
IndexName: keyIndexName,
|
|
116
118
|
ProjectionExpression: 'id',
|
|
117
119
|
KeyConditionExpression: [
|
|
118
|
-
...(sorted ? [`${
|
|
119
|
-
`${
|
|
120
|
+
...(sorted ? [`${sortIndexAttribute} = :${sortIndexName}`] : []),
|
|
121
|
+
`${keyIndexAttribute} = :${keyIndexName}`
|
|
120
122
|
]
|
|
121
123
|
.join(' and '),
|
|
122
124
|
...(computed.idPart ? {
|
|
123
125
|
FilterExpression: 'id = :id'
|
|
124
126
|
} : {}),
|
|
125
127
|
ExpressionAttributeValues: {
|
|
126
|
-
[`:${
|
|
127
|
-
...(sorted ? { [`:${
|
|
128
|
+
[`:${keyIndexName}`]: getKey(computed),
|
|
129
|
+
...(sorted ? { [`:${sortIndexName}`]: getSort(computed) } : {}),
|
|
128
130
|
...(computed.idPart ? { ':id': DynamoDBUtil.toValue(computed.idPart.value) } : {})
|
|
129
131
|
}
|
|
130
132
|
};
|
|
@@ -160,13 +162,13 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
160
162
|
const indices: Record<string, unknown> = {};
|
|
161
163
|
for (const idx of ModelRegistryIndex.getIndices(cls)) {
|
|
162
164
|
if (isModelIndexedIndex(idx)) {
|
|
163
|
-
const
|
|
165
|
+
const { keyIndexAttribute, sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
164
166
|
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
165
167
|
switch (idx.type) {
|
|
166
|
-
case 'indexed:keyed': indices[
|
|
168
|
+
case 'indexed:keyed': indices[keyIndexAttribute] = getKey(computed); break;
|
|
167
169
|
case 'indexed:sorted': {
|
|
168
|
-
indices[
|
|
169
|
-
indices[
|
|
170
|
+
indices[keyIndexAttribute] = getKey(computed);
|
|
171
|
+
indices[sortIndexAttribute] = getSort(computed);
|
|
170
172
|
break;
|
|
171
173
|
}
|
|
172
174
|
}
|
|
@@ -192,19 +194,19 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
192
194
|
|
|
193
195
|
for (const idx of ModelRegistryIndex.getIndices(cls)) {
|
|
194
196
|
if (isModelIndexedIndex(idx)) {
|
|
195
|
-
const
|
|
197
|
+
const { keyIndexAttribute, sortIndexAttribute, keyIndexName, sortIndexName } = DynamoDBUtil.indexNames(idx.name);
|
|
196
198
|
const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
|
|
197
199
|
switch (idx.type) {
|
|
198
200
|
case 'indexed:keyed': {
|
|
199
|
-
indices[`:${
|
|
200
|
-
expr.push(`${
|
|
201
|
+
indices[`:${keyIndexName}`] = getKey(computed);
|
|
202
|
+
expr.push(`${keyIndexAttribute} = :${keyIndexName}`);
|
|
201
203
|
break;
|
|
202
204
|
}
|
|
203
205
|
case 'indexed:sorted': {
|
|
204
|
-
indices[`:${
|
|
205
|
-
indices[`:${
|
|
206
|
-
expr.push(`${
|
|
207
|
-
expr.push(`${
|
|
206
|
+
indices[`:${keyIndexName}`] = getKey(computed);
|
|
207
|
+
indices[`:${sortIndexName}`] = getSort(computed);
|
|
208
|
+
expr.push(`${keyIndexAttribute} = :${keyIndexName}`);
|
|
209
|
+
expr.push(`${sortIndexAttribute} = :${sortIndexName}`);
|
|
208
210
|
break;
|
|
209
211
|
}
|
|
210
212
|
}
|
|
@@ -461,10 +463,10 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
461
463
|
if (output.length) {
|
|
462
464
|
const last: T = output.at(-1)!;
|
|
463
465
|
const computed = ModelIndexedComputedIndex.get(idx, last).validate();
|
|
464
|
-
const
|
|
466
|
+
const { keyIndexAttribute, sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
|
|
465
467
|
nextOffset = JSONUtil.toBase64({
|
|
466
|
-
[
|
|
467
|
-
[
|
|
468
|
+
[keyIndexAttribute]: getKey(computed),
|
|
469
|
+
[sortIndexAttribute]: getSort(computed),
|
|
468
470
|
id: DynamoDBUtil.toValue(last.id)
|
|
469
471
|
});
|
|
470
472
|
}
|
|
@@ -486,4 +488,32 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
|
|
|
486
488
|
yield items;
|
|
487
489
|
}
|
|
488
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
|
+
}
|
|
489
519
|
}
|
package/src/util.ts
CHANGED
|
@@ -3,9 +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 { warnIfIndexedUniqueIndex, warnIfNonIndexedIndex } from '@travetto/model-indexed';
|
|
8
|
+
import { isModelIndexedIndex, warnIfIndexedUniqueIndex, warnIfNonIndexedIndex } from '@travetto/model-indexed';
|
|
9
|
+
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Configuration for DynamoDB indices
|
|
@@ -20,7 +21,15 @@ type DynamoIndexConfig = {
|
|
|
20
21
|
*/
|
|
21
22
|
export class DynamoDBUtil {
|
|
22
23
|
|
|
23
|
-
static
|
|
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
|
+
};
|
|
24
33
|
|
|
25
34
|
/**
|
|
26
35
|
* Converts a JavaScript value to a DynamoDB AttributeValue format
|
|
@@ -53,29 +62,44 @@ export class DynamoDBUtil {
|
|
|
53
62
|
|
|
54
63
|
const filtered = indexes
|
|
55
64
|
.filter(idx => !warnIfIndexedUniqueIndex(this, cls, [idx]))
|
|
56
|
-
.filter(idx => !warnIfNonIndexedIndex(this, cls, [idx]))
|
|
65
|
+
.filter(idx => !warnIfNonIndexedIndex(this, cls, [idx]))
|
|
66
|
+
.filter(isModelIndexedIndex);
|
|
57
67
|
|
|
58
68
|
for (const idx of filtered) {
|
|
59
69
|
const keys: KeySchemaElement[] = [];
|
|
60
70
|
|
|
61
|
-
const
|
|
71
|
+
const { keyIndexName, keyIndexAttribute, sortIndexAttribute } = this.indexNames(idx.name);
|
|
62
72
|
|
|
63
73
|
switch (idx.type) {
|
|
64
|
-
case 'indexed:sorted':
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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' });
|
|
69
92
|
break;
|
|
93
|
+
}
|
|
70
94
|
case 'indexed:keyed': {
|
|
71
|
-
keys.push({ AttributeName:
|
|
72
|
-
attributes.push({ AttributeName:
|
|
95
|
+
keys.push({ AttributeName: keyIndexAttribute, KeyType: 'HASH' });
|
|
96
|
+
attributes.push({ AttributeName: keyIndexAttribute, AttributeType: 'S' });
|
|
73
97
|
break;
|
|
74
98
|
}
|
|
75
99
|
}
|
|
76
100
|
|
|
77
101
|
toCreate.push({
|
|
78
|
-
IndexName:
|
|
102
|
+
IndexName: keyIndexName,
|
|
79
103
|
// ProvisionedThroughput: '',
|
|
80
104
|
Projection: {
|
|
81
105
|
ProjectionType: 'INCLUDE',
|