diff --git a/src/condition-attribute.ts b/src/condition-attribute.ts index 01f89bd..53f4738 100644 --- a/src/condition-attribute.ts +++ b/src/condition-attribute.ts @@ -14,15 +14,24 @@ export default abstract class ConditionAttribute { } protected getAttributeUniqueIdentifier(): string { - return this.field + uuid().replace(/-/g, ''); + return this.field.split('.').map(key => key + uuid().replace(/-/g, '')).join('.'); } - protected fillMaps(id: string, value: T) { - const attr: Map = new Map(); + protected fillMaps(value: T) { + const { id, attr } = this.prepareAttributes(); const values: Map = new Map(); - attr.set(`#${id}`, this.field); - values.set(`:${id}`, value); - return { attr, values }; + values.set(`:${id.split('.').at(-1)}`, value); + return { id, attr, values }; + } + + protected prepareAttributes(): { id: string; attr: Map } { + const keys = this.field.split('.'); + const id = this.getAttributeUniqueIdentifier(); + const attr: Map = new Map(); + id.split('.').forEach((id, index) => { + attr.set(`#${id}`, keys[index]); + }) + return { id, attr }; } public abstract eq(value: T): Condition; @@ -40,14 +49,13 @@ export default abstract class ConditionAttribute { public abstract beginsWith(value: T): Condition; protected prepareOp(value: T): { id: string; attr: Map; values: Map } { - const id = this.getAttributeUniqueIdentifier(); - const { attr, values } = this.fillMaps(id, value); + const { id, attr, values } = this.fillMaps(value); return { id, attr, values }; } private arithmeticOperation(value: T, operator: ArithmeticOperator): IArgs { const { id, attr, values } = this.prepareOp(value); - return [`#${id} ${operator} :${id}`, attr, values]; + return [`#${id.replace(/\./g, '.#')} ${operator} :${id.split('.').at(-1)}`, attr, values]; } protected prepareEq(value: T): IArgs { @@ -71,18 +79,15 @@ export default abstract class ConditionAttribute { } protected prepareBetween(lower: KeyValue, upper: KeyValue): IArgs { - const id = this.getAttributeUniqueIdentifier(); - const attr: Map = new Map(); + const { id, attr } = this.prepareAttributes(); const values: Map = new Map(); - attr.set(`#${id}`, this.field); - values.set(`:${id}lower`, lower as T); - values.set(`:${id}upper`, upper as T); - return [`#${id} BETWEEN :${id}lower AND :${id}upper`, attr, values]; + values.set(`:${id.split('.').at(-1)}lower`, lower as T); + values.set(`:${id.split('.').at(-1)}upper`, upper as T); + return [`#${id.replace(/\./g, '.#')} BETWEEN :${id.split('.').at(-1)}lower AND :${id.split('.').at(-1)}upper`, attr, values]; } protected prepareBeginsWith(value: string): IArgs { - const id = this.getAttributeUniqueIdentifier(); - const { attr, values } = this.fillMaps(id, value as T); - return [`begins_with(#${id}, :${id})`, attr, values]; + const { id, attr, values } = this.fillMaps(value as T); + return [`begins_with(#${id.replace(/\./g, '.#')}, :${id.split('.').at(-1)})`, attr, values]; } } diff --git a/src/filter-attribute.ts b/src/filter-attribute.ts index c99083f..b88c48a 100644 --- a/src/filter-attribute.ts +++ b/src/filter-attribute.ts @@ -38,27 +38,17 @@ export default class FilterAttribute extends ConditionAttribute { } public neq(value: FilterValue): FilterCondition { - const id = this.getAttributeUniqueIdentifier(); - const { attr, values } = this.fillMaps(id, value); - return new FilterCondition(`#${id} <> :${id}`, attr, values); + const { id, attr, values } = this.fillMaps(value); + return new FilterCondition(`#${id.replace(/\./g, '.#')} <> :${id.split('.').at(-1)}`, attr, values); } public in(...values: FilterValue[]): FilterCondition { - const id = this.getAttributeUniqueIdentifier(); - const attr: Map = new Map(); + const { id, attr } = this.prepareAttributes(); const val: Map = new Map(); - attr.set(`#${id}`, this.field); values.forEach((value, idx) => { - val.set(`:${id}${idx}`, value); + val.set(`:${id.split('.').at(-1)}${idx}`, value); }); - return new FilterCondition(`#${id} IN (${Array.from(val.keys()).join(',')})`, attr, val); - } - - private prepareAttributes(): { id: string; attr: Map } { - const id = this.getAttributeUniqueIdentifier(); - const attr: Map = new Map(); - attr.set(`#${id}`, this.field); - return { id, attr }; + return new FilterCondition(`#${id.replace(/\./g, '.#')} IN (${Array.from(val.keys()).join(',')})`, attr, val); } private prepareAttributesAndValues(): IAttributesValues { @@ -70,19 +60,19 @@ export default class FilterAttribute extends ConditionAttribute { private nullOperation(not = false): FilterCondition { const { id, attr, values } = this.prepareAttributesAndValues(); - return new FilterCondition(`#${id} ${not ? '<>' : '='} :null`, attr, values); + return new FilterCondition(`#${id.replace(/\./g, '.#')} ${not ? '<>' : '='} :null`, attr, values); } private existsOperation(not = false): FilterCondition { const { id, attr } = this.prepareAttributes(); const operator = not ? 'attribute_not_exists' : 'attribute_exists'; - return new FilterCondition(`${operator}(#${id})`, attr, new Map()); + return new FilterCondition(`${operator}(#${id.replace(/\./g, '.#')})`, attr, new Map()); } private containsOperation(value: string, not = false): FilterCondition { const { id, attr, values } = this.prepareOp(value); const operator = not ? 'NOT contains' : 'contains'; - return new FilterCondition(`${operator}(#${id}, :${id})`, attr, values); + return new FilterCondition(`${operator}(#${id.replace(/\./g, '.#')}, :${id.split('.').at(-1)})`, attr, values); } public null(): FilterCondition { @@ -108,4 +98,4 @@ export default class FilterAttribute extends ConditionAttribute { public notContains(value: string): FilterCondition { return this.containsOperation(value, true); } -} +} \ No newline at end of file diff --git a/test/factories/index.ts b/test/factories/index.ts index 7a2f0fb..4199851 100644 --- a/test/factories/index.ts +++ b/test/factories/index.ts @@ -25,6 +25,14 @@ const generatePartial = ( optionalStringset: i % 2 === 0 ? [`string-${i}-0`, `string-${i}-1`, `string-${i}-2`] : undefined, optionalList: i % 2 === 0 ? [i, `item-${i}`] : undefined, optionalStringmap: i % 2 === 0 ? { [`key-${i}`]: `value-${i}` } : undefined, + nested: { + number: i % 2 === 0 ? i : null, + bool: generateBool(i), + string: i % 2 === 0 ? `string-${i}` : null, + optionalNumber: i % 2 === 0 ? i : undefined, + optionalBool: i % 2 === 0 ? true : undefined, + optionalString: i % 2 === 0 ? `string-${i}` : undefined, + } }); const save = async ( diff --git a/test/models/common.ts b/test/models/common.ts index cf7a945..a3243eb 100644 --- a/test/models/common.ts +++ b/test/models/common.ts @@ -4,9 +4,8 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; export default DynamoDBDocumentClient.from( new DynamoDBClient({ region: 'local', - endpoint: `http://${process.env.LOCAL_DYNAMODB_HOST}:${ - process.env.LOCAL_DYNAMODB_PORT || 8000 - }`, + endpoint: `http://${process.env.LOCAL_DYNAMODB_HOST}:${process.env.LOCAL_DYNAMODB_PORT || 8000 + }`, }), { marshallOptions: { removeUndefinedValues: true } }, ); @@ -24,4 +23,5 @@ export type CommonFields = { optionalStringset?: string[] | undefined; optionalList?: Array | undefined; optionalStringmap?: { [key: string]: string } | undefined; + nested?: Record; }; diff --git a/test/query.spec.ts b/test/query.spec.ts index d74f4c7..1ba5aa8 100644 --- a/test/query.spec.ts +++ b/test/query.spec.ts @@ -1435,6 +1435,40 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(7); expect(result.items.every((i) => i.bool)).toBe(true); }); + test('should return items where EQ condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').eq('string-2')) + .exec(); + + expect(result.count).toBe(1); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(1); + expect(result.items[0].nested?.string).toBe('string-2'); + }); + test('should return items where EQ condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').eq(6)) + .exec(); + expect(result.count).toBe(1); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(1); + expect(result.items[0].nested?.number).toBe(6); + }); + test('should return items where EQ condition is true [nestedAttr][boolean]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.bool').eq(true)) + .exec(); + expect(result.count).toBe(7); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(7); + expect(result.items.every((i) => i.nested?.bool)).toBe(true); + }); }); describe('NE', () => { test('should return items where NEQ condition is true [string]', async () => { @@ -1472,6 +1506,41 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(13); expect(result.items.every((i) => !i.bool)).toBe(true); }); + test('should return items where NEQ condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').neq('string-2')) + .exec(); + + expect(result.count).toBe(19); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(19); + expect(result.items.some((i) => i.nested!.string! === 'string-2')).toBe(false); + }); + test('should return items where NEQ condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').neq(8)) + .exec(); + + expect(result.count).toBe(19); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(19); + expect(result.items.some((i) => i.nested!.number! === 8)).toBe(false); + }); + test('should return items where NEQ condition is true [nestedAttr][boolean]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.bool').neq(true)) + .exec(); + expect(result.count).toBe(13); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(13); + expect(result.items.every((i) => !i.nested?.bool)).toBe(true); + }); }); describe('IN', () => { test('should return items where IN condition is true [string]', async () => { @@ -1511,6 +1580,43 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(14); expect(result.items.every((i) => i.bool != null)).toBe(true); }); + test('should return items where IN condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').in('string-2', 'string-12', 'string-14', 'string-0')) + .exec(); + expect(result.count).toBe(4); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(4); + expect(result.items.map((i) => i.nested?.string).sort()).toEqual( + ['string-2', 'string-12', 'string-14', 'string-0'].sort(), + ); + }); + test('should return items where IN condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').in(2, 12, 14, 0)) + .exec(); + expect(result.count).toBe(4); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(4); + expect(result.items.map((i) => i.nested?.number).sort()).toEqual( + [2, 12, 14, 0].sort(), + ); + }); + test('should return items where IN condition is true [nestedAttr][boolean]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.bool').in(false, true)) + .exec(); + expect(result.count).toBe(14); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(14); + expect(result.items.every((i) => i.nested?.bool != null)).toBe(true); + }); }); describe('LE', () => { test('should return items where LE condition is true [string]', async () => { @@ -1537,6 +1643,30 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(7); expect(result.items.every((i) => i.number! <= 12)).toBe(true); }); + test('should return items where LE condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').le('string-12')) + .exec(); + + expect(result.count).toBe(3); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(3); + expect(result.items.every((i) => (i.nested?.string as string).localeCompare('string-12') <= 0)).toBe(true); + }); + test('should return items where LE condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').le(12)) + .exec(); + + expect(result.count).toBe(7); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(7); + expect(result.items.every((i) => i.nested?.number as number <= 12)).toBe(true); + }); }); describe('LT', () => { test('should return items where LT condition is true [string]', async () => { @@ -1563,6 +1693,30 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(6); expect(result.items.every((i) => i.number! < 12)).toBe(true); }); + test('should return items where LT condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').lt('string-12')) + .exec(); + + expect(result.count).toBe(2); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(2); + expect(result.items.every((i) => (i.nested?.string as string).localeCompare('string-12') < 0)).toBe(true); + }); + test('should return items where LT condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').lt(12)) + .exec(); + + expect(result.count).toBe(6); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(6); + expect(result.items.every((i) => i.nested?.number as number < 12)).toBe(true); + }); }); describe('GE', () => { test('should return items where GE condition is true [string]', async () => { @@ -1587,6 +1741,28 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(4); expect(result.items.every((i) => i.number! >= 12)).toBe(true); }); + test('should return items where GE condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').ge('string-12')) + .exec(); + expect(result.count).toBe(8); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(8); + expect(result.items.every((i) => i.string!.localeCompare('string-12') >= 0)).toBe(true); + }); + test('should return items where GE condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').ge(12)) + .exec(); + expect(result.count).toBe(4); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(4); + expect(result.items.every((i) => i.number! >= 12)).toBe(true); + }); }); describe('GT', () => { test('should return items where GT condition is true [string]', async () => { @@ -1611,6 +1787,28 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(3); expect(result.items.every((i) => i.number! >= 12)).toBe(true); }); + test('should return items where GT condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').gt('string-12')) + .exec(); + expect(result.count).toBe(7); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(7); + expect(result.items.every((i) => (i.nested?.string as string).localeCompare('string-12') > 0)).toBe(true); + }); + test('should return items where GT condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').gt(12)) + .exec(); + expect(result.count).toBe(3); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(3); + expect(result.items.every((i) => i.nested?.number as number >= 12)).toBe(true); + }); }); describe('BETWEEN', () => { test('should return items where BETWEEN condition is true [string]', async () => { @@ -1643,6 +1841,36 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(6); expect(result.items.every((i) => i.number! >= 6 && i.number! <= 17)).toBe(true); }); + test('should return items where BETWEEN condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').between('string-3', 'string-8')) + .exec(); + + expect(result.count).toBe(3); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(3); + expect( + result.items.every( + (i) => + (i.nested?.string as string).localeCompare('string-3') >= 0 && (i.nested?.string as string).localeCompare('string-8') <= 0, + ), + ).toBe(true); + }); + test('should return items where BETWEEN condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').between(6, 17)) + + .exec(); + + expect(result.count).toBe(6); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(6); + expect(result.items.every((i) => i.nested?.number as number >= 6 && i.nested?.number as number <= 17)).toBe(true); + }); }); describe('NOT_EXISTS', () => { test('should return items where NOT_EXISTS condition is true [string]', async () => { @@ -1711,6 +1939,39 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(10); expect(result.items.every((i) => i.optionalStringmap == null)).toBe(true); }); + test('should return items where NOT_EXISTS condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.optionalString').notExists()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested?.optionalString == null)).toBe(true); + }); + test('should return items where NOT_EXISTS condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.optionalNumber').notExists()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested?.optionalNumber == null)).toBe(true); + }); + test('should return items where NOT_EXISTS condition is true [nestedAttr][boolean]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.optionalBool').notExists()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested?.optionalBool == null)).toBe(true); + }); }); describe('EXISTS', () => { test('should return items where EXISTS condition is true [string]', async () => { @@ -1779,6 +2040,39 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(10); expect(result.items.every((i) => i.optionalStringmap !== undefined)).toBe(true); }); + test('should return items where EXISTS condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.optionalString').exists()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested?.optionalString !== undefined)).toBe(true); + }); + test('should return items where EXISTS condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.optionalNumber').exists()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested?.optionalNumber !== undefined)).toBe(true); + }); + test('should return items where EXISTS condition is true [nestedAttr][boolean]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.optionalBool').exists()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested?.optionalBool !== undefined)).toBe(true); + }); }); describe('NOT_NULL', () => { test('should return items where NOT_NULL condition is true [string]', async () => { @@ -1847,6 +2141,39 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(10); expect(result.items.every((i) => i.stringmap != null)).toBe(true); }); + test('should return items where NOT_NULL condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').notNull()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested!.string! != null)).toBe(true); + }); + test('should return items where NOT_NULL condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').notNull()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested!.number! != null)).toBe(true); + }); + test('should return items where NOT_NULL condition is true [nestedAttr][boolean]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.bool').notNull()) + .exec(); + expect(result.count).toBe(14); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(14); + expect(result.items.every((i) => i.nested?.bool != null)).toBe(true); + }); }); describe('NULL', () => { test('should return items where NULL condition is true [string]', async () => { @@ -1915,6 +2242,39 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(10); expect(result.items.every((i) => i.stringmap == null)).toBe(true); }); + test('should return items where NULL condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').null()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested!.string! == null)).toBe(true); + }); + test('should return items where NULL condition is true [nestedAttr][number]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.number').null()) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect(result.items.every((i) => i.nested!.number! == null)).toBe(true); + }); + test('should return items where NULL condition is true [nestedAttr][boolean]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.bool').null()) + .exec(); + expect(result.count).toBe(6); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(6); + expect(result.items.every((i) => i.nested?.bool == null)).toBe(true); + }); }); describe('CONTAINS', () => { test('should return items where CONTAINS condition is true [string]', async () => { @@ -1928,6 +2288,17 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(5); expect(result.items.every((i) => i.string!.includes('ing-1'))).toBe(true); }); + test('should return items where CONTAINS condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').contains('ing-1')) + .exec(); + expect(result.count).toBe(5); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(5); + expect(result.items.every((i) => (i.nested?.string as string).includes('ing-1'))).toBe(true); + }); }); describe('NOT_CONTAINS', () => { test('should return items where NOT_CONTAINS condition is true [string]', async () => { @@ -1941,6 +2312,17 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(15); expect(result.items.every((i) => !(i.string! && i.string!.includes('ing-1')))).toBe(true); }); + test('should return items where NOT_CONTAINS condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').notContains('ing-1')) + .exec(); + expect(result.count).toBe(15); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(15); + expect(result.items.every((i) => !(i.nested!.string! && (i.nested!.string as string).includes('ing-1')))).toBe(true); + }); }); describe('BEGINS_WITH', () => { test('should return items where BEGINS_WITH condition is true [string]', async () => { @@ -1954,6 +2336,17 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(5); expect(result.items.every((i) => i.string!.match(/^string-1/))).toBe(true); }); + test('should return items where BEGINS_WITH condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(attr('nested.string').beginsWith('string-1')) + .exec(); + expect(result.count).toBe(5); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(5); + expect(result.items.every((i) => (i.nested?.string as string).match(/^string-1/))).toBe(true); + }); }); describe('OR', () => { test('should return items where condition is true [string]', async () => { @@ -1971,6 +2364,21 @@ describe('The query method [filtering / fluid syntax]', () => { result.items.every((i) => i.string!.match(/^string-1/) || i.string!.includes('ing-4')), ).toBe(true); }); + test('should return items where condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + // BW: 10, 12, 14, 16, 18 + // CN: 4 + .filter(attr('nested.string').beginsWith('string-1').or(attr('nested.string').contains('ing-4'))) + .exec(); + expect(result.count).toBe(6); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(6); + expect( + result.items.every((i) => (i.nested?.string as string).match(/^string-1/) || (i.nested?.string as string).includes('ing-4')), + ).toBe(true); + }); }); describe('AND', () => { test('should return items where condition is true [string]', async () => { @@ -1993,6 +2401,26 @@ describe('The query method [filtering / fluid syntax]', () => { ), ).toBe(true); }); + test('should return items where condition is true [nestedAttr]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter( + attr('nested.bool') + .eq(true) + .and(attr('nested.number').ge(12)) + .and(attr('nested.string').beginsWith('string-1')), + ) + .exec(); + expect(result.count).toBe(2); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(2); + expect( + result.items.every( + (i) => (i.nested?.string as string).match(/^string-1/) && i.nested?.bool === true && i.nested?.number as number >= 12, + ), + ).toBe(true); + }); }); describe('OR/AND no-parenthesis', () => { test('should return items where BEGINS_WITH condition is true [string]', async () => { @@ -2013,6 +2441,24 @@ describe('The query method [filtering / fluid syntax]', () => { ), ).toBe(true); }); + test('should return items where BEGINS_WITH condition is true [nestedAttr]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter( + // bool = true AND number < 8 OR begins_with(string, string-1) + attr('nested.bool').eq(true).and(attr('nested.number').lt(8)).or(attr('nested.string').beginsWith('string-1')), + ) + .exec(); + expect(result.count).toBe(7); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(7); + expect( + result.items.every( + (i) => (i.nested?.bool === true && i.nested?.number as number < 8) || (i.nested?.string as string).match(/^string-1/), + ), + ).toBe(true); + }); }); describe('OR/AND parenthesis', () => { test('should return items where BEGINS_WITH condition is true [string]', async () => { @@ -2037,6 +2483,28 @@ describe('The query method [filtering / fluid syntax]', () => { ), ).toBe(true); }); + test('should return items where BEGINS_WITH condition is true [nestedAttr]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter( + // COND1 : 0, 3, 6, 9, 12, 15, 18 + // COND2: 0, 2, 6, 10, 12, 14, 16, 18 + // bool = true AND (number < 8 OR begins_with(string, string-1)) + attr('nested.bool') + .eq(true) + .and(attr('nested.number').lt(8).or(attr('nested.string').beginsWith('string-1'))), + ) + .exec(); + expect(result.count).toBe(4); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(4); + expect( + result.items.every( + (i) => i.nested?.bool === true && (i.nested?.number as number < 8 || (i.nested?.string as string).match(/^string-1/)), + ), + ).toBe(true); + }); }); describe('NOT', () => { test('should return items where BEGINS_WITH condition is true [string]', async () => { @@ -2050,6 +2518,17 @@ describe('The query method [filtering / fluid syntax]', () => { expect(result.items.length).toBe(15); expect(result.items.every((i) => !i.string! || !i.string!.match(/^string-1/))).toBe(true); }); + test('should return items where BEGINS_WITH condition is true [nestedAttr][string]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + .filter(not(attr('nested.string').beginsWith('string-1'))) + .exec(); + expect(result.count).toBe(15); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(15); + expect(result.items.every((i) => !i.nested!.string! || !(i.nested!.string as string).match(/^string-1/))).toBe(true); + }); }); describe('NOT/OR', () => { test('should return items where BEGINS_WITH condition is true [string]', async () => { @@ -2071,6 +2550,25 @@ describe('The query method [filtering / fluid syntax]', () => { ), ).toBe(true); }); + test('should return items where BEGINS_WITH condition is true [nestedAttr]', async () => { + const result = await model + .query() + .keys(key('hashkey').eq('hashkey-1')) + // COND1: 10, 12, 14, 16, 18 + // COND2: 1, 4, 7, 10, 13, 16, 19 + // OR : 1, 4, 7, 10, 12, 13, 16, 18, 19 + // NOT: 0, 2, 3, 5, 6, 8, 9, 11, 14, 15, 17 + .filter(not(attr('nested.string').beginsWith('string-1').or(attr('nested.bool').eq(false)))) + .exec(); + expect(result.count).toBe(10); + expect(result.nextPage.lastEvaluatedKey).toBeFalsy(); + expect(result.items.length).toBe(10); + expect( + result.items.every( + (i) => !(i.nested?.bool === false || (i.nested!.string! && (i.nested!.string as string).match(/^string-1/))), + ), + ).toBe(true); + }); }); });