@travetto/model-dynamodb 8.0.0-alpha.0 → 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
 
@@ -36,7 +36,7 @@ export class Init {
36
36
  }
37
37
  ```
38
38
 
39
- where the [DynamoDBModelConfig](https://github.com/travetto/travetto/tree/main/module/model-dynamodb/src/config.ts#L7) is defined by:
39
+ where the [DynamoDBModelConfig](https://github.com/travetto/travetto/tree/main/module/model-dynamodb/src/config.ts#L8) is defined by:
40
40
 
41
41
  **Code: Structure of DynamoDBModelConfig**
42
42
  ```typescript
@@ -48,7 +48,8 @@ export class DynamoDBModelConfig {
48
48
  modifyStorage?: boolean;
49
49
  namespace?: string;
50
50
 
51
- postConstruct(): void {
51
+ @PostConstruct()
52
+ finalizeConfig(): void {
52
53
  if (!Runtime.production) {
53
54
  this.client.endpoint ??= 'http://localhost:8000'; // From docker
54
55
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-dynamodb",
3
- "version": "8.0.0-alpha.0",
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.1000.0",
30
- "@travetto/config": "^8.0.0-alpha.0",
31
- "@travetto/model": "^8.0.0-alpha.0"
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.0"
35
+ "@travetto/cli": "^8.0.0-alpha.15"
35
36
  },
36
37
  "peerDependenciesMeta": {
37
38
  "@travetto/cli": {
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type dynamodb from '@aws-sdk/client-dynamodb';
2
2
 
3
3
  import { Config } from '@travetto/config';
4
+ import { PostConstruct } from '@travetto/di';
4
5
  import { Runtime } from '@travetto/runtime';
5
6
 
6
7
  @Config('model.dynamodb')
@@ -11,7 +12,8 @@ export class DynamoDBModelConfig {
11
12
  modifyStorage?: boolean;
12
13
  namespace?: string;
13
14
 
14
- postConstruct(): void {
15
+ @PostConstruct()
16
+ finalizeConfig(): void {
15
17
  if (!Runtime.production) {
16
18
  this.client.endpoint ??= 'http://localhost:8000'; // From docker
17
19
  }
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';
4
- import { Injectable } from '@travetto/di';
3
+ import { castTo, JSONUtil, ShutdownManager, TimeUtil, type Class } from '@travetto/runtime';
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: {
@@ -111,7 +227,8 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
111
227
  }
112
228
  }
113
229
 
114
- async postConstruct(): Promise<void> {
230
+ @PostConstruct()
231
+ async initializeClient(): Promise<void> {
115
232
  this.client = new DynamoDB({ ...this.config.client });
116
233
  await ModelStorageUtil.storageInitialization(this);
117
234
  ShutdownManager.signal.addEventListener('abort', async () => this.client.destroy());
@@ -167,7 +284,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
167
284
  if (ttlEnabled !== ttlRequired) {
168
285
  await this.client.updateTimeToLive({
169
286
  TableName: table,
170
- TimeToLiveSpecification: { AttributeName: ttlRequired ? EXP_ATTR : undefined, Enabled: ttlRequired }
287
+ TimeToLiveSpecification: { AttributeName: ttlRequired ? EXPIRES_ATTRIBUTE : undefined, Enabled: ttlRequired }
171
288
  });
172
289
  }
173
290
  }
@@ -287,92 +404,86 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
287
404
  }
288
405
 
289
406
  // Indexed
290
- async #getIdByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<string> {
291
- ModelCrudUtil.ensureNotSubType(cls);
292
-
293
- const idxConfig = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
294
-
295
- const { key, sort } = ModelIndexedUtil.computeIndexKey(cls, idxConfig, body);
296
-
297
- if (idxConfig.type === 'sorted' && sort === undefined) {
298
- throw new IndexNotSupported(cls, idxConfig, 'Sorted indices require the sort field');
299
- }
300
-
301
- const idxName = DynamoDBUtil.simpleName(idx);
302
-
303
- const query = {
304
- TableName: this.#resolveTable(cls),
305
- IndexName: idxName,
306
- ProjectionExpression: 'id',
307
- KeyConditionExpression: [sort ? `${idxName}_sort__ = :${idxName}_sort` : '', `${idxName}__ = :${idxName}`]
308
- .filter(expr => !!expr)
309
- .join(' and '),
310
- ExpressionAttributeValues: {
311
- [`:${idxName}`]: DynamoDBUtil.toValue(key),
312
- ...(sort ? { [`:${idxName}_sort`]: DynamoDBUtil.toValue(+sort) } : {})
313
- }
314
- };
315
-
316
- const result = await this.client.query(query);
317
-
318
- if (result.Count && result.Items && result.Items[0]) {
319
- return result.Items[0].id.S!;
320
- }
321
- throw new NotFoundError(`${cls.name} Index=${idx}`, key);
322
- }
323
-
324
- // Indexed
325
- 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> {
326
412
  return this.get(cls, await this.#getIdByIndex(cls, idx, body));
327
413
  }
328
414
 
329
- 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> {
330
420
  return this.delete(cls, await this.#getIdByIndex(cls, idx, body));
331
421
  }
332
422
 
333
- 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> {
334
428
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
335
429
  }
336
430
 
337
- async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
338
- ModelCrudUtil.ensureNotSubType(cls);
339
-
340
- const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
341
- const { key } = ModelIndexedUtil.computeIndexKey(cls, config, body, { emptySortValue: null });
342
-
343
- 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
+ }
344
438
 
345
- let done = false;
346
- let token: Record<string, AttributeValue> | undefined;
347
- while (!done) {
348
- const batch = await this.client.query({
349
- TableName: this.#resolveTable(cls),
350
- IndexName: idxName,
351
- ProjectionExpression: 'body',
352
- KeyConditionExpression: `${idxName}__ = :${idxName}`,
353
- ExpressionAttributeValues: {
354
- [`:${idxName}`]: DynamoDBUtil.toValue(key)
355
- },
356
- ExclusiveStartKey: token
357
- });
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
+ }
358
447
 
359
- if (batch.Count && batch.Items) {
360
- for (const item of batch.Items) {
361
- try {
362
- yield await DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!);
363
- } catch (error) {
364
- if (!(error instanceof NotFoundError)) {
365
- throw error;
366
- }
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;
367
470
  }
368
471
  }
369
472
  }
473
+ }
370
474
 
371
- if (!batch.Count || !batch.LastEvaluatedKey) {
372
- done = true;
373
- } else {
374
- token = batch.LastEvaluatedKey;
375
- }
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
+ });
376
485
  }
486
+
487
+ return { items, nextOffset };
377
488
  }
378
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
  /**