-
Notifications
You must be signed in to change notification settings - Fork 40
Use mongoose to validate method to check for ID validity #113
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
base: master
Are you sure you want to change the base?
Changes from all commits
954b021
da805eb
6c51686
eb23428
08701c1
4653d58
c6ce8eb
5a1458c
e1a3815
2c5a9d3
3afbb88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import mongoose from "mongoose"; | ||
|
||
const schema = mongoose.Schema({ | ||
_id: { | ||
type: Number, | ||
required: true | ||
} | ||
}); | ||
|
||
export default mongoose.model("NumericId", schema); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import mongoose from "mongoose"; | ||
|
||
const schema = mongoose.Schema({ | ||
_id: { | ||
type: String, | ||
required: true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just out of curiosity, if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the empty string test |
||
} | ||
}); | ||
|
||
export default mongoose.model("StringId", schema); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,13 @@ | ||
import Q from "q"; | ||
import {expect} from "chai"; | ||
import APIError from "../../../../src/types/APIError"; | ||
import mongoose from "mongoose"; | ||
import MongooseAdapter from "../../../../src/db-adapters/Mongoose/MongooseAdapter"; | ||
|
||
const School = mongoose.model("School"); | ||
const NumericId = mongoose.model("NumericId"); | ||
const StringId = mongoose.model("StringId"); | ||
|
||
describe("Mongoose Adapter", () => { | ||
describe("its instances methods", () => { | ||
describe("getModel", () => { | ||
|
@@ -69,68 +74,199 @@ describe("Mongoose Adapter", () => { | |
}); | ||
|
||
describe("getIdQueryType", () => { | ||
it("should handle null input", () => { | ||
const res = MongooseAdapter.getIdQueryType(); | ||
expect(res[0]).to.equal("find"); | ||
expect(res[1]).to.be.undefined; | ||
it("should handle null input", function(done) { | ||
MongooseAdapter.getIdQueryType(null, School).then(([ mode, idQuery ]) => { | ||
expect(mode).to.equal("find"); | ||
expect(idQuery).to.be.undefined; | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
describe("string", () => { | ||
it("should throw on invalid input", () => { | ||
const fn = function() { MongooseAdapter.getIdQueryType("1"); }; | ||
expect(fn).to.throw(APIError); | ||
it("should reject on invalid ObjectId input", function(done) { | ||
MongooseAdapter.getIdQueryType("1", School).then(res => { | ||
expect(false).to.be.ok; | ||
}, err => { | ||
expect(err).to.be.instanceof(APIError); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should reject on invalid numeric ID input", function(done) { | ||
MongooseAdapter.getIdQueryType("123abc", NumericId).then(res => { | ||
expect(false).to.be.ok; | ||
}, err => { | ||
expect(err).to.be.instanceof(APIError); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should produce query on valid ObjectId input", function(done) { | ||
MongooseAdapter.getIdQueryType("552c5e1c604d41e5836bb174", School).then(([ mode, idQuery ]) => { | ||
expect(mode).to.equal("findOne"); | ||
expect(idQuery._id).to.equal("552c5e1c604d41e5836bb174"); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should produce query on valid input", () => { | ||
const res = MongooseAdapter.getIdQueryType("552c5e1c604d41e5836bb174"); | ||
expect(res[0]).to.equal("findOne"); | ||
expect(res[1]._id).to.equal("552c5e1c604d41e5836bb174"); | ||
it("should produce query on valid numeric ID input", function(done) { | ||
MongooseAdapter.getIdQueryType("0", NumericId).then(([ mode, idQuery ]) => { | ||
expect(mode).to.equal("findOne"); | ||
expect(idQuery._id).to.equal("0"); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should produce query on valid string ID input", function(done) { | ||
MongooseAdapter.getIdQueryType("null", StringId).then(([ mode, idQuery ]) => { | ||
expect(mode).to.equal("findOne"); | ||
expect(idQuery._id).to.equal("null"); | ||
done(); | ||
}).catch(done); | ||
}); | ||
}); | ||
|
||
describe("array", () => { | ||
it("should throw if any ids are invalid", () => { | ||
const fn = function() { MongooseAdapter.getIdQueryType(["1", "552c5e1c604d41e5836bb174"]); }; | ||
expect(fn).to.throw(APIError); | ||
it("should throw if any ObjectIds are invalid", function(done) { | ||
MongooseAdapter.getIdQueryType(["1", "552c5e1c604d41e5836bb174"], School).then(res => { | ||
expect(false).to.be.ok; | ||
}, err => { | ||
expect(err).to.be.instanceof(APIError); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should throw if any numeric ids are invalid", function(done) { | ||
MongooseAdapter.getIdQueryType(["abc", "123"], NumericId).then(res => { | ||
expect(false).to.be.ok; | ||
}, err => { | ||
expect(err).to.be.instanceof(APIError); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should produce query on valid input", () => { | ||
const res = MongooseAdapter.getIdQueryType(["552c5e1c604d41e5836bb174", "552c5e1c604d41e5836bb175"]); | ||
expect(res[0]).to.equal("find"); | ||
expect(res[1]._id.$in).to.be.an.Array; | ||
expect(res[1]._id.$in[0]).to.equal("552c5e1c604d41e5836bb174"); | ||
expect(res[1]._id.$in[1]).to.equal("552c5e1c604d41e5836bb175"); | ||
it("should produce query on valid ObjectId input", function(done) { | ||
MongooseAdapter.getIdQueryType(["552c5e1c604d41e5836bb174", "552c5e1c604d41e5836bb175"], School).then(([ mode, idQuery ]) => { | ||
expect(mode).to.equal("find"); | ||
expect(idQuery._id.$in).to.be.an.Array; | ||
expect(idQuery._id.$in[0]).to.equal("552c5e1c604d41e5836bb174"); | ||
expect(idQuery._id.$in[1]).to.equal("552c5e1c604d41e5836bb175"); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should produce query on valid numeric ID input", function(done) { | ||
MongooseAdapter.getIdQueryType(["0", "1"], NumericId).then(([ mode, idQuery ]) => { | ||
expect(mode).to.equal("find"); | ||
expect(idQuery._id.$in).to.be.an.Array; | ||
expect(idQuery._id.$in[0]).to.equal("0"); | ||
expect(idQuery._id.$in[1]).to.equal("1"); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should produce query on valid string ID input", function(done) { | ||
MongooseAdapter.getIdQueryType(["a", "null"], StringId).then(([ mode, idQuery ]) => { | ||
expect(mode).to.equal("find"); | ||
expect(idQuery._id.$in).to.be.an.Array; | ||
expect(idQuery._id.$in[0]).to.equal("a"); | ||
expect(idQuery._id.$in[1]).to.equal("null"); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should produce an empty query when passed an empty array of ids", function(done) { | ||
MongooseAdapter.getIdQueryType([], School).then(([ mode, idQuery ]) => { | ||
expect(mode).to.equal("find"); | ||
expect(idQuery._id.$in).to.be.an.Array | ||
expect(idQuery._id.$in).to.have.lengthOf(0); | ||
done(); | ||
}).catch(done); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("idIsValid", () => { | ||
it("should reject all == null input", () => { | ||
expect(MongooseAdapter.idIsValid()).to.not.be.ok; | ||
expect(MongooseAdapter.idIsValid(null)).to.not.be.ok; | ||
expect(MongooseAdapter.idIsValid(undefined)).to.not.be.ok; | ||
describe("validateId", () => { | ||
it("should refuse all == null input", function(done) { | ||
const tests = [ | ||
MongooseAdapter.validateId(null, School), | ||
MongooseAdapter.validateId(null, NumericId), | ||
MongooseAdapter.validateId(null, StringId), | ||
MongooseAdapter.validateId(undefined, School), | ||
MongooseAdapter.validateId(undefined, NumericId), | ||
MongooseAdapter.validateId(undefined, StringId) | ||
]; | ||
|
||
Q.allSettled(tests).then((res) => { | ||
res.forEach(result => expect(result.state).to.equal("rejected")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure es6 promises don't have a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not really. ES6 Promises don't include an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about something like this? const tests = [/*... */];
const allTestsRejectedPromise = Q.Promise(function(resolve, reject) {
let rejectedCount = 0;
const incrementRejectedCount = () => {
rejectedCount++;
if(rejectedCount === tests.length) {
resolve();
}
}
// if any test promise *resolves*, we *reject* the outer promise,
// which represents whether all the tests were *rejected*.
// And, if the test rejects as expected, we increment a counter which
// will resolve the outer promise once all the tests are rejected.
tests.forEach((testPromise) => testPromise.then(reject, incrementRejectedCount));
}); (And once you have a promise for allTestsRejected, then it's easy to tie that to an expect. And if the above is a common pattern, it should be easy to put it in a helper function for the other test cases.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well yes, it's just that's a very common pattern, so common that we have third party libraries that do that and lots of other useful things. I'm not sure what value you get out of reimplementing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Having to write just a few lines in order to remove a larger library is worth it imo; it means one less API for me to remember, and for contributors to learn. But there's also a conceptual issue here: while |
||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should reject bad input type", () => { | ||
expect(MongooseAdapter.idIsValid(true)).to.not.be.ok; | ||
it("should refuse a bad input type", function(done) { | ||
const tests = [ | ||
MongooseAdapter.validateId(true, School), | ||
MongooseAdapter.validateId(false, School), | ||
MongooseAdapter.validateId("not hex", School), | ||
MongooseAdapter.validateId([], School), | ||
MongooseAdapter.validateId("1234567890abcdef", School), | ||
MongooseAdapter.validateId(1234567890, School), | ||
MongooseAdapter.validateId("NaN", NumericId), | ||
MongooseAdapter.validateId("one", NumericId), | ||
MongooseAdapter.validateId([], NumericId), | ||
MongooseAdapter.validateId(true, NumericId), | ||
MongooseAdapter.validateId(false, NumericId) | ||
// StringId should except anything != null | ||
]; | ||
|
||
Q.allSettled(tests).then((res) => { | ||
res.forEach(result => { | ||
expect(result.state).to.equal("rejected"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above |
||
expect(result.reason.name).to.equal("CastError"); | ||
expect(result.reason.path).to.equal("_id"); | ||
}); | ||
|
||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
it("should reject empty string", () => { | ||
expect(MongooseAdapter.idIsValid("")).to.not.be.ok; | ||
it("should refuse an empty string", function(done) { | ||
const tests = [ | ||
MongooseAdapter.validateId("", School), | ||
MongooseAdapter.validateId("", NumericId), | ||
MongooseAdapter.validateId("", StringId) | ||
]; | ||
|
||
Q.allSettled(tests).then((res) => { | ||
res.forEach(result => { | ||
expect(result.state).to.equal("rejected"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above |
||
expect(result.reason.name).to.match(/ValidatorError|CastError/); | ||
expect(result.reason.path).to.equal("_id"); | ||
}); | ||
done(); | ||
}).catch(done); | ||
}); | ||
|
||
// the string coming into the MongooseAdapter needs to be the 24-character, | ||
// hex encoded version of the ObjectId, not an arbitrary 12 byte string. | ||
it("should reject 12-character strings", () => { | ||
expect(MongooseAdapter.idIsValid("aaabbbccc111")).to.not.be.ok; | ||
}); | ||
|
||
it("should reject numbers", () => { | ||
expect(MongooseAdapter.idIsValid(1)).to.not.be.ok; | ||
it("should reject 12-character strings", function(done) { | ||
MongooseAdapter.validateId("aaabbbccc111", School) | ||
.then(() => expect(false).to.be.ok) | ||
.catch(() => done()); | ||
}); | ||
|
||
it("should accpet valid hex string", () => { | ||
expect(MongooseAdapter.idIsValid("552c5e1c604d41e5836bb175")).to.be.ok; | ||
it("should accpet valid IDs", function(done) { | ||
const tests = [ | ||
MongooseAdapter.validateId("552c5e1c604d41e5836bb175", School), | ||
MongooseAdapter.validateId("123", NumericId), | ||
MongooseAdapter.validateId(123, NumericId), | ||
MongooseAdapter.validateId("0", NumericId), | ||
MongooseAdapter.validateId(0, NumericId), | ||
MongooseAdapter.validateId("0", StringId), | ||
MongooseAdapter.validateId("null", StringId) | ||
]; | ||
|
||
Q.all(tests).then((res) => done(), done); | ||
}); | ||
}); | ||
|
||
|
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.
In JSON API, a resource's id needs to be a string....so I'm not sure how we want to handle that. For now, it might make sense to only allow strings or ObjectIds??
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.
Or maybe we just need to add something in the serialization layer to make sure that, even if the id is a number, it's always serialized as a string? (including in linkage)
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.
I would go with the second option. I can imagine use cases where someone has an existing schema with numeric IDs and they want to put a JSON API-style API in front of 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.
Sounds good to me!
If we do that, though, I think we should make sure the serialization's working before/as part of merging in support for Number ids. So, do you want to add that serialization stuff to this PR? Or keep this PR just to strings and ObjectIds, and then make a second one that deals with Numbers (validation/queries + serialization)? I don't mind either way, but I could understand if you're feeling anxious to get this merged and so would prefer the former.
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.
Wouldn't
serializedId = String(unserializedId)
cover 99% of use cases? If you're thinking of providing a full serialization API, that could be useful, but surely that's a tiny, tiny edge case.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.
Yup, and that's basically what I'm thinking. That logic just has to be applied (at least in the Document type, but maybe other places I'm not thinking of).