From 773b921f7a33bb1c53478ba0c7a9204cc9c35b15 Mon Sep 17 00:00:00 2001 From: David Dobrynin Date: Tue, 8 Jan 2019 15:35:48 -0800 Subject: [PATCH 1/5] Add errorUnknown Schema option --- lib/Attribute.js | 1 + lib/Schema.js | 2 ++ test/Schema.js | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/lib/Attribute.js b/lib/Attribute.js index 790c4b5af..5e3751364 100644 --- a/lib/Attribute.js +++ b/lib/Attribute.js @@ -579,6 +579,7 @@ Attribute.prototype.parseDynamo = async function(json) { for(const [name, value] of Object.entries(v)) { let subAttr = attr.attributes[name]; // if saveUnknown is activated the input has an unknown attribute, let's create one on the fly. + if (!subAttr && attr.schema.options.errorUnknown) throw new errors.ParseError(`unknown attribute ${name}: ${JSON.stringify(value)}`); if (!subAttr && (attr.schema.options.saveUnknown || Array.isArray(attr.options.saveUnknown) && attr.options.saveUnknown.indexOf(name) >= 0)) { subAttr = createUnknownAttributeFromDynamo(attr.schema, name, value); attr.schema.attributes[name] = subAttr; diff --git a/lib/Schema.js b/lib/Schema.js index 7f1990647..d9061e30a 100644 --- a/lib/Schema.js +++ b/lib/Schema.js @@ -181,9 +181,11 @@ Schema.prototype.toDynamo = async function(model, options) { Schema.prototype.parseDynamo = async function(model, dynamoObj) { + debug('MADE IT') for(const name in dynamoObj) { let attr = this.attributes[name]; + if (!attr && this.options.errorUnknown) throw new errors.ParseError(`unknown top-level attribute ${name} on model ${model.$__.name}: ${JSON.stringify(dynamoObj[name])}`) if((!attr && this.options.saveUnknown === true) || (Array.isArray(this.options.saveUnknown) && this.options.saveUnknown.indexOf(name) >= 0)) { attr = Attribute.createUnknownAttributeFromDynamo(this, name, dynamoObj[name]); this.attributes[name] = attr; diff --git a/test/Schema.js b/test/Schema.js index 273bff9fe..dbea702d0 100644 --- a/test/Schema.js +++ b/test/Schema.js @@ -1213,6 +1213,46 @@ describe('Schema tests', function (){ }); }); + + it('Errors when encountering an unknown attribute if errorUnknown is set to true', async function () { + const schema = new Schema({ + knownAttribute: String, + }, { + errorUnknown: true, + }); + + let err; + const model = {['$__']: { + name: 'OnlyKnownAttributesModel' + }}; + try { + await schema.parseDynamo(model, { + knownAttribute: { S: 'I am known to the schema. Everything is groovy.' }, + unknownAttribute: { S: 'I am but a stranger to the schema. I should cause an error.' } + }); + } catch (e) { + err = e; + } + + err.should.be.instanceof(errors.ParseError); + err = undefined; + + try { + await schema.parseDynamo(model, { + knownAttribute: { S: 'I am known to the schema. Everything is groovy.' }, + myMap: { + M: { + nestedUnknownAttribute: { S: 'I too am a stranger. Will the schema be able to find me down here?' } + } + } + }); + } catch (e) { + err = e; + } + + err.should.be.instanceof(errors.ParseError); + }); + it('Should throw error when type is map but no map is provided', function (done) { let err; try { From 63a5ea1d91a3f51639f9502b711f9f6a741eecf4 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Thu, 10 Jan 2019 14:45:39 -0800 Subject: [PATCH 2/5] Update lib/Schema.js Co-Authored-By: dobrynin --- lib/Schema.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Schema.js b/lib/Schema.js index d9061e30a..4e84ad161 100644 --- a/lib/Schema.js +++ b/lib/Schema.js @@ -185,7 +185,9 @@ Schema.prototype.parseDynamo = async function(model, dynamoObj) { for(const name in dynamoObj) { let attr = this.attributes[name]; - if (!attr && this.options.errorUnknown) throw new errors.ParseError(`unknown top-level attribute ${name} on model ${model.$__.name}: ${JSON.stringify(dynamoObj[name])}`) + if (!attr && this.options.errorUnknown) { + throw new errors.ParseError(`Unknown top-level attribute ${name} on model ${model.$__.name}: ${JSON.stringify(dynamoObj[name])}`) + } if((!attr && this.options.saveUnknown === true) || (Array.isArray(this.options.saveUnknown) && this.options.saveUnknown.indexOf(name) >= 0)) { attr = Attribute.createUnknownAttributeFromDynamo(this, name, dynamoObj[name]); this.attributes[name] = attr; From 90889164c9581deb75a0a26f75dc47533129afc8 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Thu, 10 Jan 2019 14:45:59 -0800 Subject: [PATCH 3/5] Update lib/Attribute.js Co-Authored-By: dobrynin --- lib/Attribute.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Attribute.js b/lib/Attribute.js index 5e3751364..dba579402 100644 --- a/lib/Attribute.js +++ b/lib/Attribute.js @@ -579,7 +579,9 @@ Attribute.prototype.parseDynamo = async function(json) { for(const [name, value] of Object.entries(v)) { let subAttr = attr.attributes[name]; // if saveUnknown is activated the input has an unknown attribute, let's create one on the fly. - if (!subAttr && attr.schema.options.errorUnknown) throw new errors.ParseError(`unknown attribute ${name}: ${JSON.stringify(value)}`); + if (!subAttr && attr.schema.options.errorUnknown) { + throw new errors.ParseError(`Unknown attribute ${name}: ${JSON.stringify(value)}`); + } if (!subAttr && (attr.schema.options.saveUnknown || Array.isArray(attr.options.saveUnknown) && attr.options.saveUnknown.indexOf(name) >= 0)) { subAttr = createUnknownAttributeFromDynamo(attr.schema, name, value); attr.schema.attributes[name] = subAttr; From 0abaef797db77452b697e3229fc30d45c4f1c561 Mon Sep 17 00:00:00 2001 From: David Dobrynin Date: Thu, 10 Jan 2019 16:04:22 -0800 Subject: [PATCH 4/5] Fix bugs, improve error messages, improve tests --- lib/Attribute.js | 13 +++++++------ lib/Schema.js | 9 +++++++-- test/Schema.js | 46 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 57 insertions(+), 11 deletions(-) diff --git a/lib/Attribute.js b/lib/Attribute.js index dba579402..3e65159be 100644 --- a/lib/Attribute.js +++ b/lib/Attribute.js @@ -575,16 +575,17 @@ Attribute.prototype.parseDynamo = async function(json) { if(!v){ return; } let val = {}; + const { attributes, schema } = attr; // loop over all the properties of the input for(const [name, value] of Object.entries(v)) { - let subAttr = attr.attributes[name]; + let subAttr = attributes[name]; // if saveUnknown is activated the input has an unknown attribute, let's create one on the fly. - if (!subAttr && attr.schema.options.errorUnknown) { - throw new errors.ParseError(`Unknown attribute ${name}: ${JSON.stringify(value)}`); + if (!subAttr && schema.options.errorUnknown) { + throw new errors.ParseError(`Unknown nested attribute ${name} with value: ${JSON.stringify(value)}`); } - if (!subAttr && (attr.schema.options.saveUnknown || Array.isArray(attr.options.saveUnknown) && attr.options.saveUnknown.indexOf(name) >= 0)) { - subAttr = createUnknownAttributeFromDynamo(attr.schema, name, value); - attr.schema.attributes[name] = subAttr; + if (!subAttr && (schema.options.saveUnknown || Array.isArray(schema.options.saveUnknown) && schema.options.saveUnknown.indexOf(name) >= 0)) { + subAttr = createUnknownAttributeFromDynamo(schema, name, value); + attr.attributes[name] = subAttr; } if (subAttr) { const attrVal = await subAttr.parseDynamo(value); diff --git a/lib/Schema.js b/lib/Schema.js index 4e84ad161..a85ef63ba 100644 --- a/lib/Schema.js +++ b/lib/Schema.js @@ -181,12 +181,17 @@ Schema.prototype.toDynamo = async function(model, options) { Schema.prototype.parseDynamo = async function(model, dynamoObj) { - debug('MADE IT') for(const name in dynamoObj) { let attr = this.attributes[name]; if (!attr && this.options.errorUnknown) { - throw new errors.ParseError(`Unknown top-level attribute ${name} on model ${model.$__.name}: ${JSON.stringify(dynamoObj[name])}`) + const hashKey = this.hashKey && this.hashKey.name && dynamoObj[this.hashKey.name] && JSON.stringify(dynamoObj[this.hashKey.name]); + const rangeKey = this.rangeKey && this.rangeKey.name && JSON.stringify(dynamoObj[this.rangeKey.name]); + let errorMessage = `Unknown top-level attribute ${name} on model ${model.$__.name} with `; + if (hashKey) errorMessage += `hash-key ${hashKey} and `; + if (rangeKey) errorMessage += `range-key ${rangeKey} and `; + errorMessage += `value: ${JSON.stringify(dynamoObj[name])}`; + throw new errors.ParseError(errorMessage) } if((!attr && this.options.saveUnknown === true) || (Array.isArray(this.options.saveUnknown) && this.options.saveUnknown.indexOf(name) >= 0)) { attr = Attribute.createUnknownAttributeFromDynamo(this, name, dynamoObj[name]); diff --git a/test/Schema.js b/test/Schema.js index dbea702d0..774e19648 100644 --- a/test/Schema.js +++ b/test/Schema.js @@ -956,9 +956,11 @@ describe('Schema tests', function (){ }, anotherMap: { M: { - aNestedAttribute: { S: 'I am a nested attribute' } + aNestedAttribute: { S: 'I am a nested unknown sub-attribute of a known top-level attribute' }, + weHaveTheSameName: { S: 'I should be independent of the top-level field with the same name' }, } }, + weHaveTheSameName: { N: 123 }, listAttrib: { L: [ { S: 'v1' }, @@ -974,8 +976,10 @@ describe('Schema tests', function (){ aNumber: 5, }, anotherMap: { - aNestedAttribute: 'I am a nested attribute' + aNestedAttribute: 'I am a nested unknown sub-attribute of a known top-level attribute', + weHaveTheSameName: 'I should be independent of the top-level field with the same name' }, + weHaveTheSameName: 123, listAttrib: [ 'v1', 'v2', @@ -1216,6 +1220,14 @@ describe('Schema tests', function (){ it('Errors when encountering an unknown attribute if errorUnknown is set to true', async function () { const schema = new Schema({ + myHashKey: { + hashKey: true, + type: String, + }, + myRangeKey: { + rangeKey: true, + type: String, + }, knownAttribute: String, }, { errorUnknown: true, @@ -1227,6 +1239,8 @@ describe('Schema tests', function (){ }}; try { await schema.parseDynamo(model, { + myHashKey: 'I am the hash key', + myRangeKey: 'I am the range key', knownAttribute: { S: 'I am known to the schema. Everything is groovy.' }, unknownAttribute: { S: 'I am but a stranger to the schema. I should cause an error.' } }); @@ -1235,10 +1249,35 @@ describe('Schema tests', function (){ } err.should.be.instanceof(errors.ParseError); - err = undefined; + err.message.should.equal('Unknown top-level attribute unknownAttribute on model OnlyKnownAttributesModel with hash-key "I am the hash key" and range-key "I am the range key" and value: {"S":"I am but a stranger to the schema. I should cause an error."}'); + }); + + + it('Errors when encountering an unknown nested attribute if errorUnknown is set to true', async function () { + const schema = new Schema({ + myHashKey: { + hashKey: true, + type: String, + }, + myRangeKey: { + rangeKey: true, + type: String, + }, + knownAttribute: String, + myMap: Map, + }, { + errorUnknown: true, + }); + + let err; + const model = {['$__']: { + name: 'OnlyKnownAttributesModel' + }}; try { await schema.parseDynamo(model, { + myHashKey: 'I am the hash key', + myRangeKey: 'I am the range key', knownAttribute: { S: 'I am known to the schema. Everything is groovy.' }, myMap: { M: { @@ -1251,6 +1290,7 @@ describe('Schema tests', function (){ } err.should.be.instanceof(errors.ParseError); + err.message.should.match(/Unknown nested attribute nestedUnknownAttribute with value: {"S":"I too am a stranger. Will the schema be able to find me down here\?"}/); }); it('Should throw error when type is map but no map is provided', function (done) { From 40cb26908fef3a197abac70efd07f3c3748e7017 Mon Sep 17 00:00:00 2001 From: David Dobrynin Date: Fri, 11 Jan 2019 17:24:41 -0800 Subject: [PATCH 5/5] Add errorUnknown documentation --- docs/_docs/schema.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/_docs/schema.md b/docs/_docs/schema.md index 5051478ff..17012d2ef 100644 --- a/docs/_docs/schema.md +++ b/docs/_docs/schema.md @@ -412,6 +412,16 @@ var schema = new Schema({...}, { }); ``` +**errorUnknown**: boolean + +Specifies that any attributes not defined in the _schema_ will throw an error if encountered while parsing records from DynamoDB. This defaults to false. + +```js +var schema = new Schema({...}, { + errorUnknown: true +}); +``` + **attributeToDynamo**: function A function that accepts `name, json, model, defaultFormatter, options`.