electrodb
Advanced tools
Comparing version 1.5.0 to 1.6.0
@@ -101,11 +101,11 @@ # Changelog | ||
## [1.4.5] = 2021-10-17 | ||
## [1.4.5] - 2021-10-17 | ||
### Fixed | ||
- Improved .npmignore to remove playground oriented files, and created official directory to keep playground in sync with library changes. | ||
## [1.4.6] = 2021-10-20 | ||
## [1.4.6] - 2021-10-20 | ||
### Added, Fixed | ||
- Adding Entity identifiers to all update operations. When primary index composite attributes were added in 1.4.4, entities were written properly but did not include the identifiers. This resulted in entities being written but not being readable without the query option `ignoreOwnership` being used. | ||
## [1.4.7] = 2021-10-20 | ||
## [1.4.7] - 2021-10-20 | ||
### Changed | ||
@@ -117,11 +117,23 @@ - Using `add()` update mutation now resolves to `ADD #prop :prop` update expression instead of a `SET #prop = #prop + :prop` | ||
## [1.4.8] = 2021-11-01 | ||
## [1.4.8] - 2021-11-01 | ||
### Fixed | ||
- Addressed issue#90 to flip batchGet's response tuple type definition. | ||
## [1.5.0] = 2021-11-07 | ||
## [1.5.0] - 2021-11-07 | ||
### Changed | ||
- Queries will now fully paginate all responses. Prior to this change, ElectroDB would only return items from a single ElectroDB query result. Now ElectroDB will paginate through all query results. This will impact both uses of entity queries and service collections. [[read more](./README.md#query-method)] | ||
- The query option `limit` has an extended meaning with the change to automatically paginate records on query. The option `limit` now represents a target for the number of items to return from DynamoDB. If this option is passed, Queries on entities and through collections will paginate DynamoDB until this limit is reached or all items for that query have been returned. [[read more](./README.md#query-options)] | ||
### Added | ||
- A new query option `pages` has been added to coincide with the change to automatically paginate all records when queried. The `pages` option sets a max number of pagination iterations ElectroDB will perform on a query. When this option is paired with `limit`, ElectroDB will respect the first condition reached. [[read more](./README.md#query-options)] | ||
- A new query option `pages` has been added to coincide with the change to automatically paginate all records when queried. The `pages` option sets a max number of pagination iterations ElectroDB will perform on a query. When this option is paired with `limit`, ElectroDB will respect the first condition reached. [[read more](./README.md#query-options)] | ||
## [1.6.0] - 2021-11-21 | ||
### Added | ||
- Exporting TypeScript interfaces for `ElectroError` and `ElectroValidationError` | ||
- Errors thrown within an attribute's validate callback are now wrapped and accessible after being thrown. Prior to this change, only the `message` of the error thrown by a validation function was persisted back through to the user, now the error itself is also accessible. Reference the exported interface typedef for `ElectroValidationError` [here](./index.d.ts) to see the new properties available on a thrown validation error. | ||
### Changed | ||
- As a byproduct of enhancing validation errors, the format of message text on a validation error has changed. This could be breaking if your app had a hardcoded dependency on the exact text of a thrown validation error. | ||
### Fixed | ||
- For Set attributes, the callback functions `get`, `set`, and `validate` are now consistently given an Array of values. These functions would sometimes (incorrectly) be called with a DynamoDB DocClient Set. |
{ | ||
"name": "electrodb", | ||
"version": "1.5.0", | ||
"version": "1.6.0", | ||
"description": "A library to more easily create and interact with multiple entities and heretical relationships in dynamodb", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -210,11 +210,16 @@ // # Errors: | ||
function makeMessage(message, section) { | ||
return `${message} - For more detail on this error reference: ${getHelpLink(section)}` | ||
} | ||
class ElectroError extends Error { | ||
constructor(err, message) { | ||
constructor(code, message) { | ||
super(message); | ||
let detail = ErrorCodes.UnknownError; | ||
if (err && err.sym === ErrorCode) { | ||
detail = err | ||
if (code && code.sym === ErrorCode) { | ||
detail = code | ||
} | ||
this.message = `${message} - For more detail on this error reference: ${getHelpLink(detail.section)}`; | ||
this._message = message; | ||
// this.message = `${message} - For more detail on this error reference: ${getHelpLink(detail.section)}`; | ||
this.message = makeMessage(message, detail.section); | ||
if (Error.captureStackTrace) { | ||
@@ -225,5 +230,5 @@ Error.captureStackTrace(this, ElectroError); | ||
this.name = 'ElectroError'; | ||
this.ref = err; | ||
this.ref = code; | ||
this.code = detail.code; | ||
this.date = new Date(); | ||
this.date = Date.now(); | ||
this.isElectroError = true; | ||
@@ -233,5 +238,84 @@ } | ||
class ElectroValidationError extends ElectroError { | ||
constructor(errors = []) { | ||
const fields = []; | ||
const messages = []; | ||
for (let i = 0; i < errors.length; i++) { | ||
const error = errors[i]; | ||
const message = error ? (error._message || error.message) : undefined; | ||
messages.push(message); | ||
if (error instanceof ElectroUserValidationError) { | ||
fields.push({ | ||
field: error.field, | ||
index: error.index, | ||
reason: message, | ||
cause: error.cause, | ||
type: 'validation' | ||
}); | ||
} else if (error instanceof ElectroAttributeValidationError) { | ||
fields.push({ | ||
field: error.field, | ||
index: error.index, | ||
reason: message, | ||
cause: error.cause || error, // error | undefined | ||
type: 'validation' | ||
}); | ||
} else if (message) { | ||
fields.push({ | ||
field: '', | ||
index: error.index, | ||
reason: message, | ||
cause: error !== undefined ? error.cause || error : undefined, | ||
type: 'fatal' | ||
}); | ||
} | ||
} | ||
const message = messages | ||
.filter(message => typeof message === "string" && message.length) | ||
.join(', ') || `Invalid value(s) provided`; | ||
super(ErrorCodes.InvalidAttribute, message); | ||
this.fields = fields; | ||
this.name = "ElectroValidationError"; | ||
} | ||
} | ||
class ElectroUserValidationError extends ElectroError { | ||
constructor(field, cause) { | ||
let message; | ||
let hasCause = false; | ||
if (typeof cause === "string") { | ||
message = cause; | ||
} else if (cause !== undefined && typeof cause._message === "string" && cause._message.length) { | ||
message = cause._message; | ||
hasCause = true; | ||
} else if (cause !== undefined && typeof cause.message === "string" && cause.message.length) { | ||
message = cause.message; | ||
hasCause = true; | ||
} else { | ||
message = "Invalid value provided"; | ||
} | ||
super(ErrorCodes.InvalidAttribute, message); | ||
this.field = field; | ||
this.name = "ElectroUserValidationError"; | ||
if (hasCause) { | ||
this.cause = cause; | ||
} | ||
} | ||
} | ||
class ElectroAttributeValidationError extends ElectroError { | ||
constructor(field, reason) { | ||
super(ErrorCodes.InvalidAttribute, reason); | ||
this.field = field; | ||
} | ||
} | ||
module.exports = { | ||
ErrorCodes, | ||
ElectroError, | ||
ErrorCodes | ||
ElectroValidationError, | ||
ElectroUserValidationError, | ||
ElectroAttributeValidationError | ||
}; |
@@ -292,3 +292,3 @@ const { CastTypes, ValueTypes, KeyCasing, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes, TraverserIndexes } = require("./types"); | ||
if (cast !== undefined && !CastTypes.includes(cast)) { | ||
throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "cast" property for attribute: "${name}". Acceptable types include ${CastTypes.join(", ",)}`, | ||
throw new e.ElectroError(e.ErrorCodes.InvalidAttributeDefinition, `Invalid "cast" property for attribute: "${name}". Acceptable types include ${CastTypes.join(", ")}`, | ||
); | ||
@@ -331,4 +331,15 @@ } else if (cast === AttributeTypes.string) { | ||
return (val) => { | ||
let reason = definition(val); | ||
return [!reason, reason || ""]; | ||
try { | ||
let reason = definition(val); | ||
const isValid = !reason; | ||
if (isValid) { | ||
return [isValid, []]; | ||
} else if (typeof reason === "boolean") { | ||
return [isValid, [new e.ElectroUserValidationError(this.path, "Invalid value provided")]]; | ||
} else { | ||
return [isValid, [new e.ElectroUserValidationError(this.path, reason)]]; | ||
} | ||
} catch(err) { | ||
return [false, [new e.ElectroUserValidationError(this.path, err)]]; | ||
} | ||
}; | ||
@@ -338,10 +349,13 @@ } else if (definition instanceof RegExp) { | ||
if (val === undefined) { | ||
return [true, ""]; | ||
return [true, []]; | ||
} | ||
let isValid = definition.test(val); | ||
let reason = isValid ? "" : `Invalid value for attribute "${this.path}": Failed model defined regex`; | ||
let reason = []; | ||
if (!isValid) { | ||
reason.push(new e.ElectroUserValidationError(this.path, `Invalid value for attribute "${this.path}": Failed model defined regex`)); | ||
} | ||
return [isValid, reason]; | ||
}; | ||
} else { | ||
return (val) => [true, ""]; | ||
return () => [true, []]; | ||
} | ||
@@ -391,6 +405,10 @@ } | ||
if (value === undefined) { | ||
return [!this.required, this.required ? `Invalid value type at entity path: "${this.path}". Value is required.` : ""]; | ||
let reason = []; | ||
if (this.required) { | ||
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`)); | ||
} | ||
return [!this.required, reason]; | ||
} | ||
let isTyped = false; | ||
let reason = ""; | ||
let reason = []; | ||
switch (this.type) { | ||
@@ -400,3 +418,3 @@ case AttributeTypes.enum: | ||
if (!isTyped) { | ||
reason = `Invalid value type at entity path: "${this.path}". Value not found in set of acceptable values: ${u.commaSeparatedString(this.enumArray)}`; | ||
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value not found in set of acceptable values: ${u.commaSeparatedString(this.enumArray)}`)); | ||
} | ||
@@ -413,3 +431,3 @@ break; | ||
if (!isTyped) { | ||
reason = `Invalid value type at entity path: "${this.path}". Received value of type "${typeof value}", expected value of type "${this.type}"`; | ||
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Received value of type "${typeof value}", expected value of type "${this.type}"`)); | ||
} | ||
@@ -423,8 +441,8 @@ break; | ||
try { | ||
let [isTyped, typeError] = this._isType(value); | ||
let [isValid, validationError] = this.validate(value); | ||
let reason = [typeError, validationError].filter(Boolean).join(", "); | ||
return [isTyped && isValid, reason]; | ||
let [isTyped, typeErrorReason] = this._isType(value); | ||
let [isValid, validationError] = isTyped ? this.validate(value) : [false, []]; | ||
let errors = [...typeErrorReason, ...validationError].filter(value => value !== undefined); | ||
return [isTyped && isValid, errors]; | ||
} catch (err) { | ||
return [false, err.message]; | ||
return [false, [err]]; | ||
} | ||
@@ -443,6 +461,5 @@ } | ||
value = this.val(value); | ||
let [isValid, validationError] = this.isValid(value); | ||
let [isValid, validationErrors] = this.isValid(value); | ||
if (!isValid) { | ||
// todo: #electroerror | ||
throw new Error(validationError); | ||
throw new e.ElectroValidationError(validationErrors); | ||
} | ||
@@ -520,9 +537,13 @@ return value; | ||
if (value === undefined) { | ||
return [!this.required, this.required ? `Invalid value type at entity path: "${this.path}". Value is required.` : ""]; | ||
let reason = []; | ||
if (this.required) { | ||
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`)); | ||
} | ||
return [!this.required, reason]; | ||
} | ||
const valueType = getValueType(value); | ||
if (valueType !== ValueTypes.object) { | ||
return [false, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "object"`]; | ||
return [false, [new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "object"`)]]; | ||
} | ||
let reason = ""; | ||
let reason = []; | ||
const [childrenAreValid, childErrors] = this._validateChildren(value); | ||
@@ -538,8 +559,8 @@ if (!childrenAreValid) { | ||
const attributes = this.properties.attributes; | ||
const errors = []; | ||
let errors = []; | ||
if (valueType === ValueTypes.object) { | ||
for (const child of Object.keys(attributes)) { | ||
const [isValid, errorMessages] = attributes[child].isValid(value === undefined ? value : value[child]) | ||
const [isValid, errorValues] = attributes[child].isValid(value === undefined ? value : value[child]) | ||
if (!isValid) { | ||
errors.push(errorMessages); | ||
errors = [...errors, ...errorValues] | ||
} | ||
@@ -549,10 +570,10 @@ } | ||
errors.push( | ||
`Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"` | ||
new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`) | ||
); | ||
} else if (this.properties.hasRequiredAttributes) { | ||
errors.push( | ||
`Invalid value type at entity path: "${this.path}". Map attribute requires at least the properties ${u.commaSeparatedString(Object.keys(attributes))}` | ||
new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Map attribute requires at least the properties ${u.commaSeparatedString(Object.keys(attributes))}`) | ||
); | ||
} | ||
return [errors.length === 0, errors.filter(Boolean).join(", ")]; | ||
return [errors.length === 0, errors]; | ||
} | ||
@@ -574,3 +595,3 @@ | ||
} else if (valueType !== "object") { | ||
throw new Error(`Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`); | ||
throw new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an object to fulfill attribute type "${this.type}"`); | ||
} | ||
@@ -658,7 +679,8 @@ | ||
_validateArrayValue(value) { | ||
const reason = []; | ||
const valueType = getValueType(value); | ||
if (value !== undefined && valueType !== ValueTypes.array) { | ||
return [false, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "array"`]; | ||
return [false, [new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${valueType}", expected value of type "array"`)]]; | ||
} else { | ||
return [true, ""]; | ||
return [true, []]; | ||
} | ||
@@ -669,3 +691,7 @@ } | ||
if (value === undefined) { | ||
return [!this.required, this.required ? `Invalid value type at entity path: "${this.path}". Value is required.` : ""]; | ||
let reason = []; | ||
if (this.required) { | ||
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`)); | ||
} | ||
return [!this.required, reason]; | ||
} | ||
@@ -677,3 +703,3 @@ | ||
} | ||
let reason = ""; | ||
let reason = []; | ||
const [childrenAreValid, childErrors] = this._validateChildren(value); | ||
@@ -691,5 +717,10 @@ if (!childrenAreValid) { | ||
for (const i in value) { | ||
const [isValid, errorMessages] = this.items.isValid(value[i]); | ||
const [isValid, errorValues] = this.items.isValid(value[i]); | ||
if (!isValid) { | ||
errors.push(errorMessages + ` at index "${i}"`); | ||
for (const err of errorValues) { | ||
if (err instanceof e.ElectroAttributeValidationError || err instanceof e.ElectroUserValidationError) { | ||
err.index = parseInt(i); | ||
} | ||
errors.push(err); | ||
} | ||
} | ||
@@ -699,6 +730,6 @@ } | ||
errors.push( | ||
`Invalid value type at entity path: "${this.path}". Expected value to be an Array to fulfill attribute type "${this.type}"` | ||
new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Expected value to be an Array to fulfill attribute type "${this.type}"`) | ||
); | ||
} | ||
return [errors.length === 0, errors.filter(Boolean).join(", ")]; | ||
return [errors.length === 0, errors]; | ||
} | ||
@@ -720,3 +751,3 @@ | ||
} else if (!Array.isArray(value)) { | ||
throw new Error(`Invalid value type at entity path "${this.path}. Received value of type "${getValueType(value)}", expected value of type "array"`); | ||
throw new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path "${this.path}. Received value of type "${getValueType(value)}", expected value of type "array"`); | ||
} | ||
@@ -752,4 +783,18 @@ | ||
this.set = this._makeSet(definition.set, items); | ||
this.validate = this._makeSetValidate(definition); | ||
} | ||
_makeSetValidate(definition) { | ||
const validate = this._makeValidate(definition.validate); | ||
return (value) => { | ||
if (Array.isArray(value)) { | ||
return validate([...value]); | ||
} else if (value && value.wrapperName === 'Set') { | ||
return validate([...value.values]) | ||
} else { | ||
return validate(value); | ||
} | ||
} | ||
} | ||
fromDDBSet(value) { | ||
@@ -790,3 +835,3 @@ if (getValueType(value) === ValueTypes.aws_set) { | ||
default: | ||
throw new Error(`Invalid attribute value supplied to "set" attribute "${this.path}". Received value of type "${valueType}". Set values must be supplied as either Arrays, native JavaScript Set objects, DocumentClient Set objects, strings, or numbers.`) | ||
throw new e.ElectroAttributeValidationError(this.path, `Invalid attribute value supplied to "set" attribute "${this.path}". Received value of type "${valueType}". Set values must be supplied as either Arrays, native JavaScript Set objects, DocumentClient Set objects, strings, or numbers.`) | ||
} | ||
@@ -818,3 +863,5 @@ | ||
return (values, siblings) => { | ||
const results = setter(values, siblings); | ||
const results = values && values.wrapperName === 'Set' | ||
? setter(values.values, siblings) | ||
: setter(values, siblings) | ||
if (results !== undefined) { | ||
@@ -828,6 +875,10 @@ return this.toDDBSet(results); | ||
if (value === undefined) { | ||
return [!this.required, this.required ? `Invalid value type at entity path: "${this.path}". Value is required.` : ""]; | ||
const reason = []; | ||
if (this.required) { | ||
reason.push(new e.ElectroAttributeValidationError(this.path, `Invalid value type at entity path: "${this.path}". Value is required.`)); | ||
} | ||
return [!this.required, reason]; | ||
} | ||
let reason = ""; | ||
let reason = []; | ||
const [childrenAreValid, childErrors] = this._validateChildren(value); | ||
@@ -842,3 +893,3 @@ if (!childrenAreValid) { | ||
const valueType = getValueType(value); | ||
const errors = []; | ||
let errors = []; | ||
let arr = []; | ||
@@ -853,12 +904,12 @@ if (valueType === ValueTypes.array) { | ||
errors.push( | ||
`Invalid value type at attribute path: "${this.path}". Expected value to be an Expected value to be an Array, native JavaScript Set objects, or DocumentClient Set objects to fulfill attribute type "${this.type}"` | ||
new e.ElectroAttributeValidationError(this.path, `Invalid value type at attribute path: "${this.path}". Expected value to be an Expected value to be an Array, native JavaScript Set objects, or DocumentClient Set objects to fulfill attribute type "${this.type}"`) | ||
) | ||
} | ||
for (const item of arr) { | ||
const [isValid, errorMessage] = this.items.isValid(item); | ||
const [isValid, errorValues] = this.items.isValid(item); | ||
if (!isValid) { | ||
errors.push(errorMessage); | ||
errors = [...errors, ...errorValues]; | ||
} | ||
} | ||
return [errors.length === 0, errors.filter(Boolean).join(", ")]; | ||
return [errors.length === 0, errors]; | ||
} | ||
@@ -1259,7 +1310,7 @@ | ||
if (!attribute) { | ||
throw new Error(`Attribute "${path}" does not exist on model.`); | ||
throw new e.ElectroAttributeValidationError(path, `Attribute "${path}" does not exist on model.`); | ||
} else if (attribute.readOnly) { | ||
throw new Error(`Attribute "${attribute.path}" is Read-Only and cannot be removed`); | ||
throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be removed`); | ||
} else if (attribute.required) { | ||
throw new Error(`Attribute "${attribute.path}" is Required and cannot be removed`); | ||
throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Required and cannot be removed`); | ||
} | ||
@@ -1279,3 +1330,3 @@ } | ||
// todo: #electroerror | ||
throw new Error(`Attribute "${attribute.path}" is Read-Only and cannot be updated`); | ||
throw new e.ElectroAttributeValidationError(attribute.path, `Attribute "${attribute.path}" is Read-Only and cannot be updated`); | ||
} else { | ||
@@ -1282,0 +1333,0 @@ record[path] = attribute.getValidate(value); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
557891
7985
5542