Skip to content

Commit

Permalink
Merge 54711ce into 5e98a99
Browse files Browse the repository at this point in the history
  • Loading branch information
fishcharlie committed Apr 16, 2020
2 parents 5e98a99 + 54711ce commit 5a056a1
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 37 deletions.
27 changes: 25 additions & 2 deletions docs/api/Condition.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,38 @@ The Condition object represents a conditional that you can attach to various set

## new dynamoose.Condition([filter])

This is the basic entry point to construct a conditional. The filter property is optional and can either be an object or a string representing the key you which to first filter on.
This is the basic entry point to construct a conditional. The filter property is optional and can either be an object, existing conditional instance or a string representing the key you which to first filter on.

```js
new dynamoose.Condition("breed").contains("Terrier") // will condition for where the key `breed` contains `Terrier`
new dynamoose.Condition({"breed": {"contains": "Terrier"}}) // will condition for where the key `breed` contains `Terrier`
new dynamoose.Condition(new dynamoose.Condition({"breed": {"contains": "Terrier"}})) // will condition for where the key `breed` contains `Terrier`
```

If you pass an object into `new dynamoose.Condition()` the object for each key should contain the comparison type. For example, in the last example above, `contains` was our comparison type. This comparison type must match one of the comparison type functions listed on this page.
For a more advanced use case you pass an object into `new dynamoose.Condition()` the object for each key should contain the comparison type. For example, in the second to last example above, `contains` was our comparison type. This comparison type must match one of the comparison type functions listed on this page.

You can also pass in a raw DynamoDB condition object. Which has properties for `ExpressionAttributeNames`, `ExpressionAttributeValues` & either `FilterExpression` or `ComparisonExpression`. In the event you do this, all future condition methods called on this condition instance will be ignored. In the event you don't pass in DynamoDB objects for the `ExpressionAttributeValues` values, Dynamoose will automatically convert them to DynamoDB compatible objects to make the request.

```js
new dynamoose.Condition({
"FilterExpression": "#id = :id",
"ExpressionAttributeValues": {
":id": 100
},
"ExpressionAttributeNames": {
"#id": "id"
}
})
new dynamoose.Condition({
"FilterExpression": "#id = :id",
"ExpressionAttributeValues": {
":id": {"N": 100}
},
"ExpressionAttributeNames": {
"#id": "id"
}
})
```

## condition.and()

Expand Down
35 changes: 25 additions & 10 deletions lib/Condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ const Error = require("./Error");
const utils = require("./utils");
const OR = Symbol("OR");

const isRawConditionObject = (object) => Object.keys(object).length === 3 && ["ExpressionAttributeValues", "ExpressionAttributeNames"].every((item) => Boolean(object[item]) && typeof object[item] === "object");

class Condition {
constructor(object) {
if (object instanceof Condition) {
Expand All @@ -16,21 +18,24 @@ class Condition {
this.settings.pending = {}; // represents the pending chain of filter data waiting to be attached to the `conditions` parameter. For example, storing the key before we know what the comparison operator is.

if (typeof object === "object") {
Object.keys(object).forEach((key) => {
const value = object[key];
const valueType = typeof value === "object" && Object.keys(value).length > 0 ? Object.keys(value)[0] : "eq";
const comparisonType = types.find((item) => item.name === valueType);
if (!isRawConditionObject(object)) {
Object.keys(object).forEach((key) => {
const value = object[key];
const valueType = typeof value === "object" && Object.keys(value).length > 0 ? Object.keys(value)[0] : "eq";
const comparisonType = types.find((item) => item.name === valueType);

if (!comparisonType) {
throw new Error.InvalidFilterComparison(`The type: ${valueType} is invalid.`);
}
if (!comparisonType) {
throw new Error.InvalidFilterComparison(`The type: ${valueType} is invalid.`);
}

this.settings.conditions.push([key, {"type": comparisonType.typeName, "value": typeof value[valueType] !== "undefined" && value[valueType] !== null ? value[valueType] : value}]);
});
this.settings.conditions.push([key, {"type": comparisonType.typeName, "value": typeof value[valueType] !== "undefined" && value[valueType] !== null ? value[valueType] : value}]);
});
}
} else if (object) {
this.settings.pending.key = object;
}
}
this.settings.raw = object;

return this;
}
Expand Down Expand Up @@ -97,7 +102,17 @@ types.forEach((type) => {
});

Condition.prototype.requestObject = function(settings = {"conditionString": "ConditionExpression"}) {
if (this.settings.conditions.length === 0) {
if (this.settings.raw && utils.object.equals(Object.keys(this.settings.raw).sort(), [settings.conditionString, "ExpressionAttributeValues", "ExpressionAttributeNames"].sort())) {
return Object.entries(this.settings.raw.ExpressionAttributeValues).reduce((obj, entry) => {
const [key, value] = entry;
// TODO: we should fix this so that we can do `isDynamoItem(value)`
if (!Document.isDynamoObject({"key": value})) {
console.log(value, Object.keys(value));
obj.ExpressionAttributeValues[key] = Document.toDynamo(value, {"type": "value"});
}
return obj;
}, this.settings.raw);
} else if (this.settings.conditions.length === 0) {
return {};
}

Expand Down
42 changes: 21 additions & 21 deletions lib/Document.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,27 @@ const {internalProperties} = require("./Internal").General;

const staticMethods = {
"toDynamo": (object, settings = {"type": "object"}) => (settings.type === "value" ? aws.converter().input : aws.converter().marshall)(object),
"fromDynamo": (object) => aws.converter().unmarshall(object)
"fromDynamo": (object) => aws.converter().unmarshall(object),
// This function will return null if it's unknown if it is a Dynamo object (ex. empty object). It will return true if it is a Dynamo object and false if it's not.
"isDynamoObject": function (object, recurrsive = false) {
// This function will check to see if a nested object is valid by calling Document.isDynamoObject recursively
const isValid = (value) => {
const keys = Object.keys(value);
const key = keys[0];
const nestedResult = (typeof value[key] === "object" && !(value[key] instanceof Buffer) ? (Array.isArray(value[key]) ? value[key].every((value) => this.isDynamoObject(value, true)) : this.isDynamoObject(value[key])) : true);
const Schema = require("./Schema");
const attributeType = Schema.attributeTypes.findDynamoDBType(key);
return typeof value === "object" && keys.length === 1 && attributeType && (nestedResult || Object.keys(value[key]).length === 0 || attributeType.isSet);
};

const keys = Object.keys(object);
const values = Object.values(object);
if (keys.length === 0) {
return null;
} else {
return recurrsive ? isValid(object) : values.every((value) => isValid(value));
}
}
};
function applyStaticMethods(item) {
Object.entries(staticMethods).forEach((entry) => {
Expand Down Expand Up @@ -37,26 +57,6 @@ function DocumentCarrier(model) {
}

applyStaticMethods(Document);
// This function will return null if it's unknown if it is a Dynamo object (ex. empty object). It will return true if it is a Dynamo object and false if it's not.
Document.isDynamoObject = (object, recurrsive = false) => {
// This function will check to see if a nested object is valid by calling Document.isDynamoObject recursively
function isValid(value) {
const keys = Object.keys(value);
const key = keys[0];
const nestedResult = (typeof value[key] === "object" && !(value[key] instanceof Buffer) ? (Array.isArray(value[key]) ? value[key].every((value) => Document.isDynamoObject(value, true)) : Document.isDynamoObject(value[key])) : true);
const Schema = require("./Schema");
const attributeType = Schema.attributeTypes.findDynamoDBType(key);
return typeof value === "object" && keys.length === 1 && attributeType && (nestedResult || Object.keys(value[key]).length === 0 || attributeType.isSet);
}

const keys = Object.keys(object);
const values = Object.values(object);
if (keys.length === 0) {
return null;
} else {
return recurrsive ? isValid(object) : values.every((value) => isValid(value));
}
};
// This function will mutate the object passed in to run any actions to conform to the schema that cannot be achieved through non mutating methods in Document.objectFromSchema (setting timestamps, etc.)
Document.prepareForObjectFromSchema = function(object, settings) {
if (settings.updateTimestamps) {
Expand Down
3 changes: 2 additions & 1 deletion lib/DocumentRetriever.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const aws = require("./aws");
const Error = require("./Error");
const utils = require("./utils");
const Condition = require("./Condition");
const Document = require("./Document");

// DocumentRetriever is used for both Scan and Query since a lot of the code is shared between the two

Expand Down Expand Up @@ -52,7 +53,7 @@ function main(documentRetrieverTypeString) {
object.Limit = this.settings.limit;
}
if (this.settings.startAt) {
object.ExclusiveStartKey = model.Document.isDynamoObject(this.settings.startAt) ? this.settings.startAt : model.Document.toDynamo(this.settings.startAt);
object.ExclusiveStartKey = Document.isDynamoObject(this.settings.startAt) ? this.settings.startAt : model.Document.toDynamo(this.settings.startAt);
}
if (this.settings.attributes) {
object.AttributesToGet = this.settings.attributes;
Expand Down
15 changes: 15 additions & 0 deletions lib/utils/object/equals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const entries = require("./entries");

module.exports = (a, b) => {
const aEntries = entries(a);
const bEntries = entries(b);
const bEntriesMap = bEntries.reduce((res, value) => {
const [key, val] = value;
res[key] = val;
return res;
}, {});

return aEntries.length === bEntries.length && aEntries.every((entry) => {
return typeof entry[1] === "object" || bEntriesMap[entry[0]] === entry[1];
});
};
3 changes: 2 additions & 1 deletion lib/utils/object/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ module.exports = {
"delete": require("./delete"),
"pick": require("./pick"),
"keys": require("./keys"),
"entries": require("./entries")
"entries": require("./entries"),
"equals": require("./equals")
};
31 changes: 29 additions & 2 deletions test/Condition.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,41 @@ describe("Condition", () => {
"input": () => new Condition().where("userID").eq("1").and().where("userID").exists(),
"output": {"ConditionExpression": "#a0 = :v0 AND attribute_exists (#a1)", "ExpressionAttributeNames": {"#a0": "userID", "#a1": "userID"}, "ExpressionAttributeValues": {":v0": {"S": "1"}}}
},
{
"input": () => new Condition({"FilterExpression": "#id = :id", "ExpressionAttributeValues": {":id": {"S": "5"}}, "ExpressionAttributeNames": {"#id": "id"}}),
"settings": {"conditionString": "FilterExpression"},
"output": {"FilterExpression": "#id = :id", "ExpressionAttributeValues": {":id": {"S": "5"}}, "ExpressionAttributeNames": {"#id": "id"}}
},
{
"input": () => new Condition({"FilterExpression": "#id = :id", "ExpressionAttributeValues": {":id": "5"}, "ExpressionAttributeNames": {"#id": "id"}}),
"settings": {"conditionString": "FilterExpression"},
"output": {"FilterExpression": "#id = :id", "ExpressionAttributeValues": {":id": {"S": "5"}}, "ExpressionAttributeNames": {"#id": "id"}}
},
{
"input": () => new Condition({"FilterExpression": "#id = :id", "ExpressionAttributeValues": {":id": "5"}, "ExpressionAttributeNames": {"#id": "id"}}),
"output": {}
},
{
"input": () => new Condition({"ConditionExpression": "#id = :id", "ExpressionAttributeValues": {":id": {"S": "5"}}, "ExpressionAttributeNames": {"#id": "id"}}),
"output": {"ConditionExpression": "#id = :id", "ExpressionAttributeValues": {":id": {"S": "5"}}, "ExpressionAttributeNames": {"#id": "id"}}
},
{
"input": () => new Condition({"ConditionExpression": "#id = :id", "ExpressionAttributeValues": {":id": "5"}, "ExpressionAttributeNames": {"#id": "id"}}),
"output": {"ConditionExpression": "#id = :id", "ExpressionAttributeValues": {":id": {"S": "5"}}, "ExpressionAttributeNames": {"#id": "id"}}
},
{
"input": () => new Condition({"ConditionExpression": "#id = :id", "ExpressionAttributeValues": {":id": "5"}, "ExpressionAttributeNames": {"#id": "id"}}),
"settings": {"conditionString": "FilterExpression"},
"output": {}
},
];

tests.forEach((test) => {
it(`Should ${test.error ? "throw" : "return"} ${JSON.stringify(test.error || test.output)} for ${JSON.stringify(test.input)}`, () => {
if (test.error) {
expect(() => test.input().requestObject()).to.throw(test.error);
expect(() => test.input().requestObject(test.settings)).to.throw(test.error);
} else {
expect(test.input().requestObject()).to.eql(test.output);
expect(test.input().requestObject(test.settings)).to.eql(test.output);
}
});
});
Expand Down
57 changes: 57 additions & 0 deletions test/utils/object/equals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const {expect} = require("chai");
const utils = require("../../../lib/utils");

describe("utils.object.equals", () => {
it("Should be a function", () => {
expect(utils.object.equals).to.be.a("function");
});

const tests = [
{
"input": [{"hello": "world"}, {"hello": "world"}],
"output": true
},
{
"input": [{"hello": "world"}, {"hello": "universe"}],
"output": false
},
{
"input": [{"hello": "universe"}, {"hello": "world"}],
"output": false
},
{
"input": [{"hello": {"item": "world"}}, {"hello": "world"}],
"output": false
},
{
"input": [{"hello": {"item": "world"}}, {"hello": {"item": "universe"}}],
"output": false
},
{
"input": [{"hello": {"item": "world"}}, {"hello": {"item": "world"}}],
"output": true
},
{
"input": [[1, 2, 3], [1, 2, 3]],
"output": true
},
{
"input": [[2, 1, 3], [1, 2, 3]],
"output": false
},
{
"input": [[2, 1], [2, 1, 3]],
"output": false
},
{
"input": [[2, 1, 3], [2, 1]],
"output": false
}
];

tests.forEach((test) => {
it(`Should return ${test.output} for ${JSON.stringify(test.input)}`, () => {
expect(utils.object.equals(...test.input)).to.eql(test.output);
});
});
});

0 comments on commit 5a056a1

Please sign in to comment.