Skip to content

Commit

Permalink
Merge branch 'master' into emptyDocumentInitializer
Browse files Browse the repository at this point in the history
# Conflicts:
#	test/Document.js
  • Loading branch information
fishcharlie committed Mar 20, 2020
2 parents 0a08d96 + d183fd8 commit fea80b7
Show file tree
Hide file tree
Showing 15 changed files with 744 additions and 283 deletions.
9 changes: 8 additions & 1 deletion BREAKING_CHANGES.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
26 changes: 26 additions & 0 deletions docs/api/Schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,32 @@ You can set an attribute to have an enum array, which means it must match one of
}
```

### get: function | async function

You can use a get function on an attribute to be run whenever retrieving a document from DynamoDB. This function will only be run if the item exists in the document. Dynamoose will pass the DynamoDB value into this function and you must return the new value that you want Dynamoose to return to the application.

```js
{
"id": {
"type": String,
"get": (value) => `applicationid-${value}` // This will prepend `applicationid-` to all values for this attribute when returning from the database
}
}
```

### set: function | async function

You can use a set function on an attribute to be run whenever saving a document to DynamoDB. This function will only be run if the item exists in the document. Dynamoose will pass the value you provide into this function and you must return the new value that you want Dynamoose to save to DynamoDB.

```js
{
"name": {
"type": String,
"set": (value) => `${value.charAt(0).toUpperCase()}${value.slice(1)}` // Capitalize first letter of name when saving to database
}
}
```

### index: boolean | object | array

You can define indexes on properties to be created or updated upon model initialization. If you pass in an array for the value of this setting it must be an array of index objects. By default no indexes are specified on the attribute.
Expand Down
28 changes: 17 additions & 11 deletions lib/Document.js
Original file line number Diff line number Diff line change
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 Expand Up @@ -206,6 +201,17 @@ function DocumentCarrier(model) {
}
}));
}
if (settings.modifiers) {
await Promise.all(settings.modifiers.map((modifier) => {
return Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
const value = utils.object.get(returnObject, key);
const modifierFunction = await model.schema.getAttributeSettingValue(modifier, key, {"returnFunction": true});
if (modifierFunction && value) {
utils.object.set(returnObject, key, await modifierFunction(value));
}
}));
}));
}

return returnObject;
};
Expand All @@ -221,7 +227,7 @@ function DocumentCarrier(model) {
settings = {};
}

const paramsPromise = this.toDynamo({"defaults": true, "validate": true, "required": true, "enum": true, "forceDefault": true, "saveUnknown": true, "customTypesDynamo": true, "updateTimestamps": true}).then((item) => {
const paramsPromise = this.toDynamo({"defaults": true, "validate": true, "required": true, "enum": true, "forceDefault": true, "saveUnknown": true, "customTypesDynamo": true, "updateTimestamps": true, "modifiers": ["set"]}).then((item) => {
const putItemObj = {
"Item": item,
"TableName": Document.Model.name
Expand Down
110 changes: 87 additions & 23 deletions lib/DocumentRetriever.js
Original file line number Diff line number Diff line change
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 @@ -144,16 +210,15 @@ function main(documentRetrieverTypeString) {
[`${documentRetrieverType.pastTense}Count`]: result[`${utils.capitalize_first_letter(documentRetrieverType.pastTense)}Count`]
};
}
const array = (await Promise.all(result.Items.map(async (item) => await ((new model.Document(item, {"fromDynamo": true})).conformToSchema({"customTypesDynamo": true, "checkExpiredItem": true, "saveUnknown": true, "type": "fromDynamo"}))))).filter((a) => Boolean(a));
const array = (await Promise.all(result.Items.map(async (item) => await ((new model.Document(item, {"fromDynamo": true})).conformToSchema({"customTypesDynamo": true, "checkExpiredItem": true, "saveUnknown": true, "modifiers": ["get"], "type": "fromDynamo"}))))).filter((a) => Boolean(a));
array.lastKey = result.LastEvaluatedKey ? (Array.isArray(result.LastEvaluatedKey) ? result.LastEvaluatedKey.map((key) => model.Document.fromDynamo(key)) : model.Document.fromDynamo(result.LastEvaluatedKey)) : undefined;
array.count = result.Count;
array[`${documentRetrieverType.pastTense}Count`] = result[`${utils.capitalize_first_letter(documentRetrieverType.pastTense)}Count`];
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
Loading

0 comments on commit fea80b7

Please sign in to comment.