import { ClassType , CompilerContext } from '@deepkit/core' ; import { ClassSchema , getClassSchema , getSortedUnionTypes , PropertySchema } from '@deepkit/type' ; import { BaseParser , ParserV2 } from './bson-parser' ; import { bsonTypeGuards } from './bson-typeguards' ; import { seekElementSize } from './continuation' ; import { BSON_BINARY_SUBTYPE_UUID , BSONType , digitByteSize } from './utils' ; function createPropertyConverter ( setter , property , compiler , parentProperty ? ) { const defaultValue = ! property . hasDefaultValue && property . defaultValue !== undefined ? `${ compiler . reserveVariable ( 'defaultValue' , property . defaultValue ) }()` : 'undefined' ; if ( property . type === 'promise' ) { property = property . templateArgs [ 0 ] || new PropertySchema ( property . name ) ; } const nullCheck = `
    if (elementType === ${ BSONType . UNDEFINED }) {
        if (${ property . isOptional }) ${ setter } = ${ defaultValue };
    } else if (elementType === ${ BSONType . NULL }) {
        if (${ property . isOptional }) ${ setter } = ${ defaultValue };
        if (${ property . isNullable }) ${ setter } = null;
    }` ; const nullOrSeek = `
        ${ nullCheck } else {
            seekElementSize(elementType, parser);
        }
    ` ; const propertyVar = compiler . reserveVariable ( '_property_' + property . name , property ) ; if ( property . type === 'string' ) { return `
            if (elementType === ${ BSONType . STRING }) {
                const size = parser.eatUInt32(); //size
                ${ setter } = parser.eatString(size);
            } else {
                ${ nullOrSeek }
            }
        ` ; } else if ( property . type === 'literal' ) { const literalValue = compiler . reserveVariable ( 'literalValue' , property . literalValue ) ; if ( property . isOptional || property . isNullable ) { return `
                if (${ property . isOptional } && (elementType === ${ BSONType . UNDEFINED } || elementType === ${ BSONType . NULL })) {
                    ${ setter } = ${ defaultValue };
                } else if (${ property . isNullable } && elementType === ${ BSONType . NULL }) {
                    ${ setter } = null;
                } else {
                    ${ setter } = ${ literalValue };
                    seekElementSize(elementType, parser);
                }
            ` ; } else { return `
                ${ setter } = ${ literalValue };
                seekElementSize(elementType, parser);
            ` ; } } else if ( property . type === 'enum' ) { return `
        if (elementType === ${ BSONType . STRING } || elementType === ${ BSONType . NUMBER } || elementType === ${ BSONType . INT } || elementType === ${ BSONType . LONG }) {
            ${ setter } = parser.parse(elementType);
        } else {
            ${ nullOrSeek }
        }` ; } else if ( property . type === 'boolean' ) { return `
            if (elementType === ${ BSONType . BOOLEAN }) {
                ${ setter } = parser.parseBoolean();
            } else {
                ${ nullOrSeek }
            }
        ` ; } else if ( property . type === 'date' ) { return `
            if (elementType === ${ BSONType . DATE }) {
                ${ setter } = parser.parseDate();
            } else {
                ${ nullOrSeek }
            }
        ` ; } else if ( property . type === 'bigint' ) { return `
            if (elementType === ${ BSONType . INT }) {
                ${ setter } = BigInt(parser.parseInt());
            } else if (elementType === ${ BSONType . NUMBER }) {
                ${ setter } = BigInt(parser.parseNumber());
            } else if (elementType === ${ BSONType . LONG } || elementType === ${ BSONType . TIMESTAMP }) {
                ${ setter } = parser.parseLong();
            } else if (elementType === ${ BSONType . BINARY }) {
                ${ setter } = parser.parseBinary();
            } else {
                ${ nullOrSeek }
            }
        ` ; } else if ( property . type === 'number' ) { return `
            if (elementType === ${ BSONType . INT }) {
                ${ setter } = parser.parseInt();
            } else if (elementType === ${ BSONType . NUMBER }) {
                ${ setter } = parser.parseNumber();
            } else if (elementType === ${ BSONType . LONG } || elementType === ${ BSONType . TIMESTAMP }) {
                ${ setter } = parser.parseLong();
            } else {
                ${ nullOrSeek }
            }
        ` ; } else if ( property . type === 'uuid' ) { return `
            if (elementType === ${ BSONType . BINARY }) {
                parser.eatUInt32(); //size
                const subType = parser.eatByte();
                if (subType !== ${ BSON_BINARY_SUBTYPE_UUID }) throw new Error('${ property . name } BSON binary type invalid. Expected UUID(4), but got ' + subType);
                ${ setter } = parser.parseUUID();
            } else {
                ${ nullOrSeek }
            }
            ` ; } else if ( property . type === 'objectId' ) { return `
            if (elementType === ${ BSONType . OID }) {
                ${ setter } = parser.parseOid();
            } else {
                ${ nullOrSeek }
            }
            ` ; } else if ( property . type === 'partial' ) { const object = compiler . reserveVariable ( 'partialObject' , { } ) ; const propertyCode = [ ] ; const schema = property . getResolvedClassSchema ( ) ; for ( let subProperty of schema . getProperties ( ) ) { const bufferCompare = [ ] ; for ( let i = 0 ; i < subProperty . name . length ; i ++ ) { bufferCompare . push ( `parser.buffer[parser.offset + ${ i }] === ${ subProperty . name . charCodeAt ( i ) }` ) ; } bufferCompare . push ( `parser.buffer[parser.offset + ${ subProperty . name . length }] === 0` ) ; propertyCode . push ( `
            //partial: property ${ subProperty . name } (${ subProperty . toString ( ) })
            if (${ bufferCompare . join ( ' && ' ) }) {
                parser.offset += ${ subProperty . name . length } + 1;
                ${ createPropertyConverter ( `${ object }.${ subProperty . name }` , subProperty , compiler ) }
                continue;
            }
        ` ) ; } return `
        if (elementType === ${ BSONType . OBJECT }) {
            ${ object } = {};
            const end = parser.eatUInt32() + parser.offset;

            while (parser.offset < end) {
                const elementType = parser.eatByte();
                if (elementType === 0) break;

                ${ propertyCode . join ( '\n' ) }

                //jump over this property when not registered in schema
                while (parser.offset < end && parser.buffer[parser.offset++] != 0);

                //seek property value
                if (parser.offset >= end) break;
                seekElementSize(elementType, parser);
            }
            ${ setter } = ${ object };
        } else {
            ${ nullOrSeek }
        }
    ` ; } else if ( property . type === 'class' ) { const schema = property . getResolvedClassSchema ( ) ; if ( schema . decorator ) { const forwardProperty = schema . getDecoratedPropertySchema ( ) ; const decoratedVar = compiler . reserveVariable ( 'decorated' ) ; const classTypeVar = compiler . reserveVariable ( 'classType' , property . classType ) ; let arg = '' ; const propertyAssign = [ ] ; const check = forwardProperty . isOptional ? `'v' in ${ decoratedVar }` : `${ decoratedVar }.v !== undefined` ; if ( forwardProperty . methodName === 'constructor' ) { arg = `(${ check } ? ${ decoratedVar }.v : undefined)` ; } else { propertyAssign . push ( `if (${ check }) ${ setter }.${ forwardProperty . name } = ${ decoratedVar }.v;` ) ; } return `
            //decorated
            if (elementType !== ${ BSONType . UNDEFINED } && elementType !== ${ BSONType . NULL }) {
                ${ decoratedVar } = {};
                ${ createPropertyConverter ( `${ decoratedVar }.v` , schema . getDecoratedPropertySchema ( ) , compiler ) }

                ${ setter } = new ${ classTypeVar }(${ arg });
                ${ propertyAssign . join ( '\n' ) }
            } else {
                ${ nullOrSeek }
            }
            ` ; } const propertySchema = compiler . reserveVariable ( '_propertySchema_' + property . name , property . getResolvedClassSchema ( ) ) ; compiler . context . set ( 'getRawBSONDecoder' , getRawBSONDecoder ) ; let primaryKeyHandling = '' ; if ( property . isReference ) { primaryKeyHandling = createPropertyConverter ( setter , property . getResolvedClassSchema ( ) . getPrimaryField ( ) , compiler ) ; } return `
            if (elementType === ${ BSONType . OBJECT }) {
                ${ setter } = getRawBSONDecoder(${ propertySchema })(parser);
            } else if (${ property . isReference }) {
                ${ primaryKeyHandling }
            } else {
                ${ nullOrSeek }
            }
            ` ; } else if ( property . type === 'array' ) { compiler . context . set ( 'digitByteSize' , digitByteSize ) ; const v = compiler . reserveName ( 'v' ) ; return `
        if (elementType === ${ BSONType . ARRAY }) {
            ${ setter } = [];
            parser.seek(4);
            for (let i = 0; ; i++) {
                const elementType = parser.eatByte();
                if (elementType === 0) break;

                //arrays are represented as objects, so we skip the key name
                parser.seek(digitByteSize(i));

                let ${ v } = undefined;
                ${ createPropertyConverter ( v , property . getSubType ( ) , compiler , property ) }
                ${ setter }.push(${ v });
            }
        } else {
            ${ nullOrSeek }
        }
        ` ; } else if ( property . type === 'map' ) { const name = compiler . reserveVariable ( 'propertyName' ) ; return `
        if (elementType === ${ BSONType . OBJECT }) {
            ${ setter } = {};
            parser.seek(4);
            while (true) {
                const elementType = parser.eatByte();
                if (elementType === 0) break;

                ${ name } = parser.eatObjectPropertyName();

                ${ createPropertyConverter ( `${ setter }[${ name }]` , property . getSubType ( ) , compiler ) }
            }
        } else {
            ${ nullOrSeek }
        }
        ` ; } else if ( property . type === 'union' ) { let discriminator = [ `if (false) {\n}` ] ; const discriminants = [ ] ; for ( const unionType of getSortedUnionTypes ( property , bsonTypeGuards ) ) { discriminants . push ( unionType . property . type ) ; } for ( const unionType of getSortedUnionTypes ( property , bsonTypeGuards ) ) { const guardVar = compiler . reserveVariable ( 'guard_' + unionType . property . type , unionType . guard ) ; discriminator . push ( `
                //guard:${ unionType . property . type }
                else if (${ guardVar }(elementType, parser)) {
                    ${ createPropertyConverter ( setter , unionType . property , compiler , property ) }
                }
            ` ) ; } return `
            ${ discriminator . join ( '\n' ) }
            else {
                ${ nullOrSeek }
            }
        ` ; } return `
        ${ nullCheck } else {
            ${ setter } = parser.parse(elementType, ${ propertyVar });
        }
    ` ; } interface DecoderFn { buildId : number ; ( parser , offset ? ) : any ; } function createSchemaDecoder ( schema ) { const compiler = new CompilerContext ( ) ; compiler . context . set ( 'seekElementSize' , seekElementSize ) ; const setProperties = [ ] ; const constructorArgumentNames = [ ] ; const constructorParameter = schema . getMethodProperties ( 'constructor' ) ; const resetDefaultSets = [ ] ; const setDefaults = [ ] ; const propertyCode = [ ] ; for ( const property of schema . getProperties ( ) ) { const bufferCompare = [ ] ; for ( let i = 0 ; i < property . name . length ; i ++ ) { bufferCompare . push ( `parser.buffer[parser.offset + ${ i }] === ${ property . name . charCodeAt ( i ) }` ) ; } bufferCompare . push ( `parser.buffer[parser.offset + ${ property . name . length }] === 0` ) ; const valueSetVar = compiler . reserveVariable ( 'valueSetVar' , false ) ; const defaultValue = property . defaultValue !== undefined ? compiler . reserveVariable ( 'defaultValue' , property . defaultValue ) : 'undefined' ; if ( property . hasManualDefaultValue ( ) || property . type === 'literal' ) { resetDefaultSets . push ( `${ valueSetVar } = false;` ) ; if ( property . defaultValue !== undefined ) { setDefaults . push ( `if (!${ valueSetVar }) object.${ property . name } = ${ defaultValue };` ) ; } else if ( property . type === 'literal' && ! property . isOptional ) { setDefaults . push ( `if (!${ valueSetVar }) object.${ property . name } = ${ JSON . stringify ( property . literalValue ) };` ) ; } } else if ( property . isNullable ) { resetDefaultSets . push ( `${ valueSetVar } = false;` ) ; setDefaults . push ( `if (!${ valueSetVar }) object.${ property . name } = null;` ) ; } const check = property . isOptional ? `${ JSON . stringify ( property . name ) } in object` : `object.${ property . name } !== undefined` ; if ( property . methodName === 'constructor' ) { constructorArgumentNames [ constructorParameter . indexOf ( property ) ] = `(${ check } ? object.${ property . name } : ${ defaultValue })` ; } else { if ( property . isParentReference ) { throw new Error ( 'Parent references not supported in BSON.' ) ; } else { setProperties . push ( `if (${ check }) _instance.${ property . name } = object.${ property . name };` ) ; } } propertyCode . push ( `
            //property ${ property . name } (${ property . toString ( ) })
            if (${ bufferCompare . join ( ' && ' ) }) {
                parser.offset += ${ property . name . length } + 1;
                ${ valueSetVar ? `${ valueSetVar } = true;` : '' }
                ${ createPropertyConverter ( `object.${ property . name }` , property , compiler ) }
                continue;
            } ` ) ; } let instantiate = '' ; compiler . context . set ( '_classType' , schema . classType ) ; if ( schema . isCustomClass ( ) ) { instantiate = `
            _instance = new _classType(${ constructorArgumentNames . join ( ', ' ) });
            ${ setProperties . join ( '\n' ) }
            return _instance;
        ` ; } const functionCode = `
        var object = ${ schema . isCustomClass ( ) ? '{}' : 'new _classType' };

        ${ schema . isCustomClass ( ) ? 'var _instance;' : '' }
        const end = parser.eatUInt32() + parser.offset;

        ${ resetDefaultSets . join ( '\n' ) }

        while (parser.offset < end) {
            const elementType = parser.eatByte();
            if (elementType === 0) break;

            ${ propertyCode . join ( '\n' ) }

            //jump over this property when not registered in schema
            while (parser.offset < end && parser.buffer[parser.offset++] != 0);
            //seek property value
            if (parser.offset >= end) break;
            seekElementSize(elementType, parser);
        }

        ${ setDefaults . join ( '\n' ) }
        ${ schema . isCustomClass ( ) ? instantiate : 'return object;' }
    ` ; const fn = compiler . build ( functionCode , 'parser' , 'partial' ) ; fn . buildId = schema . buildId ; return fn ; } export function getRawBSONDecoder < T > ( schema ) { schema = schema instanceof ClassSchema ? schema : getClassSchema ( schema ) ; if ( schema . jit . rawBson ) return schema . jit . rawBson ; schema . jit . rawBson = createSchemaDecoder ( schema ) ; return schema . jit . rawBson ; } export type BSONDecoder < T > = ( bson : Uint8Array , offset ? : number ) => T ; export function getBSONDecoder < T > ( schema ) { const fn = getRawBSONDecoder ( schema ) ; schema = schema instanceof ClassSchema ? schema : getClassSchema ( schema ) ; if ( schema . jit . bsonEncoder ) return schema . jit . bsonEncoder ; schema . jit . bsonEncoder = ( bson , offset = 0 ) => { return fn ( new ParserV2 ( bson , offset ) ) ; } ; return schema . jit . bsonEncoder ; }