sylvainpolletvillard/ObjectModel

How to nest SealedModels?

eponymous301 opened this issue · 8 comments

Using the SealedModel() function from http://objectmodel.js.org/docs/examples/sealed.js, if I try to nest one sealed model inside another I get a validation error, e.g.:

    const NameModel = SealedModel({first: String, last: String})

    const PersonModel = SealedModel({age: Number, name: NameModel})

    const input = {
      age: 20,
      name: {
        first: 'John',
        last: 'Smith'
      }
    }

    PersonModel(input)

produces

TypeError: Undeclared properties in the sealed model definition: name.first,name.last

Is there a way to allow above composition?

(I am using v4.3.0 with node.js v16.15.0)

Thanks!

Hello,

Yeah I did not think about nested models when writing this example of SealedModel, but that's an easy fix.

We will consider than when encoutering another nested model in the definition, the parent model is trusting its child model to validate its properties. So if you use SealedModel for child models as well, you should be fine.

Here is the completed SealedModel code:

import { ObjectModel } from "objectmodel";

const SealedModel = def => {
	let model = ObjectModel(def);
	model.sealed = true;
	model.extend = () => {
		throw new Error(`Sealed models cannot be extended`);
	};

	const checkUndeclaredProps = (obj, def, undeclaredProps, path) => {
		Object.keys(obj).forEach(key => {
			let val = obj[key],
				subpath = path ? path + "." + key : key;
			if(def instanceof Model){
				// trust nested model props validation
			} else if (!Object.prototype.hasOwnProperty.call(def, key)) {
				undeclaredProps.push(subpath);
			} else if (val && typeof val === "object" && Object.getPrototypeOf(val) === Object.prototype) {
				checkUndeclaredProps(val, def[key], undeclaredProps, subpath);
			}
		});
	};

	return model.assert(
		function hasNoUndeclaredProps(obj) {
			if (!model.sealed) return true;
			let undeclaredProps = [];
			checkUndeclaredProps(obj, this.definition, undeclaredProps);
			return undeclaredProps.length === 0 ? true : undeclaredProps;
		},
		undeclaredProps =>
			`Undeclared properties in the sealed model definition: ${undeclaredProps}`
	);
};

export default SealedModel;

Thanks for your report, I'll update it on the website as well.

Excellent, thank you!

Doc note - missing line import {Model} from 'objectmodel' at top of http://objectmodel.js.org/docs/examples/sealed.js

I noticed an issue when trying to add a nested sealed model as an optional field:

const NameModel = SealedModel({
  first: String,
  last: String
})

const PossiblyAnonymousPersonModel = SealedModel({
  age: Number,
  name: [NameModel]
})

const x = PossiblyAnonymousPersonModel({
  age: 20,
  name: {
	first: 'John',
	last: 'Doe'
  }
})

// TypeError: Undeclared properties in the sealed model definition: name.first,name.last

Omitting the optional nested model altogether works fine:

const y = PossiblyAnonymousPersonModel({
  age: 20
})

Setting the fields in the inner model to optional still produces the undeclared properties message, although only for provided input fields:

const NameModel = SealedModel({
  first: [String],
  last: [String]
})

const PossiblyAnonymousPersonModel = SealedModel({
  age: Number,
  name: [NameModel]
})

const z = PossiblyAnonymousPersonModel({
  age: 20,
  name: {
	first: "John"
  }
})

//  Undeclared properties in the sealed model definition: name.first

Yes, the code of the SealedModel example is perfectible. I updated it , is it better now ?
http://objectmodel.js.org/docs/examples/sealed.js

Yes, optional nested models working now with SealedModel(). Thank you!

Ah, sorry, now getting Undeclared properties in the sealed model definition: 0,1,2... appended to errors if I pass in a string, e.g.

const z = PossiblyAnonymousPersonModel('foobar')

// TypeError: expecting {
//         age: Number, 
//         name: [{
//                         first: [String], 
//                         last: [String] 
//                 }] 
// }, got String "foobar"
// Undeclared properties in the sealed model definition: 0,1,2,3,4,5

Oh yeah that's an easy fix, the assertion should not be run at all if the argument is not an object

Add if(typeof obj !== "object") return; at the first line of checkUndeclaredProps assertion, like so:

import { ObjectModel } from "objectmodel";

const SealedModel = def => {
	const model = ObjectModel(def);
	model.sealed = true;
	model.extend = () => {
		throw new Error(`Sealed models cannot be extended`);
	};

	const isPlainObject = obj => typeof obj === "object" && Object.getPrototypeOf(obj) === Object.prototype
	const checkUndeclaredProps = (obj, def, undeclaredProps, path) => {
                if(typeof obj !== "object" || obj === null) return;
		Object.keys(obj).forEach(key => {
			let val = obj[key],
				subpath = path ? path + "." + key : key;
			if(isPlainObject(def) && !Object.prototype.hasOwnProperty.call(def, key)) {
				undeclaredProps.push(subpath);
			} else if (isPlainObject(val) && isPlainObject(def)) {
				checkUndeclaredProps(val, def[key], undeclaredProps, subpath);
			}
		});
	};

	return model.assert(
		function hasNoUndeclaredProps(obj) {
			if (!model.sealed) return true;
			let undeclaredProps = [];
			checkUndeclaredProps(obj, this.definition, undeclaredProps);
			return undeclaredProps.length === 0 ? true : undeclaredProps;
		},
		undeclaredProps =>
			`Undeclared properties in the sealed model definition: ${undeclaredProps}`
	);
};

export default SealedModel;

That fixed, thank you!