-
Notifications
You must be signed in to change notification settings - Fork 41
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
Use mongoose to validate method to check for ID validity #113
base: master
Are you sure you want to change the base?
Conversation
assuming every _id field needs an ObjectId This method is slightly inefficient as it does use try to validate every other field in the schema as well. If there's a way to limit mongoose to validating a single field, we should do that, but this is the best method I've found so far. Potential solution to ethanresnick#80
Thanks for submitting this. I'll try to take a look at it soon but, as I'm sure you've noticed, I haven't had much time for this project recently. Is there any chance you can reformat these changes to make the diff as small as possible? That will make it much easier for me to follow what's changed and get things merged quickly. |
It's not actually as bad as it looks. I had to change Not sure why that one test is failing. I've spent so much time trying to figure out this project's tests and CI system, but it still does stuff I don't understand. They pass for me locally, even with the same node and npm versions. The only other thing I can think of to reproduce the failure is using Ubuntu rather than OS X, but I have no idea why that would be an issue. |
TIL you can add https://github.com/ethanresnick/json-api/pull/113/files?diff=unified&w=1 |
Thanks Carl! The smaller diff definitely helps. I'll try to add comments inline. Also, can you add some new tests for the case that this change is supposed to address? |
if (!this.idIsValid(idOrIds)) { | ||
throw new APIError(404, undefined, "No matching resource found.", "Invalid ID."); | ||
} | ||
ids = ids.filter(id => Boolean(id)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this line for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove falsely values. because if we passed null
as the _id
when validating then the validation passes. Could maybe be changed to
ids = ids.filter(id => typeof id === "string");
or we could change the change the line 617 to
const testDoc = new model({ _id: String(id) });
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both of those seem like they'd reject the integer 0 as an id. But 0 should be allowed right, if the point is to go beyond ObjectIds? I think it's legal in mongo.
Honestly, null, false, etc could arguably be allowed too, but I'm not sure if mongoose treats them specially (e.g. as though the user hasn't provided a value), or if we have code elsewhere that assumes the id isn't present if it's falsey.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The perfect is the enemy of the good, though, so I could also be persuaded to leave this as is (i.e. with just the falsey check), so long as we documented that. (I.e., "Ids can be ObjectIds or any truthy value".)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit: actually, we probably shouldn't just silently filter out invalid ids; instead, they should trigger an error too, right? In which case this check could also be incorporated into the definition of idIsValid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For idIsValid
, yes, null
should reject. It's a bit more complicated for getIdQueryType
though because a null
input is used to say no ID query should be returned, and therefore all records will eventually be resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. But I think that case only applies if a single id is passed in. (I.e., if idOrIds
is [null]
, that should be an error, right?) So I think I'd replace the filter with a conditional that only checks when a single ids is passed in. (I'm not sure what the resolution there should be though; see comment.)
Also, it'd be nice to clarify that mode
and ids
are based on the same isArray check. Finally, I think idIsValid
should resolve to true or false, not to the id/error (since it has "is" in the name). Edit: Or, idIsValid
should be renamed, which might be better!
Putting those together:
static getIdQueryType(idOrIds) {
if(idOrIds == null) {
return Q(["find", undefined]);
}
const [mode, ids] = Array.isArray(idOrIds) ? ["find", idOrIds] : ["findOne", [idOrIds]];
const idValidationPromises = ids.map(id => this.idIsValid(id, model));
return Q.all(idValidationPromises).then(() => {
const idQuery = { _id: mode === "find" ? { $in: ids } : ids[0] };
return [ mode, idQuery];
}, (err) => {
// ....
});
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, that method is nicer. Pushed.
I'm not sure which resolution makes more sense, but the latter makes the tests pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Carl! Re the resolution, maybe resolving with ["find", undefined]
is correct? (i.e. not using findOne at all). Does that make the tests pass?
(I updated code the in comment above accordingly.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed
Somehow that test that used to fail on Circle has started passing. It's just eslint that's failing now. |
eslint@latest and babel-eslint@latest are currently incompatible and so I have pinned [email protected] for now, as per the advice in babel/babel-eslint#267
Linting now passes, but coverall is failing. I've never used coverall, so I haven't got a clue what the problem is. This might be helpful: lemurheavy/coveralls-public#632 |
|
||
return [mode, idQuery]; | ||
return Q.all(idValidationPromises).then((idsAreValid) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, now we're back to waiting for all the validations to finish before we run the then
block, even if we could skip to throwing an error earlier.
This is my fault... I said earlier that, if we're going to keep isIdValid
named as it is, it should resolve with true or false. But, what I meant was that it should resolve with true if the id is valid but reject with false otherwise. That way, the basic structure of getIdQueryType
can be as it was in my last comment.
I realize that rejecting with false is a bit weird semantically, though, which is why I edited my earlier comment to say that it's probably better to just rename isIdValid
. So that's what I'd do now.
I.e., I'd rename isIdValid
to validateId
, which can then resolve/reject as you had originally (i.e., with the valid id on success, and the validation error on failure). And then the getIdQueryType
can work with the two different handlers.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh, right. I think I finally understand what you were saying earlier. Yes, we should be failing as soon as possible. We could maintain the semantics of idIsValid()
by doing something like this:
const idValidationPromises = ids.map(id => this.idIsValid(id, Model).then(isValid => {
if (!isValid) {
throw new Error('ID is invalid');
}
}));
Do you think retaining the semantics of idIsValid
is worth the extra complexity in getIdQueryType
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could maintain the semantics of idIsValid() by doing something like this
That's a pretty clever solution! I like it.
Still, though, I think I'd slightly prefer just renaming idIsValid
so that settling the promise with richer values (i.e. the id and the error, respectively) then makes sense. I lean that way only because we have the validation error information within that function, so we might as well not throw it away (in case future callers want to do something with it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Pushed
@carlbennettnz Don't worry about coveralls. It's going to fail for you no matter what you do, because of a permissions error. (Specifically, my coveralls API key isn't available to your code when CircleCI runs it.) This makes some sense, but it's annoying. I opened an issue somewhere to see if this could be worked around, but there hasn't been any movement. As long as the tests pass, you're fine. |
Awesome! Thanks @carlbennettnz! I'm heading into a meeting now, but I'll take a final look at this later tonight. I think it should be good to go! |
Sweet. Do you want me to merge commits now, or after you get a chance to review? |
if (!this.idIsValid(idOrIds)) { | ||
return Q.all(idValidationPromises).then(() => { | ||
const idQuery = { _id: mode === "find" ? { $in: ids } : ids[0] }; | ||
return [ mode, ids.length ? idQuery : undefined ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this ids.length ? idQuery : undefined
ternary needed?
If I read correctly, it means that calling getIdQueryOrType
with []
will resolve to ["find", undefined]
, which I don't think is what we want, since I'd interpret an array of ids—even if it's empty—to be a query for docs whose ids are from that array, rather than interpreting it as a findAll query. (This seems pedantic, but I can imagine cases where it might come up.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. It's left over from a previous version
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, wouldn't the proper test be idOrIds != null ? idQuery : undefined
? If you pass []
you get an empty query, if you pass null
you get undefined
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty sure that's right. Pushed.
Just finished making notes! Re merging the commits now or later: I don't mind either way, but it probably makes sense to wait until all the code's finalized, just so you don't have to do it again if we catch any last bits to change |
Which is better than assuming every _id field needs an ObjectId.
This method is slightly inefficient as it does try to validate every other field in the schema as well. If there's a way to limit mongoose to validating a single field, we should do that, but this is the best method I've found so far.
Potential solution to #80