@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 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#L15)
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.16",
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.1030.0",
30
- "@travetto/config": "^8.0.0-alpha.14",
31
- "@travetto/model": "^8.0.0-alpha.14",
32
- "@travetto/model-indexed": "^8.0.0-alpha.16"
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.19"
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, K, S>,
82
- body: KeyedIndexBody<T, K>,
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 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);
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 safeName = DynamoDBUtil.toSafeName(idx.name);
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: safeName,
117
+ IndexName: keyIndexName,
116
118
  ProjectionExpression: 'id',
117
119
  KeyConditionExpression: [
118
- ...(sorted ? [`${safeName}_sort__ = :${safeName}_sort`] : []),
119
- `${safeName}__ = :${safeName}`
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
- [`:${safeName}`]: getKey(computed),
127
- ...(sorted ? { [`:${safeName}_sort`]: getSort(computed) } : {}),
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 safeName = DynamoDBUtil.toSafeName(idx.name);
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[`${safeName}__`] = getKey(computed); break;
168
+ case 'indexed:keyed': indices[keyIndexAttribute] = getKey(computed); break;
167
169
  case 'indexed:sorted': {
168
- indices[`${safeName}__`] = getKey(computed);
169
- indices[`${safeName}_sort__`] = getSort(computed);
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 safeName = DynamoDBUtil.toSafeName(idx.name);
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[`:${safeName}`] = getKey(computed);
200
- expr.push(`${safeName}__ = :${safeName}`);
201
+ indices[`:${keyIndexName}`] = getKey(computed);
202
+ expr.push(`${keyIndexAttribute} = :${keyIndexName}`);
201
203
  break;
202
204
  }
203
205
  case 'indexed:sorted': {
204
- indices[`:${safeName}`] = getKey(computed);
205
- indices[`:${safeName}_sort`] = getSort(computed);
206
- expr.push(`${safeName}__ = :${safeName}`);
207
- expr.push(`${safeName}_sort__ = :${safeName}_sort`);
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 safeName = DynamoDBUtil.toSafeName(idx.name);
466
+ const { keyIndexAttribute, sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
465
467
  nextOffset = JSONUtil.toBase64({
466
- [`${safeName}__`]: getKey(computed),
467
- [`${safeName}_sort__`]: getSort(computed),
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 { Class } from '@travetto/runtime';
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 toSafeName = (name: string): string => name.toLowerCase().replace(/[^A-Za-z0-9]+/g, '_');
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 safeName = this.toSafeName(idx.name);
71
+ const { keyIndexName, keyIndexAttribute, sortIndexAttribute } = this.indexNames(idx.name);
62
72
 
63
73
  switch (idx.type) {
64
- case 'indexed:sorted':
65
- keys.push({ AttributeName: `${safeName}__`, KeyType: 'HASH' });
66
- keys.push({ AttributeName: `${safeName}_sort__`, KeyType: 'RANGE', });
67
- attributes.push({ AttributeName: `${safeName}__`, AttributeType: 'S' });
68
- attributes.push({ AttributeName: `${safeName}_sort__`, AttributeType: 'N' });
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: `${safeName}__`, KeyType: 'HASH' });
72
- attributes.push({ AttributeName: `${safeName}__`, AttributeType: 'S' });
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: safeName,
102
+ IndexName: keyIndexName,
79
103
  // ProvisionedThroughput: '',
80
104
  Projection: {
81
105
  ProjectionType: 'INCLUDE',