Skip to content

Commit

Permalink
Merge branch 'master' into setgetAttributeSettings
Browse files Browse the repository at this point in the history
  • Loading branch information
fishcharlie committed Mar 20, 2020
2 parents dece81a + b015515 commit 609f2a8
Show file tree
Hide file tree
Showing 13 changed files with 594 additions and 280 deletions.
9 changes: 8 additions & 1 deletion BREAKING_CHANGES.md
Expand Up @@ -2,14 +2,16 @@

## 2.0 - incomplete list

- Everything in Dynamoose are now classes. This includes `Model` and `Schema`. This means to initalize a new instance of one of these items, you must use the `new` keyword before it
- Everything in Dynamoose are now classes. This includes `Model` and `Schema`. This means to initialize a new instance of one of these items, you must use the `new` keyword before it
- `scan.count()` has been removed, and `scan.counts()` has been renamed to `scan.count()`.
- Schema `default` value does not pass the model instance into `default` functions any more.
- `Model.update`
- `$LISTAPPEND` has been removed, and `$ADD` now includes the behavior of `$LISTAPPEND`
- `$DELETE` now maps to the correct underlying DynamoDB method instead of the previous behavior of mapping to `$REMOVE`
- `$PUT` has been replaced with `$SET`
- `dynamoose.model` has been renamed to `dynamoose.Model`
- `dynamoose.local` has been renamed to `dynamoose.aws.ddb.local`
- `dynamoose.setDDB` has been renamed to `dynamoose.aws.ddb.set`
- `Model.getTableReq` has been renamed to `Model.table.create.request`
- `Model.table.create.request` (formerly `Model.getTableReq`) is now an async function
- `model.originalItem` has been renamed to `model.original` (or `Document.original`)
Expand All @@ -21,3 +23,8 @@
- `Model.transaction.conditionCheck` has been renamed to `Model.transaction.condition`
- `Model.transaction.condition` options parameter now gets appended to the object returned. This means you can no longer use the helpers that Dynamoose provided to make conditions. Instead, pass in the DynamoDB API level conditions you wish to use
- In the past the `saveUnknown` option for attribute names would handle all nested properties. Now you must use `*` to indicate one level of wildcard or `**` to indicate infinate levels of wildcard. So if you have an object property (`address`) and want to parse one level of values (no sub objects) you can use `address.*`, or `address.**` to all for infinate levels of values (including sub objects)
- `useNativeBooleans` & `useDocumentTypes` have been removed from the Model settings
- `Map` attribute type has been replaced with `Object`
- `List` attribute type has been replaced with `Array`
- `Scan.null` & `Query.null` have been removed
- DynamoDB set types are now returned as JavaScript Set's instead of Array's
12 changes: 1 addition & 11 deletions docs/api/Query.md
Expand Up @@ -98,22 +98,12 @@ Cat.query().where("id"); // Currently this query has no behavior and will query
Cat.query().where("id").eq(1); // Since this query has a comparison function (eq) after the conditional it will complete the conditional and only query items where `id` = 1
```

## query.null()

This comparison function will check to see if the given filter key is null.

```js
Cat.query().filter("name").null(); // Return all items where `name` is null
```

## query.eq(value)

This comparison function will check to see if the given filter key is equal to the value you pass in as a parameter. If `null`, `undefined`, or an empty string is passed as the value parameter, this function will behave just like [`query.null()`](#querynull).
This comparison function will check to see if the given filter key is equal to the value you pass in as a parameter.

```js
Cat.query().filter("name").eq("Tom"); // Return all items where `name` equals `Tom`

Cat.query().filter("name").eq(); // Same as `Cat.query().filter("name").null()`
```

## query.lt(value)
Expand Down
12 changes: 1 addition & 11 deletions docs/api/Scan.md
Expand Up @@ -91,22 +91,12 @@ Cat.scan().filter("id").eq(1); // Since this scan has a comparison function (eq)

This function is identical to [`scan.filter(key)`](#scanfilterkey) and just used as an alias.

## scan.null()

This comparison function will check to see if the given filter key is null.

```js
Cat.scan().filter("name").null(); // Return all items where `name` is null
```

## scan.eq(value)

This comparison function will check to see if the given filter key is equal to the value you pass in as a parameter. If `null`, `undefined`, or an empty string is passed as the value parameter, this function will behave just like [`scan.null()`](#scannull).
This comparison function will check to see if the given filter key is equal to the value you pass in as a parameter.

```js
Cat.scan().filter("name").eq("Tom"); // Return all items where `name` equals `Tom`

Cat.scan().filter("name").eq(); // Same as `Cat.scan().filter("name").null()`
```

## scan.lt(value)
Expand Down
15 changes: 5 additions & 10 deletions lib/Document.js
Expand Up @@ -112,18 +112,13 @@ function DocumentCarrier(model) {
function mainCheck(finalObjectPre, key, value) {
const finalObject = {...finalObjectPre};
const genericKey = key.replace(/\d+/gu, "0"); // This is a key replacing all list numbers with 0 to standardize things like checking if it exists in the schema
const keyParts = key.split(".");
const parentKey = [...keyParts].splice(0, keyParts.length - 1).join(".");
let parentKeyIsArray;
try {
parentKeyIsArray = parentKey && model.schema.getAttributeType(parentKey) === "L";
} catch (e) {} // eslint-disable-line no-empty
const existsInSchema = model.schema.attributes().includes(genericKey) || parentKeyIsArray;
const isCustomType = existsInSchema && model.schema.getAttributeTypeDetails(key).customType;
const existsInSchema = model.schema.attributes().includes(genericKey);
const typeDetails = existsInSchema && model.schema.getAttributeTypeDetails(key);
const isCustomType = existsInSchema && typeDetails.customType;
const valueType = typeof value;
const customValue = isCustomType ? model.schema.getAttributeTypeDetails(key).customType.functions[settings.type](value) : null;
const customValue = isCustomType ? typeDetails.customType.functions[settings.type](value) : null;
const customValueType = isCustomType ? typeof customValue : null;
let attributeType = existsInSchema || parentKeyIsArray ? utils.attribute_types.find((type) => type.dynamodbType === model.schema.getAttributeType(key)) : null;
let attributeType = existsInSchema ? utils.attribute_types.find((type) => type.dynamodbType === model.schema.getAttributeType(key)) : null;
const expectedType = existsInSchema ? attributeType.javascriptType : null;
let typeMatches = existsInSchema ? valueType === expectedType || (attributeType.isOfType && attributeType.isOfType(value, settings.type)) : null;
if (existsInSchema && attributeType.isOfType && attributeType.isOfType(isCustomType && settings.type === "toDynamo" ? customValue : value, settings.type)) {
Expand Down
108 changes: 86 additions & 22 deletions lib/DocumentRetriever.js
Expand Up @@ -37,31 +37,92 @@ function main(documentRetrieverTypeString) {
});
} else if (object) {
this.settings.pending.key = object;
if (documentRetrieverType.type === "query") {
this.settings.pending.queryCondition = "hash";
}
}

return this;
}
};
C.prototype[`get${utils.capitalize_first_letter(documentRetrieverType.type)}Request`] = function() {
C.prototype[`get${utils.capitalize_first_letter(documentRetrieverType.type)}Request`] = async function() {
const object = {
"TableName": model.name,
[`${utils.capitalize_first_letter(documentRetrieverType.type)}Filter`]: {}
"TableName": model.name
};
if (documentRetrieverType.type === "query") {
object.KeyConditions = {};
object.KeyConditionExpression = "#qha = :qhv";
}

Object.keys(this.settings.filters).forEach((key) => {
Object.keys(this.settings.filters).forEach((key, index) => {
if (!object.ExpressionAttributeNames || !object.ExpressionAttributeValues) {
object.ExpressionAttributeNames = {};
object.ExpressionAttributeValues = {};
}

const filter = this.settings.filters[key];
let value = filter.value;
if (!Array.isArray(value)) {
value = [value];
const value = filter.value;
// if (!Array.isArray(value)) {
// value = [value];
// }
// value = value.map((item) => aws.converter().input(item));
// object[filter.queryCondition ? "KeyConditions" : `${utils.capitalize_first_letter(documentRetrieverType.type)}Filter`][key] = {
// "ComparisonOperator": filter.type,
// "AttributeValueList": value
// };
let keys = {"name": `#a${index}`, "value": `:v${index}`};
if (filter.queryCondition === "hash") {
keys = {"name": "#qha", "value": ":qhv"};
} else if (filter.queryCondition === "range") {
keys = {"name": "#qra", "value": ":qrv"};
}
object.ExpressionAttributeNames[keys.name] = key;
object.ExpressionAttributeValues[keys.value] = aws.converter().input(value);

if (!filter.queryCondition) {
if (!object.FilterExpression) {
object.FilterExpression = "";
}
if (object.FilterExpression !== "") {
object.FilterExpression = `${object.FilterExpression} AND `;
}

let expression = "";
switch (filter.type) {
case "EQ":
case "NE":
expression = `${keys.name} ${filter.type === "EQ" ? "=" : "<>"} ${keys.value}`;
break;
case "IN":
delete object.ExpressionAttributeValues[keys.value];
expression = `${keys.name} IN (${value.map((v, i) => `${keys.value}-${i + 1}`).join(", ")})`;
value.forEach((valueItem, i) => {
object.ExpressionAttributeValues[`${keys.value}-${i + 1}`] = aws.converter().input(valueItem);
});
break;
case "GT":
case "GE":
case "LT":
case "LE":
expression = `${keys.name} ${filter.type.startsWith("G") ? ">" : "<"}${filter.type.endsWith("E") ? "=" : ""} ${keys.value}`;
break;
case "BETWEEN":
expression = `${keys.name} BETWEEN ${keys.value}-1 AND ${keys.value}-2`;
object.ExpressionAttributeValues[`${keys.value}-1`] = aws.converter().input(value[0]);
object.ExpressionAttributeValues[`${keys.value}-2`] = aws.converter().input(value[1]);
delete object.ExpressionAttributeValues[keys.value];
break;
case "CONTAINS":
case "NOT_CONTAINS":
expression = `${filter.type === "NOT_CONTAINS" ? "NOT " : ""}contains (${keys.name}, ${keys.value})`;
break;
case "BEGINS_WITH":
expression = `begins_with (${keys.name}, ${keys.value})`;
break;
}
object.FilterExpression = `${object.FilterExpression}${expression}`;
} else if (filter.queryCondition === "range") {
object.KeyConditionExpression = `${object.KeyConditionExpression} AND ${keys.name} = ${keys.value}`;
}
value = value.map((item) => aws.converter().input(item));
object[filter.queryCondition ? "KeyConditions" : `${utils.capitalize_first_letter(documentRetrieverType.type)}Filter`][key] = {
"ComparisonOperator": filter.type,
"AttributeValueList": value
};
});
if (this.settings.limit) {
object.Limit = this.settings.limit;
Expand All @@ -74,6 +135,12 @@ function main(documentRetrieverTypeString) {
}
if (this.settings.index) {
object.IndexName = this.settings.index;
} else if (documentRetrieverType.type === "query") {
const indexes = await model.schema.getIndexes(model);
// TODO change `Array.prototype.concat.apply` to be a custom flatten function
const preferredIndexes = Array.prototype.concat.apply([], Object.values(indexes)).filter((index) => Boolean(index.KeySchema.find((key) => key.AttributeName === object.ExpressionAttributeNames["#qha"] && key.KeyType === "HASH")));
const index = !object.ExpressionAttributeNames["#qra"] ? preferredIndexes[0] : preferredIndexes.find((index) => Boolean(index.KeySchema.find((key) => key.AttributeName === object.ExpressionAttributeNames["#qra"] && key.KeyType === "RANGE")));
object.IndexName = index.IndexName;
}
if (this.settings.consistent) {
object.ConsistentRead = this.settings.consistent;
Expand All @@ -95,7 +162,6 @@ function main(documentRetrieverTypeString) {
"LE": "GT",
"LT": "GE",
"BETWEEN": null,
"NULL": "NOT_NULL",
"CONTAINS": "NOT_CONTAINS",
"BEGINS_WITH": null
};
Expand Down Expand Up @@ -126,7 +192,7 @@ function main(documentRetrieverTypeString) {
};

if (pending.queryCondition) {
instance.settings.filters[pending.key].queryCondition = true;
instance.settings.filters[pending.key].queryCondition = pending.queryCondition;
}

instance.settings.pending = {};
Expand All @@ -151,9 +217,8 @@ function main(documentRetrieverTypeString) {
array[`times${utils.capitalize_first_letter(documentRetrieverType.pastTense)}`] = timesRequested;
return array;
};
const promise = model.pendingTaskPromise().then(() => {
const promise = model.pendingTaskPromise().then(() => this[`get${utils.capitalize_first_letter(documentRetrieverType.type)}Request`]()).then((request) => {
const ddb = aws.ddb();
const request = this[`get${utils.capitalize_first_letter(documentRetrieverType.type)}Request`]();

const allRequest = (extraParameters = {}) => {
let promise = ddb[documentRetrieverType.type]({...request, ...extraParameters}).promise();
Expand Down Expand Up @@ -219,13 +284,12 @@ function main(documentRetrieverTypeString) {
C.prototype.where = C.prototype.filter;
} else {
C.prototype.where = function(key) {
this.settings.pending = {key, "queryCondition": true};
this.settings.pending = {key, "queryCondition": "range"};
return this;
};
}
const filterTypes = [
{"name": "null", "typeName": "NULL", "value": []},
{"name": "eq", "typeName": "EQ", "default": {"typeName": "null"}},
{"name": "eq", "typeName": "EQ"},
{"name": "lt", "typeName": "LT"},
{"name": "le", "typeName": "LE"},
{"name": "gt", "typeName": "GT"},
Expand All @@ -237,8 +301,8 @@ function main(documentRetrieverTypeString) {
];
filterTypes.forEach((item) => {
C.prototype[item.name] = function(value) {
if (!value && item.default) {
return this[item.default.typeName](value);
if (this.settings.pending.queryCondition && item.name !== "eq") {
throw new Error.InvalidParameter("Equals must follow range or hash key when querying data");
}

this.settings.pending.value = item.value || (item.multipleArguments ? [...arguments] : value);
Expand Down
21 changes: 4 additions & 17 deletions lib/Model.js
Expand Up @@ -132,8 +132,8 @@ async function createTable(model) {
async function createTableRequest(model) {
return {
"TableName": model.name,
...getProvisionedThroughput(model),
...await model.schema.getCreateTableAttributeParams()
...utils.dynamoose.get_provisioned_throughput(model.options),
...await model.schema.getCreateTableAttributeParams(model)
};
}
function updateTimeToLive(model) {
Expand All @@ -145,20 +145,6 @@ function updateTimeToLive(model) {
}
});
}
function getProvisionedThroughput(model) {
if (model.options.throughput === "ON_DEMAND") {
return {
"BillingMode": "PAY_PER_REQUEST"
};
} else {
return {
"ProvisionedThroughput": {
"ReadCapacityUnits": typeof model.options.throughput === "number" ? model.options.throughput : model.options.throughput.read,
"WriteCapacityUnits": typeof model.options.throughput === "number" ? model.options.throughput : model.options.throughput.write
}
};
}
}
function waitForActive(model) {
return () => new Promise((resolve, reject) => {
const start = Date.now();
Expand Down Expand Up @@ -203,8 +189,9 @@ async function getTableDetails(model, settings = {}) {
}
async function updateTable(model) {
const currentThroughput = (await getTableDetails(model)).Table;
const expectedThroughput = getProvisionedThroughput(model);
const expectedThroughput = utils.dynamoose.get_provisioned_throughput(model.options);
if ((expectedThroughput.BillingMode === currentThroughput.BillingMode && expectedThroughput.BillingMode) || ((currentThroughput.ProvisionedThroughput || {}).ReadCapacityUnits === (expectedThroughput.ProvisionedThroughput || {}).ReadCapacityUnits && currentThroughput.ProvisionedThroughput.WriteCapacityUnits === expectedThroughput.ProvisionedThroughput.WriteCapacityUnits)) {
// if ((expectedThroughput.BillingMode === currentThroughput.BillingModeSummary.BillingMode && expectedThroughput.BillingMode) || ((currentThroughput.ProvisionedThroughput || {}).ReadCapacityUnits === (expectedThroughput.ProvisionedThroughput || {}).ReadCapacityUnits && currentThroughput.ProvisionedThroughput.WriteCapacityUnits === expectedThroughput.ProvisionedThroughput.WriteCapacityUnits)) {
return {"promise": () => Promise.resolve()};
}

Expand Down

0 comments on commit 609f2a8

Please sign in to comment.