In the vein of mongoose-uuid2, I am trying to create a custom SchemaType for replacing the regular _id with UUID types. The goal is to create not just a SchemaType, but a related Type class as well, and mimic how ObjectId is actually a type. So I've implemented a SchemaType and Type class, as recommended, and overrode things like toString, toObject, and toBSON so that the Type class will return the underlying Buffer.Binary object that gets saved to the MongoDB for native operations.
All is good EXCEPT when doing populates. Mongoose use String(id) to relate the _id of the fetched documents to their linked parent document. So that's why I override the toString function to return String([underlying buffer]) and that fixed Model.populate but not Document.populate.
Let's get to the code. Here is the large not-so-minimal example:
const config = require( './lib/config' );
const mongoose = require( 'mongoose' );
const bson = require( 'bson' );
const util = require( 'util' );
const uuidParse = require( 'uuid-parse' );
const uuidv4 = require( 'uuid/v4' );
/**
* Convert a buffer to a UUID string
*/
stringFromBuffer = function( buf, len ) {
var hex = '';
if( len==null )
len = buf.length;
for (var i = 0; i < len; i++) {
var n = buf.readUInt8(i);
if (n < 16){
hex += '0' + n.toString(16);
} else {
hex += n.toString(16);
}
}
const s = hex.substr(0, 8) + '-' + hex.substr(8, 4) + '-' + hex.substr(12, 4) + '-' + hex.substr(16, 4) + '-' + hex.substr(20, 12);
//console.log( "getter returning hex" , s );
return( s );
}
/**
* Our helper TypeUUID class
*/
class TypeUUID {
/**
* Construct
*/
constructor( value ) {
// Set our type for checks
this.isMongooseUUID = true;
// Set our internal value
this.buffer = TypeUUID.convertToBuffer( value );
}
/**
* To BSON
*/
toBSON() {
if( this.buffer==null ) {
console.log( "toBSON returning null" )
return( null );
}
const r = this.buffer.toBSON();
console.log( "toBSON returning buffer converted.", r );
return( r );
}
/**
* To object
*/
toObject( options ) {
if( this.buffer==null ) {
console.log( "toObject returning null" );
return( null );
}
const r = this.buffer.toObject( options );
console.log( "toObject returning buffer converted.", r );
return( r );
}
/**
* The equals
*/
equals( other ) {
if( this.buffer==other )
return( true );
return( this.buffer.equals( TypeUUID.convertToBuffer( other ) ) );
}
/**
* Generate it
*/
static generate() {
// Generate
return( new TypeUUID( uuidv4() ) );
}
/**
* Convert the value from whatever it is
*/
static convertToBuffer( value ) {
let r;
// To buffer
if( value instanceof TypeUUID )
r = new mongoose.Types.Buffer( value.value );
else if( value==null )
return( null );
else if( value instanceof mongoose.Types.Buffer.Binary )
r = new mongoose.Types.Buffer( value.buffer );
else if( typeof( value )=== 'string' )
r = new mongoose.Types.Buffer( uuidParse.parse( value ) );
else if( value instanceof mongoose.Types.Buffer )
r = new mongoose.Types.Buffer( value );
else // How did this happen?
throw new Error( 'Could not cast ' + value + ' to UUID.' );
// Set the correct subtype
r.subtype( bson.Binary.SUBTYPE_UUID );
// Return it
return( r );
}
/**
* Stringy
*/
toString() {
if( this.buffer==null )
return( null );
//return( stringFromBuffer( this.buffer ) );
// The above will break Model.populate
return( String( this.buffer ) );
}
/**
* To JSON
*/
toJSON() {
return( this.valueOf() );
}
/**
* Value of
*/
valueOf() {
return( stringFromBuffer( this.buffer ) );
//return( String( this.buffer ) );
//return( this.toString() );
}
/**
* Convenience function
*/
static from( value ) {
return( new TypeUUID( value ) );
}
}
/**
* Schema type
*/
function SchemaUUID( path, options ) {
mongoose.SchemaTypes.Buffer.call( this, path, options );
}
SchemaUUID.get = mongoose.SchemaType.get;
SchemaUUID.set = mongoose.SchemaType.set;
util.inherits( SchemaUUID, mongoose.SchemaTypes.Buffer );
SchemaUUID.schemaName = 'UUID';
SchemaUUID.prototype.checkRequired = function( value ) {
console.log( "checkRequired", value );
// Either one
return( value && value.isMongooseUUID || value instanceof mongoose.Types.Buffer.Binary );
};
SchemaUUID.prototype.cast = function( value, doc, init ) {
// Nulls and undefineds aren't helpfill
if( value==null )
return( value );
// Is it a mongoose UUID dingy?
if( value.isMongooseUUID )
return( value );
// Is it already a binary?
if( value instanceof mongoose.Types.Buffer.Binary )
return( TypeUUID.from( value ) );
// It's a UUID string?
if( typeof( value )==='string' )
return( TypeUUID.from( value ) );
// Does this helpful?
if( value._id )
return( value._id );
throw new Error('Could not cast ' + value + ' to UUID.');
};
SchemaUUID.prototype.castForQuery = function( $conditional, val ) {
console.log( "castForQuery", $conditional, val );
var handler;
if (arguments.length === 2) {
handler = this.$conditionalHandlers[$conditional];
console.log( "Got handler", handler );
if (!handler) {
throw new Error("Can't use " + $conditional + " with UUID.");
}
return handler.call(this, val);
}
return( this.cast( $conditional ) )
};
// Add them to mongoose
mongoose.Types.UUID = TypeUUID;
mongoose.SchemaTypes.UUID = SchemaUUID;
// Do what the warnings say
mongoose.set( 'useNewUrlParser', true );
mongoose.set( 'useUnifiedTopology', true );
// Connect
{
const {
user,
password,
servers,
database,
authSource,
ssl
} = config.mongoose;
mongoose.connect( `mongodb://${user}:${password}@${servers.join(',')}/${database}?authSource=${authSource}&ssl=${ssl?"true":"false"}` );
}
const db = mongoose.connection
.on( 'error', console.log )
.on( 'open', ()=>console.log( "MongoDB connexion opened." ) );
// Shorthand to allow lazy
const { Schema, Types } = mongoose;
/**
* Current texting code pair
*/
const Aschema = new Schema( {
'_id' : { 'type' : Schema.Types.UUID, 'default' : Types.UUID.generate }, // Our ID
'singleB' : { 'type' : Schema.Types.UUID, 'ref' : "B" },
'multiB' : [ { 'type' : Schema.Types.UUID, 'ref' : "B" } ],
} );
const A = mongoose.model( "A", Aschema );
Aschema.virtual( "multiC", {
'ref' : "C",
'foreignField' : "aID",
'localField' : "_id",
'justOne' : false
} );
const B = mongoose.model( "B", new Schema( {
'_id' : { 'type' : Schema.Types.UUID, 'default' : Types.UUID.generate }, // Our ID
'name' : String
} ) );
const C = mongoose.model( "C", new Schema( {
'_id' : { 'type' : Schema.Types.UUID, 'default' : Types.UUID.generate }, // Our ID
'name' : String,
'aID' : { 'type' : Schema.Types.UUID, 'ref' : "A" }
} ) );
// Do testing here
(async function(){
try {
// Insert B's
const bs = await Promise.all( [ new B( { 'name' : "Beam" } ).save(), new B( { 'name' : "Truth" } ).save() ] );
console.log( "Our B's.", bs );
// Now insert A with B's.
let a = await new A( {
'singleB' : bs[ 0 ],
'multiB' : bs
} ).save();
// Check both
console.log( "Our A", a );
// Insert C's
const cs = await Promise.all( [
new C( { 'name' : "Got", 'aID' : a } ).save(),
new C( { 'name' : "Milk", 'aID' : a } ).save()
] );
//console.log( "C's are", await C.find( {} ).exec() );
// Fetch A using Model.populate
//a = await A.findOne( {} ).populate( "multiC" ).exec();
a = await A.findOne( {} ).populate( "singleB" ).populate( "multiB" ).populate( "multiC" ).exec();
// WORKS!
console.log( "Fetched A", a );
// Now fetch using document populate
a = await A.findOne( {} ).exec();
console.log( "Unpopulated A", a );
//await a.populate( "multiC" ).execPopulate();
await a.populate( "singleB" ).populate( "multiB" ).populate( "multiC" ).execPopulate();
// Does not work
console.log( "Populated A", a );
// Clear test
await A.deleteMany( {} ).exec();
await B.deleteMany( {} ).exec();
await C.deleteMany( {} ).exec();
}
catch( e ) {
console.error( "Why did we get here?", e );
}
// Exit not hang
process.exit( 0 );
})();
Now here is the output (with some debug console.logs redacted):
Our B's. [ { name: 'Beam',
_id: '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
__v: 0 },
{ name: 'Truth',
_id: '35d79f47-9706-4b31-812b-dd50c79fa3fa',
__v: 0 } ]
Our A { multiB:
[ '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
'35d79f47-9706-4b31-812b-dd50c79fa3fa' ],
singleB: '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
_id: '0be17cc0-9f01-40c4-b344-edd8ef11d761',
__v: 0 }
Fetched A { multiB:
[ { _id: '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
name: 'Beam',
__v: 0 },
{ _id: '35d79f47-9706-4b31-812b-dd50c79fa3fa',
name: 'Truth',
__v: 0 } ],
_id: '0be17cc0-9f01-40c4-b344-edd8ef11d761',
singleB:
{ _id: '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
name: 'Beam',
__v: 0 },
__v: 0,
multiC:
[ { _id: '4d61aa05-dea8-4f57-9835-b6b0e6d38d92',
name: 'Got',
aID: '0be17cc0-9f01-40c4-b344-edd8ef11d761',
__v: 0 },
{ _id: '276ffc29-1f56-412e-9adb-9ab32069bd20',
name: 'Milk',
aID: '0be17cc0-9f01-40c4-b344-edd8ef11d761',
__v: 0 } ] }
Unpopulated A { multiB:
[ '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
'35d79f47-9706-4b31-812b-dd50c79fa3fa' ],
_id: '0be17cc0-9f01-40c4-b344-edd8ef11d761',
singleB: '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
__v: 0 }
Populated A { multiB:
[ { _id: '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
name: 'Beam',
__v: 0 },
{ _id: '35d79f47-9706-4b31-812b-dd50c79fa3fa',
name: 'Truth',
__v: 0 } ],
_id: '0be17cc0-9f01-40c4-b344-edd8ef11d761',
singleB:
{ _id: '1f6f47ac-c454-4495-9fc1-f8d5041648d6',
name: 'Beam',
__v: 0 },
__v: 0 }
I can see a few things. The non-virtual singleB and multiB relations work no problem when doing Document.populate. The multiC virtual relation fails with multiC in the case of Document.populate but succeeds in Model.populate.
How can I fix it so that all cases will work as expected?
Bonus! I'd love if the toString function would return a UUID and not the raw binary value. Would that ever be a possibility?