Skip to content

Commit

Permalink
feat: add support for escaped composite keys on expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
ruicsh committed Mar 29, 2023
1 parent 5871688 commit 82224f0
Show file tree
Hide file tree
Showing 12 changed files with 589 additions and 316 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x, 16.x, 18.x, 19.x]
node-version: [14.x, 16.x, 18.x]
steps:
- uses: actions/checkout@main
with:
fetch-depth: "0"
- uses: actions/setup-node@main
with:
node-version: 19
node-version: 18
- uses: actions/cache@main
with:
path: node_modules
Expand All @@ -37,7 +37,7 @@ jobs:
fetch-depth: "0"
- uses: actions/setup-node@main
with:
node-version: 19
node-version: 18
- uses: actions/cache@main
with:
path: node_modules
Expand All @@ -59,7 +59,7 @@ jobs:
fetch-depth: "0"
- uses: actions/setup-node@main
with:
node-version: 19
node-version: 18
registry-url: https://registry.npmjs.org/
- run: yarn install --frozen-lockfile
- run: yarn build
Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.14.2
18.15.0
18 changes: 9 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,22 @@
"test": "vitest --watch"
},
"devDependencies": {
"@types/node": "18.15.5",
"@typescript-eslint/eslint-plugin": "5.56.0",
"@typescript-eslint/parser": "5.56.0",
"@vitest/coverage-c8": "0.29.7",
"aws-sdk": "^2.1340.0",
"esbuild": "0.17.12",
"eslint": "8.36.0",
"@types/node": "18.15.11",
"@typescript-eslint/eslint-plugin": "5.57.0",
"@typescript-eslint/parser": "5.57.0",
"@vitest/coverage-c8": "0.29.8",
"aws-sdk": "^2.1346.0",
"esbuild": "0.17.14",
"eslint": "8.37.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-airbnb-typescript": "17.0.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-import": "2.27.5",
"nyc": "15.1.0",
"prettier": "2.8.6",
"prettier": "2.8.7",
"tsx": "3.12.6",
"typescript": "5.0.2",
"vitest": "0.29.7",
"vitest": "0.29.8",
"zx": "7.2.1"
}
}
39 changes: 39 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,45 @@ const params = dynoexpr({
*/
```

### Using object paths on expressions

You can provide a path to an attribute on a deep object, each node will be escaped.

```typescript
const params = dynoexpr({
Update: {
'foo.bar.baz': 'foo.bar.baz + 1'
}
});

/*
{
ExpressionAttributeNames: {
"#n22f4f0ae": "bar",
"#n5f0025bb": "foo",
"#n82504b33": "baz",
},
ExpressionAttributeValues: {
":vc823bd86": 1,
},
UpdateExpression:
"SET #n5f0025bb.#n22f4f0ae.#n82504b33 = #n5f0025bb.#n22f4f0ae.#n82504b33 + :vc823bd86",
}
*/

```

If one of the nodes needs to escape some of its characters, use double quotes around it, like this:

```typescript
const params = dynoexpr({
Update: {
'foo."bar-cuz".baz': 'foo."bar-cuz".baz + 1'
}
});
```


### Parsing atomic requests, only expressions will be replaced

You can pass the whole request parameters to `dynoexpr` - only the expression builders will be replaced.
Expand Down
50 changes: 50 additions & 0 deletions src/bug-reports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,54 @@ describe("bug reports", () => {
};
expect(actual).toStrictEqual(expected);
});

it("handles composite keys on updates with math operations", () => {
const args = {
Update: {
"foo.bar.baz": "foo.bar.baz + 1",
},
};
const actual = dynoexpr(args);

const expected = {
ExpressionAttributeNames: {
"#n22f4f0ae": "bar",
"#n5f0025bb": "foo",
"#n82504b33": "baz",
},
ExpressionAttributeValues: {
":vc823bd86": 1,
},
UpdateExpression:
"SET #n5f0025bb.#n22f4f0ae.#n82504b33 = #n5f0025bb.#n22f4f0ae.#n82504b33 + :vc823bd86",
};
expect(actual).toStrictEqual(expected);
});

it("escape dynamic keys in objects", () => {
const dynamicKey = "key.with-chars";
const args = {
Update: {
[`object."${dynamicKey}".value`]: `object."${dynamicKey}".value + 1`,
},
Condition: { [`object."${dynamicKey}".value`]: "> 2" },
};
const actual = dynoexpr(args);

const expected = {
ConditionExpression: "(#nbb017076.#n0327a04a.#n10d6f4c5 > :vaeeabc63)",
ExpressionAttributeNames: {
"#nbb017076": "object",
"#n0327a04a": "key.with-chars",
"#n10d6f4c5": "value",
},
ExpressionAttributeValues: {
":vaeeabc63": 2,
":vc823bd86": 1,
},
UpdateExpression:
"SET #nbb017076.#n0327a04a.#n10d6f4c5 = #nbb017076.#n0327a04a.#n10d6f4c5 + :vc823bd86",
};
expect(actual).toStrictEqual(expected);
});
});
12 changes: 12 additions & 0 deletions src/expressions/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,18 @@ describe("helpers for condition helpers", () => {
expect(actual).toStrictEqual(expected);
});

it("builds the attribute names map with composite keys", () => {
const Condition2 = { 'object."key.with-chars".value': "> 2" };
const actual = buildConditionAttributeNames(Condition2);

const expected = {
"#n10d6f4c5": "value",
"#nbb017076": "object",
"#n0327a04a": "key.with-chars",
};
expect(actual).toStrictEqual(expected);
});

it("builds the ExpressionAttributesValueMap with an existing map", () => {
const Condition2 = { b: "foo" };
const args: IConditionAttributeValuesParams = {
Expand Down
11 changes: 8 additions & 3 deletions src/expressions/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import type {
ILogicalOperatorType,
} from "src/dynoexpr.d";

import { getAttrName, getAttrValue } from "../utils";
import {
getAttrName,
getAttrValue,
getSingleAttrName,
splitByDot,
} from "../utils";

type IValue = string | boolean | number | null;
export function convertValue(value: string): IValue {
Expand Down Expand Up @@ -152,8 +157,8 @@ export function buildConditionAttributeNames(
params: IConditionAttributeNamesParams = {}
) {
return Object.keys(condition).reduce((acc, key) => {
key.split(".").forEach((k) => {
acc[getAttrName(k)] = k;
splitByDot(key).forEach((k) => {
acc[getSingleAttrName(k)] = k;
});
return acc;
}, params.ExpressionAttributeNames || ({} as { [key: string]: string }));
Expand Down
76 changes: 76 additions & 0 deletions src/expressions/update.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,40 @@ describe("update expression", () => {
expect(actual).toStrictEqual(expected);
});

it("builds ExpressionAttributesMap with composite keys", () => {
const args = {
Update: {
'object."key.with-chars".value': 'object."key.with-chars".value + 1',
},
ConditionExpression: "(#nbb017076.#n0327a04a.#n10d6f4c5 > :vaeeabc63)",
ExpressionAttributeNames: {
"#nbb017076": "object",
"#n0327a04a": "key.with-chars",
"#n10d6f4c5": "value",
},
ExpressionAttributeValues: { ":vaeeabc63": 2 },
};

const actual = getExpressionAttributes(args);

const expected = {
ConditionExpression: "(#nbb017076.#n0327a04a.#n10d6f4c5 > :vaeeabc63)",
ExpressionAttributeNames: {
"#n0327a04a": "key.with-chars",
"#n10d6f4c5": "value",
"#nbb017076": "object",
},
ExpressionAttributeValues: {
":vaeeabc63": 2,
":vc823bd86": 1,
},
Update: {
'object."key.with-chars".value': 'object."key.with-chars".value + 1',
},
};
expect(actual).toStrictEqual(expected);
});

it("updates attributes - SET", () => {
const args = {
Update: {
Expand Down Expand Up @@ -332,4 +366,46 @@ describe("update expression", () => {
};
expect(actual).toStrictEqual(expected);
});

it("gets update expression with composite keys and math", () => {
const args = { Update: { "foo.bar.baz": "foo.bar.baz + 1" } };
const actual = getUpdateExpression(args);

const expected = {
ExpressionAttributeNames: {
"#n22f4f0ae": "bar",
"#n5f0025bb": "foo",
"#n82504b33": "baz",
},
ExpressionAttributeValues: {
":vc823bd86": 1,
},
UpdateExpression:
"SET #n5f0025bb.#n22f4f0ae.#n82504b33 = #n5f0025bb.#n22f4f0ae.#n82504b33 + :vc823bd86",
};
expect(actual).toStrictEqual(expected);
});

it("gets update expression with composite keys (escaped)", () => {
const args = {
Update: {
'object."key.with-chars".value': 'object."key.with-chars".value + 1',
},
};
const actual = getUpdateExpression(args);

const expected = {
ExpressionAttributeNames: {
"#n0327a04a": "key.with-chars",
"#n10d6f4c5": "value",
"#nbb017076": "object",
},
ExpressionAttributeValues: {
":vc823bd86": 1,
},
UpdateExpression:
"SET #nbb017076.#n0327a04a.#n10d6f4c5 = #nbb017076.#n0327a04a.#n10d6f4c5 + :vc823bd86",
};
expect(actual).toStrictEqual(expected);
});
});
28 changes: 21 additions & 7 deletions src/expressions/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import type {
IUpdateOutput,
} from "src/dynoexpr.d";

import { getAttrName, getAttrValue } from "../utils";
import {
getAttrName,
getAttrValue,
getSingleAttrName,
splitByDot,
splitByOperator,
} from "../utils";

export function parseOperationValue(expr: string, key: string) {
const v = expr.replace(key, "").replace(/[+-]/, "");
Expand Down Expand Up @@ -78,8 +84,8 @@ export function getExpressionAttributes(params: IUpdateInput) {
if (!acc.ExpressionAttributeNames) acc.ExpressionAttributeNames = {};
if (!acc.ExpressionAttributeValues) acc.ExpressionAttributeValues = {};

key.split(".").forEach((k) => {
acc.ExpressionAttributeNames[getAttrName(k)] = k;
splitByDot(key).forEach((k) => {
acc.ExpressionAttributeNames[getSingleAttrName(k)] = k;
});

if (UpdateAction !== "REMOVE") {
Expand Down Expand Up @@ -134,16 +140,24 @@ export function getUpdateExpression(params: IUpdateInput = {}) {
}

if (isMathExpression(name, value)) {
const [, operator] = /([+-])/.exec(value as string) || [];
const expr = (value as string)
.split(/[+-]/)
const [, operator] = /(\s-|-\s|[+])/.exec(value as string) || [];
const val = value?.toString() || "unknown";
const operands = [];
if (/\+/.test(val)) {
operands.push(...splitByOperator("+", val));
} else if (/-/.test(val)) {
operands.push(...splitByOperator("-", val));
}

const expr = operands
.map((operand: string) => operand.trim())
.map((operand: string) => {
if (operand === name) return getAttrName(name);
const v = parseOperationValue(operand, name);

return getAttrValue(v);
})
.join(` ${operator} `);
.join(` ${operator?.trim()} `);

return `${getAttrName(name)} = ${expr}`;
}
Expand Down

0 comments on commit 82224f0

Please sign in to comment.