All files / src/cmap/wire_protocol/on_demand document.ts

95.49% Statements 106/111
93.58% Branches 73/78
100% Functions 11/11
96.15% Lines 100/104

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354732x                                 732x 732x 732x 732x 732x 732x                                                               732x               83476653x     83476653x             83476653x   83476653x   83476653x       83476653x         2387668085x 2387668085x   2387668085x   107243390x 107243390x 107243390x       725459367x     89629037x                               217338270x 217338270x   213917462x 75546062x     138371400x 25566116x 25566112x 25566112x 25566112x 25566112x 25566112x 25566112x         4x       112805284x 2451033383x     2451033383x 89629037x 89629037x 89629037x 89629037x       23176247x 23176247x                               173121204x 173121204x 173121204x   173121204x 57926524x     115194680x     16x   19288102x   53510x   4649182x   20x   4x   22791763x   4259743x   12x 12x   12x 8x 8x   8x   8x   8x           4x             4x     59502964x   4649356x     4x               48290032x                         9697168x 9697168x 9659820x 9659820x                                                         207678450x 207678450x 21587052x 241x   21586811x       186091398x 173121204x 173121200x 57926540x 8x   57926532x       115194660x     128164854x                                     21139558x 21139558x   21139558x 21139558x   21139558x   21139558x 8x     21139550x               57852478x                 152798x 152798x      
import {
  Binary,
  type BSONElement,
  BSONError,
  BSONType,
  deserialize,
  type DeserializeOptions,
  getBigInt64LE,
  getFloat64LE,
  getInt32LE,
  ObjectId,
  parseToElementsToArray,
  Timestamp,
  toUTF8
} from '../../../bson';
 
// eslint-disable-next-line no-restricted-syntax
const enum BSONElementOffset {
  type = 0,
  nameOffset = 1,
  nameLength = 2,
  offset = 3,
  length = 4
}
 
/** @internal */
export type JSTypeOf = {
  [BSONType.null]: null;
  [BSONType.undefined]: null;
  [BSONType.double]: number;
  [BSONType.int]: number;
  [BSONType.long]: bigint;
  [BSONType.timestamp]: Timestamp;
  [BSONType.binData]: Binary;
  [BSONType.bool]: boolean;
  [BSONType.objectId]: ObjectId;
  [BSONType.string]: string;
  [BSONType.date]: Date;
  [BSONType.object]: OnDemandDocument;
  [BSONType.array]: OnDemandDocument;
};
 
/** @internal */
type CachedBSONElement = { element: BSONElement; value: any | undefined };
 
/**
 * @internal
 *
 * Options for `OnDemandDocument.toObject()`. Validation is required to ensure
 * that callers provide utf8 validation options. */
export type OnDemandDocumentDeserializeOptions = Omit<DeserializeOptions, 'validation'> &
  Required<Pick<DeserializeOptions, 'validation'>>;
 
/** @internal */
export class OnDemandDocument {
  /**
   * Maps JS strings to elements and jsValues for speeding up subsequent lookups.
   * - If `false` then name does not exist in the BSON document
   * - If `CachedBSONElement` instance name exists
   * - If `cache[name].value == null` jsValue has not yet been parsed
   *   - Null/Undefined values do not get cached because they are zero-length values.
   */
  private readonly cache: Record<string, CachedBSONElement | false | undefined> =
    Object.create(null);
  /** Caches the index of elements that have been named */
  private readonly indexFound: Record<number, boolean> = Object.create(null);
 
  /** All bson elements in this document */
  private readonly elements: ReadonlyArray<BSONElement>;
 
  constructor(
    /** BSON bytes, this document begins at offset */
    protected readonly bson: Uint8Array,
    /** The start of the document */
    private readonly offset = 0,
    /** If this is an embedded document, indicates if this was a BSON array */
    public readonly isArray = false,
    /** If elements was already calculated */
    elements?: BSONElement[]
  ) {
    this.elements = elements ?? parseToElementsToArray(this.bson, offset);
  }
 
  /** Only supports basic latin strings */
  private isElementName(name: string, element: BSONElement): boolean {
    const nameLength = element[BSONElementOffset.nameLength];
    const nameOffset = element[BSONElementOffset.nameOffset];
 
    if (name.length !== nameLength) return false;
 
    const nameEnd = nameOffset + nameLength;
    for (
      let byteIndex = nameOffset, charIndex = 0;
      charIndex < name.length && byteIndex < nameEnd;
      charIndex++, byteIndex++
    ) {
      if (this.bson[byteIndex] !== name.charCodeAt(charIndex)) return false;
    }
 
    return true;
  }
 
  /**
   * Seeks into the elements array for an element matching the given name.
   *
   * @remarks
   * Caching:
   * - Caches the existence of a property making subsequent look ups for non-existent properties return immediately
   * - Caches names mapped to elements to avoid reiterating the array and comparing the name again
   * - Caches the index at which an element has been found to prevent rechecking against elements already determined to belong to another name
   *
   * @param name - a basic latin string name of a BSON element
   * @returns
   */
  private getElement(name: string | number): CachedBSONElement | null {
    const cachedElement = this.cache[name];
    if (cachedElement === false) return null;
 
    if (cachedElement != null) {
      return cachedElement;
    }
 
    if (typeof name === 'number') {
      if (this.isArray) {
        Eif (name < this.elements.length) {
          const element = this.elements[name];
          const cachedElement = { element, value: undefined };
          this.cache[name] = cachedElement;
          this.indexFound[name] = true;
          return cachedElement;
        } else {
          return null;
        }
      } else {
        return null;
      }
    }
 
    for (let index = 0; index < this.elements.length; index++) {
      const element = this.elements[index];
 
      // skip this element if it has already been associated with a name
      if (!(index in this.indexFound) && this.isElementName(name, element)) {
        const cachedElement = { element, value: undefined };
        this.cache[name] = cachedElement;
        this.indexFound[index] = true;
        return cachedElement;
      }
    }
 
    this.cache[name] = false;
    return null;
  }
 
  /**
   * Translates BSON bytes into a javascript value. Checking `as` against the BSON element's type
   * this methods returns the small subset of BSON types that the driver needs to function.
   *
   * @remarks
   * - BSONType.null and BSONType.undefined always return null
   * - If the type requested does not match this returns null
   *
   * @param element - The element to revive to a javascript value
   * @param as - A type byte expected to be returned
   */
  private toJSValue<T extends keyof JSTypeOf>(element: BSONElement, as: T): JSTypeOf[T];
  private toJSValue(element: BSONElement, as: keyof JSTypeOf): any {
    const type = element[BSONElementOffset.type];
    const offset = element[BSONElementOffset.offset];
    const length = element[BSONElementOffset.length];
 
    if (as !== type) {
      return null;
    }
 
    switch (as) {
      case BSONType.null:
      case BSONType.undefined:
        return null;
      case BSONType.double:
        return getFloat64LE(this.bson, offset);
      case BSONType.int:
        return getInt32LE(this.bson, offset);
      case BSONType.long:
        return getBigInt64LE(this.bson, offset);
      case BSONType.bool:
        return Boolean(this.bson[offset]);
      case BSONType.objectId:
        return new ObjectId(this.bson.subarray(offset, offset + 12));
      case BSONType.timestamp:
        return new Timestamp(getBigInt64LE(this.bson, offset));
      case BSONType.string:
        return toUTF8(this.bson, offset + 4, offset + length - 1, false);
      case BSONType.binData: {
        const totalBinarySize = getInt32LE(this.bson, offset);
        const subType = this.bson[offset + 4];
 
        if (subType === 2) {
          const subType2BinarySize = getInt32LE(this.bson, offset + 1 + 4);
          Iif (subType2BinarySize < 0)
            throw new BSONError('Negative binary type element size found for subtype 0x02');
          Iif (subType2BinarySize > totalBinarySize - 4)
            throw new BSONError('Binary type with subtype 0x02 contains too long binary size');
          Iif (subType2BinarySize < totalBinarySize - 4)
            throw new BSONError('Binary type with subtype 0x02 contains too short binary size');
          return new Binary(
            this.bson.subarray(offset + 1 + 4 + 4, offset + 1 + 4 + 4 + subType2BinarySize),
            2
          );
        }
 
        return new Binary(
          this.bson.subarray(offset + 1 + 4, offset + 1 + 4 + totalBinarySize),
          subType
        );
      }
      case BSONType.date:
        // Pretend this is correct.
        return new Date(Number(getBigInt64LE(this.bson, offset)));
 
      case BSONType.object:
        return new OnDemandDocument(this.bson, offset);
      case BSONType.array:
        return new OnDemandDocument(this.bson, offset, true);
 
      default:
        throw new BSONError(`Unsupported BSON type: ${as}`);
    }
  }
 
  /**
   * Returns the number of elements in this BSON document
   */
  public size() {
    return this.elements.length;
  }
 
  /**
   * Checks for the existence of an element by name.
   *
   * @remarks
   * Uses `getElement` with the expectation that will populate caches such that a `has` call
   * followed by a `getElement` call will not repeat the cost paid by the first look up.
   *
   * @param name - element name
   */
  public has(name: string): boolean {
    const cachedElement = this.cache[name];
    if (cachedElement === false) return false;
    Iif (cachedElement != null) return true;
    return this.getElement(name) != null;
  }
 
  /**
   * Turns BSON element with `name` into a javascript value.
   *
   * @typeParam T - must be one of the supported BSON types determined by `JSTypeOf` this will determine the return type of this function.
   * @param name - the element name
   * @param as - the bson type expected
   * @param required - whether or not the element is expected to exist, if true this function will throw if it is not present
   */
  public get<const T extends keyof JSTypeOf>(
    name: string | number,
    as: T,
    required?: boolean | undefined
  ): JSTypeOf[T] | null;
 
  /** `required` will make `get` throw if name does not exist or is null/undefined */
  public get<const T extends keyof JSTypeOf>(
    name: string | number,
    as: T,
    required: true
  ): JSTypeOf[T];
 
  public get<const T extends keyof JSTypeOf>(
    name: string | number,
    as: T,
    required?: boolean
  ): JSTypeOf[T] | null {
    const element = this.getElement(name);
    if (element == null) {
      if (required === true) {
        throw new BSONError(`BSON element "${name}" is missing`);
      } else {
        return null;
      }
    }
 
    if (element.value == null) {
      const value = this.toJSValue(element.element, as);
      if (value == null) {
        if (required === true) {
          throw new BSONError(`BSON element "${name}" is missing`);
        } else {
          return null;
        }
      }
      // It is important to never store null
      element.value = value;
    }
 
    return element.value;
  }
 
  /**
   * Supports returning int, double, long, and bool as javascript numbers
   *
   * @remarks
   * **NOTE:**
   * - Use this _only_ when you believe the potential precision loss of an int64 is acceptable
   * - This method does not cache the result as Longs or booleans would be stored incorrectly
   *
   * @param name - element name
   * @param required - throws if name does not exist
   */
  public getNumber<const Req extends boolean = false>(
    name: string,
    required?: Req
  ): Req extends true ? number : number | null;
  public getNumber(name: string, required: boolean): number | null {
    const maybeBool = this.get(name, BSONType.bool);
    const bool = maybeBool == null ? null : maybeBool ? 1 : 0;
 
    const maybeLong = this.get(name, BSONType.long);
    const long = maybeLong == null ? null : Number(maybeLong);
 
    const result = bool ?? long ?? this.get(name, BSONType.int) ?? this.get(name, BSONType.double);
 
    if (required === true && result == null) {
      throw new BSONError(`BSON element "${name}" is missing`);
    }
 
    return result;
  }
 
  /**
   * Deserialize this object, DOES NOT cache result so avoid multiple invocations
   * @param options - BSON deserialization options
   */
  public toObject(options?: OnDemandDocumentDeserializeOptions): Record<string, any> {
    return deserialize(this.bson, {
      ...options,
      index: this.offset,
      allowObjectSmallerThanBufferSize: true
    });
  }
 
  /** Returns this document's bytes only */
  toBytes() {
    const size = getInt32LE(this.bson, this.offset);
    return this.bson.subarray(this.offset, this.offset + size);
  }
}