Skip to content

fix(util-dynamodb): allow marshall function to handle more input types #3539

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 22, 2022
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jspm_packages
.DS_Store
.vscode/launch.json

Makefile
lerna-debug.log
package-lock.json

Expand Down
92 changes: 92 additions & 0 deletions lib/lib-dynamodb/src/commands/marshallInput.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { marshallInput } from "./utils";

describe("marshallInput and processObj", () => {
it("marshallInput should not ignore falsy values", () => {
expect(marshallInput({ Items: [0, false, null, ""] }, [{ key: "Items" }])).toEqual({
Items: [{ N: "0" }, { BOOL: false }, { NULL: true }, { S: "" }],
});
});
});

describe("marshallInput for commands", () => {
it("marshals QueryCommand input", () => {
const input = {
TableName: "TestTable",
KeyConditions: {
id: {
AttributeValueList: ["test"],
ComparisonOperator: "EQ",
},
},
};
const inputKeyNodes = [
{
key: "KeyConditions",
children: {
children: [{ key: "AttributeValueList" }],
},
},
{
key: "QueryFilter",
children: {
children: [{ key: "AttributeValueList" }],
},
},
{ key: "ExclusiveStartKey" },
{ key: "ExpressionAttributeValues" },
];
const output = {
TableName: "TestTable",
KeyConditions: { id: { AttributeValueList: [{ S: "test" }], ComparisonOperator: "EQ" } },
QueryFilter: undefined,
ExclusiveStartKey: undefined,
ExpressionAttributeValues: undefined,
};
expect(marshallInput(input, inputKeyNodes)).toEqual(output);
});
it("marshals ExecuteStatementCommand input", () => {
const input = {
Statement: `SELECT col_1
FROM some_table
WHERE contains("col_1", ?)`,
Parameters: ["some_param"],
};
const inputKeyNodes = [{ key: "Parameters" }];
const output = {
Statement: input.Statement,
Parameters: [{ S: "some_param" }],
};
expect(marshallInput(input, inputKeyNodes)).toEqual(output);
});
it("marshals BatchExecuteStatementCommand input", () => {
const input = {
Statements: [
{
Statement: `
UPDATE "table"
SET field1=?
WHERE field2 = ?
AND field3 = ?
`,
Parameters: [false, "field 2 value", 1234],
},
],
};
const inputKeyNodes = [{ key: "Statements", children: [{ key: "Parameters" }] }];
const output = {
Statements: [
{
Statement: input.Statements[0].Statement,
Parameters: [
{
BOOL: false,
},
{ S: "field 2 value" },
{ N: "1234" },
],
},
],
};
expect(marshallInput(input, inputKeyNodes)).toEqual(output);
});
});
2 changes: 1 addition & 1 deletion lib/lib-dynamodb/src/commands/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export type AllNodes = {
};

const processObj = (obj: any, processFunc: Function, children?: KeyNode[] | AllNodes): any => {
if (obj) {
if (obj !== undefined) {
if (!children || (Array.isArray(children) && children.length === 0)) {
// Leaf of KeyNode, process the object.
return processFunc(obj);
Expand Down
39 changes: 35 additions & 4 deletions packages/util-dynamodb/src/marshall.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AttributeValue } from "@aws-sdk/client-dynamodb";

import { convertToAttr } from "./convertToAttr";
import { NativeAttributeValue } from "./models";
import { NativeAttributeBinary, NativeAttributeValue } from "./models";

/**
* An optional configuration object for `marshall`
Expand All @@ -26,8 +26,39 @@ export interface marshallOptions {
*
* @param {any} data - The data to convert to a DynamoDB record
* @param {marshallOptions} options - An optional configuration object for `marshall`
*
*/
export const marshall = <T extends { [K in keyof T]: NativeAttributeValue }>(
data: T,
export function marshall(data: Set<string>, options?: marshallOptions): AttributeValue.SSMember;
export function marshall(data: Set<number>, options?: marshallOptions): AttributeValue.NSMember;
export function marshall(data: Set<NativeAttributeBinary>, options?: marshallOptions): AttributeValue.BSMember;
export function marshall<M extends { [K in keyof M]: NativeAttributeValue }>(
data: M,
options?: marshallOptions
): { [key: string]: AttributeValue } => convertToAttr(data, options).M as { [key: string]: AttributeValue };
): Record<string, AttributeValue>;
export function marshall<L extends NativeAttributeValue[]>(data: L, options?: marshallOptions): AttributeValue[];
export function marshall(data: string, options?: marshallOptions): AttributeValue.SMember;
export function marshall(data: number, options?: marshallOptions): AttributeValue.NMember;
export function marshall(data: NativeAttributeBinary, options?: marshallOptions): AttributeValue.BMember;
export function marshall(data: null, options?: marshallOptions): AttributeValue.NULLMember;
export function marshall(data: boolean, options?: marshallOptions): AttributeValue.BOOLMember;
export function marshall(data: unknown, options?: marshallOptions): AttributeValue.$UnknownMember;
export function marshall(data: unknown, options?: marshallOptions) {
const attributeValue: AttributeValue = convertToAttr(data, options);
const [key, value] = Object.entries(attributeValue)[0];
switch (key) {
case "M":
case "L":
return value;
case "SS":
case "NS":
case "BS":
case "S":
case "N":
case "B":
case "NULL":
case "BOOL":
case "$unknown":
default:
return attributeValue;
}
}
67 changes: 67 additions & 0 deletions packages/util-dynamodb/src/marshallTypes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { convertToAttr } from "./convertToAttr";
import { marshall } from "./marshall";

describe("marshall type discernment", () => {
describe("behaves as convertToAttr for non-collection values or Sets", () => {
it("marshals string", () => {
const value = "hello";
expect(marshall(value)).toEqual(convertToAttr(value));
});

it("marshals number", () => {
const value = 1578;
expect(marshall(value)).toEqual(convertToAttr(value));
});

it("marshals binary", () => {
const value = new Uint8Array([0, 1, 0, 1]);
expect(marshall(value)).toEqual(convertToAttr(value));
});

it("marshals boolean", () => {
let value = false;
expect(marshall(value)).toEqual(convertToAttr(value));
value = true;
expect(marshall(value)).toEqual(convertToAttr(value));
});

it("marshals null", () => {
const value = null;
expect(marshall(value)).toEqual(convertToAttr(value));
});
it("marshals string set", () => {
const value = new Set(["a", "b"]);
expect(marshall(value)).toEqual(convertToAttr(value));
});

it("marshals number set", () => {
const value = new Set([1, 2]);
expect(marshall(value)).toEqual(convertToAttr(value));
});

it("marshals binary set", () => {
const value = new Set([new Uint8Array([1, 0]), new Uint8Array([0, 1])]);
expect(marshall(value)).toEqual(convertToAttr(value));
});
});

describe("unwraps one level for input data which are lists or maps", () => {
it("marshals and unwraps map", () => {
expect(marshall({ a: 1, b: { a: 2, b: [1, 2, 3] } })).toEqual({
a: { N: "1" },
b: {
M: {
a: { N: "2" },
b: {
L: [{ N: "1" }, { N: "2" }, { N: "3" }],
},
},
},
});
});

it("marshals and unwraps list", () => {
expect(marshall(["test", 2, null])).toEqual([{ S: "test" }, { N: "2" }, { NULL: true }]);
});
});
});