@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 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/indexed.ts#L11)
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.1",
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.1004.0",
30
- "@travetto/config": "^8.0.0-alpha.1",
31
- "@travetto/model": "^8.0.0-alpha.1"
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.1"
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, type DeepPartial } from '@travetto/runtime';
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 ModelIndexedSupport, type ModelType, NotFoundError, ExistsError,
8
- IndexNotSupported, type OptionalId,
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 EXP_ATTR = 'expires_at__';
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 config.indices ?? []) {
52
- const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, item);
53
- const property = DynamoDBUtil.simpleName(idx.name);
54
- indices[`${property}__`] = DynamoDBUtil.toValue(key);
55
- if (sort) {
56
- indices[`${property}_sort__`] = DynamoDBUtil.toValue(+sort);
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 ? { [EXP_ATTR]: DynamoDBUtil.toValue(expiry) } : {}),
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
- for (const idx of config.indices ?? []) {
75
- const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idx, item);
76
- const property = DynamoDBUtil.simpleName(idx.name);
77
- indices[`:${property}`] = DynamoDBUtil.toValue(key);
78
- expr.push(`${property}__ = :${property}`);
79
- if (sort) {
80
- indices[`:${property}_sort`] = DynamoDBUtil.toValue(+sort);
81
- expr.push(`${property}_sort__ = :${property}_sort`);
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 ? `${EXP_ATTR}=:expr` : 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 ? EXP_ATTR : undefined, Enabled: 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 #getIdByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<string> {
292
- ModelCrudUtil.ensureNotSubType(cls);
293
-
294
- const idxConfig = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
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<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
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<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
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 * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
339
- ModelCrudUtil.ensureNotSubType(cls);
340
-
341
- const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
342
- const { key } = ModelIndexedUtil.computeIndexKey(cls, config, body, { emptySortValue: null });
343
-
344
- const idxName = DynamoDBUtil.simpleName(idx);
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
- let done = false;
347
- let token: Record<string, AttributeValue> | undefined;
348
- while (!done) {
349
- const batch = await this.client.query({
350
- TableName: this.#resolveTable(cls),
351
- IndexName: idxName,
352
- ProjectionExpression: 'body',
353
- KeyConditionExpression: `${idxName}__ = :${idxName}`,
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
- if (batch.Count && batch.Items) {
361
- for (const item of batch.Items) {
362
- try {
363
- yield await DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!);
364
- } catch (error) {
365
- if (!(error instanceof NotFoundError)) {
366
- throw error;
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
- if (!batch.Count || !batch.LastEvaluatedKey) {
373
- done = true;
374
- } else {
375
- token = batch.LastEvaluatedKey;
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 config = ModelRegistryIndex.getConfig(cls);
50
+ const indexes = ModelRegistryIndex.getIndices(cls);
55
51
  const attributes: AttributeDefinition[] = [];
56
- const indices: GlobalSecondaryIndex[] = [];
57
-
58
- for (const idx of config.indices ?? []) {
59
- const idxName = this.simpleName(idx.name);
60
- attributes.push({ AttributeName: `${idxName}__`, AttributeType: 'S' });
61
-
62
- const keys: KeySchemaElement[] = [{
63
- AttributeName: `${idxName}__`,
64
- KeyType: 'HASH'
65
- }];
66
-
67
- if (idx.type === 'sorted') {
68
- keys.push({
69
- AttributeName: `${idxName}_sort__`,
70
- KeyType: 'RANGE'
71
- });
72
- attributes.push({ AttributeName: `${idxName}_sort__`, AttributeType: 'N' });
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
- indices.push({
76
- IndexName: idxName,
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: indices.length ? indices : undefined, attributes };
89
+ return { indices: toCreate.length ? toCreate : undefined, attributes };
87
90
  }
88
91
 
89
92
  /**