New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nested schema hooks doesn't run in some case #6669
Comments
@Iworb The idea of calling pre update query hooks on nested schemas was brought up a while back in a comment on #964, and #3125 but it doesn't appear to have been implemented. Can you elaborate on why you want to define the hook on the nested schema instead of the top level one? Also, |
Okay, @lineus, I want to be sure that |
Here's a complicated example: var itemSchema = function (path) {
const s = new Schema({
kind: {
type: String,
required: true,
validate: [
function (v) {
return mongoose.modelNames().includes(v)
},
'Not Found'
]
},
item: {
type: ObjectId,
refPath: path + '.kind', // <-- (1)
required: true
}
})
const validate = async function (doc, next) {
let Model
try {
Model = mongoose.model(doc.kind)
} catch (err) {
return next(false)
}
const found = await Model.findById(doc.item)
return found ? next() : next(false)
}
s.post('validate', validate)
s.pre('findOneAndUpdate', async function (next) {
return validate(this.getUpdate()['$set'][path], next)
})
return s
}
var parentSchema = new Schema({
name: String,
connections: [itemSchema('connections')],
users: [itemSchema('users')],
agents: [itemSchema('agents')],
});
const validate = async function (doc, next) {
// some parent document validation
return next()
}
parentSchema.post('validate', validate)
parentSchema.pre('findOneAndUpdate', async function (next) {
return validate(this.getUpdate()['$set'], next)
}) 1 - another question: is there a way to retrieve |
I'm going to break this into 2 separate answers as there are 2 questions here. Reusing a schema for multiple schemaPaths with dynamic refsWith mongoose as it exists today, your solution of returning the schema from a function with the refPath set based on the parameter of that function is as good a solution as any that I can think of. I do think it would be cool if refPath could accept a function that gets the doc and the current path as it's parameters. I've tested this out and have created PR #6683. With my proposed change the following example would make what you're doing easier. 6669_complete.js#!/usr/bin/env node
'use strict';
const assert = require('assert');
const mongoose = require('mongoose');
const opts = { useNewUrlParser: true };
mongoose.connect('mongodb://localhost:27017/gh-6669', opts);
const conn = mongoose.connection;
const Schema = mongoose.Schema;
const connectionSchema = new Schema({
destination: String
});
const Conn = mongoose.model('connection', connectionSchema);
const userSchema = new Schema({
name: String
});
const User = mongoose.model('user', userSchema);
const agentSchema = new Schema({
vendor: String
});
const Agent = mongoose.model('agent', agentSchema);
const subSchema = new Schema({
kind: {
type: String,
required: true,
validate: {
validator: function(v) {
return mongoose.modelNames().includes(v);
},
message: 'KindError: {VALUE} Collection: -1'
}
},
item: {
type: Schema.Types.ObjectId,
refPath: function(doc, path) {
return path.replace(/\.item$/,'.kind');
},
required: true
}
});
const recordSchema = new Schema({
name: String,
connections: [subSchema],
users: [subSchema],
agents: [subSchema]
});
const Record = mongoose.model('record', recordSchema);
const connection = new Conn({ destination: '192.168.1.15' });
const user = new User({ name: 'Kev' });
const agent = new Agent({ vendor: 'chrome' });
const record = new Record({
connections: [{ kind: 'connection', item: connection._id}],
users: [{ kind: 'user', item: user._id }],
agents: [{ kind: 'agent', item: agent._id }]
});
async function run() {
await conn.dropDatabase();
await connection.save();
await user.save();
await agent.save();
await record.save();
let doc = await Record.findOne()
.populate('connections.item')
.populate('users.item')
.populate('agents.item');
assert.strictEqual(doc.connections[0].item.destination, '192.168.1.15');
assert.strictEqual(doc.users[0].item.name, 'Kev');
assert.strictEqual(doc.agents[0].item.vendor, 'chrome');
return conn.close();
}
run(); Output:
Defining the
|
I agree with 1st part of your answer, really great job, I'll be waiting for this PR acception. recordSchema.pre('findOneAndUpdate', function() {
let update = this.getUpdate();
let promises = [];
for (let path of Object.keys(update)) {
update[path].forEach((p) => {
promises.push(validate(p)); // (1)
});
}
return Promise.all(promises);
});
const validate = function (doc) {
let Model;
try {
Model = mongoose.model(doc.kind); // (2)
return Model.findById(doc.item); // (3)
} catch (err) {
return Promise.reject(err);
}
}; (1) - bad idea. We could update recor's name or any other kind of field and this will throw exception about bad model name. |
I wasn't trying to write all of your code for you 😄, just the relevant part to your question. Hopefully I didn't do anything too silly in my PR. Your reuse of the subSchema is neat, I'm glad you brought this up! |
I'm going to close this one for now since #6683 is in. Running query middleware on subschemas is a tricky API to design because 1) what context do they run in? top-level query? and 2) do they run if the subdoc isn't in the query? How about if a nested path is in the query? I'm down to re-open if someone has a solid suggestion for how this API would work. |
Mongoose version: 5.2.1
In
findByIdAndUpdate
case nested hook doesn't run.The text was updated successfully, but these errors were encountered: