electrodb
Advanced tools
Comparing version 1.3.2 to 1.4.0
@@ -69,2 +69,8 @@ # Changelog | ||
### Fixed | ||
- Newly added method `parse()` had critical typo. Method now has an improved api, and appropriate tests [[read more]](./README.md#parse) | ||
- Newly added method `parse()` had critical typo. Method now has an improved api, and appropriate tests [[read more]](./README.md#parse) | ||
### [1.4.0] = 2021-08-22 | ||
### Added | ||
- Added support for choosing the case ElectroDB will use when modeling a Partition or Sort Key. [[read more]](./README.md#using-electrodb-with-existing-data) | ||
- Added support for indexes to use fields that are shared with attribute fields. This should help users leverage ElectroDB with existing tables. [[read more]](./README.md#using-electrodb-with-existing-data) | ||
- Added Query Option `ignoreOwnership` to bypass ElectroDB checks/interrogations for ownership of an item before returning it. [[read more]](./README.md#query-options) |
{ | ||
"name": "electrodb", | ||
"version": "1.3.2", | ||
"version": "1.4.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", |
@@ -118,2 +118,14 @@ // # Errors: | ||
}, | ||
InvalidIndexWithAttributeName: { | ||
code: 1018, | ||
section: "invalid-index-with-attribute-name", | ||
name: "InvalidIndexWithAttributeName", | ||
sym: ErrorCode, | ||
}, | ||
InvalidCollectionOnIndexWithAttributeFieldNames: { | ||
code: 1019, | ||
section: "invalid-collection-on-index-with-attribute-field-names", | ||
name: "InvalidIndexCompositeWithAttributeName", | ||
sym: ErrorCode, | ||
}, | ||
MissingAttribute: { | ||
@@ -120,0 +132,0 @@ code: 2001, |
@@ -233,3 +233,3 @@ const {AttributeTypes, ItemOperations, AttributeProxySymbol, BuilderTypes} = require("./types"); | ||
class ExpressionState { | ||
constructor({prefix} = {}) { | ||
constructor({prefix, singleOccurrence} = {}) { | ||
this.names = {}; | ||
@@ -242,5 +242,9 @@ this.values = {}; | ||
this.prefix = prefix || ""; | ||
this.singleOccurrence = singleOccurrence; | ||
} | ||
incrementName(name) { | ||
if (this.singleOccurrence) { | ||
return `${this.prefix}${0}` | ||
} | ||
if (this.counts[name] === undefined) { | ||
@@ -373,3 +377,4 @@ this.counts[name] = 0; | ||
const attributeValues = []; | ||
for (const value of values) { | ||
for (let value of values) { | ||
value = target.format(value); | ||
// template.length is to see if function takes value argument | ||
@@ -376,0 +381,0 @@ if (template.length > 2) { |
@@ -1,2 +0,2 @@ | ||
const { CastTypes, ValueTypes, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes, TraverserIndexes } = require("./types"); | ||
const { CastTypes, ValueTypes, KeyCasing, AttributeTypes, AttributeMutationMethods, AttributeWildCard, PathTypes, TraverserIndexes } = require("./types"); | ||
const AttributeTypeNames = Object.keys(AttributeTypes); | ||
@@ -6,2 +6,3 @@ const ValidFacetTypes = [AttributeTypes.string, AttributeTypes.number, AttributeTypes.boolean, AttributeTypes.enum]; | ||
const u = require("./util"); | ||
const v = require("./validations"); | ||
const {DynamoDBSet} = require("./set"); | ||
@@ -100,2 +101,5 @@ | ||
this.validate = this._makeValidate(definition.validate); | ||
this.isKeyField = !!definition.isKeyField; | ||
this.unformat = this._makeDestructureKey(definition); | ||
this.format = this._makeStructureKey(definition); | ||
this.indexes = [...(definition.indexes || [])]; | ||
@@ -220,7 +224,8 @@ let {isWatched, isWatcher, watchedBy, watching, watchAll} = Attribute._destructureWatcher(definition); | ||
const getter = get || ((attr) => attr); | ||
return (values, siblings) => { | ||
return (value, siblings) => { | ||
if (this.hidden) { | ||
return; | ||
} | ||
return getter(values, siblings); | ||
value = this.unformat(value); | ||
return getter(value, siblings); | ||
} | ||
@@ -234,2 +239,28 @@ } | ||
_makeStructureKey({prefix = "", postfix = "", casing= KeyCasing.none} = {}) { | ||
return (key) => { | ||
let value = key; | ||
if (this.type === AttributeTypes.string && v.isStringHasLength(key)) { | ||
value = `${prefix}${key}${postfix}`; | ||
} | ||
return u.formatAttributeCasing(value, casing); | ||
} | ||
} | ||
_makeDestructureKey({prefix = "", postfix = "", casing= KeyCasing.none} = {}) { | ||
return (key) => { | ||
let value = ""; | ||
if (![AttributeTypes.string, AttributeTypes.enum].includes(this.type) || typeof key !== "string") { | ||
return key; | ||
} else if (key.length > prefix.length) { | ||
for (let i = prefix.length; i < key.length - postfix.length; i++) { | ||
value += key[i]; | ||
} | ||
} else { | ||
value = key; | ||
} | ||
return u.formatAttributeCasing(value, casing); | ||
}; | ||
} | ||
getPathType(type, parentType) { | ||
@@ -858,16 +889,74 @@ if (parentType === AttributeTypes.list || parentType === AttributeTypes.set) { | ||
} | ||
if (facets.fields && facets.fields.includes(name)) { | ||
continue; | ||
const field = attribute.field || name; | ||
let isKeyField = false; | ||
let prefix = ""; | ||
let postfix = ""; | ||
let casing = KeyCasing.none; | ||
if (facets.byField && facets.byField[field] !== undefined) { | ||
for (const indexName of Object.keys(facets.byField[field])) { | ||
let definition = facets.byField[field][indexName]; | ||
if (definition.facets.length > 1) { | ||
throw new e.ElectroError( | ||
e.ErrorCodes.InvalidIndexCompositeWithAttributeName, | ||
`Invalid definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore is not allowed to contain composite references to other attributes. Please either change the field name of the attribute, or redefine the index to use only the single attribute "${definition.field}".` | ||
) | ||
} | ||
if (definition.isCustom) { | ||
const keyFieldLabels = facets.labels[indexName][definition.type].labels; | ||
// I am not sure how more than two would happen but it would mean either | ||
// 1. Code prior has an unknown edge-case. | ||
// 2. Method is being incorrectly used. | ||
if (keyFieldLabels.length > 2) { | ||
throw new e.ElectroError( | ||
e.ErrorCodes.InvalidIndexWithAttributeName, | ||
`Unexpected definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore is not possible to have more than two labels as part of it's template. Please either change the field name of the attribute, or reformat the key template to reduce all pre-fixing or post-fixing text around the attribute reference to two.` | ||
) | ||
} | ||
isKeyField = true; | ||
casing = definition.casing; | ||
// Walk through the labels, given the above exception handling, I'd expect the first element to | ||
// be the prefix and the second element to be the postfix. | ||
for (const value of keyFieldLabels) { | ||
if (value.name === field) { | ||
prefix = value.label || ""; | ||
} else { | ||
postfix = value.label || ""; | ||
} | ||
} | ||
if (attribute.type !== AttributeTypes.string && !Array.isArray(attribute.type)) { | ||
if (prefix.length > 0 || postfix.length > 0) { | ||
throw new e.ElectroError(e.ErrorCodes.InvalidIndexWithAttributeName, `definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". Index templates may only have prefix or postfix values on "string" or "enum" type attributes. The ${definition.type} field "${field}" is type "${attribute.type}", and therefore cannot be used with prefixes or postfixes. Please either remove the prefixed or postfixed values from the template or change the field name of the attribute.`); | ||
} | ||
} | ||
} else { | ||
// Upstream middleware should have taken care of this. An error here would mean: | ||
// 1. Code prior has an unknown edge-case. | ||
// 2. Method is being incorrectly used. | ||
throw new e.ElectroError( | ||
e.ErrorCodes.InvalidIndexCompositeWithAttributeName, | ||
`Unexpected definition for "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore must be defined with a template. Please either change the field name of the attribute, or add a key template to the "${definition.type}" field on index "${u.formatIndexNameForDisplay(indexName)}" with the value: "\${${definition.field}}"` | ||
) | ||
} | ||
if (definition.inCollection) { | ||
throw new e.ElectroError( | ||
e.ErrorCodes.InvalidCollectionOnIndexWithAttributeFieldNames, | ||
`Invalid use of a collection on index "${u.formatIndexNameForDisplay(indexName)}". The ${definition.type} field "${definition.field}" shares a field name with an attribute defined on the Entity, and therefore the index is not allowed to participate in a Collection. Please either change the field name of the attribute, or remove all collection(s) from the index.` | ||
) | ||
} | ||
} | ||
} | ||
if (attribute.field && facets.fields && facets.fields.includes(attribute.field)) { | ||
continue; | ||
} | ||
let isKey = !!facets.byIndex && facets.byIndex[""].all.find((facet) => facet.name === name); | ||
let definition = { | ||
name, | ||
field, | ||
client, | ||
casing, | ||
prefix, | ||
postfix, | ||
traverser, | ||
client, | ||
isKeyField, | ||
label: attribute.label, | ||
required: !!attribute.required, | ||
field: attribute.field || name, | ||
default: attribute.default, | ||
@@ -874,0 +963,0 @@ validate: attribute.validate, |
const { Entity } = require("./entity"); | ||
const { clauses } = require("./clauses"); | ||
const { ServiceVersions, Pager, ElectroInstance, ElectroInstanceTypes, ModelVersions } = require("./types"); | ||
const { KeyCasing, ServiceVersions, Pager, ElectroInstance, ElectroInstanceTypes, ModelVersions } = require("./types"); | ||
const { FilterFactory } = require("./filters"); | ||
@@ -17,3 +17,3 @@ const { FilterOperations } = require("./operations"); | ||
unknown: "unknown" | ||
} | ||
}; | ||
@@ -284,3 +284,3 @@ function inferConstructorType(service) { | ||
for (let entity of Object.values(this.collectionSchema[collection].entities)) { | ||
if (entity.ownsPager(this.collectionSchema[collection].index, pager)) { | ||
if (entity.ownsPager(pager, this.collectionSchema[collection].index)) { | ||
matchingEntities.push(entity); | ||
@@ -333,3 +333,3 @@ } | ||
for (let entity of Object.values(this.collectionSchema[collection].entities)) { | ||
if (entity.ownsPager(this.collectionSchema[collection].index, pager)) { | ||
if (entity.ownsPager(pager, this.collectionSchema[collection].index)) { | ||
matchingIdentifiers.push({ | ||
@@ -408,4 +408,13 @@ [entity.identifiers.entity]: entity.getName(), | ||
_validateIndexCasingMatch(definition = {}, providedIndex = {}) { | ||
const definitionSk = definition.sk || {}; | ||
const providedSk = providedIndex.sk || {}; | ||
const pkCasingMatch = v.isMatchingCasing(definition.pk.casing, providedIndex.pk.casing); | ||
const skCasingMatch = v.isMatchingCasing(definitionSk.casing, providedSk.casing); | ||
return { | ||
pk: pkCasingMatch, | ||
sk: skCasingMatch | ||
}; | ||
} | ||
_validateCollectionDefinition(definition = {}, providedIndex = {}) { | ||
@@ -417,4 +426,5 @@ let indexMatch = definition.index === providedIndex.index; | ||
let collectionDifferences = []; | ||
let definitionIndexName = definition.index || "(Primary Index)"; | ||
let providedIndexName = providedIndex.index || "(Primary Index)"; | ||
let definitionIndexName = u.formatIndexNameForDisplay(definition.index); | ||
let providedIndexName = u.formatIndexNameForDisplay(providedIndex.index); | ||
let matchingKeyCasing = this._validateIndexCasingMatch(definition, providedIndex); | ||
if (pkFacetLengthMatch) { | ||
@@ -446,2 +456,14 @@ for (let i = 0; i < definition.pk.labels.length; i++) { | ||
} | ||
if (!matchingKeyCasing.pk) { | ||
collectionDifferences.push( | ||
`The pk property "casing" provided "${providedIndex.pk.casing || KeyCasing.default}" does not match established casing "${definition.pk.casing || KeyCasing.default}" on index "${providedIndexName}". Index casing options must match across all entities participating in a collection` | ||
); | ||
} | ||
if (!matchingKeyCasing.sk) { | ||
const definedSk = definition.sk || {}; | ||
const providedSk = providedIndex.sk || {}; | ||
collectionDifferences.push( | ||
`The sk property "casing" provided "${definedSk.casing || KeyCasing.default}" does not match established casing "${providedSk.casing || KeyCasing.default}" on index "${providedIndexName}". Index casing options must match across all entities participating in a collection` | ||
); | ||
} | ||
if (!indexMatch) { | ||
@@ -509,9 +531,9 @@ collectionDifferences.push( | ||
_processEntityKeys(definition = {}, providedIndex = {}) { | ||
_processEntityKeys(name, definition = {}, providedIndex = {}) { | ||
if (!Object.keys(definition).length) { | ||
definition = providedIndex; | ||
} | ||
let [invalidDefinition, invalidIndexMessages] = this._validateCollectionDefinition(definition, providedIndex); | ||
const [invalidDefinition, invalidIndexMessages] = this._validateCollectionDefinition(definition, providedIndex); | ||
if (invalidDefinition) { | ||
throw new e.ElectroError(e.ErrorCodes.InvalidJoin, invalidIndexMessages.join(", ")); | ||
throw new e.ElectroError(e.ErrorCodes.InvalidJoin, `Validation Error while joining entity, "${name}". ${invalidIndexMessages.join(", ")}`); | ||
} | ||
@@ -618,3 +640,3 @@ return definition; | ||
} | ||
this.collectionSchema[collection].keys = this._processEntityKeys(this.collectionSchema[collection].keys, providedIndex); | ||
this.collectionSchema[collection].keys = this._processEntityKeys(name, this.collectionSchema[collection].keys, providedIndex); | ||
this.collectionSchema[collection].attributes = this._processEntityAttributes(this.collectionSchema[collection].attributes, entity.model.schema.attributes); | ||
@@ -626,2 +648,3 @@ this.collectionSchema[collection].entities[name] = entity; | ||
this.collectionSchema[collection].collection[collectionIndex] = collection; | ||
} | ||
@@ -628,0 +651,0 @@ |
@@ -177,2 +177,9 @@ const KeyTypes = { | ||
const KeyCasing = { | ||
none: "none", | ||
upper: "upper", | ||
lower: "lower", | ||
default: "default", | ||
} | ||
module.exports = { | ||
@@ -182,2 +189,3 @@ Pager, | ||
CastTypes, | ||
KeyCasing, | ||
PathTypes, | ||
@@ -184,0 +192,0 @@ QueryTypes, |
const {AttributeOperationProxy, ExpressionState} = require("./operations"); | ||
const {ItemOperations, BuilderTypes} = require("./types"); | ||
const u = require("./util"); | ||
class UpdateExpression extends ExpressionState { | ||
constructor(props) { | ||
super(props); | ||
constructor(props = {}) { | ||
super({...props, singleOccurrence: true}); | ||
this.operations = { | ||
@@ -9,0 +8,0 @@ set: new Set(), |
@@ -81,2 +81,41 @@ const v = require("./validations"); | ||
function formatStringCasing(str, casing, defaultCase) { | ||
if (typeof str !== "string") { | ||
return str; | ||
} | ||
let strCase = defaultCase; | ||
if (v.isStringHasLength(casing) && typeof t.KeyCasing[casing] === "string") { | ||
strCase = t.KeyCasing.default === casing | ||
? defaultCase | ||
: t.KeyCasing[casing]; | ||
} | ||
switch (strCase) { | ||
case t.KeyCasing.upper: | ||
return str.toUpperCase(); | ||
case t.KeyCasing.none: | ||
return str; | ||
case t.KeyCasing.lower: | ||
return str.toLowerCase(); | ||
case t.KeyCasing.default: | ||
default: | ||
return str; | ||
} | ||
} | ||
function formatKeyCasing(str, casing) { | ||
return formatStringCasing(str, casing, t.KeyCasing.lower); | ||
} | ||
function formatAttributeCasing(str, casing) { | ||
return formatStringCasing(str, casing, t.KeyCasing.none); | ||
} | ||
function formatIndexNameForDisplay(index) { | ||
if (index) { | ||
return index; | ||
} else { | ||
return "(Primary Index)"; | ||
} | ||
} | ||
module.exports = { | ||
@@ -87,5 +126,8 @@ batchItems, | ||
getModelVersion, | ||
formatKeyCasing, | ||
genericizeJSONPath, | ||
commaSeparatedString, | ||
applyBetaModelOverrides | ||
formatAttributeCasing, | ||
applyBetaModelOverrides, | ||
formatIndexNameForDisplay | ||
}; |
const e = require("./errors"); | ||
const {KeyCasing} = require("./types") | ||
@@ -101,2 +102,7 @@ const Validator = require("jsonschema").Validator; | ||
}, | ||
casing: { | ||
type: "string", | ||
enum: ["upper", "lower", "none", "default"], | ||
required: false, | ||
} | ||
}, | ||
@@ -130,2 +136,7 @@ }, | ||
}, | ||
casing: { | ||
type: "string", | ||
enum: ["upper", "lower", "none", "default"], | ||
required: false, | ||
} | ||
}, | ||
@@ -328,13 +339,32 @@ }, | ||
function isMatchingCasing(casing1, casing2) { | ||
const equivalentCasings = [KeyCasing.default, KeyCasing.lower]; | ||
if (isStringHasLength(casing1) && isStringHasLength(casing2)) { | ||
let isRealCase = KeyCasing[casing1.toLowerCase()] !== undefined; | ||
let casingsMatch = casing1 === casing2; | ||
let casingsAreEquivalent = [casing1, casing2].every(casing => { | ||
return casing === KeyCasing.lower || casing === KeyCasing.default; | ||
}); | ||
return isRealCase && (casingsMatch || casingsAreEquivalent); | ||
} else if (isStringHasLength(casing1)) { | ||
return equivalentCasings.includes(casing1.toLowerCase()); | ||
} else if (isStringHasLength(casing2)) { | ||
return equivalentCasings.includes(casing2.toLowerCase()); | ||
} else { | ||
return casing1 === undefined && casing2 === undefined; | ||
} | ||
} | ||
module.exports = { | ||
model: validateModel, | ||
testModel, | ||
isFunction, | ||
stringArrayMatch, | ||
isMatchingCasing, | ||
isArrayHasLength, | ||
isNameModelRecordType, | ||
isStringHasLength, | ||
isObjectHasLength, | ||
stringArrayMatch, | ||
isFunction, | ||
isBetaServiceConfig, | ||
isNameEntityRecordType | ||
isNameModelRecordType, | ||
isNameEntityRecordType, | ||
model: validateModel | ||
}; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
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
642559
11161
5253