Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- JSDoc documentation was added to the project ([#40](https://github.com/blazejkustra/dynamode/pull/40))
- Enhanced entity conversion to handle attribute selection in EntityManager `get()` and `batchGet()` methods

### Changed
- Bump AWS SDK packages to version 3.883.0 & improve type safety ([#42](https://github.com/blazejkustra/dynamode/pull/40))

### Fixed
- Fix import example in documentation ([#37](https://github.com/blazejkustra/dynamode/pull/37)) - Thanks @gmreburn!

## [1.5.0] - 2024-08-15

### Added
- `@attribute.customName()` decorator to set custom names for entity attributes ([#33](https://github.com/blazejkustra/dynamode/pull/33))
- Tests for `customName` decorator

### Fixed
- Fixed entity renaming logic
- Improved code safety and error handling

## [1.4.0] - 2024-07-21

### Added
- 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))
- Allow GSI decorators to decorate both partition and sort keys interchangeably ([#31](https://github.com/blazejkustra/dynamode/pull/31))

### Changed
- Moved test fixtures to a dedicated catalog
- Improved test organization and coverage

### Fixed
- Fixed issue where multiple attribute decorators could not be added to primary keys
- Fixed typecheck issues

## [1.3.0] - 2024-06-23

### Added
- Support for decorating an attribute with multiple indexes ([#28](https://github.com/blazejkustra/dynamode/pull/28))

## [1.2.0] - 2024-04-17

### Added
- Binary data type support ([#26](https://github.com/blazejkustra/dynamode/pull/26))

## [1.1.0] - 2024-04-13

### Fixed
- Fixed `startAt` fails when querying secondary indices

## [1.0.0] - 2024-03-24

### Added
- 🎉 Dynamode is now out of beta!
- DynamoDB stream support ([#21](https://github.com/blazejkustra/dynamode/pull/21))

## Earlier Versions

For changes in earlier versions (< 1.0.0), please refer to the [Git commit history](https://github.com/blazejkustra/dynamode/commits/main).

42 changes: 24 additions & 18 deletions lib/entity/entityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
EntityGetOptions,
EntityKey,
EntityPutOptions,
EntitySelectedAttributes,
EntityTransactionDeleteOptions,
EntityTransactionGetOptions,
EntityTransactionPutOptions,
Expand Down Expand Up @@ -170,10 +171,10 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
* });
* ```
*/
function get(
function get<const Attributes extends Array<EntityKey<E>> | undefined = undefined>(
primaryKey: TablePrimaryKey<M, E>,
options?: EntityGetOptions<E> & { return?: 'default' },
): Promise<InstanceType<E>>;
options?: EntityGetOptions<E, Attributes> & { return?: 'default' },
): Promise<EntitySelectedAttributes<E, Attributes>>;

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

/**
Expand All @@ -196,7 +197,7 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
*/
function get(
primaryKey: TablePrimaryKey<M, E>,
options: EntityGetOptions<E> & { return: 'input' },
options: EntityGetOptions<E, any> & { return: 'input' },
): GetItemCommandInput;

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

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

return convertAttributeValuesToEntity(entity, result.Item);
return convertAttributeValuesToEntity(entity, result.Item, options?.attributes) as EntitySelectedAttributes<
E,
Attributes
>;
})();
}

Expand Down Expand Up @@ -624,10 +628,10 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
* });
* ```
*/
function batchGet(
function batchGet<const Attributes extends Array<EntityKey<E>> | undefined = undefined>(
primaryKeys: Array<TablePrimaryKey<M, E>>,
options?: EntityBatchGetOptions<E> & { return?: 'default' },
): Promise<EntityBatchGetOutput<M, E>>;
options?: EntityBatchGetOptions<E, Attributes> & { return?: 'default' },
): Promise<EntityBatchGetOutput<M, E, Attributes>>;

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

/**
Expand All @@ -650,7 +654,7 @@ export default function EntityManager<M extends Metadata<E>, E extends typeof En
*/
function batchGet(
primaryKeys: Array<TablePrimaryKey<M, E>>,
options: EntityBatchGetOptions<E> & { return: 'input' },
options: EntityBatchGetOptions<E, any> & { return: 'input' },
): BatchGetItemCommandInput;

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

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

return {
items: items.map((item) => convertAttributeValuesToEntity(entity, item)),
items: items.map((item) => convertAttributeValuesToEntity(entity, item, options?.attributes)) as Array<
EntitySelectedAttributes<E, Attributes>
>,
unprocessedKeys,
};
})();
Expand Down
20 changes: 19 additions & 1 deletion lib/entity/helpers/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AttributeValues, fromDynamo, GenericObject, objectToDynamo } from '@lib
export function convertAttributeValuesToEntity<E extends typeof Entity>(
entity: E,
dynamoItem: AttributeValues,
selectedAttributes?: Array<string>,
): InstanceType<E> {
const object = fromDynamo(dynamoItem);
const attributes = Dynamode.storage.getEntityAttributes(entity.name);
Expand All @@ -25,7 +26,24 @@ export function convertAttributeValuesToEntity<E extends typeof Entity>(
object[attribute.propertyName] = truncateValue(entity, attribute.propertyName, value);
});

return new entity(object) as InstanceType<E>;
const instance = new entity(object) as InstanceType<E>;

if (selectedAttributes && selectedAttributes.length > 0) {
// Remove nested attributes from the selected attributes
const selectedAttributesSet = new Set([
'dynamodeEntity',
...selectedAttributes.map((attribute) => attribute.split('.')[0]),
]);

Object.values(attributes)
.filter((attribute) => !selectedAttributesSet.has(attribute.propertyName))
.forEach((attribute) => {
// @ts-expect-error undefined is not assignable to every Entity's property
instance[attribute.propertyName] = undefined;
});
}

return instance;
}

export function convertEntityToAttributeValues<E extends typeof Entity>(
Expand Down
37 changes: 30 additions & 7 deletions lib/entity/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RequireAtLeastOne } from 'type-fest';
import { NonEmptyTuple, RequireAtLeastOne } from 'type-fest';

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

/**
* Helper type to select only specified attributes from an entity instance.
* If attributes are not specified, returns the full entity type.
*
* @template E - The entity class type
* @template Attributes - Array of attribute keys to select (optional)
*/
export type EntitySelectedAttributes<
E extends typeof Entity,
Attributes extends Array<EntityKey<E>> | undefined = undefined,
> =
Attributes extends NonEmptyTuple<EntityKey<E>>
? Omit<InstanceType<E>, Exclude<EntityKey<E>, Attributes[number] | 'dynamodeEntity'>>
: InstanceType<E>;

/**
* Options for entity get operations.
*
* @template E - The entity class type
* @template Attributes - Array of attribute keys to select (optional)
*/
export type EntityGetOptions<E extends typeof Entity> = {
export type EntityGetOptions<E extends typeof Entity, Attributes extends Array<EntityKey<E>> | undefined> = {
/** Additional DynamoDB input parameters */
extraInput?: Partial<GetItemCommandInput>;
/** Return type option */
return?: ReturnOption;
/** Specific attributes to retrieve */
attributes?: Array<EntityKey<E>>;
attributes?: Attributes;
/** Whether to use consistent read */
consistent?: boolean;
};
Expand Down Expand Up @@ -178,10 +194,13 @@ export type BuildDeleteConditionExpression = {

// entityManager.batchGet

export type EntityBatchGetOptions<E extends typeof Entity> = {
export type EntityBatchGetOptions<
E extends typeof Entity,
Attributes extends ReadonlyArray<EntityKey<E>> | undefined = undefined,
> = {
extraInput?: Partial<BatchGetItemCommandInput>;
return?: ReturnOption;
attributes?: Array<EntityKey<E>>;
attributes?: Attributes;
consistent?: boolean;
};

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

// entityManager.batchGet

export type EntityBatchGetOutput<M extends Metadata<E>, E extends typeof Entity> = {
items: Array<InstanceType<E>>;
export type EntityBatchGetOutput<
M extends Metadata<E>,
E extends typeof Entity,
Attributes extends Array<EntityKey<E>> | undefined = undefined,
> = {
items: Array<EntitySelectedAttributes<E, Attributes>>;
unprocessedKeys: Array<TablePrimaryKey<M, E>>;
};

Expand Down
2 changes: 1 addition & 1 deletion lib/query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export default class Query<M extends Metadata<E>, E extends typeof Entity> exten
} while (all && !!lastKey && count < max);

return {
items: items.map((item) => convertAttributeValuesToEntity(this.entity, item)),
items: items.map((item) => convertAttributeValuesToEntity(this.entity, item, this.selectedAttributes)),
lastKey: lastKey && convertAttributeValuesToLastKey(this.entity, lastKey),
count,
scannedCount,
Expand Down
3 changes: 3 additions & 0 deletions lib/retriever/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { AttributeNames, AttributeValues } from '@lib/utils';
export default class RetrieverBase<M extends Metadata<E>, E extends typeof Entity> extends Condition<E> {
/** The DynamoDB input object (QueryInput or ScanInput) */
protected input: QueryInput | ScanInput;
/** The list of attributes actually fetched from DynamoDB */
protected selectedAttributes: Array<EntityKey<E>> = [];
/** Attribute names mapping for expression attribute names */
protected attributeNames: AttributeNames = {};
/** Attribute values mapping for expression attribute values */
Expand Down Expand Up @@ -165,6 +167,7 @@ export default class RetrieverBase<M extends Metadata<E>, E extends typeof Entit
attributes,
this.attributeNames,
).projectionExpression;
this.selectedAttributes = attributes;
return this;
}
}
2 changes: 1 addition & 1 deletion lib/scan/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default class Scan<M extends Metadata<E>, E extends typeof Entity> extend
const items = result.Items || [];

return {
items: items.map((item) => convertAttributeValuesToEntity(this.entity, item)),
items: items.map((item) => convertAttributeValuesToEntity(this.entity, item, this.selectedAttributes)),
count: result.Count || 0,
scannedCount: result.ScannedCount || 0,
lastKey: result.LastEvaluatedKey
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "dynamode",
"version": "1.5.1-rc.0",
"version": "1.6.0-rc.1",
"description": "Dynamode is a modeling tool for Amazon's DynamoDB",
"main": "index.js",
"types": "index.d.ts",
Expand Down
8 changes: 4 additions & 4 deletions tests/e2e/entity/batchGet.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest';

import { MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable';
import { MockEntity, MockEntityManager, TEST_TABLE_NAME, TestTableManager } from '../../fixtures/TestTable';
import { mockEntityFactory } from '../mockEntityFactory';

describe('EntityManager.batchGet', () => {
Expand Down Expand Up @@ -84,15 +84,15 @@ describe('EntityManager.batchGet', () => {
expect(mocks).not.toEqual([mock1, mock2, mock3]);
expect(mocks[0].number).toEqual(1);
expect(mocks[0].string).toEqual('string');
expect(mocks[0].object).toEqual(undefined);
expect((mocks[0] as MockEntity).object).toEqual(undefined);

expect(mocks[1].number).toEqual(1);
expect(mocks[1].string).toEqual('string');
expect(mocks[1].object).toEqual(undefined);
expect((mocks[1] as MockEntity).object).toEqual(undefined);

expect(mocks[2].number).toEqual(1);
expect(mocks[2].string).toEqual('string');
expect(mocks[2].object).toEqual(undefined);
expect((mocks[2] as MockEntity).object).toEqual(undefined);
});
});
});
Loading
Loading