В духе mon goose -uuid2 я пытаюсь создать собственный SchemaType для замены обычного _id типами UUID. Цель состоит в том, чтобы создать не только SchemaType, но и связанный с ним класс Type, и mimi c, как ObjectId на самом деле является типом. Поэтому я реализовал классы SchemaType и Type, как рекомендовано, и переопределил такие вещи, как toString, toObject и toBSON, чтобы класс Type возвращал базовый объект Buffer.Binary, который сохраняется в MongoDB для собственных операций.
Все хорошо, КРОМЕ ТОГО, когда занимаешься населением. Пн goose используйте String (id) , чтобы связать _id выбранных документов с их связанным родительским документом. Вот почему я переопределяю функцию toString, чтобы она возвращала String ([лежащий в основе буфер]) и исправил Model.populate, но не Document.populate.
Давайте перейдем к коду. Вот большой, но не минимальный пример:
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 );
})();
Теперь вот вывод (с некоторыми отредактированными отладочными console.logs):
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 }
Я вижу несколько вещей , Не виртуальные отношения singleB и multiB не создают проблем при работе с Document.populate. Виртуальное отношение multi C завершается ошибкой с multi C в случае Document.populate, но преуспевает в Model.populate.
Как это исправить, чтобы все случаи работали должным образом?
Бонус! Я хотел бы, чтобы функция toString возвращала UUID, а не необработанное двоичное значение. Это когда-нибудь было бы возможно?