I got a document structured like the following. My question is how do I do the nested part "roles" validation on the database side. My requirements are:
the presence of name and created_by for a role if a role is created.
{
"_id": "123456",
"name": "User Name",
"roles": [
{
"name": "mobiles_user",
"last_usage_at": {
"$date": 1457000592991
},
"created_by": "987654",
"created_at": {
"$date": 1457000592991
}
},
{
"name": "webs_user",
"last_usage_at": {
"$date": 1457000592991
},
"created_by": "987654",
"created_at": {
"$date": 1457000592991
}
},
]
}
At the moment, I am only doing the following for those none nested attributes:
db.createCollection( "users",
{ "validator" : {
"_id" : {
"$type" : "string"
},
"email" : {
"$regex" : /@gmail\.com$/
},
"name" : {
"$type" : "string"
}
}
} )
Could anyone please advise that how to do the nested document validation?
Yes, you can validate all sub-documents in a document by negating $elemMatch
, and you can ensure that the size is not 1. It's sure not pretty though! And not exactly obvious either.
> db.createCollection('users', {
... validator: {
... name: {$type: 'string'},
... roles: {$exists: 'true'},
... $nor: [
... {roles: {$size: 1}},
... {roles: {$elemMatch: {
... $or: [
... {name: {$not: {$type: 'string'}}},
... {created_by: {$not: {$type: 'string'}}},
... ]
... }}}
... ],
... }
... })
{ "ok" : 1 }
This is confusing, but it works! What it means is only accept documents where neither the size of roles
is 1 nor roles
has an element with a name
that isn't a string
or a created_by
that isn't a string
.
This is based upon the fact that in logic terms,
for all x: f(x) and g(x)
Is equivalent to
not exists x s.t.: not f(x) or not g(x)
We have to use the latter since MongoDB only gives us an exists operator.
Valid documents work:
> db.users.insert({
... name: 'hello',
... roles: [],
... })
WriteResult({ "nInserted" : 1 })
> db.users.insert({
... name: 'hello',
... roles: [
... {name: 'foo', created_by: '2222'},
... {name: 'bar', created_by: '3333'},
... ]
... })
WriteResult({ "nInserted" : 1 })
If a field is missing from roles
, it fails:
> db.users.insert({
... name: 'hello',
... roles: [
... {name: 'foo', created_by: '2222'},
... {created_by: '3333'},
... ]
... })
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 121,
"errmsg" : "Document failed validation"
}
})
If a field in roles
has the wrong type, it fails:
> db.users.insert({
... name: 'hello',
... roles: [
... {name: 'foo', created_by: '2222'},
... {name: 'bar', created_by: 3333},
... ]
... })
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 121,
"errmsg" : "Document failed validation"
}
})
If roles
has size 1 it fails:
> db.users.insert({
... name: 'hello',
... roles: [
... {name: 'foo', created_by: '2222'},
... ]
... })
WriteResult({
"nInserted" : 0,
"writeError" : {
"code" : 121,
"errmsg" : "Document failed validation"
}
})
The only thing I can't figure out unfortunately is how to ensure that roles is an array. roles: {$type: 'array'}
seems to fail everything, I presume because it's actually checking that the elements are of type 'array'
?
Edit: this answer is not correct, it is possible to validate all sub-documents in the array. See answer: https://stackoverflow.com/a/43102783/200224
You can't really. You can do things like:
"roles.name": { "$type": "string" }
But all that really means is at "at least one" of those properties need match the specified type. That means this would actually be valid:
{
"_id" : "123456",
"name" : "User Name",
"roles" : [
{
"name" : "mobiles_user",
"last_usage_at" : ISODate("2016-03-03T10:23:12.991Z"),
"created_by" : "987654",
"created_at" : ISODate("2016-03-03T10:23:12.991Z")
},
{
"name" : "webs_user",
"last_usage_at" : ISODate("2016-03-03T10:23:12.991Z"),
"created_by" : "987654",
"created_at" : ISODate("2016-03-03T10:23:12.991Z")
},
{
"name" : 1
}
]
}
It is afterall "documement validation" and that is by nature not well suited to sub-documents in arrays, or any data in a contained array really.
The core of the implementation relies on expressions available to query operators, and since MongoDB lacks anythin in standard query expressions that equates to "All array entries must match this value" without being directly specific then it's not possible to express as a validator condition.
The only posibility to check array content like that in a "query" expression is using $where
, and that is noted to not be an available option with document validation.
Even the $size
operator available for queries must match a specific "size" value, and cannot use an in-equality condition. So you "could" verify a strict size, but not a minimal size, unless:
"roles.0": { "$exists": true }
This is a feature in "infancy" and somewhat experimental, so there is the possibility that future releases may address this.
But for now, your better option is to do such "schema validation" in client side code ( where you will get a lot better exception reporting ) instead. There are many libraries already existing that take that approach.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With