Пн goose пользовательская схема / тип с виртуальным заполнением работает с Model.populate, но не с Document.populate. - PullRequest
4 голосов
/ 06 апреля 2020

В духе 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, а не необработанное двоичное значение. Это когда-нибудь было бы возможно?

...