CoolFace
Apppublic

opusdev/vector-similarity-api

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes
extended_json.ts517 linesDownload Raw Back to src
1import { Binary } from './binary';2import type { Document } from './bson';3import { Code } from './code';4import {5  BSON_INT32_MAX,6  BSON_INT32_MIN,7  BSON_INT64_MAX,8  BSON_INT64_MIN,9  BSON_MAJOR_VERSION,10  BSON_VERSION_SYMBOL11} from './constants';12import { DBRef, isDBRefLike } from './db_ref';13import { Decimal128 } from './decimal128';14import { Double } from './double';15import { BSONError, BSONRuntimeError, BSONVersionError } from './error';16import { Int32 } from './int_32';17import { Long } from './long';18import { MaxKey } from './max_key';19import { MinKey } from './min_key';20import { ObjectId } from './objectid';21import { isDate, isRegExp, isMap } from './parser/utils';22import { BSONRegExp } from './regexp';23import { BSONSymbol } from './symbol';24import { Timestamp } from './timestamp';25 26/** @public */27export type EJSONOptions = {28  /**29   * Output using the Extended JSON v1 spec30   * @defaultValue `false`31   */32  legacy?: boolean;33  /**34   * Enable Extended JSON's `relaxed` mode, which attempts to return native JS types where possible, rather than BSON types35   * @defaultValue `false` */36  relaxed?: boolean;37  /**38   * Enable native bigint support39   * @defaultValue `false`40   */41  useBigInt64?: boolean;42};43 44/** @internal */45type BSONType =46  | Binary47  | Code48  | DBRef49  | Decimal12850  | Double51  | Int3252  | Long53  | MaxKey54  | MinKey55  | ObjectId56  | BSONRegExp57  | BSONSymbol58  | Timestamp;59 60function isBSONType(value: unknown): value is BSONType {61  return (62    value != null &&63    typeof value === 'object' &&64    '_bsontype' in value &&65    typeof value._bsontype === 'string'66  );67}68 69// all the types where we don't need to do any special processing and can just pass the EJSON70//straight to type.fromExtendedJSON71const keysToCodecs = {72  $oid: ObjectId,73  $binary: Binary,74  $uuid: Binary,75  $symbol: BSONSymbol,76  $numberInt: Int32,77  $numberDecimal: Decimal128,78  $numberDouble: Double,79  $numberLong: Long,80  $minKey: MinKey,81  $maxKey: MaxKey,82  $regex: BSONRegExp,83  $regularExpression: BSONRegExp,84  $timestamp: Timestamp85} as const;86 87// eslint-disable-next-line @typescript-eslint/no-explicit-any88function deserializeValue(value: any, options: EJSONOptions = {}) {89  if (typeof value === 'number') {90    // TODO(NODE-4377): EJSON js number handling diverges from BSON91    const in32BitRange = value <= BSON_INT32_MAX && value >= BSON_INT32_MIN;92    const in64BitRange = value <= BSON_INT64_MAX && value >= BSON_INT64_MIN;93 94    if (options.relaxed || options.legacy) {95      return value;96    }97 98    if (Number.isInteger(value) && !Object.is(value, -0)) {99      // interpret as being of the smallest BSON integer type that can represent the number exactly100      if (in32BitRange) {101        return new Int32(value);102      }103      if (in64BitRange) {104        if (options.useBigInt64) {105          // eslint-disable-next-line no-restricted-globals -- This is allowed here as useBigInt64=true106          return BigInt(value);107        }108        return Long.fromNumber(value);109      }110    }111 112    // If the number is a non-integer or out of integer range, should interpret as BSON Double.113    return new Double(value);114  }115 116  // from here on out we're looking for bson types, so bail if its not an object117  if (value == null || typeof value !== 'object') return value;118 119  // upgrade deprecated undefined to null120  if (value.$undefined) return null;121 122  const keys = Object.keys(value).filter(123    k => k.startsWith('$') && value[k] != null124  ) as (keyof typeof keysToCodecs)[];125  for (let i = 0; i < keys.length; i++) {126    const c = keysToCodecs[keys[i]];127    if (c) return c.fromExtendedJSON(value, options);128  }129 130  if (value.$date != null) {131    const d = value.$date;132    const date = new Date();133 134    if (options.legacy) {135      if (typeof d === 'number') date.setTime(d);136      else if (typeof d === 'string') date.setTime(Date.parse(d));137      else if (typeof d === 'bigint') date.setTime(Number(d));138      else throw new BSONRuntimeError(`Unrecognized type for EJSON date: ${typeof d}`);139    } else {140      if (typeof d === 'string') date.setTime(Date.parse(d));141      else if (Long.isLong(d)) date.setTime(d.toNumber());142      else if (typeof d === 'number' && options.relaxed) date.setTime(d);143      else if (typeof d === 'bigint') date.setTime(Number(d));144      else throw new BSONRuntimeError(`Unrecognized type for EJSON date: ${typeof d}`);145    }146    return date;147  }148 149  if (value.$code != null) {150    const copy = Object.assign({}, value);151    if (value.$scope) {152      copy.$scope = deserializeValue(value.$scope);153    }154 155    return Code.fromExtendedJSON(value);156  }157 158  if (isDBRefLike(value) || value.$dbPointer) {159    const v = value.$ref ? value : value.$dbPointer;160 161    // we run into this in a "degenerate EJSON" case (with $id and $ref order flipped)162    // because of the order JSON.parse goes through the document163    if (v instanceof DBRef) return v;164 165    const dollarKeys = Object.keys(v).filter(k => k.startsWith('$'));166    let valid = true;167    dollarKeys.forEach(k => {168      if (['$ref', '$id', '$db'].indexOf(k) === -1) valid = false;169    });170 171    // only make DBRef if $ keys are all valid172    if (valid) return DBRef.fromExtendedJSON(v);173  }174 175  return value;176}177 178type EJSONSerializeOptions = EJSONOptions & {179  seenObjects: { obj: unknown; propertyName: string }[];180};181 182// eslint-disable-next-line @typescript-eslint/no-explicit-any183function serializeArray(array: any[], options: EJSONSerializeOptions): any[] {184  return array.map((v: unknown, index: number) => {185    options.seenObjects.push({ propertyName: `index ${index}`, obj: null });186    try {187      return serializeValue(v, options);188    } finally {189      options.seenObjects.pop();190    }191  });192}193 194function getISOString(date: Date) {195  const isoStr = date.toISOString();196  // we should only show milliseconds in timestamp if they're non-zero197  return date.getUTCMilliseconds() !== 0 ? isoStr : isoStr.slice(0, -5) + 'Z';198}199 200// eslint-disable-next-line @typescript-eslint/no-explicit-any201function serializeValue(value: any, options: EJSONSerializeOptions): any {202  if (value instanceof Map || isMap(value)) {203    const obj: Record<string, unknown> = Object.create(null);204    for (const [k, v] of value) {205      if (typeof k !== 'string') {206        throw new BSONError('Can only serialize maps with string keys');207      }208      obj[k] = v;209    }210 211    return serializeValue(obj, options);212  }213 214  if ((typeof value === 'object' || typeof value === 'function') && value !== null) {215    const index = options.seenObjects.findIndex(entry => entry.obj === value);216    if (index !== -1) {217      const props = options.seenObjects.map(entry => entry.propertyName);218      const leadingPart = props219        .slice(0, index)220        .map(prop => `${prop} -> `)221        .join('');222      const alreadySeen = props[index];223      const circularPart =224        ' -> ' +225        props226          .slice(index + 1, props.length - 1)227          .map(prop => `${prop} -> `)228          .join('');229      const current = props[props.length - 1];230      const leadingSpace = ' '.repeat(leadingPart.length + alreadySeen.length / 2);231      const dashes = '-'.repeat(232        circularPart.length + (alreadySeen.length + current.length) / 2 - 1233      );234 235      throw new BSONError(236        'Converting circular structure to EJSON:\n' +237          `    ${leadingPart}${alreadySeen}${circularPart}${current}\n` +238          `    ${leadingSpace}\\${dashes}/`239      );240    }241    options.seenObjects[options.seenObjects.length - 1].obj = value;242  }243 244  if (Array.isArray(value)) return serializeArray(value, options);245 246  if (value === undefined) return null;247 248  if (value instanceof Date || isDate(value)) {249    const dateNum = value.getTime(),250      // is it in year range 1970-9999?251      inRange = dateNum > -1 && dateNum < 253402318800000;252 253    if (options.legacy) {254      return options.relaxed && inRange255        ? { $date: value.getTime() }256        : { $date: getISOString(value) };257    }258    return options.relaxed && inRange259      ? { $date: getISOString(value) }260      : { $date: { $numberLong: value.getTime().toString() } };261  }262 263  if (typeof value === 'number' && (!options.relaxed || !isFinite(value))) {264    if (Number.isInteger(value) && !Object.is(value, -0)) {265      // interpret as being of the smallest BSON integer type that can represent the number exactly266      if (value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) {267        return { $numberInt: value.toString() };268      }269      if (value >= BSON_INT64_MIN && value <= BSON_INT64_MAX) {270        // TODO(NODE-4377): EJSON js number handling diverges from BSON271        return { $numberLong: value.toString() };272      }273    }274    return { $numberDouble: Object.is(value, -0) ? '-0.0' : value.toString() };275  }276 277  if (typeof value === 'bigint') {278    /* eslint-disable no-restricted-globals -- This is allowed as we are accepting a bigint as input */279    if (!options.relaxed) {280      return { $numberLong: BigInt.asIntN(64, value).toString() };281    }282    return Number(BigInt.asIntN(64, value));283    /* eslint-enable */284  }285 286  if (value instanceof RegExp || isRegExp(value)) {287    let flags = value.flags;288    if (flags === undefined) {289      const match = value.toString().match(/[gimuy]*$/);290      if (match) {291        flags = match[0];292      }293    }294 295    const rx = new BSONRegExp(value.source, flags);296    return rx.toExtendedJSON(options);297  }298 299  if (value != null && typeof value === 'object') return serializeDocument(value, options);300  return value;301}302 303const BSON_TYPE_MAPPINGS = {304  Binary: (o: Binary) => new Binary(o.value(), o.sub_type),305  Code: (o: Code) => new Code(o.code, o.scope),306  DBRef: (o: DBRef) => new DBRef(o.collection || o.namespace, o.oid, o.db, o.fields), // "namespace" for 1.x library backwards compat307  Decimal128: (o: Decimal128) => new Decimal128(o.bytes),308  Double: (o: Double) => new Double(o.value),309  Int32: (o: Int32) => new Int32(o.value),310  Long: (311    o: Long & {312      low_: number;313      high_: number;314      unsigned_: boolean | undefined;315    }316  ) =>317    Long.fromBits(318      // underscore variants for 1.x backwards compatibility319      o.low != null ? o.low : o.low_,320      o.low != null ? o.high : o.high_,321      o.low != null ? o.unsigned : o.unsigned_322    ),323  MaxKey: () => new MaxKey(),324  MinKey: () => new MinKey(),325  ObjectId: (o: ObjectId) => new ObjectId(o),326  BSONRegExp: (o: BSONRegExp) => new BSONRegExp(o.pattern, o.options),327  BSONSymbol: (o: BSONSymbol) => new BSONSymbol(o.value),328  Timestamp: (o: Timestamp) => Timestamp.fromBits(o.low, o.high)329} as const;330 331// eslint-disable-next-line @typescript-eslint/no-explicit-any332function serializeDocument(doc: any, options: EJSONSerializeOptions) {333  if (doc == null || typeof doc !== 'object') throw new BSONError('not an object instance');334 335  const bsontype: BSONType['_bsontype'] = doc._bsontype;336  if (typeof bsontype === 'undefined') {337    // It's a regular object. Recursively serialize its property values.338    const _doc: Document = {};339    for (const name of Object.keys(doc)) {340      options.seenObjects.push({ propertyName: name, obj: null });341      try {342        const value = serializeValue(doc[name], options);343        if (name === '__proto__') {344          Object.defineProperty(_doc, name, {345            value,346            writable: true,347            enumerable: true,348            configurable: true349          });350        } else {351          _doc[name] = value;352        }353      } finally {354        options.seenObjects.pop();355      }356    }357    return _doc;358  } else if (359    doc != null &&360    typeof doc === 'object' &&361    typeof doc._bsontype === 'string' &&362    doc[BSON_VERSION_SYMBOL] !== BSON_MAJOR_VERSION363  ) {364    throw new BSONVersionError();365  } else if (isBSONType(doc)) {366    // the "document" is really just a BSON type object367    // eslint-disable-next-line @typescript-eslint/no-explicit-any368    let outDoc: any = doc;369    if (typeof outDoc.toExtendedJSON !== 'function') {370      // There's no EJSON serialization function on the object. It's probably an371      // object created by a previous version of this library (or another library)372      // that's duck-typing objects to look like they were generated by this library).373      // Copy the object into this library's version of that type.374      const mapper = BSON_TYPE_MAPPINGS[doc._bsontype];375      if (!mapper) {376        throw new BSONError('Unrecognized or invalid _bsontype: ' + doc._bsontype);377      }378      outDoc = mapper(outDoc);379    }380 381    // Two BSON types may have nested objects that may need to be serialized too382    if (bsontype === 'Code' && outDoc.scope) {383      outDoc = new Code(outDoc.code, serializeValue(outDoc.scope, options));384    } else if (bsontype === 'DBRef' && outDoc.oid) {385      outDoc = new DBRef(386        serializeValue(outDoc.collection, options),387        serializeValue(outDoc.oid, options),388        serializeValue(outDoc.db, options),389        serializeValue(outDoc.fields, options)390      );391    }392 393    return outDoc.toExtendedJSON(options);394  } else {395    throw new BSONError('_bsontype must be a string, but was: ' + typeof bsontype);396  }397}398 399/**400 * Parse an Extended JSON string, constructing the JavaScript value or object described by that401 * string.402 *403 * @example404 * ```js405 * const { EJSON } = require('bson');406 * const text = '{ "int32": { "$numberInt": "10" } }';407 *408 * // prints { int32: { [String: '10'] _bsontype: 'Int32', value: '10' } }409 * console.log(EJSON.parse(text, { relaxed: false }));410 *411 * // prints { int32: 10 }412 * console.log(EJSON.parse(text));413 * ```414 */415// eslint-disable-next-line @typescript-eslint/no-explicit-any416function parse(text: string, options?: EJSONOptions): any {417  const ejsonOptions = {418    useBigInt64: options?.useBigInt64 ?? false,419    relaxed: options?.relaxed ?? true,420    legacy: options?.legacy ?? false421  };422  return JSON.parse(text, (key, value) => {423    if (key.indexOf('\x00') !== -1) {424      throw new BSONError(425        `BSON Document field names cannot contain null bytes, found: ${JSON.stringify(key)}`426      );427    }428    return deserializeValue(value, ejsonOptions);429  });430}431 432/**433 * Converts a BSON document to an Extended JSON string, optionally replacing values if a replacer434 * function is specified or optionally including only the specified properties if a replacer array435 * is specified.436 *437 * @param value - The value to convert to extended JSON438 * @param replacer - A function that alters the behavior of the stringification process, or an array of String and Number objects that serve as a whitelist for selecting/filtering the properties of the value object to be included in the JSON string. If this value is null or not provided, all properties of the object are included in the resulting JSON string439 * @param space - A String or Number object that's used to insert white space into the output JSON string for readability purposes.440 * @param options - Optional settings441 *442 * @example443 * ```js444 * const { EJSON } = require('bson');445 * const Int32 = require('mongodb').Int32;446 * const doc = { int32: new Int32(10) };447 *448 * // prints '{"int32":{"$numberInt":"10"}}'449 * console.log(EJSON.stringify(doc, { relaxed: false }));450 *451 * // prints '{"int32":10}'452 * console.log(EJSON.stringify(doc));453 * ```454 */455function stringify(456  // eslint-disable-next-line @typescript-eslint/no-explicit-any457  value: any,458  // eslint-disable-next-line @typescript-eslint/no-explicit-any459  replacer?: (number | string)[] | ((this: any, key: string, value: any) => any) | EJSONOptions,460  space?: string | number,461  options?: EJSONOptions462): string {463  if (space != null && typeof space === 'object') {464    options = space;465    space = 0;466  }467  if (replacer != null && typeof replacer === 'object' && !Array.isArray(replacer)) {468    options = replacer;469    replacer = undefined;470    space = 0;471  }472  const serializeOptions = Object.assign({ relaxed: true, legacy: false }, options, {473    seenObjects: [{ propertyName: '(root)', obj: null }]474  });475 476  const doc = serializeValue(value, serializeOptions);477  return JSON.stringify(doc, replacer as Parameters<JSON['stringify']>[1], space);478}479 480/**481 * Serializes an object to an Extended JSON string, and reparse it as a JavaScript object.482 *483 * @param value - The object to serialize484 * @param options - Optional settings passed to the `stringify` function485 */486// eslint-disable-next-line @typescript-eslint/no-explicit-any487function EJSONserialize(value: any, options?: EJSONOptions): Document {488  options = options || {};489  return JSON.parse(stringify(value, options));490}491 492/**493 * Deserializes an Extended JSON object into a plain JavaScript object with native/BSON types494 *495 * @param ejson - The Extended JSON object to deserialize496 * @param options - Optional settings passed to the parse method497 */498// eslint-disable-next-line @typescript-eslint/no-explicit-any499function EJSONdeserialize(ejson: Document, options?: EJSONOptions): any {500  options = options || {};501  return parse(JSON.stringify(ejson), options);502}503 504/** @public */505const EJSON: {506  parse: typeof parse;507  stringify: typeof stringify;508  serialize: typeof EJSONserialize;509  deserialize: typeof EJSONdeserialize;510} = Object.create(null);511EJSON.parse = parse;512EJSON.stringify = stringify;513EJSON.serialize = EJSONserialize;514EJSON.deserialize = EJSONdeserialize;515Object.freeze(EJSON);516export { EJSON };517