import { Binary } from "./binary.ts";import type { Document } from "./bson.ts";import { Code } from "./code.ts";import { DBRef, isDBRefLike } from "./db_ref.ts";import { Decimal128 } from "./decimal128.ts";import { Double } from "./double.ts";import { BSONError, BSONTypeError } from "./error.ts";import { Int32 } from "./int_32.ts";import { Long } from "./long.ts";import { MaxKey } from "./max_key.ts";import { MinKey } from "./min_key.ts";import { ObjectId } from "./objectid.ts";import { isObjectLike } from "./parser/utils.ts";import { BSONRegExp } from "./regexp.ts";import { BSONSymbol } from "./symbol.ts";import { Timestamp } from "./timestamp.ts";
export type EJSONOptions = Options;
type BSONType = | Binary | Code | DBRef | Decimal128 | Double | Int32 | Long | MaxKey | MinKey | ObjectId | BSONRegExp | BSONSymbol | Timestamp;
export function isBSONType(value: unknown): value is BSONType { return ( isObjectLike(value) && Reflect.has(value, "_bsontype") && typeof value._bsontype === "string" );}
const BSON_INT32_MAX = 0x7fffffff;const BSON_INT32_MIN = -0x80000000;const BSON_INT64_MAX = 0x7fffffffffffffff;const BSON_INT64_MIN = -0x8000000000000000;
const keysToCodecs = { $oid: ObjectId, $binary: Binary, $uuid: Binary, $symbol: BSONSymbol, $numberInt: Int32, $numberDecimal: Decimal128, $numberDouble: Double, $numberLong: Long, $minKey: MinKey, $maxKey: MaxKey, $regex: BSONRegExp, $regularExpression: BSONRegExp, $timestamp: Timestamp,} as const;
function deserializeValue(value: any, options: Options = {}) { if (typeof value === "number") { if (options.relaxed || options.legacy) { return value; }
if (Math.floor(value) === value) { if (value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) { return new Int32(value); } if (value >= BSON_INT64_MIN && value <= BSON_INT64_MAX) { return Long.fromNumber(value); } }
return new Double(value); }
if (value == null || typeof value !== "object") return value;
if (value.$undefined) return null;
const keys = Object.keys(value).filter( (k) => k.startsWith("$") && value[k] != null, ) as (keyof typeof keysToCodecs)[]; for (let i = 0; i < keys.length; i++) { const c = keysToCodecs[keys[i]]; if (c) return c.fromExtendedJSON(value, options); }
if (value.$date != null) { const d = value.$date; const date = new Date();
if (options.legacy) { if (typeof d === "number") date.setTime(d); else if (typeof d === "string") date.setTime(Date.parse(d)); } else { if (typeof d === "string") date.setTime(Date.parse(d)); else if (Long.isLong(d)) date.setTime(d.toNumber()); else if (typeof d === "number" && options.relaxed) date.setTime(d); } return date; }
if (value.$code != null) { const copy = Object.assign({}, value); if (value.$scope) { copy.$scope = deserializeValue(value.$scope); }
return Code.fromExtendedJSON(value); }
if (isDBRefLike(value) || value.$dbPointer) { const v = value.$ref ? value : value.$dbPointer;
if (v instanceof DBRef) return v;
const dollarKeys = Object.keys(v).filter((k) => k.startsWith("$")); let valid = true; dollarKeys.forEach((k) => { if (["$ref", "$id", "$db"].indexOf(k) === -1) valid = false; });
if (valid) return DBRef.fromExtendedJSON(v); }
return value;}
type EJSONSerializeOptions = Options & { seenObjects: { obj: unknown; propertyName: string }[];};
function serializeArray(array: any[], options: EJSONSerializeOptions): any[] { return array.map((v: unknown, index: number) => { options.seenObjects.push({ propertyName: `index ${index}`, obj: null }); try { return serializeValue(v, options); } finally { options.seenObjects.pop(); } });}
function getISOString(date: Date) { const isoStr = date.toISOString(); return date.getUTCMilliseconds() !== 0 ? isoStr : isoStr.slice(0, -5) + "Z";}
function serializeValue(value: any, options: EJSONSerializeOptions): any { if ( (typeof value === "object" || typeof value === "function") && value !== null ) { const index = options.seenObjects.findIndex((entry) => entry.obj === value); if (index !== -1) { const props = options.seenObjects.map((entry) => entry.propertyName); const leadingPart = props .slice(0, index) .map((prop) => `${prop} -> `) .join(""); const alreadySeen = props[index]; const circularPart = " -> " + props .slice(index + 1, props.length - 1) .map((prop) => `${prop} -> `) .join(""); const current = props[props.length - 1]; const leadingSpace = " ".repeat( leadingPart.length + alreadySeen.length / 2, ); const dashes = "-".repeat( circularPart.length + (alreadySeen.length + current.length) / 2 - 1, );
throw new BSONTypeError( "Converting circular structure to EJSON:\n" + ` ${leadingPart}${alreadySeen}${circularPart}${current}\n` + ` ${leadingSpace}\\${dashes}/`, ); } options.seenObjects[options.seenObjects.length - 1].obj = value; }
if (Array.isArray(value)) return serializeArray(value, options);
if (value === undefined) return null;
if (value instanceof Date) { const dateNum = value.getTime(), inRange = dateNum > -1 && dateNum < 253402318800000;
if (options.legacy) { return options.relaxed && inRange ? { $date: value.getTime() } : { $date: getISOString(value) }; } return options.relaxed && inRange ? { $date: getISOString(value) } : { $date: { $numberLong: value.getTime().toString() } }; }
if (typeof value === "number" && (!options.relaxed || !isFinite(value))) { if (Math.floor(value) === value) { const int32Range = value >= BSON_INT32_MIN && value <= BSON_INT32_MAX, int64Range = value >= BSON_INT64_MIN && value <= BSON_INT64_MAX;
if (int32Range) return { $numberInt: value.toString() }; if (int64Range) return { $numberLong: value.toString() }; } return { $numberDouble: value.toString() }; }
if (value instanceof RegExp) { let flags = value.flags; if (flags === undefined) { const match = value.toString().match(/[gimuy]*$/); if (match) { flags = match[0]; } }
const rx = new BSONRegExp(value.source, flags); return rx.toExtendedJSON(options); }
if (value != null && typeof value === "object") { return serializeDocument(value, options); } return value;}
const BSON_TYPE_MAPPINGS = { Binary: (o: Binary) => new Binary(o.buffer, o.subType), Code: (o: Code) => new Code(o.code, o.scope), DBRef: (o: DBRef) => new DBRef(o.collection, o.oid, o.db, o.fields), Decimal128: (o: Decimal128) => new Decimal128(o.bytes), Double: (o: Double) => new Double(o.value), Int32: (o: Int32) => new Int32(o.value), Long: ( o: Long & { low_: number; high_: number; unsigned_: boolean | undefined; }, ) => Long.fromBits( o.low != null ? o.low : o.low_, o.low != null ? o.high : o.high_, o.low != null ? o.unsigned : o.unsigned_, ), MaxKey: () => new MaxKey(), MinKey: () => new MinKey(), ObjectId: (o: ObjectId) => new ObjectId(o), ObjectID: (o: ObjectId) => new ObjectId(o), BSONRegExp: (o: BSONRegExp) => new BSONRegExp(o.pattern, o.options), Symbol: (o: BSONSymbol) => new BSONSymbol(o.value), Timestamp: (o: Timestamp) => Timestamp.fromBits(o.low, o.high),} as const;
function serializeDocument(doc: any, options: EJSONSerializeOptions) { if (doc == null || typeof doc !== "object") { throw new BSONError("not an object instance"); }
const bsontype: BSONType["_bsontype"] = doc._bsontype; if (typeof bsontype === "undefined") { const _doc: Document = {}; for (const name in doc) { options.seenObjects.push({ propertyName: name, obj: null }); try { _doc[name] = serializeValue(doc[name], options); } finally { options.seenObjects.pop(); } } return _doc; } else if (isBSONType(doc)) { let outDoc: any = doc; if (typeof outDoc.toExtendedJSON !== "function") { const mapper = BSON_TYPE_MAPPINGS[doc._bsontype]; if (!mapper) { throw new BSONTypeError( "Unrecognized or invalid _bsontype: " + doc._bsontype, ); } outDoc = mapper(outDoc); }
if (bsontype === "Code" && outDoc.scope) { outDoc = new Code(outDoc.code, serializeValue(outDoc.scope, options)); } else if (bsontype === "DBRef" && outDoc.oid) { outDoc = new DBRef( serializeValue(outDoc.collection, options), serializeValue(outDoc.oid, options), serializeValue(outDoc.db, options), serializeValue(outDoc.fields, options), ); }
return outDoc.toExtendedJSON(options); } else { throw new BSONError( "_bsontype must be a string, but was: " + typeof bsontype, ); }}
export interface Options { legacy?: boolean; relaxed?: boolean; strict?: boolean;}
export function parse( text: string, options?: Options,): SerializableTypes { const finalOptions = Object.assign( {}, { relaxed: true, legacy: false }, options, );
if (typeof finalOptions.relaxed === "boolean") { finalOptions.strict = !finalOptions.relaxed; } if (typeof finalOptions.strict === "boolean") { finalOptions.relaxed = !finalOptions.strict; }
return JSON.parse(text, (key, value) => { if (key.indexOf("\x00") !== -1) { throw new BSONError( `BSON Document field names cannot contain null bytes, found: ${ JSON.stringify(key) }`, ); } return deserializeValue(value, finalOptions); });}
export type JSONPrimitive = string | number | boolean | null;export type SerializableTypes = | Document | Array<JSONPrimitive | Document> | JSONPrimitive;
export function stringify( value: SerializableTypes, replacer?: | (number | string)[] | ((this: any, key: string, value: any) => any) | Options, space?: string | number, options?: Options,): string { if (space != null && typeof space === "object") { options = space; space = 0; } if ( replacer != null && typeof replacer === "object" && !Array.isArray(replacer) ) { options = replacer; replacer = undefined; space = 0; } const serializeOptions = Object.assign( { relaxed: true, legacy: false }, options, { seenObjects: [{ propertyName: "(root)", obj: null }], }, );
const doc = serializeValue(value, serializeOptions); return JSON.stringify( doc, replacer as Parameters<JSON["stringify"]>[1], space, );}
export function serialize( value: SerializableTypes, options?: Options,): Document { options = options || {}; return JSON.parse(stringify(value, options));}
export function deserialize( ejson: Document, options?: Options,): SerializableTypes { options = options || {}; return parse(JSON.stringify(ejson), options);}