opusdev/vector-similarity-api
1
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 