UNPKG

@deepkit/bson

Version:
1,267 lines (1,146 loc) 66.3 kB
/* * Deepkit Framework * Copyright (C) 2021 Deepkit UG, Marc J. Schmidt * * This program is free software: you can redistribute it and/or modify * it under the terms of the MIT License. * * You should have received a copy of the MIT License along with this program. */ import { CompilerContext, isArray, isIterable, isObject, toFastProperties } from '@deepkit/core'; import { binaryBigIntAnnotation, BinaryBigIntType, buildFunction, callExtractedFunctionIfAvailable, collapsePath, ContainerAccessor, copyAndSetParent, createReference, excludedAnnotation, executeTemplates, extractStateToFunctionAndCallIt, getIndexCheck, getNameExpression, getTypeJitContainer, handleUnion, hasCircularReference, isBackReferenceType, isBinaryBigIntType, isMongoIdType, isNullable, isOptional, isReferenceHydrated, isReferenceInstance, isReferenceType, isUUIDType, JitStack, memberNameToString, mongoIdAnnotation, NamingStrategy, ReceiveType, referenceAnnotation, ReflectionClass, ReflectionKind, resolveReceiveType, resolveTypeMembers, RuntimeCode, Serializer, sortSignatures, TemplateRegistry, TemplateState, Type, TypeBigInt, TypeClass, TypeGuardRegistry, TypeIndexSignature, TypeLiteral, TypeObjectLiteral, typeSettings, TypeTuple, UnpopulatedCheck, unpopulatedSymbol, uuidAnnotation } from '@deepkit/type'; import { bsonTypeGuardArray, bsonTypeGuardForBsonTypes, bsonTypeGuardLiteral, bsonTypeGuardObjectLiteral, bsonTypeGuardTemplateLiteral, bsonTypeGuardTuple, bsonTypeGuardUnion, deserializeAny, deserializeArray, deserializeBigInt, deserializeBinary, deserializeBoolean, deserializeDate, deserializeLiteral, deserializeNull, deserializeNumber, deserializeObjectLiteral, deserializeRegExp, deserializeString, deserializeTemplateLiteral, deserializeTuple, deserializeUndefined, deserializeUnion } from './bson-deserializer-templates'; import { seekElementSize } from './continuation'; import { BSONError } from './model'; import { BSON_BINARY_SUBTYPE_DEFAULT, BSON_BINARY_SUBTYPE_UUID, BSONType, digitByteSize, isSerializable, TWO_PWR_32_DBL_N } from './utils'; export function createBuffer(size: number): Uint8Array { return 'undefined' !== typeof Buffer && 'function' === typeof Buffer.allocUnsafe ? Buffer.allocUnsafe(size) : new Uint8Array(size); } // BSON MAX VALUES const BSON_INT32_MAX = 0x7fffffff; const BSON_INT32_MIN = -0x80000000; // JS MAX PRECISE VALUES export const JS_INT_MAX = 0x20000000000000; // Any integer up to 2^53 can be precisely represented by a double. export const JS_INT_MIN = -0x20000000000000; // Any integer down to -2^53 can be precisely represented by a double. const LONG_MAX = 'undefined' !== typeof BigInt ? BigInt('9223372036854775807') : 9223372036854775807; const LONG_MIN = 'undefined' !== typeof BigInt ? BigInt('-9223372036854775807') : -9223372036854775807; export function hexToByte(hex: string, index: number = 0, offset: number = 0): number { let code1 = hex.charCodeAt(index * 2 + offset) - 48; if (code1 > 9) code1 -= 39; let code2 = hex.charCodeAt((index * 2) + offset + 1) - 48; if (code2 > 9) code2 -= 39; return code1 * 16 + code2; } export function uuidStringToByte(hex: string, index: number = 0): number { let offset = 0; //e.g. bef8de96-41fe-442f-b70c-c3a150f8c96c if (index > 3) offset += 1; if (index > 5) offset += 1; if (index > 7) offset += 1; if (index > 9) offset += 1; return hexToByte(hex, index, offset); } export function stringByteLength(str: string): number { if (!str) return 0; let size = 0; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); if (c < 128) size += 1; else if (c > 127 && c < 2048) size += 2; else size += 3; } return size; } function getBinaryBigIntSize(value: bigint): number { let hex = value.toString(16); if (hex[0] === '-') hex = hex.slice(1); if (hex === '0') return 4 + 1; if (hex.length % 2) hex = '0' + hex; return 4 + 1 + Math.ceil(hex.length / 2); } function getSignedBinaryBigIntSize(value: bigint): number { let hex = value.toString(16); if (hex[0] === '-') hex = hex.slice(1); if (hex === '0') return 4 + 1; if (hex.length % 2) hex = '0' + hex; return 4 + 1 + 1 + Math.ceil(hex.length / 2); } export function getValueSize(value: any): number { if (value instanceof ValueWithBSONSerializer) { if (isUUIDType(value.type)) { return 4 + 1 + 16; } else if (isMongoIdType(value.type)) { return 12; } else if (isBinaryBigIntType(value.type)) { const binaryBigInt = binaryBigIntAnnotation.getFirst(value.type)!; return binaryBigInt === BinaryBigIntType.unsigned ? getBinaryBigIntSize(value.value) : getSignedBinaryBigIntSize(value.value); } else { return getValueSize(value.value); } } else if ('boolean' === typeof value) { return 1; } else if ('string' === typeof value) { //size + content + null return 4 + stringByteLength(value) + 1; } else if ('bigint' === typeof value) { //per default bigint will be serialized as long, to be compatible with default mongo driver and mongo database. return 8; } else if ('number' === typeof value) { if (Math.floor(value) === value) { //it's an int if (value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) { //32bit return 4; } else if (value >= JS_INT_MIN && value <= JS_INT_MAX) { //double, 64bit return 8; } else { //long return 8; } } else { //double return 8; } } else if (value instanceof Date) { return 8; } else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { let size = 4; //size size += 1; //sub type size += value.byteLength; return size; } else if (isArray(value)) { let size = 4; //object size for (let i = 0; i < value.length; i++) { size += 1; //element type size += digitByteSize(i); //element name size += getValueSize(value[i]); } size += 1; //null return size; } else if (value && value['_bsontype'] === 'Binary') { let size = 4; //size size += 1; //sub type size += value.buffer.byteLength; return size; } else if (value instanceof RegExp) { return stringByteLength(value.source) + 1 + (value.global ? 1 : 0) + (value.ignoreCase ? 1 : 0) + (value.multiline ? 1 : 0) + 1; } else if (isObject(value)) { let size = 4; //object size for (let i in value) { if (!value.hasOwnProperty(i)) continue; size += 1; //element type size += stringByteLength(i) + 1; //element name + null size += getValueSize(value[i]); } size += 1; //null return size; } //isObject() should be last return 0; } export class ValueWithBSONSerializer { constructor(public value: any, public type: Type) { } } export class Writer { public dataView: DataView; constructor(public buffer: Uint8Array, public offset: number = 0) { this.dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); } writeUint32(v: number) { this.dataView.setUint32(this.offset, v, true); this.offset += 4; } writeInt32(v: number) { this.dataView.setInt32(this.offset, v, true); this.offset += 4; } writeDouble(v: number) { this.dataView.setFloat64(this.offset, v, true); this.offset += 8; } writeDelayedSize(v: number, position: number) { this.dataView.setUint32(position, v, true); } writeByte(v: number) { this.buffer[this.offset++] = v; } writeBuffer(buffer: Uint8Array, offset: number = 0) { // buffer.copy(this.buffer, this.buffer.byteOffset + this.offset); for (let i = offset; i < buffer.byteLength; i++) { this.buffer[this.offset++] = buffer[i]; } // this.offset += buffer.byteLength; } writeNull() { this.writeByte(0); } writeAsciiString(str: string | number) { str = 'string' === typeof str ? str : '' + str; for (let i = 0; i < str.length; i++) { this.buffer[this.offset++] = str.charCodeAt(i); } } writeString(str: string) { if (!str) return; if (typeof str !== 'string') return; for (let i = 0; i < str.length; i++) { const c = str.charCodeAt(i); if (c < 128) { this.buffer[this.offset++] = c; } else if (c > 127 && c < 2048) { this.buffer[this.offset++] = (c >> 6) | 192; this.buffer[this.offset++] = ((c & 63) | 128); } else { this.buffer[this.offset++] = (c >> 12) | 224; this.buffer[this.offset++] = ((c >> 6) & 63) | 128; this.buffer[this.offset++] = (c & 63) | 128; } } } getBigIntBSONType(value: bigint): number { if (BSON_INT32_MIN <= value && value <= BSON_INT32_MAX) { return BSONType.INT; } else if (LONG_MIN <= value && value <= LONG_MAX) { return BSONType.LONG; } else { return BSONType.BINARY; } } writeBigIntLong(value: bigint) { if (value < 0) { this.writeInt32(~Number(-value % BigInt(TWO_PWR_32_DBL_N)) + 1 | 0); //low this.writeInt32(~(Number(-value / BigInt(TWO_PWR_32_DBL_N))) | 0); //high } else { this.writeInt32(Number(value % BigInt(TWO_PWR_32_DBL_N)) | 0); //low this.writeInt32(Number(value / BigInt(TWO_PWR_32_DBL_N)) | 0); //high } } writeBigIntBinary(value: bigint) { //custom binary let hex = value.toString(16); if (hex[0] === '-') hex = hex.slice(1); if (hex === '0') { this.writeUint32(0); this.writeByte(BSON_BINARY_SUBTYPE_DEFAULT); return; } if (hex.length % 2) hex = '0' + hex; let size = Math.ceil(hex.length / 2); this.writeUint32(size); this.writeByte(BSON_BINARY_SUBTYPE_DEFAULT); for (let i = 0; i < size; i++) { this.buffer[this.offset++] = hexToByte(hex, i); } } writeSignedBigIntBinary(value: bigint) { //custom binary let hex = value.toString(16); let signum = 0; if (hex[0] === '-') { //negative number signum = 1; hex = hex.slice(1); } if (hex === '0') { this.writeUint32(0); this.writeByte(BSON_BINARY_SUBTYPE_DEFAULT); return; } if (hex.length % 2) hex = '0' + hex; let size = Math.ceil(hex.length / 2); this.writeUint32(1 + size); this.writeByte(BSON_BINARY_SUBTYPE_DEFAULT); this.buffer[this.offset++] = signum === 1 ? 255 : 0; //0xff means negative, 0 means positive for (let i = 0; i < size; i++) { this.buffer[this.offset++] = hexToByte(hex, i); } } writeLong(value: number) { if (value > 9223372036854775807) value = 9223372036854775807; if (value < -9223372036854775807) value = -9223372036854775807; if (value < 0) { this.writeInt32(~(-value % TWO_PWR_32_DBL_N) + 1 | 0); //low this.writeInt32(~(-value / TWO_PWR_32_DBL_N) | 0); //high } else { this.writeInt32((value % TWO_PWR_32_DBL_N) | 0); //low this.writeInt32((value / TWO_PWR_32_DBL_N) | 0); //high } } writeUUID(value: string) { this.writeUint32(16); this.writeByte(BSON_BINARY_SUBTYPE_UUID); this.buffer[this.offset + 0] = uuidStringToByte(value, 0); this.buffer[this.offset + 1] = uuidStringToByte(value, 1); this.buffer[this.offset + 2] = uuidStringToByte(value, 2); this.buffer[this.offset + 3] = uuidStringToByte(value, 3); //- this.buffer[this.offset + 4] = uuidStringToByte(value, 4); this.buffer[this.offset + 5] = uuidStringToByte(value, 5); //- this.buffer[this.offset + 6] = uuidStringToByte(value, 6); this.buffer[this.offset + 7] = uuidStringToByte(value, 7); //- this.buffer[this.offset + 8] = uuidStringToByte(value, 8); this.buffer[this.offset + 9] = uuidStringToByte(value, 9); //- this.buffer[this.offset + 10] = uuidStringToByte(value, 10); this.buffer[this.offset + 11] = uuidStringToByte(value, 11); this.buffer[this.offset + 12] = uuidStringToByte(value, 12); this.buffer[this.offset + 13] = uuidStringToByte(value, 13); this.buffer[this.offset + 14] = uuidStringToByte(value, 14); this.buffer[this.offset + 15] = uuidStringToByte(value, 15); this.offset += 16; } writeObjectId(value: string) { this.buffer[this.offset + 0] = hexToByte(value, 0); this.buffer[this.offset + 1] = hexToByte(value, 1); this.buffer[this.offset + 2] = hexToByte(value, 2); this.buffer[this.offset + 3] = hexToByte(value, 3); this.buffer[this.offset + 4] = hexToByte(value, 4); this.buffer[this.offset + 5] = hexToByte(value, 5); this.buffer[this.offset + 6] = hexToByte(value, 6); this.buffer[this.offset + 7] = hexToByte(value, 7); this.buffer[this.offset + 8] = hexToByte(value, 8); this.buffer[this.offset + 9] = hexToByte(value, 9); this.buffer[this.offset + 10] = hexToByte(value, 10); this.buffer[this.offset + 11] = hexToByte(value, 11); this.offset += 12; } write(value: any, nameWriter?: () => void): void { if (value instanceof ValueWithBSONSerializer) { if (isUUIDType(value.type)) { if (nameWriter) { this.writeByte(BSONType.BINARY); nameWriter(); } this.writeUUID(value.value); } else if (isMongoIdType(value.type)) { if (nameWriter) { this.writeByte(BSONType.OID); nameWriter(); } this.writeObjectId(value.value); } else if (isBinaryBigIntType(value.type)) { if (nameWriter) { this.writeByte(BSONType.BINARY); nameWriter(); } const binary = binaryBigIntAnnotation.getFirst(value.type)!; if (binary === BinaryBigIntType.signed) { this.writeSignedBigIntBinary(value.value); } else { this.writeBigIntBinary(value.value); } } else { this.write(value.value, nameWriter); } } else if ('boolean' === typeof value) { if (nameWriter) { this.writeByte(BSONType.BOOLEAN); nameWriter(); } this.writeByte(value ? 1 : 0); } else if (value instanceof RegExp) { if (nameWriter) { this.writeByte(BSONType.REGEXP); nameWriter(); } this.writeString(value.source); this.writeNull(); if (value.ignoreCase) this.writeString('i'); if (value.global) this.writeString('s'); //BSON does not use the RegExp flag format if (value.multiline) this.writeString('m'); this.writeNull(); } else if ('string' === typeof value) { //size + content + null if (nameWriter) { this.writeByte(BSONType.STRING); nameWriter(); } const start = this.offset; this.offset += 4; //size placeholder this.writeString(value); this.writeByte(0); //null this.writeDelayedSize(this.offset - start - 4, start); } else if ('number' === typeof value) { if (Math.floor(value) === value && value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) { //32bit int if (nameWriter) { this.writeByte(BSONType.INT); nameWriter(); } this.writeInt32(value); } else { //double if (nameWriter) { this.writeByte(BSONType.NUMBER); nameWriter(); } this.writeDouble(value); } } else if (value instanceof Date) { if (nameWriter) { this.writeByte(BSONType.DATE); nameWriter(); } this.writeLong(value.valueOf()); } else if ('bigint' === typeof value) { //this is only called for bigint in any structures. //to make sure the deserializing yields a bigint as well, we have to always use binary representation if (nameWriter) { this.writeByte(BSONType.BINARY); nameWriter(); } this.writeBigIntBinary(value); } else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) { if (nameWriter) { this.writeByte(BSONType.BINARY); nameWriter(); } this.writeArrayBuffer(value); } else if (isArray(value)) { if (nameWriter) { this.writeByte(BSONType.ARRAY); nameWriter(); } const start = this.offset; this.offset += 4; //size for (let i = 0; i < value.length; i++) { this.write(value[i], () => { this.writeAsciiString('' + i); this.writeByte(0); }); } this.writeNull(); this.writeDelayedSize(this.offset - start, start); } else if (value === undefined) { if (nameWriter) { this.writeByte(BSONType.UNDEFINED); nameWriter(); } } else if (value === null) { if (nameWriter) { this.writeByte(BSONType.NULL); nameWriter(); } } else if (isObject(value)) { if (nameWriter) { this.writeByte(BSONType.OBJECT); nameWriter(); } const start = this.offset; this.offset += 4; //size for (let i in value) { if (!value.hasOwnProperty(i)) continue; this.write(value[i], () => { this.writeString(i); this.writeByte(0); }); } this.writeNull(); this.writeDelayedSize(this.offset - start, start); } else { //the sizer incldues the type and name, so we have to write that if (nameWriter) { this.writeByte(BSONType.UNDEFINED); nameWriter(); } } } writeArrayBuffer(value: ArrayBuffer | ArrayBufferView) { let view = value instanceof ArrayBuffer ? new Uint8Array(value) : new Uint8Array(value.buffer, value.byteOffset, value.byteLength); if ((value as any)['_bsontype'] === 'Binary') { view = (value as any).buffer; } this.writeUint32(value.byteLength); this.writeByte(BSON_BINARY_SUBTYPE_DEFAULT); for (let i = 0; i < value.byteLength; i++) { this.buffer[this.offset++] = view[i]; } } } function getNameWriterCode(name: string): string { const nameSetter: string[] = []; //todo: support utf8 names for (let i = 0; i < name.length; i++) { nameSetter.push(`state.writer.buffer[state.writer.offset++] = ${name.charCodeAt(i)};`); } return ` //write name: '${name}' ${nameSetter.join('\n')} state.writer.writeByte(0); //null `; } function sizerObjectLiteral(type: TypeClass | TypeObjectLiteral, state: TemplateState, options: BSONSerializerOptions) { handleObjectLiteral(type, state, 'sizer', options); } function serializeObjectLiteral(type: TypeClass | TypeObjectLiteral, state: TemplateState, options: BSONSerializerOptions) { handleObjectLiteral(type, state, 'serialization', options); } function handleObjectLiteral( type: TypeClass | TypeObjectLiteral, state: TemplateState, target: 'serialization' | 'sizer', options: BSONSerializerOptions ) { let before: string = 'state.size += 4; //object size'; let after: string = 'state.size += 1; //null'; if (target === 'serialization') { const start = state.compilerContext.reserveName('start'); before = ` var ${start} = state.writer.offset; state.writer.offset += 4; //size`; after = ` state.writer.writeNull(); state.writer.writeDelayedSize(state.writer.offset - ${start}, ${start});`; } //emdedded for the moment disabled. treat it as normal property. // const embedded = embeddedAnnotation.getFirst(type); // if (embedded) { // if (type.kind !== ReflectionKind.class) throw new SerializationError(`Object literals can not be embedded`, collapsePath(state.path)); // const constructorProperties = getConstructorProperties(type); // if (!constructorProperties.properties.length) throw new BSONError(`Can not embed class ${getClassName(type.classType)} since it has no constructor properties`); // // if (constructorProperties.properties.length === 1) { // const first = constructorProperties.properties[0]; // let name = getNameExpression(state.namingStrategy.getPropertyName(first), state); // const setter = getEmbeddedAccessor(type, false, '', state.namingStrategy, first, embedded); // state.addCode(executeTemplates(state.fork('', new ContainerAccessor(state.accessor, name)).forPropertyName(setter || state.propertyName), first.type)); // } else { // const lines: string[] = []; // const containerProperty = getEmbeddedProperty(type); // // for (const property of constructorProperties.properties) { // const setter = getEmbeddedAccessor(type, true, '', state.namingStrategy, property, embedded); // lines.push(executeTemplates(state.fork('', new ContainerAccessor(state.accessor, JSON.stringify(property.name))).forPropertyName(setter), property.type)); // } // // if (containerProperty) { // state.addCode(` // ${lines.join('\n')} // `); // } else { // if (target === 'serialization') { // serializePropertyNameAware(type, state, BSONType.OBJECT, `'object' === typeof ${state.accessor}`, ` // //embedded class with multiple properties // ${before} // ${lines.join('\n')} // ${after} // `); // } else { // sizerPropertyNameAware(type, state, `'object' === typeof ${state.accessor}`, ` // //embedded class with multiple properties // ${before} // ${lines.join('\n')} // ${after} // `); // } // } // } // return; // } const existingCalled = callExtractedFunctionIfAvailable(state, type); const extract = existingCalled ? undefined : extractStateToFunctionAndCallIt(state, type); if (target === 'serialization') { serializePropertyNameAware(type, state, BSONType.OBJECT, `'object' === typeof ${state.accessor}`, ''); } else { sizerPropertyNameAware(type, state, `'object' === typeof ${state.accessor}`, ''); } if (type.kind === ReflectionKind.class && referenceAnnotation.hasAnnotations(type)) { state.setContext({ isObject, isReferenceInstance, isReferenceHydrated }); const reflection = ReflectionClass.from(type.classType); //the primary key is serialised for unhydrated references const index = getNameExpression(reflection.getPrimary().getName(), state); const primaryKey = reflection.getPrimary().getType(); //if a reference or forMongoDatabase=true only the foreign primary key is serialized state.replaceTemplate(` if ((${options.forMongoDatabase === true}) || (isReferenceInstance(${state.accessor}) && !isReferenceHydrated(${state.accessor}))) { ${executeTemplates(state.fork(state.setter, `${state.accessor}[${index}]`).forPropertyName(state.propertyName), primaryKey)} } else { ${state.template} } `); } //wrap circular check if necessary if (hasCircularReference(type)) { state.replaceTemplate(` if (!state._stack || !state._stack.includes(${state.accessor})) { ${state.template} } `); } if (!extract) return; state = extract.state; const lines: string[] = []; const signatures: TypeIndexSignature[] = []; const existing: string[] = []; state.setContext({ unpopulatedSymbol }); for (const member of resolveTypeMembers(type)) { if (member.kind === ReflectionKind.indexSignature) { if (excludedAnnotation.isExcluded(member.type, state.registry.serializer.name)) continue; signatures.push(member); } if (member.kind !== ReflectionKind.property && member.kind !== ReflectionKind.propertySignature) continue; if (!isSerializable(member.type)) continue; const writeName = String(state.namingStrategy.getPropertyName(member, state.registry.serializer.name)); const readName = getNameExpression(memberNameToString(member.name), state); existing.push(readName); //back references are only serialized when it's not forMongoDatabase if (isBackReferenceType(member.type) && options.forMongoDatabase === true) continue; if (excludedAnnotation.isExcluded(member.type, state.registry.serializer.name)) continue; const accessor = `${state.accessor}[${readName}]`; const propertyState = state.fork('', accessor).extendPath(writeName); const setUndefined = isOptional(member) ? executeTemplates(propertyState.fork().forPropertyName(writeName), { kind: ReflectionKind.undefined }) : isNullable(member) ? executeTemplates(propertyState.fork().forPropertyName(writeName), { kind: ReflectionKind.null }) : ''; const template = executeTemplates(propertyState.fork().forPropertyName(writeName), member.type); if (!template) { throw new BSONError(`No template found for ${member.type.kind}`); } let converter = ` if (${accessor} === undefined || ${accessor} === unpopulatedSymbol) { ${setUndefined} } else { ${template} } `; if (isOptional(member)) { lines.push(` if (${readName} in ${state.accessor}) { ${converter} } `); } else { lines.push(converter); } } if (signatures.length) { const i = state.compilerContext.reserveName('i'); const existingCheck = existing.map(v => `${i} === ${v}`).join(' || ') || 'false'; const signatureLines: string[] = []; sortSignatures(signatures); for (const signature of signatures) { const accessor = new ContainerAccessor(state.accessor, i); const propertyState = state.fork(undefined, accessor).extendPath(new RuntimeCode(i)).forPropertyName(new RuntimeCode(i)); const setUndefined = isOptional(signature.type) ? executeTemplates(propertyState.fork().forPropertyName(new RuntimeCode(i)), { kind: ReflectionKind.undefined }) : isNullable(signature.type) ? executeTemplates(propertyState.fork().forPropertyName(new RuntimeCode(i)), { kind: ReflectionKind.null }) : ''; signatureLines.push(`else if (${getIndexCheck(state, i, signature.index)}) { if (${accessor} === undefined) { ${setUndefined} } else { ${executeTemplates(propertyState, signature.type)} } }`); } //the index signature type could be: string, number, symbol. //or a literal when it was constructed by a mapped type. lines.push(` for (const ${i} in ${state.accessor}) { if (!${state.accessor}.hasOwnProperty(${i})) continue; if (${existingCheck}) continue; if (false) {} ${signatureLines.join(' ')} } `); } state.addCode(` //handle objectLiteral via propertyName ${state.propertyName ? collapsePath([state.propertyName]) : ''} ${before} ${lines.join('\n')} ${after} `); extract.setFunction(buildFunction(state, type)); } function propertyNameWriter(state: TemplateState) { if (state.propertyName) { if (state.propertyName instanceof RuntimeCode) { return ` state.writer.writeAsciiString(${state.propertyName.code}); state.writer.writeByte(0); `; } else { return getNameWriterCode(state.propertyName); } } return ''; } function serializePropertyNameAware(type: Type, state: TemplateState, bsonType: BSONType, typeChecker: string, code: string): void { //when this call is reached first, and it's an object, then no type byte is needed. //todo: that does not work when arbitrary offset and already prefilled buffer is given const isInitialObject = `${bsonType === BSONType.OBJECT} && state.writer.offset === 0`; state.template = ` //serializer for ${type.kind} ${typeChecker ? `if (!(${typeChecker})) ${state.throwCode(type)}` : ''} if (${!!state.propertyName}) state.writer.writeByte(${bsonType}); ${propertyNameWriter(state)} ${state.template} ${code} `; } export class DigitByteRuntimeCode extends RuntimeCode { constructor(public code: string) { super(code); } } function sizerPropertyNameAware(type: Type, state: TemplateState, typeChecker: string, code: string): void { if (state.propertyName) { if (state.propertyName instanceof DigitByteRuntimeCode) { state.setContext({ digitByteSize }); //type + string size + null code = ` state.size += 1 + digitByteSize(${state.propertyName.code}); //type + byte of ${state.propertyName.code} ${code} `; } else if (state.propertyName instanceof RuntimeCode) { state.setContext({ stringByteLength }); //type + string size + null code = ` state.size += 1 + stringByteLength(${state.propertyName.code}) + 1; //type + string size of ${state.propertyName.code} + null ${code} `; } else { //type + string size + null code = ` state.size += 1 + ${stringByteLength(state.propertyName)} + 1; //type + string size of ${state.propertyName} + null ${code} `; } } const checker = typeChecker ? `if (!(${typeChecker})) ${state.throwCode(type)}` : ''; state.template = ` ${checker} ${state.template} ${code} `; } function sizerAny(type: Type, state: TemplateState) { state.setContext({ getValueSize }); sizerPropertyNameAware(type, state, ``, `state.size += getValueSize(${state.accessor});`); } function serializeAny(type: Type, state: TemplateState) { state.addCode(` state.writer.write(${state.accessor}, () => { ${propertyNameWriter(state)} }); `); } function sizerBoolean(type: Type, state: TemplateState) { sizerPropertyNameAware(type, state, `typeof ${state.accessor} === 'boolean'`, ` state.size += 1; `); } function sizerNumber(type: Type, state: TemplateState) { state.setContext({ getValueSize }); //per default bigint will be serialized as long, to be compatible with default mongo driver and mongo database. //We should add a new annotation, maybe like `bigint & Binary` to make it binary (unlimited size) sizerPropertyNameAware(type, state, `(typeof ${state.accessor} === 'number' || typeof ${state.accessor} === 'bigint') && !Number.isNaN(${state.accessor})`, ` state.size += getValueSize(${state.accessor}); `); } function serializeBoolean(type: Type, state: TemplateState) { serializePropertyNameAware(type, state, BSONType.BOOLEAN, `typeof ${state.accessor} === 'boolean'`, ` state.writer.writeByte(${state.accessor} ? 1 : 0); `); } function serializeString(type: Type, state: TemplateState) { if (uuidAnnotation.getFirst(type)) { serializePropertyNameAware(type, state, BSONType.BINARY, `typeof ${state.accessor} === 'string' && ${state.accessor}.length === 36`, `state.writer.writeUUID(${state.accessor});`); return; } if (mongoIdAnnotation.getFirst(type)) { serializePropertyNameAware(type, state, BSONType.OID, `typeof ${state.accessor} === 'string' && ${state.accessor}.length === 24`, `state.writer.writeObjectId(${state.accessor});`); return; } const start = state.compilerContext.reserveName('start'); serializePropertyNameAware(type, state, BSONType.STRING, `typeof ${state.accessor} === 'string'`, ` var ${start} = state.writer.offset; state.writer.offset += 4; //size placeholder state.writer.writeString(${state.accessor}); state.writer.writeByte(0); //null state.writer.writeDelayedSize(state.writer.offset - ${start} - 4, ${start}); `); } function sizeString(type: Type, state: TemplateState) { if (uuidAnnotation.getFirst(type)) { sizerPropertyNameAware(type, state, `typeof ${state.accessor} === 'string' && ${state.accessor}.length === 36`, ` state.size += 4 + 1 + 16; `); return; } if (mongoIdAnnotation.getFirst(type)) { sizerPropertyNameAware(type, state, `typeof ${state.accessor} === 'string' && ${state.accessor}.length === 24`, ` state.size += 12; `); return; } state.setContext({ getValueSize }); sizerPropertyNameAware(type, state, `typeof ${state.accessor} === 'string'`, ` state.size += getValueSize(${state.accessor}); `); } function serializeNumber(type: Type, state: TemplateState) { const nameWriter = propertyNameWriter(state); state.addCode(` if ('bigint' === typeof ${state.accessor}) { //long state.writer.writeByte(${BSONType.LONG}); ${nameWriter} state.writer.writeBigIntLong(${state.accessor}); } else if ('number' === typeof ${state.accessor} && !Number.isNaN(${state.accessor})) { if (Math.floor(${state.accessor}) === ${state.accessor} && ${state.accessor} >= ${BSON_INT32_MIN} && ${state.accessor} <= ${BSON_INT32_MAX}) { //32bit int state.writer.writeByte(${BSONType.INT}); ${nameWriter} state.writer.writeInt32(${state.accessor}); } else { //double, 64bit state.writer.writeByte(${BSONType.NUMBER}); ${nameWriter} state.writer.writeDouble(${state.accessor}); } } `); } function sizerBigInt(type: TypeBigInt, state: TemplateState) { const binaryBigInt = binaryBigIntAnnotation.getFirst(type); if (binaryBigInt !== undefined) { state.setContext({ getBinaryBigIntSize, getSignedBinaryBigIntSize }); const bigIntSize = binaryBigInt === BinaryBigIntType.unsigned ? 'getBinaryBigIntSize' : 'getSignedBinaryBigIntSize'; //per default bigint will be serialized as long, to be compatible with default mongo driver and mongo database. //We should add a new annotation, maybe like `bigint & Binary` to make it binary (unlimited size) sizerPropertyNameAware(type, state, `(typeof ${state.accessor} === 'number' || typeof ${state.accessor} === 'bigint') && !Number.isNaN(${state.accessor})`, ` state.size += ${bigIntSize}(${state.accessor}); `); } else { sizerNumber(type, state); } } function serializeBigInt(type: TypeBigInt, state: TemplateState) { const binaryBigInt = binaryBigIntAnnotation.getFirst(type); if (binaryBigInt !== undefined) { const nameWriter = propertyNameWriter(state); const writeBigInt = binaryBigInt === BinaryBigIntType.unsigned ? 'writeBigIntBinary' : 'writeSignedBigIntBinary'; state.addCode(` if (('bigint' === typeof ${state.accessor} || 'number' === typeof ${state.accessor}) && !Number.isNaN(${state.accessor})) { //long state.writer.writeByte(${BSONType.BINARY}); ${nameWriter} state.writer.${writeBigInt}(${state.accessor}); }`); } else { serializeNumber(type, state); } } function sizerRegExp(type: Type, state: TemplateState) { state.setContext({ stringByteLength }); sizerPropertyNameAware(type, state, `${state.accessor} instanceof RegExp`, ` state.size += stringByteLength(${state.accessor}.source) + 1 + (${state.accessor}.global ? 1 : 0) + (${state.accessor}.ignoreCase ? 1 : 0) + (${state.accessor}.multiline ? 1 : 0) + 1; `); } function serializeRegExp(type: Type, state: TemplateState) { serializePropertyNameAware(type, state, BSONType.REGEXP, `${state.accessor} instanceof RegExp`, ` state.writer.writeString(${state.accessor}.source); state.writer.writeNull(); if (${state.accessor}.ignoreCase) state.writer.writeString('i'); if (${state.accessor}.global) state.writer.writeString('s'); //BSON does not use the RegExp flag format if (${state.accessor}.multiline) state.writer.writeString('m'); state.writer.writeNull(); `); } function sizerLiteral(type: TypeLiteral, state: TemplateState) { if ('string' === typeof type.literal) { sizeString(type, state); } else if ('number' === typeof type.literal || 'bigint' === typeof type.literal) { sizerNumber(type, state); } else if ('boolean' === typeof type.literal) { sizerBoolean(type, state); } else if (type.literal instanceof RegExp) { sizerRegExp(type, state); } } function serializeLiteral(type: TypeLiteral, state: TemplateState) { if ('string' === typeof type.literal) { serializeString(type, state); } else if ('number' === typeof type.literal || 'bigint' === typeof type.literal) { serializeNumber(type, state); } else if ('boolean' === typeof type.literal) { serializeBoolean(type, state); } else if (type.literal instanceof RegExp) { serializeRegExp(type, state); } } function sizerBinary(type: TypeClass, state: TemplateState) { state.setContext({ ArrayBuffer }); sizerPropertyNameAware(type, state, `${state.accessor} instanceof ArrayBuffer || ArrayBuffer.isView(${state.accessor})`, ` state.size += 4 + 1 + ${state.accessor}.byteLength; `); } function serializeBinary(type: TypeClass, state: TemplateState) { state.setContext({ ArrayBuffer }); serializePropertyNameAware(type, state, BSONType.BINARY, `${state.accessor} instanceof ArrayBuffer || ArrayBuffer.isView(${state.accessor})`, ` state.writer.writeArrayBuffer(${state.accessor}); `); } function sizerArray(elementType: Type, state: TemplateState) { state.setContext({ isIterable }); const i = state.compilerContext.reserveName('i'); const item = state.compilerContext.reserveName('item'); sizerPropertyNameAware(elementType, state, `isIterable(${state.accessor})`, ` state.size += 4; //array size let ${i} = 0; for (const ${item} of ${state.accessor}) { ${executeTemplates(state.fork('', item).extendPath(new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), elementType)} ${i}++; } state.size += 1; //null `); } function serializeArray(elementType: Type, state: TemplateState) { state.setContext({ isIterable }); const start = state.compilerContext.reserveName('start'); const i = state.compilerContext.reserveName('i'); const item = state.compilerContext.reserveName('item'); serializePropertyNameAware(elementType, state, BSONType.ARRAY, `isIterable(${state.accessor})`, ` var ${start} = state.writer.offset; state.writer.offset += 4; //size let ${i} = 0; for (const ${item} of ${state.accessor}) { ${executeTemplates(state.fork('', item).extendPath(new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), elementType)} ${i}++; } state.writer.writeNull(); state.writer.writeDelayedSize(state.writer.offset - ${start}, ${start}); `); } function serializeTuple(type: TypeTuple, state: TemplateState) { //[string, number], easy //[...string, number], easy //[number, ...string], easy //[number, ...string, number, string], medium const lines: string[] = []; let restEndOffset = 0; const i = state.compilerContext.reserveName('i'); for (let i = 0; i < type.types.length; i++) { if (type.types[i].type.kind === ReflectionKind.rest) { restEndOffset = type.types.length - (i + 1); break; } } for (const member of type.types) { if (member.type.kind === ReflectionKind.rest) { lines.push(` for (; ${i} < ${state.accessor}.length - ${restEndOffset}; ${i}++) { ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), member.type.type)} } `); } else { const optionalCheck = member.optional ? `${state.accessor}[${i}] !== undefined` : 'true'; lines.push(` if (${optionalCheck}) { ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), member.type)} } ${i}++; `); } } const start = state.compilerContext.reserveName('start'); state.setContext({ isArray }); serializePropertyNameAware(type, state, BSONType.ARRAY, `isArray(${state.accessor})`, ` let ${i} = 0; var ${start} = state.writer.offset; state.writer.offset += 4; //size ${lines.join('\n')} state.writer.writeNull(); state.writer.writeDelayedSize(state.writer.offset - ${start}, ${start}); `); } function sizerTuple(type: TypeTuple, state: TemplateState) { //[string, number], easy //[...string, number], easy //[number, ...string], easy //[number, ...string, number, string], medium const lines: string[] = []; let restEndOffset = 0; const i = state.compilerContext.reserveName('i'); for (let i = 0; i < type.types.length; i++) { if (type.types[i].type.kind === ReflectionKind.rest) { restEndOffset = type.types.length - (i + 1); break; } } for (const member of type.types) { if (member.type.kind === ReflectionKind.rest) { lines.push(` for (; ${i} < ${state.accessor}.length - ${restEndOffset}; ${i}++) { ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), member.type.type)} } `); } else { const optionalCheck = member.optional ? `${state.accessor}[${i}] !== undefined` : 'true'; lines.push(` if (${optionalCheck}) { ${executeTemplates(state.fork('', `${state.accessor}[${i}]`).extendPath(member.name || new RuntimeCode(i)).forPropertyName(new DigitByteRuntimeCode(i)), member.type)} } ${i}++; `); } } state.setContext({ isArray }); sizerPropertyNameAware(type, state, `isArray(${state.accessor})`, ` let ${i} = 0; state.size += 4; //array size ${lines.join('\n')} state.size += 1; //null `); } interface BSONSerializerOptions { /** * If true the serializes changes slightly its behaviour to make it compatible with the mongo database. * For example are fields marked as BackReference excluded. * Fields marked as Reference() will only serialize its primary key. * * */ forMongoDatabase?: true; } export class BSONBinarySerializer extends Serializer { name = 'bson'; serializeId: symbol = Symbol('BSONBinarySerializer'); deserializeId: symbol = Symbol('BSONBinarySerializer'); sizerId: symbol = Symbol('BSONBinarySerializer'); public sizerRegistry = new TemplateRegistry(this); public bsonSerializeRegistry = new TemplateRegistry(this); public bsonDeserializeRegistry = new TemplateRegistry(this); public bsonTypeGuards = new TypeGuardRegistry(this); constructor(protected options: BSONSerializerOptions = {}) { super(); this.registerSizer(); this.registerBsonSerializers(); this.registerBsonDeserializers(); this.registerBsonTypeGuards(); } protected registerSizer() { this.sizerRegistry.register(ReflectionKind.any, sizerAny); this.sizerRegistry.register(ReflectionKind.unknown, sizerAny); this.sizerRegistry.register(ReflectionKind.never, () => undefined); this.sizerRegistry.register(ReflectionKind.class, (type, state) => sizerObjectLiteral(type, state, this.options)); this.sizerRegistry.register(ReflectionKind.objectLiteral, (type, state) => sizerObjectLiteral(type, state, this.options)); this.sizerRegistry.register(ReflectionKind.string, sizeString); this.sizerRegistry.register(ReflectionKind.templateLiteral, sizeString); this.sizerRegistry.register(ReflectionKind.boolean, sizerBoolean); this.sizerRegistry.register(ReflectionKind.promise, ((type, state) => executeTemplates(state, type.type))); this.sizerRegistry.register(ReflectionKind.number, sizerNumber); this.sizerRegistry.register(ReflectionKind.bigint, sizerBigInt); this.sizerRegistry.register(ReflectionKind.literal, sizerLiteral); this.sizerRegistry.register(ReflectionKind.regexp, sizerRegExp); this.sizerRegistry.register(ReflectionKind.array, (type, state) => sizerArray(type.type as Type, state)); this.sizerRegistry.register(ReflectionKind.tuple, sizerTuple); this.sizerRegistry.registerClass(Map, (type, state) => sizerArray(copyAndSetParent({ kind: ReflectionKind.tuple, types: [ { kind: ReflectionKind.tupleMember, name: 'key', type: type.arguments![0] }, { kind: ReflectionKind.tupleMember, name: 'value', type: type.arguments![1] }, ] }), state)); this.sizerRegistry.registerClass(Set, (type, state) => sizerArray(type.arguments![0] as Type, state)); this.sizerRegistry.registerClass(Date, (type, state) => sizerPropertyNameAware(type, state, `${state.accessor} instanceof Date`, `state.size += 8;`)); this.sizerRegistry.register(ReflectionKind.undefined, (type, state) => sizerPropertyNameAware(type, state, `${state.accessor} === undefined || ${state.accessor} === null`, ``)); this.sizerRegistry.register(ReflectionKind.void, (type, state) => sizerPropertyNameAware(type, state, `${state.accessor} === undefined || ${state.accessor} === null`, ``)); this.sizerRegistry.register(ReflectionKind.null, (type, state) => sizerPropertyNameAware(type, state, `${state.accessor} === undefined || ${state.accessor} === null`, ``)); this.sizerRegistry.registerBinary(sizerBinary); this.sizerRegistry.register(ReflectionKind.union, handleUnion); this.sizerRegistry.register(ReflectionKind.promise, (type, state) => executeTemplates(state, type.type)); this.sizerRegistry.register(ReflectionKind.enum, (type, state) => executeTemplates(state, type.indexType)); } protected registerBsonSerializers() { this.bsonSerializeRegistry.register(ReflectionKind.any, serializeAny); this.bsonSerializeRegistry.register(ReflectionKind.unknown, serializeAny); this.bsonSerializeRegistry.register(ReflectionKind.never, () => undefined); this.bsonSerializeRegistry.register(ReflectionKind.class, (type, state) => serializeObjectLiteral(type, state, this.options)); this.bsonSerializeRegistry.register(ReflectionKind.ob