Skip to content

Commit

Permalink
Merge pull request #3 from localstack/dynamodb-condition-expression
Browse files Browse the repository at this point in the history
Implement dynamodb condition expression
  • Loading branch information
simonrw authored Oct 19, 2023
2 parents b22a67c + 46b82ac commit 8169c36
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 112 deletions.
2 changes: 2 additions & 0 deletions __tests__/__snapshots__/index.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,8 @@ exports[`DynamoDB operations replace 1`] = `

exports[`DynamoDB operations updateListItem 1`] = `undefined`;

exports[`Transformations toDynamoDBConditionExpression 1`] = `"{"expression":"(attribute_exists(#id)) AND (#version = :version_eq)","expressionNames":{"#id":"id","#version":"version"},"expressionValues":{":version_eq":{"N":10}}}"`;

exports[`Transformations toDynamoDBFilterMap 1`] = `"{"expression":"(contains(#title,:title_contains))","expressionNames":{"#title":"title"},"expressionValues":{":title_contains":{"S":"Hello World"}}}"`;

exports[`dynamodb helpers s3 objects four parameter function 1`] = `
Expand Down
5 changes: 4 additions & 1 deletion __tests__/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ const runOnAWS = async (s, context) => {
}

// If TEST_TARGET is AWS_CLOUD then run the check against AWS. Otherwise, run locally.
export const checkValid = async (s, context) => {
export const checkValid = async (s, context, postProcess) => {
let result;
if (process.env.TEST_TARGET === "AWS_CLOUD") {
result = await runOnAWS(s, context);
} else {
result = eval(s);
}
if (postProcess) {
result = postProcess(result);
}
expect(result).toMatchSnapshot();
}
28 changes: 28 additions & 0 deletions __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,34 @@ describe("Transformations", () => {
test("toDynamoDBFilterMap", async () => {
await checkValid(`util.transform.toDynamoDBFilterExpression({ "title":{ "contains":"Hello World" } })`);
});

test("toDynamoDBConditionExpression", async () => {
// attribute keys are not guaranteed to be ordered
const postProcess = (result) => {

const sortObjectByKeys = (obj) => {
return Object.keys(obj).sort().reduce(
(res, key) => {
res[key] = obj[key];
return res;
},
{}
);
};

const { expression, expressionNames, expressionValues } = JSON.parse(result);
const transformed = {
expression,
expressionNames: sortObjectByKeys(expressionNames),
expressionValues: sortObjectByKeys(expressionValues),
};
return JSON.stringify(transformed);
};
await checkValid(`util.transform.toDynamoDBConditionExpression({
id: { attributeExists: true },
version: { eq: 10 },
})`, {}, postProcess);
});
});

describe("DynamoDB operations", () => {
Expand Down
112 changes: 112 additions & 0 deletions dynamodb-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
export const dynamodbUtils = {
toDynamoDB: function(value) {
if (typeof (value) === "number") {
return this.toNumber(value);
} else if (typeof (value) === "string") {
return this.toString(value);
} else if (typeof (value) === "boolean") {
return this.toBoolean(value);
} else if (typeof (value) === "object") {
if (value.length !== undefined) {
return this.toList(value);
} else {
return this.toMap(value);
}
} else {
throw new Error(`Not implemented for ${value}`);
}
},

toString: function(value) {
if (value === null) { return null; };

return { S: value };
},

toStringSet: function(value) {
if (value === null) { return null; };

return { SS: value };
},

toNumber: function(value) {
if (value === null) { return null; };

return { N: value };
},

toNumberSet: function(value) {
if (value === null) { return null; };

return { NS: value };
},

toBinary: function(value) {
if (value === null) { return null; };

return { B: value };
},

toBinarySet: function(value) {
if (value === null) { return null; };

return { BS: value };
},

toBoolean: function(value) {
if (value === null) { return null; };

return { BOOL: value };
},

toNull: function() {
return { NULL: null };
},

toList: function(values) {
let out = [];
for (const value of values) {
out.push(this.toDynamoDB(value));
}
return { L: out }
},

toMap: function(mapping) {
return { M: this.toMapValues(mapping) };
},

toMapValues: function(mapping) {
let out = {};
for (const [k, v] of Object.entries(mapping)) {
out[k] = this.toDynamoDB(v);
}
return out;
},

toS3Object: function(key, bucket, region, version) {
let payload;
if (version === undefined) {
payload = {
s3: {
key,
bucket,
region,
}
};
} else {
payload = {
s3: {
key,
bucket,
region,
version,
}
};
};
return this.toString(JSON.stringify(payload));
},

fromS3ObjectJson: function(value) {
throw new Error("not implemented");
},
}
123 changes: 12 additions & 111 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { v4 as uuidv4 } from 'uuid';

import { generateFilterExpression } from "./transform/dynamodb-filter";
import { dynamodbUtils } from './dynamodb-utils';

const FILTER_CONTAINS = "contains";

export const util = {
Expand Down Expand Up @@ -42,117 +45,15 @@ export const util = {
return JSON.stringify({ expression, expressionNames, expressionValues });

},
},
dynamodb: {
toDynamoDB: function(value) {
if (typeof (value) === "number") {
return this.toNumber(value);
} else if (typeof (value) === "string") {
return this.toString(value);
} else if (typeof (value) === "boolean") {
return this.toBoolean(value);
} else if (typeof (value) === "object") {
if (value.length !== undefined) {
return this.toList(value);
} else {
return this.toMap(value);
}
} else {
throw new Error(`Not implemented for ${value}`);
}
},

toString: function(value) {
if (value === null) { return null; };

return { S: value };
},

toStringSet: function(value) {
if (value === null) { return null; };

return { SS: value };
},

toNumber: function(value) {
if (value === null) { return null; };

return { N: value };
},

toNumberSet: function(value) {
if (value === null) { return null; };

return { NS: value };
},

toBinary: function(value) {
if (value === null) { return null; };

return { B: value };
},

toBinarySet: function(value) {
if (value === null) { return null; };

return { BS: value };
},

toBoolean: function(value) {
if (value === null) { return null; };

return { BOOL: value };
},

toNull: function() {
return { NULL: null };
},

toList: function(values) {
let out = [];
for (const value of values) {
out.push(this.toDynamoDB(value));
}
return { L: out }
},

toMap: function(mapping) {
return { M: this.toMapValues(mapping) };
},

toMapValues: function(mapping) {
let out = {};
for (const [k, v] of Object.entries(mapping)) {
out[k] = this.toDynamoDB(v);
}
return out;
},

toS3Object: function(key, bucket, region, version) {
let payload;
if (version === undefined) {
payload = {
s3: {
key,
bucket,
region,
}
};
} else {
payload = {
s3: {
key,
bucket,
region,
version,
}
};
};
return this.toString(JSON.stringify(payload));
},

fromS3ObjectJson: function(value) {
throw new Error("not implemented");
toDynamoDBConditionExpression(condition) {
const result = generateFilterExpression(condition);
return JSON.stringify({
expression: result.expressions.join(' ').trim(),
expressionNames: result.expressionNames,
// upstream is missing this value: https://github.com/aws-amplify/amplify-cli/blob/5cc1b556d8081421dc68ee264dac02d5660ffee7/packages/amplify-appsync-simulator/src/velocity/util/transform/index.ts#L11
expressionValues: result.expressionValues,
});
},
},
dynamodb: dynamodbUtils,
};
Loading

0 comments on commit 8169c36

Please sign in to comment.