@travetto/model-dynamodb 8.0.0-alpha.23 → 8.0.0-alpha.25

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
@@ -13,20 +13,19 @@ npm install @travetto/model-dynamodb
13
13
  yarn add @travetto/model-dynamodb
14
14
  ```
15
15
 
16
- This module provides an [DynamoDB](https://aws.amazon.com/dynamodb/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [DynamoDB](https://aws.amazon.com/dynamodb/). The entire document is stored as a single value, so nothing is needed to handle schema updates in real time. Indices on the other hand are more complicated, and will not be retroactively computed for new values.
16
+ This module provides an [DynamoDB](https://aws.amazon.com/dynamodb/)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [DynamoDB](https://aws.amazon.com/dynamodb/). The entire document is stored as a single value, so nothing is needed to handle schema updates in real time. Indices on the other hand are more complicated, and will not be retroactively computed for new values.
17
17
 
18
18
  Supported features:
19
- * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
19
+ * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
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#L16)
21
+ * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L21)
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
 
25
25
  **Code: Wiring up a custom Model Source**
26
26
  ```typescript
27
27
  import { InjectableFactory } from '@travetto/di';
28
-
29
- import { DynamoDBModelService, type DynamoDBModelConfig } from '@travetto/model-dynamodb';
28
+ import { type DynamoDBModelConfig, DynamoDBModelService } from '@travetto/model-dynamodb';
30
29
 
31
30
  export class Init {
32
31
  @InjectableFactory({ primary: true })
package/__index__.ts CHANGED
@@ -1,3 +1,3 @@
1
+ export * from './src/config.ts';
1
2
  export * from './src/service.ts';
2
3
  export * from './src/util.ts';
3
- export * from './src/config.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-dynamodb",
3
- "version": "8.0.0-alpha.23",
3
+ "version": "8.0.0-alpha.25",
4
4
  "type": "module",
5
5
  "description": "DynamoDB backing for the travetto model module.",
6
6
  "keywords": [
@@ -27,12 +27,12 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@aws-sdk/client-dynamodb": "^3.1081.0",
30
- "@travetto/config": "^8.0.0-alpha.20",
31
- "@travetto/model": "^8.0.0-alpha.21",
32
- "@travetto/model-indexed": "^8.0.0-alpha.23"
30
+ "@travetto/config": "^8.0.0-alpha.22",
31
+ "@travetto/model": "^8.0.0-alpha.23",
32
+ "@travetto/model-indexed": "^8.0.0-alpha.25"
33
33
  },
34
34
  "peerDependencies": {
35
- "@travetto/cli": "^8.0.0-alpha.26"
35
+ "@travetto/cli": "^8.0.0-alpha.28"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@travetto/cli": {
package/src/config.ts CHANGED
@@ -18,4 +18,4 @@ export class DynamoDBModelConfig {
18
18
  this.client.endpoint ??= 'http://localhost:8000'; // From docker
19
19
  }
20
20
  }
21
- }
21
+ }
package/src/service.ts CHANGED
@@ -1,26 +1,53 @@
1
- import { type AttributeValue, DynamoDB, type PutItemCommandInput, type PutItemCommandOutput, type QueryCommandInput, type QueryCommandOutput } from '@aws-sdk/client-dynamodb';
1
+ import {
2
+ type AttributeValue,
3
+ DynamoDB,
4
+ type PutItemCommandInput,
5
+ type PutItemCommandOutput,
6
+ type QueryCommandInput,
7
+ type QueryCommandOutput
8
+ } from '@aws-sdk/client-dynamodb';
2
9
 
3
- import { castTo, JSONUtil, ShutdownManager, TimeUtil, type Class } from '@travetto/runtime';
4
10
  import { Injectable, PostConstruct } from '@travetto/di';
5
11
  import {
6
- type ModelCrudSupport, type ModelExpirySupport, ModelRegistryIndex, type ModelStorageSupport,
7
- type ModelType, NotFoundError, ExistsError, type OptionalId, ModelCrudUtil,
8
- ModelExpiryUtil, ModelStorageUtil,
12
+ ExistsError,
13
+ type ModelCrudSupport,
14
+ ModelCrudUtil,
15
+ type ModelExpirySupport,
16
+ ModelExpiryUtil,
9
17
  type ModelListOptions,
18
+ ModelRegistryIndex,
19
+ type ModelStorageSupport,
20
+ ModelStorageUtil,
21
+ type ModelType,
22
+ NotFoundError,
23
+ type OptionalId
10
24
  } from '@travetto/model';
11
25
  import {
12
- isModelIndexedIndex, ModelIndexedUtil, type KeyedIndexBody, type KeyedIndexSelection,
13
- type ModelPageOptions, type ModelPageResult, type ModelIndexedSupport, type SingleItemIndex,
14
- type FullKeyedIndexBody, type FullKeyedIndexWithPartialBody, type SortedIndex, type SortedIndexSelection,
15
- ModelIndexedComputedIndex, type ModelIndexedSearchOptions, type SortedIndexSelectionType
26
+ type FullKeyedIndexBody,
27
+ type FullKeyedIndexWithPartialBody,
28
+ isModelIndexedIndex,
29
+ type KeyedIndexBody,
30
+ type KeyedIndexSelection,
31
+ ModelIndexedComputedIndex,
32
+ type ModelIndexedSearchOptions,
33
+ type ModelIndexedSupport,
34
+ ModelIndexedUtil,
35
+ type ModelPageOptions,
36
+ type ModelPageResult,
37
+ type SingleItemIndex,
38
+ type SortedIndex,
39
+ type SortedIndexSelection,
40
+ type SortedIndexSelectionType
16
41
  } from '@travetto/model-indexed';
42
+ import { type Class, castTo, JSONUtil, ShutdownManager, TimeUtil } from '@travetto/runtime';
17
43
 
18
44
  import type { DynamoDBModelConfig } from './config.ts';
19
45
  import { DynamoDBUtil } from './util.ts';
20
46
 
21
47
  const EXPIRES_ATTRIBUTE = 'expires_at__';
22
48
 
23
- const getKey = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue => DynamoDBUtil.toValue(computed.getKey() || 'NULL');
49
+ const getKey = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue =>
50
+ DynamoDBUtil.toValue(computed.getKey() || 'NULL');
24
51
  const getSort = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): AttributeValue => DynamoDBUtil.toValue(computed.getSort());
25
52
 
26
53
  /**
@@ -28,12 +55,13 @@ const getSort = <T extends ModelType>(computed: ModelIndexedComputedIndex<T>): A
28
55
  */
29
56
  @Injectable()
30
57
  export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySupport, ModelStorageSupport, ModelIndexedSupport {
31
-
32
58
  idSource = ModelCrudUtil.uuidSource();
33
59
  client: DynamoDB;
34
60
  config: DynamoDBModelConfig;
35
61
 
36
- constructor(config: DynamoDBModelConfig) { this.config = config; }
62
+ constructor(config: DynamoDBModelConfig) {
63
+ this.config = config;
64
+ }
37
65
 
38
66
  #resolveTable(cls: Class): string {
39
67
  let table = ModelRegistryIndex.getStoreName(cls);
@@ -43,11 +71,11 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
43
71
  return table;
44
72
  }
45
73
 
46
- async * #scanCollection<T extends ModelType>(
74
+ async *#scanCollection<T extends ModelType>(
47
75
  cls: Class<T>,
48
76
  query: (batchSize: number, lastKey: Record<string, AttributeValue> | undefined) => Promise<QueryCommandOutput>,
49
- options?: ModelListOptions & ModelPageOptions<Record<string, AttributeValue>>,
50
- ): AsyncIterable<{ items: T[], lastKey?: Record<string, AttributeValue> }> {
77
+ options?: ModelListOptions & ModelPageOptions<Record<string, AttributeValue>>
78
+ ): AsyncIterable<{ items: T[]; lastKey?: Record<string, AttributeValue> }> {
51
79
  const batchSize = options?.batchSizeHint ?? 100;
52
80
  const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
53
81
  let startKey = options?.offset ?? undefined;
@@ -59,26 +87,25 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
59
87
  if (batch.Count && batch.Items) {
60
88
  produced += batch.Count;
61
89
 
62
- const items = (produced > limit) ? batch.Items.slice(0, remaining) : batch.Items;
90
+ const items = produced > limit ? batch.Items.slice(0, remaining) : batch.Items;
63
91
  startKey = batch.LastEvaluatedKey;
64
92
  yield {
65
- items: await ModelCrudUtil.filterOutNotFound(
66
- items.map(item => DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!))),
93
+ items: await ModelCrudUtil.filterOutNotFound(items.map(item => DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!))),
67
94
  lastKey: startKey
68
95
  };
69
96
  } else {
70
97
  startKey = undefined;
71
98
  }
72
- } while (startKey && produced < limit && !(options?.abort?.aborted));
99
+ } while (startKey && produced < limit && !options?.abort?.aborted);
73
100
  }
74
101
 
75
- async * #scanIndex<T extends ModelType>(
102
+ async *#scanIndex<T extends ModelType>(
76
103
  cls: Class<T>,
77
104
  idx: SortedIndex<T>,
78
105
  body: KeyedIndexBody<T>,
79
106
  options?: ModelPageOptions<Record<string, AttributeValue>> & ModelListOptions,
80
107
  transform?: (query: QueryCommandInput) => QueryCommandInput
81
- ): AsyncIterable<{ items: T[], lastKey?: Record<string, AttributeValue> }> {
108
+ ): AsyncIterable<{ items: T[]; lastKey?: Record<string, AttributeValue> }> {
82
109
  ModelCrudUtil.ensureNotSubType(cls);
83
110
  const computed = ModelIndexedComputedIndex.get(idx, body).validate();
84
111
  const { keyIndexName, keyIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
@@ -86,25 +113,29 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
86
113
 
87
114
  const finalTransform = transform ?? ((query): QueryCommandInput => query);
88
115
 
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);
116
+ yield* this.#scanCollection(
117
+ cls,
118
+ (batchSize, lastKey) => {
119
+ const finalized = finalTransform({
120
+ TableName: this.#resolveTable(cls),
121
+ IndexName: keyIndexName,
122
+ ProjectionExpression: 'body',
123
+ KeyConditionExpression: `${keyIndexAttribute} = :${keyIndexName}`,
124
+ ExpressionAttributeValues: expression,
125
+ Limit: batchSize,
126
+ ExclusiveStartKey: lastKey
127
+ });
128
+ return this.client.query(finalized);
129
+ },
130
+ options
131
+ );
101
132
  }
102
133
 
103
- async #getIdByIndex<
104
- T extends ModelType,
105
- K extends KeyedIndexSelection<T>,
106
- S extends SortedIndexSelection<T>
107
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<string> {
134
+ async #getIdByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
135
+ cls: Class<T>,
136
+ idx: SingleItemIndex<T, K, S>,
137
+ body: FullKeyedIndexBody<T, K, S>
138
+ ): Promise<string> {
108
139
  ModelCrudUtil.ensureNotSubType(cls);
109
140
 
110
141
  const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
@@ -119,11 +150,12 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
119
150
  KeyConditionExpression: [
120
151
  ...(sorted ? [`${sortIndexAttribute} = :${sortIndexName}`] : []),
121
152
  `${keyIndexAttribute} = :${keyIndexName}`
122
- ]
123
- .join(' and '),
124
- ...(computed.idPart ? {
125
- FilterExpression: 'id = :id'
126
- } : {}),
153
+ ].join(' and '),
154
+ ...(computed.idPart
155
+ ? {
156
+ FilterExpression: 'id = :id'
157
+ }
158
+ : {}),
127
159
  ExpressionAttributeValues: {
128
160
  [`:${keyIndexName}`]: getKey(computed),
129
161
  ...(sorted ? { [`:${sortIndexName}`]: getSort(computed) } : {}),
@@ -134,7 +166,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
134
166
  try {
135
167
  const result = await this.client.query(query);
136
168
 
137
- if (result.Count && result.Items && result.Items[0]) {
169
+ if (result.Count && result.Items?.[0]) {
138
170
  return result.Items[0].id.S!;
139
171
  }
140
172
  throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey({ sort: true }));
@@ -146,7 +178,12 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
146
178
  }
147
179
  }
148
180
 
149
- async #putItem<T extends ModelType>(cls: Class<T>, id: string, item: T, mode: 'create' | 'update' | 'upsert'): Promise<PutItemCommandOutput> {
181
+ async #putItem<T extends ModelType>(
182
+ cls: Class<T>,
183
+ id: string,
184
+ item: T,
185
+ mode: 'create' | 'update' | 'upsert'
186
+ ): Promise<PutItemCommandOutput> {
150
187
  const config = ModelRegistryIndex.getConfig(cls);
151
188
  let expiry: number | undefined;
152
189
 
@@ -165,7 +202,9 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
165
202
  const { keyIndexAttribute, sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
166
203
  const computed = ModelIndexedComputedIndex.get(idx, item).validate({ sort: true });
167
204
  switch (idx.type) {
168
- case 'indexed:keyed': indices[keyIndexAttribute] = getKey(computed); break;
205
+ case 'indexed:keyed':
206
+ indices[keyIndexAttribute] = getKey(computed);
207
+ break;
169
208
  case 'indexed:sorted': {
170
209
  indices[keyIndexAttribute] = getKey(computed);
171
210
  indices[sortIndexAttribute] = getSort(computed);
@@ -219,11 +258,9 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
219
258
  TableName: this.#resolveTable(cls),
220
259
  ConditionExpression: mode === 'update' ? 'attribute_exists(body)' : undefined,
221
260
  Key: { id: { S: id } },
222
- UpdateExpression: `SET ${[
223
- 'body=:body',
224
- expiry !== undefined ? `${EXPIRES_ATTRIBUTE}=:expr` : undefined,
225
- ...expr
226
- ].filter(part => !!part).join(', ')}`,
261
+ UpdateExpression: `SET ${['body=:body', expiry !== undefined ? `${EXPIRES_ATTRIBUTE}=:expr` : undefined, ...expr]
262
+ .filter(part => !!part)
263
+ .join(', ')}`,
227
264
  ExpressionAttributeValues: {
228
265
  ':body': DynamoDBUtil.toValue(JSONUtil.toUTF8(item)),
229
266
  ...(expiry !== undefined ? { ':expr': DynamoDBUtil.toValue(expiry) } : {}),
@@ -272,10 +309,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
272
309
  TableName: table,
273
310
  KeySchema: [{ KeyType: 'HASH', AttributeName: 'id' }],
274
311
  BillingMode: 'PAY_PER_REQUEST',
275
- AttributeDefinitions: [
276
- { AttributeName: 'id', AttributeType: 'S' },
277
- ...idx.attributes
278
- ],
312
+ AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }, ...idx.attributes],
279
313
  GlobalSecondaryIndexes: idx.indices
280
314
  });
281
315
  } else {
@@ -287,10 +321,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
287
321
  if (changedAttributes.length || indexUpdates?.length) {
288
322
  await this.client.updateTable({
289
323
  TableName: table,
290
- AttributeDefinitions: [
291
- { AttributeName: 'id', AttributeType: 'S' },
292
- ...idx.attributes
293
- ],
324
+ AttributeDefinitions: [{ AttributeName: 'id', AttributeType: 'S' }, ...idx.attributes],
294
325
  GlobalSecondaryIndexUpdates: indexUpdates
295
326
  });
296
327
  }
@@ -312,7 +343,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
312
343
  */
313
344
  async deleteModel(cls: Class<ModelType>): Promise<void> {
314
345
  const table = this.#resolveTable(cls);
315
- const { Table: verify } = (await this.client.describeTable({ TableName: table }).catch(() => ({ Table: undefined })));
346
+ const { Table: verify } = await this.client.describeTable({ TableName: table }).catch(() => ({ Table: undefined }));
316
347
  if (verify) {
317
348
  await this.client.deleteTable({ TableName: table });
318
349
  }
@@ -324,9 +355,11 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
324
355
 
325
356
  async deleteStorage(): Promise<void> {
326
357
  for (const model of ModelRegistryIndex.getClasses()) {
327
- await this.client.deleteTable({
328
- TableName: this.#resolveTable(model)
329
- }).catch(() => { });
358
+ await this.client
359
+ .deleteTable({
360
+ TableName: this.#resolveTable(model)
361
+ })
362
+ .catch(() => {});
330
363
  }
331
364
  }
332
365
 
@@ -337,7 +370,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
337
370
  Key: { id: DynamoDBUtil.toValue(id) }
338
371
  });
339
372
 
340
- if (result && result.Item && result.Item.body) {
373
+ if (result?.Item?.body) {
341
374
  return DynamoDBUtil.loadAndCheckExpiry(cls, result.Item.body.S!);
342
375
  }
343
376
  throw new NotFoundError(cls, id);
@@ -386,12 +419,17 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
386
419
  }
387
420
  }
388
421
 
389
- async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
390
- for await (const { items } of this.#scanCollection(cls, (batchSize, lastKey) => this.client.scan({
391
- TableName: this.#resolveTable(cls),
392
- ExclusiveStartKey: lastKey,
393
- Limit: batchSize
394
- }), options)) {
422
+ async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
423
+ for await (const { items } of this.#scanCollection(
424
+ cls,
425
+ (batchSize, lastKey) =>
426
+ this.client.scan({
427
+ TableName: this.#resolveTable(cls),
428
+ ExclusiveStartKey: lastKey,
429
+ Limit: batchSize
430
+ }),
431
+ options
432
+ )) {
395
433
  yield items;
396
434
  }
397
435
  }
@@ -402,56 +440,52 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
402
440
  }
403
441
 
404
442
  // Indexed
405
- async getByIndex<
406
- T extends ModelType,
407
- K extends KeyedIndexSelection<T>,
408
- S extends SortedIndexSelection<T>
409
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
443
+ async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
444
+ cls: Class<T>,
445
+ idx: SingleItemIndex<T, K, S>,
446
+ body: FullKeyedIndexBody<T, K, S>
447
+ ): Promise<T> {
410
448
  return this.get(cls, await this.#getIdByIndex(cls, idx, body));
411
449
  }
412
450
 
413
- async deleteByIndex<
414
- T extends ModelType,
415
- K extends KeyedIndexSelection<T>,
416
- S extends SortedIndexSelection<T>
417
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
451
+ async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
452
+ cls: Class<T>,
453
+ idx: SingleItemIndex<T, K, S>,
454
+ body: FullKeyedIndexBody<T, K, S>
455
+ ): Promise<void> {
418
456
  return this.delete(cls, await this.#getIdByIndex(cls, idx, body));
419
457
  }
420
458
 
421
- upsertByIndex<
422
- T extends ModelType,
423
- K extends KeyedIndexSelection<T>,
424
- S extends SortedIndexSelection<T>
425
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
459
+ upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
460
+ cls: Class<T>,
461
+ idx: SingleItemIndex<T, K, S>,
462
+ body: OptionalId<T>
463
+ ): Promise<T> {
426
464
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
427
465
  }
428
466
 
429
- async updateByIndex<
430
- T extends ModelType,
431
- K extends KeyedIndexSelection<T>,
432
- S extends SortedIndexSelection<T>
433
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
467
+ async updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
468
+ cls: Class<T>,
469
+ idx: SingleItemIndex<T, K, S>,
470
+ body: T
471
+ ): Promise<T> {
434
472
  return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
435
473
  }
436
474
 
437
- async updatePartialByIndex<
438
- T extends ModelType,
439
- K extends KeyedIndexSelection<T>,
440
- S extends SortedIndexSelection<T>
441
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
475
+ async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
476
+ cls: Class<T>,
477
+ idx: SingleItemIndex<T, K, S>,
478
+ body: FullKeyedIndexWithPartialBody<T, K, S>
479
+ ): Promise<T> {
442
480
  const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
443
481
  return this.update(cls, item);
444
482
  }
445
483
 
446
- async pageByIndex<
447
- T extends ModelType,
448
- K extends KeyedIndexSelection<T>,
449
- S extends SortedIndexSelection<T>
450
- >(
484
+ async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
451
485
  cls: Class<T>,
452
486
  idx: SortedIndex<T, K, S>,
453
487
  body: KeyedIndexBody<T, K>,
454
- options?: ModelPageOptions,
488
+ options?: ModelPageOptions
455
489
  ): Promise<ModelPageResult<T>> {
456
490
  const output: T[] = [];
457
491
  const offset = options?.offset ? JSONUtil.fromBase64<Record<string, AttributeValue>>(options.offset) : undefined;
@@ -459,7 +493,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
459
493
  output.push(...items);
460
494
  }
461
495
 
462
- let nextOffset;
496
+ let nextOffset: string | undefined;
463
497
  if (output.length) {
464
498
  const last: T = output.at(-1)!;
465
499
  const computed = ModelIndexedComputedIndex.get(idx, last).validate();
@@ -474,11 +508,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
474
508
  return { items: output, nextOffset };
475
509
  }
476
510
 
477
- async * listByIndex<
478
- T extends ModelType,
479
- K extends KeyedIndexSelection<T>,
480
- S extends SortedIndexSelection<T>
481
- >(
511
+ async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
482
512
  cls: Class<T>,
483
513
  idx: SortedIndex<T, K, S>,
484
514
  body: KeyedIndexBody<T, K>,
@@ -499,21 +529,17 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
499
529
 
500
530
  const { sortIndexAttribute } = DynamoDBUtil.indexNames(idx.name);
501
531
 
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
- )) {
532
+ for await (const { items } of this.#scanIndex(cls, idx, body, { limit: 10, ...options }, query => ({
533
+ ...query,
534
+ KeyConditionExpression: [query.KeyConditionExpression, `begins_with(${sortIndexAttribute}, :prefix)`].filter(Boolean).join(' AND '),
535
+ ExpressionAttributeValues: {
536
+ ...query.ExpressionAttributeValues,
537
+ ':prefix': { S: prefix }
538
+ }
539
+ }))) {
514
540
  results.push(...items);
515
541
  }
516
542
 
517
543
  return results;
518
544
  }
519
- }
545
+ }
package/src/util.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import type {
2
- AttributeDefinition, AttributeValue, GlobalSecondaryIndex, GlobalSecondaryIndexDescription,
3
- GlobalSecondaryIndexUpdate, KeySchemaElement
2
+ AttributeDefinition,
3
+ AttributeValue,
4
+ GlobalSecondaryIndex,
5
+ GlobalSecondaryIndexDescription,
6
+ GlobalSecondaryIndexUpdate,
7
+ KeySchemaElement
4
8
  } from '@aws-sdk/client-dynamodb';
5
9
 
6
- import { type Class, castTo } from '@travetto/runtime';
7
- import { ModelCrudUtil, ModelExpiryUtil, ModelRegistryIndex, NotFoundError, type ModelType } from '@travetto/model';
10
+ import { ModelCrudUtil, ModelExpiryUtil, ModelRegistryIndex, type ModelType, NotFoundError } from '@travetto/model';
8
11
  import { isModelIndexedIndex, warnIfIndexedUniqueIndex, warnIfNonIndexedIndex } from '@travetto/model-indexed';
12
+ import { type Class, castTo } from '@travetto/runtime';
9
13
  import { SchemaRegistryIndex } from '@travetto/schema';
10
14
 
11
15
  /**
@@ -20,8 +24,9 @@ type DynamoIndexConfig = {
20
24
  * Utility class for DynamoDB operations and transformations.
21
25
  */
22
26
  export class DynamoDBUtil {
23
-
24
- static indexNames = (name: string): { keyIndexName: string, sortIndexName: string, keyIndexAttribute: string, sortIndexAttribute: string } => {
27
+ static indexNames = (
28
+ name: string
29
+ ): { keyIndexName: string; sortIndexName: string; keyIndexAttribute: string; sortIndexAttribute: string } => {
25
30
  const base = name.toLowerCase().replace(/[^A-Za-z0-9]+/g, '_');
26
31
  return {
27
32
  keyIndexName: base,
@@ -86,7 +91,7 @@ export class DynamoDBUtil {
86
91
  }
87
92
 
88
93
  keys.push({ AttributeName: keyIndexAttribute, KeyType: 'HASH' });
89
- keys.push({ AttributeName: sortIndexAttribute, KeyType: 'RANGE', });
94
+ keys.push({ AttributeName: sortIndexAttribute, KeyType: 'RANGE' });
90
95
  attributes.push({ AttributeName: keyIndexAttribute, AttributeType: 'S' });
91
96
  attributes.push({ AttributeName: sortIndexAttribute, AttributeType: castTo(fieldType) === String ? 'S' : 'N' });
92
97
  break;
@@ -116,7 +121,10 @@ export class DynamoDBUtil {
116
121
  * Identifies attribute definitions that have changed between current and requested configurations.
117
122
  * Compares attribute names and types to determine additions, removals, or modifications.
118
123
  */
119
- static findChangedAttributes(current: AttributeDefinition[] | undefined, requested: AttributeDefinition[] | undefined): AttributeDefinition[] {
124
+ static findChangedAttributes(
125
+ current: AttributeDefinition[] | undefined,
126
+ requested: AttributeDefinition[] | undefined
127
+ ): AttributeDefinition[] {
120
128
  const currentMap = Object.fromEntries((current ?? []).map(attr => [attr.AttributeName, attr]));
121
129
  const pendingMap = Object.fromEntries((requested ?? []).map(attr => [attr.AttributeName, attr]));
122
130
 
@@ -145,7 +153,10 @@ export class DynamoDBUtil {
145
153
  * Identifies global secondary indices that have changed between current and requested configurations.
146
154
  * Generates update operations for creating new indices or deleting removed indices.
147
155
  */
148
- static findChangedGlobalIndexes(current: GlobalSecondaryIndexDescription[] | undefined, requested: GlobalSecondaryIndex[] | undefined): GlobalSecondaryIndexUpdate[] {
156
+ static findChangedGlobalIndexes(
157
+ current: GlobalSecondaryIndexDescription[] | undefined,
158
+ requested: GlobalSecondaryIndex[] | undefined
159
+ ): GlobalSecondaryIndexUpdate[] {
149
160
  const existingMap = Object.fromEntries((current ?? []).map(index => [index.IndexName, index]));
150
161
  const pendingMap = Object.fromEntries((requested ?? []).map(index => [index.IndexName, index]));
151
162
 
@@ -7,4 +7,4 @@ export const service: ServiceDescriptor = {
7
7
  version,
8
8
  port: 8000,
9
9
  image: `amazon/dynamodb-local:${version}`
10
- };
10
+ };