@travetto/model-dynamodb 8.0.0-alpha.10 → 8.0.0-alpha.12

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#L23)
21
+ * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L15)
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.10",
3
+ "version": "8.0.0-alpha.12",
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.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"
29
+ "@aws-sdk/client-dynamodb": "^3.1024.0",
30
+ "@travetto/config": "^8.0.0-alpha.11",
31
+ "@travetto/model": "^8.0.0-alpha.11",
32
+ "@travetto/model-indexed": "^8.0.0-alpha.12"
33
33
  },
34
34
  "peerDependencies": {
35
- "@travetto/cli": "^8.0.0-alpha.15"
35
+ "@travetto/cli": "^8.0.0-alpha.16"
36
36
  },
37
37
  "peerDependenciesMeta": {
38
38
  "@travetto/cli": {
package/src/service.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AttributeValue, DynamoDB, type PutItemCommandInput, type PutItemCommandOutput, type QueryCommandOutput } from '@aws-sdk/client-dynamodb';
1
+ import { type AttributeValue, DynamoDB, type PutItemCommandInput, type PutItemCommandOutput, type QueryCommandInput, type QueryCommandOutput } from '@aws-sdk/client-dynamodb';
2
2
 
3
3
  import { castTo, JSONUtil, ShutdownManager, TimeUtil, type Class } from '@travetto/runtime';
4
4
  import { Injectable, PostConstruct } from '@travetto/di';
@@ -6,10 +6,11 @@ import {
6
6
  type ModelCrudSupport, type ModelExpirySupport, ModelRegistryIndex, type ModelStorageSupport,
7
7
  type ModelType, NotFoundError, ExistsError, type OptionalId, ModelCrudUtil,
8
8
  ModelExpiryUtil, ModelStorageUtil,
9
+ type ModelListOptions,
9
10
  } from '@travetto/model';
10
11
  import {
11
12
  isModelIndexedIndex, ModelIndexedUtil, type KeyedIndexBody, type KeyedIndexSelection,
12
- type ListPageOptions, type ListPageResult, type ModelIndexedSupport, type SingleItemIndex,
13
+ type ModelPageOptions, type ModelPageResult, type ModelIndexedSupport, type SingleItemIndex,
13
14
  type FullKeyedIndexBody, type FullKeyedIndexWithPartialBody, type SortedIndex, type SortedIndexSelection,
14
15
  ModelIndexedComputedIndex
15
16
  } from '@travetto/model-indexed';
@@ -42,51 +43,59 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
42
43
  return table;
43
44
  }
44
45
 
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
-
46
+ async * #scanCollection<T extends ModelType>(
47
+ cls: Class<T>,
48
+ 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> }> {
51
+ const batchSize = options?.batchSizeHint ?? 100;
52
+ const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
61
53
  let startKey = options?.offset ?? undefined;
62
54
  let produced = 0;
63
-
64
55
  do {
65
56
  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
- });
57
+ const batch = await query(Math.min(remaining, batchSize), startKey);
75
58
 
76
59
  if (batch.Count && batch.Items) {
77
60
  produced += batch.Count;
78
61
 
79
- if (produced > limit) {
80
- const items = batch.Items.slice(0, remaining);
81
- yield { ...batch, Items: items, };
82
- } else {
83
- yield batch;
84
- }
62
+ const items = (produced > limit) ? batch.Items.slice(0, remaining) : batch.Items;
85
63
  startKey = batch.LastEvaluatedKey;
64
+ yield {
65
+ items: await ModelCrudUtil.filterOutNotFound(
66
+ items.map(item => DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!))),
67
+ lastKey: startKey
68
+ };
86
69
  } else {
87
70
  startKey = undefined;
88
71
  }
89
- } while (startKey && produced < limit);
72
+ } while (startKey && produced < limit && !(options?.abort?.aborted));
73
+ }
74
+
75
+ async * #scanIndex<
76
+ T extends ModelType,
77
+ K extends KeyedIndexSelection<T>,
78
+ S extends SortedIndexSelection<T>
79
+ >(
80
+ cls: Class<T>,
81
+ idx: SortedIndex<T, K, S>,
82
+ body: KeyedIndexBody<T, K>,
83
+ options?: ModelPageOptions<Record<string, AttributeValue>> & ModelListOptions
84
+ ): AsyncIterable<{ items: T[], lastKey?: Record<string, AttributeValue> }> {
85
+ ModelCrudUtil.ensureNotSubType(cls);
86
+ 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);
90
99
  }
91
100
 
92
101
  async #getIdByIndex<
@@ -101,16 +110,22 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
101
110
  const safeName = DynamoDBUtil.toSafeName(idx.name);
102
111
  const sorted = idx.type === 'indexed:sorted';
103
112
 
104
- const query = {
113
+ const query: QueryCommandInput = {
105
114
  TableName: this.#resolveTable(cls),
106
115
  IndexName: safeName,
107
116
  ProjectionExpression: 'id',
108
- KeyConditionExpression: [sorted ? `${safeName}_sort__ = :${safeName}_sort` : '', `${safeName}__ = :${safeName}`]
109
- .filter(expr => !!expr)
117
+ KeyConditionExpression: [
118
+ ...(sorted ? [`${safeName}_sort__ = :${safeName}_sort`] : []),
119
+ `${safeName}__ = :${safeName}`
120
+ ]
110
121
  .join(' and '),
122
+ ...(computed.idPart ? {
123
+ FilterExpression: 'id = :id'
124
+ } : {}),
111
125
  ExpressionAttributeValues: {
112
126
  [`:${safeName}`]: getKey(computed),
113
- ...(sorted ? { [`:${safeName}_sort`]: getSort(computed) } : {})
127
+ ...(sorted ? { [`:${safeName}_sort`]: getSort(computed) } : {}),
128
+ ...(computed.idPart ? { ':id': DynamoDBUtil.toValue(computed.idPart.value) } : {})
114
129
  }
115
130
  };
116
131
 
@@ -369,32 +384,13 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
369
384
  }
370
385
  }
371
386
 
372
- async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
373
- let done = false;
374
- let token: Record<string, AttributeValue> | undefined;
375
- while (!done) {
376
- const batch = await this.client.scan({
377
- TableName: this.#resolveTable(cls),
378
- ExclusiveStartKey: token
379
- });
380
-
381
- if (batch.Count && batch.Items) {
382
- for (const item of batch.Items) {
383
- try {
384
- yield await DynamoDBUtil.loadAndCheckExpiry(cls, item.body.S!);
385
- } catch (error) {
386
- if (!(error instanceof NotFoundError)) {
387
- throw error;
388
- }
389
- }
390
- }
391
- }
392
-
393
- if (!batch.Count || !batch.LastEvaluatedKey) {
394
- done = true;
395
- } else {
396
- token = batch.LastEvaluatedKey;
397
- }
387
+ async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
388
+ for await (const { items } of this.#scanCollection(cls, (batchSize, lastKey) => this.client.scan({
389
+ TableName: this.#resolveTable(cls),
390
+ ExclusiveStartKey: lastKey,
391
+ Limit: batchSize
392
+ }), options)) {
393
+ yield items;
398
394
  }
399
395
  }
400
396
 
@@ -445,7 +441,7 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
445
441
  return this.update(cls, item);
446
442
  }
447
443
 
448
- async listByIndex<
444
+ async pageByIndex<
449
445
  T extends ModelType,
450
446
  K extends KeyedIndexSelection<T>,
451
447
  S extends SortedIndexSelection<T>
@@ -453,28 +449,17 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
453
449
  cls: Class<T>,
454
450
  idx: SortedIndex<T, K, S>,
455
451
  body: KeyedIndexBody<T, K>,
456
- options?: ListPageOptions,
457
- ): Promise<ListPageResult<T>> {
458
- const items: T[] = [];
452
+ options?: ModelPageOptions,
453
+ ): Promise<ModelPageResult<T>> {
454
+ const output: T[] = [];
459
455
  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;
470
- }
471
- }
472
- }
456
+ for await (const { items } of this.#scanIndex(cls, idx, body, { limit: 100, ...options, offset })) {
457
+ output.push(...items);
473
458
  }
474
459
 
475
460
  let nextOffset;
476
- if (items.length) {
477
- const last: T = items.at(-1)!;
461
+ if (output.length) {
462
+ const last: T = output.at(-1)!;
478
463
  const computed = ModelIndexedComputedIndex.get(idx, last).validate();
479
464
  const safeName = DynamoDBUtil.toSafeName(idx.name);
480
465
  nextOffset = JSONUtil.toBase64({
@@ -484,6 +469,21 @@ export class DynamoDBModelService implements ModelCrudSupport, ModelExpirySuppor
484
469
  });
485
470
  }
486
471
 
487
- return { items, nextOffset };
472
+ return { items: output, nextOffset };
473
+ }
474
+
475
+ async * listByIndex<
476
+ T extends ModelType,
477
+ K extends KeyedIndexSelection<T>,
478
+ S extends SortedIndexSelection<T>
479
+ >(
480
+ cls: Class<T>,
481
+ idx: SortedIndex<T, K, S>,
482
+ body: KeyedIndexBody<T, K>,
483
+ options?: ModelListOptions
484
+ ): AsyncIterable<T[]> {
485
+ for await (const { items } of this.#scanIndex(cls, idx, body, options)) {
486
+ yield items;
487
+ }
488
488
  }
489
489
  }