From e859b7516c39684a09976201979b16732a1bcaf1 Mon Sep 17 00:00:00 2001 From: Sebastian Wahn Date: Fri, 13 Mar 2026 13:04:59 +0100 Subject: [PATCH 1/2] fix(todos): fix open todos --- .../unique-attribute-value-name.function.ts | 17 +++++++++ ...are-and-add-update-expressions.function.ts | 3 +- .../request-expression-builder.spec.ts | 10 +++--- .../expression/request-expression-builder.ts | 4 +-- .../update-expression-definition-function.ts | 2 +- .../update-expression-builder.spec.ts | 36 +++++++++---------- .../expression/update-expression-builder.ts | 10 +++--- src/dynamo/request/get/get.request.ts | 6 +--- src/dynamo/request/get/get.response.ts | 2 +- 9 files changed, 51 insertions(+), 39 deletions(-) diff --git a/src/dynamo/expression/functions/unique-attribute-value-name.function.ts b/src/dynamo/expression/functions/unique-attribute-value-name.function.ts index 851a0edc0..fc257ffd9 100644 --- a/src/dynamo/expression/functions/unique-attribute-value-name.function.ts +++ b/src/dynamo/expression/functions/unique-attribute-value-name.function.ts @@ -28,3 +28,20 @@ export function uniqueAttributeValueName(key: string, existingValueNames?: strin return potentialName } + +export function uniqueAttributeValueNameRecord(key: string, existingValueNames?: Record): string { + key = key.replace(/\./g, '__').replace(BRACED_INDEX_REGEX, attributeNameReplacer) + let potentialName = `:${key}` + let idx = 1 + + if (existingValueNames && existingValueNames.length) { + const recordKeys = Object.keys(existingValueNames) + const recordValues = Object.values(existingValueNames) + while (recordKeys.includes(potentialName) || recordValues.includes(potentialName)) { + idx++ + potentialName = `:${key}_${idx}` + } + } + + return potentialName +} diff --git a/src/dynamo/expression/prepare-and-add-update-expressions.function.ts b/src/dynamo/expression/prepare-and-add-update-expressions.function.ts index feec88095..f373ce217 100644 --- a/src/dynamo/expression/prepare-and-add-update-expressions.function.ts +++ b/src/dynamo/expression/prepare-and-add-update-expressions.function.ts @@ -22,8 +22,7 @@ export function prepareAndAddUpdateExpressions( const sortedByActionKeyWord: Map = updateDefFns .map((updateDefFn) => { // TODO post-v3:: investigate on how to remove any - // tslint:disable-next-line:no-unnecessary-type-assertion - return updateDefFn(params.ExpressionAttributeNames as any, metadata) + return updateDefFn(params.ExpressionAttributeNames, metadata) }) .reduce((result, expr) => { const actionKeyword = expr.type diff --git a/src/dynamo/expression/request-expression-builder.spec.ts b/src/dynamo/expression/request-expression-builder.spec.ts index 87a7f7785..ef1bbd699 100644 --- a/src/dynamo/expression/request-expression-builder.spec.ts +++ b/src/dynamo/expression/request-expression-builder.spec.ts @@ -57,7 +57,7 @@ describe('updateDefinitionFunction', () => { }) it('set property', () => { - const expr = updateDefinitionFunction('nestedObj').set({ id: 'ok' })([], metadata) + const expr = updateDefinitionFunction('nestedObj').set({ id: 'ok' })({}, metadata) expect(expr.statement).toBe('#nestedObj = :nestedObj') expect(expr.attributeNames).toEqual({ '#nestedObj': 'my_nested_object' }) @@ -66,7 +66,7 @@ describe('updateDefinitionFunction', () => { }) it('set nested property', () => { - const expr = updateDefinitionFunction('nestedObj.id').set('ok')([], metadata) + const expr = updateDefinitionFunction('nestedObj.id').set('ok')({}, metadata) expect(expr.statement).toBe('#nestedObj.#id = :nestedObj__id') expect(expr.attributeNames).toEqual({ '#nestedObj': 'my_nested_object', '#id': 'id' }) @@ -75,7 +75,7 @@ describe('updateDefinitionFunction', () => { }) it('set list item at position', () => { - const expr = updateDefinitionFunction('sortedComplexSet[0]').set({ id: 'ok' })([], metadata) + const expr = updateDefinitionFunction('sortedComplexSet[0]').set({ id: 'ok' })({}, metadata) expect(expr.statement).toBe('#sortedComplexSet[0] = :sortedComplexSet_at_0') expect(expr.attributeNames).toEqual({ '#sortedComplexSet': 'sortedComplexSet' }) @@ -84,7 +84,7 @@ describe('updateDefinitionFunction', () => { }) it('set nested property of list item at position', () => { - const expr = updateDefinitionFunction('sortedComplexSet[1].id').set('ok')([], metadata) + const expr = updateDefinitionFunction('sortedComplexSet[1].id').set('ok')({}, metadata) expect(expr.statement).toBe('#sortedComplexSet[1].#id = :sortedComplexSet_at_1__id') expect(expr.attributeNames).toEqual({ '#sortedComplexSet': 'sortedComplexSet', '#id': 'id' }) @@ -93,7 +93,7 @@ describe('updateDefinitionFunction', () => { }) it('set nested property of list item with decorators', () => { - const expr = updateDefinitionFunction('sortedComplexSet[1].date').set(aDate)([], metadata) + const expr = updateDefinitionFunction('sortedComplexSet[1].date').set(aDate)({}, metadata) expect(expr.statement).toBe('#sortedComplexSet[1].#date = :sortedComplexSet_at_1__date') expect(expr.attributeNames).toEqual({ '#sortedComplexSet': 'sortedComplexSet', '#date': 'my_date' }) diff --git a/src/dynamo/expression/request-expression-builder.ts b/src/dynamo/expression/request-expression-builder.ts index 7555f9463..ef982e2d9 100644 --- a/src/dynamo/expression/request-expression-builder.ts +++ b/src/dynamo/expression/request-expression-builder.ts @@ -53,7 +53,7 @@ export function addUpdate( string, UpdateActionDef, any[], - string[] | undefined, + Record | undefined, Metadata | undefined, UpdateExpression >(buildUpdateExpression) @@ -193,7 +193,7 @@ export function updateDefinitionFunction(attributePath: keyof T): UpdateExpre string, UpdateActionDef, any[], - string[] | undefined, + Record | undefined, Metadata | undefined, UpdateExpression >(buildUpdateExpression) diff --git a/src/dynamo/expression/type/update-expression-definition-function.ts b/src/dynamo/expression/type/update-expression-definition-function.ts index c5bd098f5..597c08f3e 100644 --- a/src/dynamo/expression/type/update-expression-definition-function.ts +++ b/src/dynamo/expression/type/update-expression-definition-function.ts @@ -8,6 +8,6 @@ import { UpdateExpression } from './update-expression.type' * @hidden */ export type UpdateExpressionDefinitionFunction = ( - expressionAttributeValues: string[] | undefined, + expressionAttributeValues: Record | undefined, metadata: Metadata | undefined, ) => UpdateExpression diff --git a/src/dynamo/expression/update-expression-builder.spec.ts b/src/dynamo/expression/update-expression-builder.spec.ts index e4e0008d0..bd544ae39 100644 --- a/src/dynamo/expression/update-expression-builder.spec.ts +++ b/src/dynamo/expression/update-expression-builder.spec.ts @@ -10,14 +10,14 @@ describe('buildUpdateExpression', () => { it('should throw when operation.action is unknown', () => { const unknownOp = new UpdateActionDef('SET', 'subtract') - expect(() => buildUpdateExpression('age', unknownOp, [3], [], metaDataS)).toThrow() + expect(() => buildUpdateExpression('age', unknownOp, [3], {}, metaDataS)).toThrow() }) describe('incrementBy', () => { const op = new UpdateActionDef('SET', 'incrementBy') it('should build expression', () => { - const exp = buildUpdateExpression('age', op, [23], [], metaDataS) + const exp = buildUpdateExpression('age', op, [23], {}, metaDataS) expect(exp).toEqual({ attributeNames: { '#age': 'age' }, attributeValues: { ':age': { N: '23' } }, @@ -27,7 +27,7 @@ describe('buildUpdateExpression', () => { }) it('should build expression for number at document path ', () => { - const exp = buildUpdateExpression('numberValues[0]', op, [23], [], metaDataU) + const exp = buildUpdateExpression('numberValues[0]', op, [23], {}, metaDataU) expect(exp).toEqual({ attributeNames: { '#numberValues': 'numberValues' }, attributeValues: { ':numberValues_at_0': { N: '23' } }, @@ -37,14 +37,14 @@ describe('buildUpdateExpression', () => { }) it('should throw when not number', () => { - expect(() => buildUpdateExpression('age', op, ['notANumber'], [], metaDataS)).toThrow() + expect(() => buildUpdateExpression('age', op, ['notANumber'], {}, metaDataS)).toThrow() }) }) describe('decrementBy', () => { const op = new UpdateActionDef('SET', 'decrementBy') it('should build expression', () => { - const exp = buildUpdateExpression('age', op, [23], [], metaDataS) + const exp = buildUpdateExpression('age', op, [23], {}, metaDataS) expect(exp).toEqual({ attributeNames: { '#age': 'age' }, attributeValues: { ':age': { N: '23' } }, @@ -54,7 +54,7 @@ describe('buildUpdateExpression', () => { }) it('should build expression for number at document path ', () => { - const exp = buildUpdateExpression('numberValues[0]', op, [23], [], metaDataU) + const exp = buildUpdateExpression('numberValues[0]', op, [23], {}, metaDataU) expect(exp).toEqual({ attributeNames: { '#numberValues': 'numberValues' }, attributeValues: { ':numberValues_at_0': { N: '23' } }, @@ -64,15 +64,15 @@ describe('buildUpdateExpression', () => { }) it('should throw when not number', () => { - expect(() => buildUpdateExpression('age', op, ['notANumber'], [], metaDataS)).toThrow() + expect(() => buildUpdateExpression('age', op, ['notANumber'], {}, metaDataS)).toThrow() }) }) describe('set', () => { const op = new UpdateActionDef('SET', 'set') - it('should build set expression for number[]', () => { - const exp = buildUpdateExpression('numberValues', op, [[23]], [], metaDataU) + it('should build set expression for number{}', () => { + const exp = buildUpdateExpression('numberValues', op, [[23]], {}, metaDataU) expect(exp).toEqual({ attributeNames: { '#numberValues': 'numberValues' }, attributeValues: { ':numberValues': { L: [{ N: '23' }] } }, @@ -83,7 +83,7 @@ describe('buildUpdateExpression', () => { describe('should build set expression for empty collection', () => { it('array', () => { - const exp = buildUpdateExpression('addresses', op, [[]], [], metaDataU) + const exp = buildUpdateExpression('addresses', op, [[]], {}, metaDataU) expect(exp).toEqual({ attributeNames: { '#addresses': 'addresses' }, attributeValues: { ':addresses': { L: [] } }, @@ -94,7 +94,7 @@ describe('buildUpdateExpression', () => { }) it('should build set expression for number at document path', () => { - const exp = buildUpdateExpression('numberValues[0]', op, [23], [], metaDataU) + const exp = buildUpdateExpression('numberValues[0]', op, [23], {}, metaDataU) expect(exp).toEqual({ attributeNames: { '#numberValues': 'numberValues' }, attributeValues: { ':numberValues_at_0': { N: '23' } }, @@ -123,7 +123,7 @@ describe('buildUpdateExpression', () => { const op = new UpdateActionDef('ADD', 'add') it('should build add expression for numbers', () => { - const exp = buildUpdateExpression('age', op, [23], [], metaDataS) + const exp = buildUpdateExpression('age', op, [23], {}, metaDataS) expect(exp).toEqual({ attributeNames: { '#age': 'age' }, attributeValues: { ':age': { N: '23' } }, @@ -134,7 +134,7 @@ describe('buildUpdateExpression', () => { it('should work with customMapper (1)', () => { const metaDataC = metadataForModel(SpecialCasesModel) - const exp = buildUpdateExpression('myChars', op, ['abc'], [], metaDataC) + const exp = buildUpdateExpression('myChars', op, ['abc'], {}, metaDataC) expect(exp).toEqual({ attributeNames: { '#myChars': 'myChars' }, attributeValues: { ':myChars': { SS: ['a', 'b', 'c'] } }, @@ -144,11 +144,11 @@ describe('buildUpdateExpression', () => { }) it('should throw when not number or a set value', () => { - expect(() => buildUpdateExpression('age', op, ['notANumber'], [], metaDataS)).toThrow() + expect(() => buildUpdateExpression('age', op, ['notANumber'], {}, metaDataS)).toThrow() }) it('should throw when no value for attributeValue was given', () => { - expect(() => buildUpdateExpression('age', op, [], [], metaDataS)).toThrow() + expect(() => buildUpdateExpression('age', op, [], {}, metaDataS)).toThrow() }) }) @@ -156,7 +156,7 @@ describe('buildUpdateExpression', () => { const op = new UpdateActionDef('DELETE', 'removeFromSet') it('should build the expression', () => { - const exp = buildUpdateExpression('topics', op, [new Set(['val1', 'val2'])], [], metaDataU) + const exp = buildUpdateExpression('topics', op, [new Set(['val1', 'val2'])], {}, metaDataU) expect(exp).toEqual({ attributeNames: { '#topics': 'topics' }, attributeValues: { ':topics': { SS: ['val1', 'val2'] } }, @@ -166,11 +166,11 @@ describe('buildUpdateExpression', () => { }) it('should throw when not a set value', () => { - expect(() => buildUpdateExpression('topics', op, ['notASet'], [], metaDataU)).toThrow() + expect(() => buildUpdateExpression('topics', op, ['notASet'], {}, metaDataU)).toThrow() }) it('should throw when no value was given', () => { - expect(() => buildUpdateExpression('topics', op, [], [], metaDataU)).toThrow() + expect(() => buildUpdateExpression('topics', op, [], {}, metaDataU)).toThrow() }) }) }) diff --git a/src/dynamo/expression/update-expression-builder.ts b/src/dynamo/expression/update-expression-builder.ts index da498dfcc..2aa3a018a 100644 --- a/src/dynamo/expression/update-expression-builder.ts +++ b/src/dynamo/expression/update-expression-builder.ts @@ -12,7 +12,7 @@ import { Attribute, Attributes } from '../../mapper/type/attribute.type' import { getPropertyPath, isSet } from '../../mapper/util' import { deepFilter } from './condition-expression-builder' import { resolveAttributeNames } from './functions/attribute-names.function' -import { uniqueAttributeValueName } from './functions/unique-attribute-value-name.function' +import { uniqueAttributeValueNameRecord } from './functions/unique-attribute-value-name.function' import { UpdateActionDef } from './type/update-action-def' import { UpdateAction } from './type/update-action.type' import { UpdateExpression } from './type/update-expression.type' @@ -24,7 +24,7 @@ import { UpdateExpression } from './type/update-expression.type' * @param {string} attributePath * @param {ConditionOperator} operation * @param {any[]} values Depending on the operation the amount of values differs - * @param {string[]} existingValueNames If provided the existing names are used to make sure we have a unique name for the current attributePath + * @param {Record} existingValueNames If provided the existing names are used to make sure we have a unique name for the current attributePath * @param {Metadata} metadata If provided we use the metadata to define the attribute name and use it to map the given value(s) to attributeValue(s) * @returns {Expression} * @hidden @@ -33,7 +33,7 @@ export function buildUpdateExpression( attributePath: string, operation: UpdateActionDef, values: any[], - existingValueNames: string[] | undefined, + existingValueNames: Record | undefined, metadata: Metadata | undefined, ): UpdateExpression { // get rid of undefined values @@ -54,7 +54,7 @@ export function buildUpdateExpression( * person.age */ const resolvedAttributeNames = resolveAttributeNames(attributePath, metadata) - const valuePlaceholder = uniqueAttributeValueName(attributePath, existingValueNames) + const valuePlaceholder = uniqueAttributeValueNameRecord(attributePath, existingValueNames) /* * build the statement @@ -80,7 +80,7 @@ function buildDefaultExpression( valuePlaceholder: string, attributeNames: Record, values: any[], - _existingValueNames: string[] | undefined, + _existingValueNames: Record | undefined, propertyMetadata: PropertyMetadata | undefined, operator: UpdateActionDef, ): UpdateExpression { diff --git a/src/dynamo/request/get/get.request.ts b/src/dynamo/request/get/get.request.ts index acec59066..153873c74 100644 --- a/src/dynamo/request/get/get.request.ts +++ b/src/dynamo/request/get/get.request.ts @@ -52,14 +52,10 @@ export class GetRequest = T> extends StandardRequest< .getItem(this.params) .then(promiseTap((response) => this.logger.debug('response', response))) .then((getItemResponse) => { - // TODO post-v3: investigate on how to remove any - // tslint:disable-next-line:no-unnecessary-type-assertion - const response: GetResponse = { ...(getItemResponse as any) } + const response: GetResponse = { ...getItemResponse, ...{ Item: undefined } } if (getItemResponse.Item) { response.Item = fromDb(>getItemResponse.Item, this.modelClazz) - } else { - response.Item = null } return response diff --git a/src/dynamo/request/get/get.response.ts b/src/dynamo/request/get/get.response.ts index ec247fda2..797ff4f3b 100644 --- a/src/dynamo/request/get/get.response.ts +++ b/src/dynamo/request/get/get.response.ts @@ -11,7 +11,7 @@ export interface GetResponse { /** * A map of attribute names to AttributeValue objects (subset if ProjectionExpression was defined). */ - Item: T | null + Item: T | undefined /** * The capacity units consumed by the GetItem operation. The data returned includes the total provisioned throughput consumed, along with statistics for the table and any indexes involved in the operation. ConsumedCapacity is only returned if the ReturnConsumedCapacity parameter was specified. For more information, see Provisioned Throughput in the Amazon DynamoDB Developer Guide. */ From 79032dbec61c6eaf6b5244fbdb096e7b16d7a5de Mon Sep 17 00:00:00 2001 From: Sebastian Wahn Date: Sat, 14 Mar 2026 12:55:15 +0100 Subject: [PATCH 2/2] chore(update): update migration.md --- v2-v3-migration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2-v3-migration.md b/v2-v3-migration.md index 6b9223e2f..ddaf013f4 100644 --- a/v2-v3-migration.md +++ b/v2-v3-migration.md @@ -3,7 +3,7 @@ ## 📋 To complete before releasing - [x] Make sure the snippets compile - [x] Make tests compile and run successfully -- [ ] Search for `TODO v3:` in the code to see open issues / notes, and create followup issues to decide if we have more to fix prior to a major release or keep it for later follow-up +- [x] Search for `TODO v3:` in the code to see open issues / notes, and create followup issues to decide if we have more to fix prior to a major release or keep it for later follow-up - [ ] Check on new attribute type [$UnknownAttribute](./src/mapper/type/attribute.type.ts) and implement tests - [ ] Remove [sessionValidityEnsurer](./src/config/dynamo-easy-config.ts) and add demo with using [middleware stack](https://github.com/aws/aws-sdk-js-v3#middleware-stack) to implement the same