Skip to content

Commit 4f0e7ea

Browse files
authored
Merge pull request #45 from blazejkustra/selected-attributes
Fix: Entity constructor overrides attributes on projection/selective Get
2 parents 51d6d70 + ef80f24 commit 4f0e7ea

18 files changed

Lines changed: 466 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
- JSDoc documentation was added to the project ([#40](https://github.com/blazejkustra/dynamode/pull/40))
12+
- Enhanced entity conversion to handle attribute selection in EntityManager `get()` and `batchGet()` methods
13+
14+
### Changed
15+
- Bump AWS SDK packages to version 3.883.0 & improve type safety ([#42](https://github.com/blazejkustra/dynamode/pull/40))
16+
17+
### Fixed
18+
- Fix import example in documentation ([#37](https://github.com/blazejkustra/dynamode/pull/37)) - Thanks @gmreburn!
19+
20+
## [1.5.0] - 2024-08-15
21+
22+
### Added
23+
- `@attribute.customName()` decorator to set custom names for entity attributes ([#33](https://github.com/blazejkustra/dynamode/pull/33))
24+
- Tests for `customName` decorator
25+
26+
### Fixed
27+
- Fixed entity renaming logic
28+
- Improved code safety and error handling
29+
30+
## [1.4.0] - 2024-07-21
31+
32+
### Added
33+
- Support for multiple GSI decorators on the same attribute ([#28](https://github.com/blazejkustra/dynamode/pull/28), [#29](https://github.com/blazejkustra/dynamode/pull/29))
34+
- Allow GSI decorators to decorate both partition and sort keys interchangeably ([#31](https://github.com/blazejkustra/dynamode/pull/31))
35+
36+
### Changed
37+
- Moved test fixtures to a dedicated catalog
38+
- Improved test organization and coverage
39+
40+
### Fixed
41+
- Fixed issue where multiple attribute decorators could not be added to primary keys
42+
- Fixed typecheck issues
43+
44+
## [1.3.0] - 2024-06-23
45+
46+
### Added
47+
- Support for decorating an attribute with multiple indexes ([#28](https://github.com/blazejkustra/dynamode/pull/28))
48+
49+
## [1.2.0] - 2024-04-17
50+
51+
### Added
52+
- Binary data type support ([#26](https://github.com/blazejkustra/dynamode/pull/26))
53+
54+
## [1.1.0] - 2024-04-13
55+
56+
### Fixed
57+
- Fixed `startAt` fails when querying secondary indices
58+
59+
## [1.0.0] - 2024-03-24
60+
61+
### Added
62+
- 🎉 Dynamode is now out of beta!
63+
- DynamoDB stream support ([#21](https://github.com/blazejkustra/dynamode/pull/21))
64+
65+
## Earlier Versions
66+
67+
For changes in earlier versions (< 1.0.0), please refer to the [Git commit history](https://github.com/blazejkustra/dynamode/commits/main).
68+

lib/entity/entityManager.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
EntityGetOptions,
3939
EntityKey,
4040
EntityPutOptions,
41+
EntitySelectedAttributes,
4142
EntityTransactionDeleteOptions,
4243
EntityTransactionGetOptions,
4344
EntityTransactionPutOptions,
@@ -170,10 +171,10 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
170171
* });
171172
* ```
172173
*/
173-
function get(
174+
function get<const Attributes extends Array<EntityKey<E>> | undefined = undefined>(
174175
primaryKey: TablePrimaryKey<M, E>,
175-
options?: EntityGetOptions<E> & { return?: 'default' },
176-
): Promise<InstanceType<E>>;
176+
options?: EntityGetOptions<E, Attributes> & { return?: 'default' },
177+
): Promise<EntitySelectedAttributes<E, Attributes>>;
177178

178179
/**
179180
* Retrieves a single item from the table by its primary key, returning the raw AWS response.
@@ -184,7 +185,7 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
184185
*/
185186
function get(
186187
primaryKey: TablePrimaryKey<M, E>,
187-
options: EntityGetOptions<E> & { return: 'output' },
188+
options: EntityGetOptions<E, any> & { return: 'output' },
188189
): Promise<GetItemCommandOutput>;
189190

190191
/**
@@ -196,7 +197,7 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
196197
*/
197198
function get(
198199
primaryKey: TablePrimaryKey<M, E>,
199-
options: EntityGetOptions<E> & { return: 'input' },
200+
options: EntityGetOptions<E, any> & { return: 'input' },
200201
): GetItemCommandInput;
201202

202203
/**
@@ -207,10 +208,10 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
207208
* @returns A promise that resolves to the entity instance, raw AWS response, or command input
208209
* @throws {NotFoundError} When the item is not found and return type is 'default'
209210
*/
210-
function get(
211+
function get<const Attributes extends Array<EntityKey<E>> | undefined = undefined>(
211212
primaryKey: TablePrimaryKey<M, E>,
212-
options?: EntityGetOptions<E>,
213-
): Promise<InstanceType<E> | GetItemCommandOutput> | GetItemCommandInput {
213+
options?: EntityGetOptions<E, Attributes>,
214+
): Promise<EntitySelectedAttributes<E, Attributes> | GetItemCommandOutput> | GetItemCommandInput {
214215
const { projectionExpression, attributeNames } = buildGetProjectionExpression(options?.attributes);
215216

216217
const commandInput: GetItemCommandInput = {
@@ -237,7 +238,10 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
237238
throw new NotFoundError();
238239
}
239240

240-
return convertAttributeValuesToEntity(entity, result.Item);
241+
return convertAttributeValuesToEntity(entity, result.Item, options?.attributes) as EntitySelectedAttributes<
242+
E,
243+
Attributes
244+
>;
241245
})();
242246
}
243247

@@ -624,10 +628,10 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
624628
* });
625629
* ```
626630
*/
627-
function batchGet(
631+
function batchGet<const Attributes extends Array<EntityKey<E>> | undefined = undefined>(
628632
primaryKeys: Array<TablePrimaryKey<M, E>>,
629-
options?: EntityBatchGetOptions<E> & { return?: 'default' },
630-
): Promise<EntityBatchGetOutput<M, E>>;
633+
options?: EntityBatchGetOptions<E, Attributes> & { return?: 'default' },
634+
): Promise<EntityBatchGetOutput<M, E, Attributes>>;
631635

632636
/**
633637
* Retrieves multiple items, returning the raw AWS response.
@@ -638,7 +642,7 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
638642
*/
639643
function batchGet(
640644
primaryKeys: Array<TablePrimaryKey<M, E>>,
641-
options: EntityBatchGetOptions<E> & { return: 'output' },
645+
options: EntityBatchGetOptions<E, any> & { return: 'output' },
642646
): Promise<BatchGetItemCommandOutput>;
643647

644648
/**
@@ -650,7 +654,7 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
650654
*/
651655
function batchGet(
652656
primaryKeys: Array<TablePrimaryKey<M, E>>,
653-
options: EntityBatchGetOptions<E> & { return: 'input' },
657+
options: EntityBatchGetOptions<E, any> & { return: 'input' },
654658
): BatchGetItemCommandInput;
655659

656660
/**
@@ -660,10 +664,10 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
660664
* @param options - Optional configuration for the batch get operation
661665
* @returns A promise that resolves to the batch get result, raw AWS response, or command input
662666
*/
663-
function batchGet(
667+
function batchGet<const Attributes extends Array<EntityKey<E>> | undefined = undefined>(
664668
primaryKeys: Array<TablePrimaryKey<M, E>>,
665-
options?: EntityBatchGetOptions<E>,
666-
): Promise<EntityBatchGetOutput<M, E> | BatchGetItemCommandOutput> | BatchGetItemCommandInput {
669+
options?: EntityBatchGetOptions<E, Attributes>,
670+
): Promise<EntityBatchGetOutput<M, E, Attributes> | BatchGetItemCommandOutput> | BatchGetItemCommandInput {
667671
const { projectionExpression, attributeNames } = buildGetProjectionExpression(options?.attributes);
668672

669673
const commandInput: BatchGetItemCommandInput = {
@@ -701,7 +705,9 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
701705
result.UnprocessedKeys?.[tableName]?.Keys?.map((key) => fromDynamo(key) as TablePrimaryKey<M, E>) || [];
702706

703707
return {
704-
items: items.map((item) => convertAttributeValuesToEntity(entity, item)),
708+
items: items.map((item) => convertAttributeValuesToEntity(entity, item, options?.attributes)) as Array<
709+
EntitySelectedAttributes<E, Attributes>
710+
>,
705711
unprocessedKeys,
706712
};
707713
})();

lib/entity/helpers/converters.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AttributeValues, fromDynamo, GenericObject, objectToDynamo } from '@lib
77
export function convertAttributeValuesToEntity<E extends typeof Entity>(
88
entity: E,
99
dynamoItem: AttributeValues,
10+
selectedAttributes?: Array<string>,
1011
): InstanceType<E> {
1112
const object = fromDynamo(dynamoItem);
1213
const attributes = Dynamode.storage.getEntityAttributes(entity.name);
@@ -25,7 +26,24 @@ export function convertAttributeValuesToEntity<E extends typeof Entity>(
2526
object[attribute.propertyName] = truncateValue(entity, attribute.propertyName, value);
2627
});
2728

28-
return new entity(object) as InstanceType<E>;
29+
const instance = new entity(object) as InstanceType<E>;
30+
31+
if (selectedAttributes && selectedAttributes.length > 0) {
32+
// Remove nested attributes from the selected attributes
33+
const selectedAttributesSet = new Set([
34+
'dynamodeEntity',
35+
...selectedAttributes.map((attribute) => attribute.split('.')[0]),
36+
]);
37+
38+
Object.values(attributes)
39+
.filter((attribute) => !selectedAttributesSet.has(attribute.propertyName))
40+
.forEach((attribute) => {
41+
// @ts-expect-error undefined is not assignable to every Entity's property
42+
instance[attribute.propertyName] = undefined;
43+
});
44+
}
45+
46+
return instance;
2947
}
3048

3149
export function convertEntityToAttributeValues<E extends typeof Entity>(

lib/entity/types.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RequireAtLeastOne } from 'type-fest';
1+
import { NonEmptyTuple, RequireAtLeastOne } from 'type-fest';
22

33
import {
44
BatchGetItemCommandInput,
@@ -61,18 +61,34 @@ export type EntityKey<E extends typeof Entity> = keyof EntityProperties<E> exten
6161
*/
6262
export type EntityValue<E extends typeof Entity, K extends EntityKey<E>> = FlattenObject<InstanceType<E>>[K];
6363

64+
/**
65+
* Helper type to select only specified attributes from an entity instance.
66+
* If attributes are not specified, returns the full entity type.
67+
*
68+
* @template E - The entity class type
69+
* @template Attributes - Array of attribute keys to select (optional)
70+
*/
71+
export type EntitySelectedAttributes<
72+
E extends typeof Entity,
73+
Attributes extends Array<EntityKey<E>> | undefined = undefined,
74+
> =
75+
Attributes extends NonEmptyTuple<EntityKey<E>>
76+
? Omit<InstanceType<E>, Exclude<EntityKey<E>, Attributes[number] | 'dynamodeEntity'>>
77+
: InstanceType<E>;
78+
6479
/**
6580
* Options for entity get operations.
6681
*
6782
* @template E - The entity class type
83+
* @template Attributes - Array of attribute keys to select (optional)
6884
*/
69-
export type EntityGetOptions<E extends typeof Entity> = {
85+
export type EntityGetOptions<E extends typeof Entity, Attributes extends Array<EntityKey<E>> | undefined> = {
7086
/** Additional DynamoDB input parameters */
7187
extraInput?: Partial<GetItemCommandInput>;
7288
/** Return type option */
7389
return?: ReturnOption;
7490
/** Specific attributes to retrieve */
75-
attributes?: Array<EntityKey<E>>;
91+
attributes?: Attributes;
7692
/** Whether to use consistent read */
7793
consistent?: boolean;
7894
};
@@ -178,10 +194,13 @@ export type BuildDeleteConditionExpression = {
178194

179195
// entityManager.batchGet
180196

181-
export type EntityBatchGetOptions<E extends typeof Entity> = {
197+
export type EntityBatchGetOptions<
198+
E extends typeof Entity,
199+
Attributes extends ReadonlyArray<EntityKey<E>> | undefined = undefined,
200+
> = {
182201
extraInput?: Partial<BatchGetItemCommandInput>;
183202
return?: ReturnOption;
184-
attributes?: Array<EntityKey<E>>;
203+
attributes?: Attributes;
185204
consistent?: boolean;
186205
};
187206

@@ -191,8 +210,12 @@ export type EntityBatchDeleteOutput<PrimaryKey> = {
191210

192211
// entityManager.batchGet
193212

194-
export type EntityBatchGetOutput<M extends Metadata<E>, E extends typeof Entity> = {
195-
items: Array<InstanceType<E>>;
213+
export type EntityBatchGetOutput<
214+
M extends Metadata<E>,
215+
E extends typeof Entity,
216+
Attributes extends Array<EntityKey<E>> | undefined = undefined,
217+
> = {
218+
items: Array<EntitySelectedAttributes<E, Attributes>>;
196219
unprocessedKeys: Array<TablePrimaryKey<M, E>>;
197220
};
198221

lib/query/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export default class Query<M extends Metadata<E>, E extends typeof Entity> exten
160160
} while (all && !!lastKey && count < max);
161161

162162
return {
163-
items: items.map((item) => convertAttributeValuesToEntity(this.entity, item)),
163+
items: items.map((item) => convertAttributeValuesToEntity(this.entity, item, this.selectedAttributes)),
164164
lastKey: lastKey && convertAttributeValuesToLastKey(this.entity, lastKey),
165165
count,
166166
scannedCount,

lib/retriever/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { AttributeNames, AttributeValues } from '@lib/utils';
1818
export default class RetrieverBase<M extends Metadata<E>, E extends typeof Entity> extends Condition<E> {
1919
/** The DynamoDB input object (QueryInput or ScanInput) */
2020
protected input: QueryInput | ScanInput;
21+
/** The list of attributes actually fetched from DynamoDB */
22+
protected selectedAttributes: Array<EntityKey<E>> = [];
2123
/** Attribute names mapping for expression attribute names */
2224
protected attributeNames: AttributeNames = {};
2325
/** Attribute values mapping for expression attribute values */
@@ -165,6 +167,7 @@ export default class RetrieverBase<M extends Metadata<E>, E extends typeof Entit
165167
attributes,
166168
this.attributeNames,
167169
).projectionExpression;
170+
this.selectedAttributes = attributes;
168171
return this;
169172
}
170173
}

lib/scan/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export default class Scan<M extends Metadata<E>, E extends typeof Entity> extend
113113
const items = result.Items || [];
114114

115115
return {
116-
items: items.map((item) => convertAttributeValuesToEntity(this.entity, item)),
116+
items: items.map((item) => convertAttributeValuesToEntity(this.entity, item, this.selectedAttributes)),
117117
count: result.Count || 0,
118118
scannedCount: result.ScannedCount || 0,
119119
lastKey: result.LastEvaluatedKey

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dynamode",
3-
"version": "1.5.1-rc.0",
3+
"version": "1.6.0-rc.1",
44
"description": "Dynamode is a modeling tool for Amazon's DynamoDB",
55
"main": "index.js",
66
"types": "index.d.ts",

tests/e2e/entity/batchGet.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';
22

3-
import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable';
3+
import { MockEntity, MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable';
44
import { mockEntityFactory } from '../mockEntityFactory';
55

66
describe('EntityManager.batchGet', () => {
@@ -84,15 +84,15 @@ describe('EntityManager.batchGet', () => {
8484
expect(mocks).not.toEqual([mock1, mock2, mock3]);
8585
expect(mocks[0].number).toEqual(1);
8686
expect(mocks[0].string).toEqual('string');
87-
expect(mocks[0].object).toEqual(undefined);
87+
expect((mocks[0] as MockEntity).object).toEqual(undefined);
8888

8989
expect(mocks[1].number).toEqual(1);
9090
expect(mocks[1].string).toEqual('string');
91-
expect(mocks[1].object).toEqual(undefined);
91+
expect((mocks[1] as MockEntity).object).toEqual(undefined);
9292

9393
expect(mocks[2].number).toEqual(1);
9494
expect(mocks[2].string).toEqual('string');
95-
expect(mocks[2].object).toEqual(undefined);
95+
expect((mocks[2] as MockEntity).object).toEqual(undefined);
9696
});
9797
});
9898
});

0 commit comments

Comments
 (0)