Skip to content

Commit

Permalink
fix: filter expression using nested attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
SajidHamza9 authored and MarioArnt committed Jan 30, 2024
1 parent 0225c7e commit 83fa2c4
Show file tree
Hide file tree
Showing 5 changed files with 541 additions and 40 deletions.
41 changes: 23 additions & 18 deletions src/condition-attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@ export default abstract class ConditionAttribute<T> {
}

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<string, string> = new Map();
protected fillMaps(value: T) {
const { id, attr } = this.prepareAttributes();
const values: Map<string, T> = 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<string, string> } {
const keys = this.field.split('.');
const id = this.getAttributeUniqueIdentifier();
const attr: Map<string, string> = new Map();
id.split('.').forEach((id, index) => {
attr.set(`#${id}`, keys[index]);
})
return { id, attr };
}

public abstract eq(value: T): Condition<T>;
Expand All @@ -40,14 +49,13 @@ export default abstract class ConditionAttribute<T> {
public abstract beginsWith(value: T): Condition<T>;

protected prepareOp(value: T): { id: string; attr: Map<string, string>; values: Map<string, T> } {
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<T> {
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<T> {
Expand All @@ -71,18 +79,15 @@ export default abstract class ConditionAttribute<T> {
}

protected prepareBetween(lower: KeyValue, upper: KeyValue): IArgs<T> {
const id = this.getAttributeUniqueIdentifier();
const attr: Map<string, string> = new Map();
const { id, attr } = this.prepareAttributes();
const values: Map<string, T> = 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<T> {
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];
}
}
28 changes: 9 additions & 19 deletions src/filter-attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,17 @@ export default class FilterAttribute extends ConditionAttribute<FilterValue> {
}

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<string, string> = new Map();
const { id, attr } = this.prepareAttributes();
const val: Map<string, FilterValue> = 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<string, string> } {
const id = this.getAttributeUniqueIdentifier();
const attr: Map<string, string> = 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 {
Expand All @@ -70,19 +60,19 @@ export default class FilterAttribute extends ConditionAttribute<FilterValue> {

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 {
Expand All @@ -108,4 +98,4 @@ export default class FilterAttribute extends ConditionAttribute<FilterValue> {
public notContains(value: string): FilterCondition {
return this.containsOperation(value, true);
}
}
}
8 changes: 8 additions & 0 deletions test/factories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
6 changes: 3 additions & 3 deletions test/models/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
);
Expand All @@ -24,4 +23,5 @@ export type CommonFields = {
optionalStringset?: string[] | undefined;
optionalList?: Array<number | string> | undefined;
optionalStringmap?: { [key: string]: string } | undefined;
nested?: Record<string, string | number | boolean | null | undefined>;
};
Loading

0 comments on commit 83fa2c4

Please sign in to comment.