extended_json.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. 'use strict';
  2. // const Buffer = require('buffer').Buffer;
  3. // const Map = require('./map');
  4. const Long = require('./long');
  5. const Double = require('./double');
  6. const Timestamp = require('./timestamp');
  7. const ObjectId = require('./objectid');
  8. const BSONRegExp = require('./regexp');
  9. const Symbol = require('./symbol');
  10. const Int32 = require('./int_32');
  11. const Code = require('./code');
  12. const Decimal128 = require('./decimal128');
  13. const MinKey = require('./min_key');
  14. const MaxKey = require('./max_key');
  15. const DBRef = require('./db_ref');
  16. const Binary = require('./binary');
  17. /**
  18. * @namespace EJSON
  19. */
  20. // all the types where we don't need to do any special processing and can just pass the EJSON
  21. //straight to type.fromExtendedJSON
  22. const keysToCodecs = {
  23. $oid: ObjectId,
  24. $binary: Binary,
  25. $symbol: Symbol,
  26. $numberInt: Int32,
  27. $numberDecimal: Decimal128,
  28. $numberDouble: Double,
  29. $numberLong: Long,
  30. $minKey: MinKey,
  31. $maxKey: MaxKey,
  32. $regularExpression: BSONRegExp,
  33. $timestamp: Timestamp
  34. };
  35. function deserializeValue(self, key, value, options) {
  36. if (typeof value === 'number') {
  37. if (options.relaxed) {
  38. return value;
  39. }
  40. // if it's an integer, should interpret as smallest BSON integer
  41. // that can represent it exactly. (if out of range, interpret as double.)
  42. if (Math.floor(value) === value) {
  43. if (value >= BSON_INT32_MIN && value <= BSON_INT32_MAX) return new Int32(value);
  44. if (value >= BSON_INT64_MIN && value <= BSON_INT64_MAX) return new Long.fromNumber(value);
  45. }
  46. // If the number is a non-integer or out of integer range, should interpret as BSON Double.
  47. return new Double(value);
  48. }
  49. // from here on out we're looking for bson types, so bail if its not an object
  50. if (value == null || typeof value !== 'object') return value;
  51. // upgrade deprecated undefined to null
  52. if (value.$undefined) return null;
  53. const keys = Object.keys(value).filter(k => k.startsWith('$') && value[k] != null);
  54. for (let i = 0; i < keys.length; i++) {
  55. let c = keysToCodecs[keys[i]];
  56. if (c) return c.fromExtendedJSON(value, options);
  57. }
  58. if (value.$date != null) {
  59. const d = value.$date;
  60. const date = new Date();
  61. if (typeof d === 'string') date.setTime(Date.parse(d));
  62. else if (d instanceof Long) date.setTime(d.toNumber());
  63. else if (typeof d === 'number' && options.relaxed) date.setTime(d);
  64. return date;
  65. }
  66. if (value.$code != null) {
  67. let copy = Object.assign({}, value);
  68. if (value.$scope) {
  69. copy.$scope = deserializeValue(self, null, value.$scope);
  70. }
  71. return Code.fromExtendedJSON(value);
  72. }
  73. if (value.$ref != null || value.$dbPointer != null) {
  74. let v = value.$ref ? value : value.$dbPointer;
  75. // we run into this in a "degenerate EJSON" case (with $id and $ref order flipped)
  76. // because of the order JSON.parse goes through the document
  77. if (v instanceof DBRef) return v;
  78. const dollarKeys = Object.keys(v).filter(k => k.startsWith('$'));
  79. let valid = true;
  80. dollarKeys.forEach(k => {
  81. if (['$ref', '$id', '$db'].indexOf(k) === -1) valid = false;
  82. });
  83. // only make DBRef if $ keys are all valid
  84. if (valid) return DBRef.fromExtendedJSON(v);
  85. }
  86. return value;
  87. }
  88. /**
  89. * Parse an Extended JSON string, constructing the JavaScript value or object described by that
  90. * string.
  91. *
  92. * @memberof EJSON
  93. * @param {string} text
  94. * @param {object} [options] Optional settings
  95. * @param {boolean} [options.relaxed=true] Attempt to return native JS types where possible, rather than BSON types (if true)
  96. * @return {object}
  97. *
  98. * @example
  99. * const { EJSON } = require('bson');
  100. * const text = '{ "int32": { "$numberInt": "10" } }';
  101. *
  102. * // prints { int32: { [String: '10'] _bsontype: 'Int32', value: '10' } }
  103. * console.log(EJSON.parse(text, { relaxed: false }));
  104. *
  105. * // prints { int32: 10 }
  106. * console.log(EJSON.parse(text));
  107. */
  108. function parse(text, options) {
  109. options = Object.assign({}, { relaxed: true }, options);
  110. // relaxed implies not strict
  111. if (typeof options.relaxed === 'boolean') options.strict = !options.relaxed;
  112. if (typeof options.strict === 'boolean') options.relaxed = !options.strict;
  113. return JSON.parse(text, (key, value) => deserializeValue(this, key, value, options));
  114. }
  115. //
  116. // Serializer
  117. //
  118. // MAX INT32 boundaries
  119. const BSON_INT32_MAX = 0x7fffffff,
  120. BSON_INT32_MIN = -0x80000000,
  121. BSON_INT64_MAX = 0x7fffffffffffffff,
  122. BSON_INT64_MIN = -0x8000000000000000;
  123. /**
  124. * Converts a BSON document to an Extended JSON string, optionally replacing values if a replacer
  125. * function is specified or optionally including only the specified properties if a replacer array
  126. * is specified.
  127. *
  128. * @memberof EJSON
  129. * @param {object} value The value to convert to extended JSON
  130. * @param {function|array} [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 string
  131. * @param {string|number} [space] A String or Number object that's used to insert white space into the output JSON string for readability purposes.
  132. * @param {object} [options] Optional settings
  133. * @param {boolean} [options.relaxed=true] Enabled Extended JSON's `relaxed` mode
  134. * @returns {string}
  135. *
  136. * @example
  137. * const { EJSON } = require('bson');
  138. * const Int32 = require('mongodb').Int32;
  139. * const doc = { int32: new Int32(10) };
  140. *
  141. * // prints '{"int32":{"$numberInt":"10"}}'
  142. * console.log(EJSON.stringify(doc, { relaxed: false }));
  143. *
  144. * // prints '{"int32":10}'
  145. * console.log(EJSON.stringify(doc));
  146. */
  147. function stringify(value, replacer, space, options) {
  148. if (space != null && typeof space === 'object') (options = space), (space = 0);
  149. if (replacer != null && typeof replacer === 'object')
  150. (options = replacer), (replacer = null), (space = 0);
  151. options = Object.assign({}, { relaxed: true }, options);
  152. const doc = Array.isArray(value)
  153. ? serializeArray(value, options)
  154. : serializeDocument(value, options);
  155. return JSON.stringify(doc, replacer, space);
  156. }
  157. /**
  158. * Serializes an object to an Extended JSON string, and reparse it as a JavaScript object.
  159. *
  160. * @memberof EJSON
  161. * @param {object} bson The object to serialize
  162. * @param {object} [options] Optional settings passed to the `stringify` function
  163. * @return {object}
  164. */
  165. function serialize(bson, options) {
  166. options = options || {};
  167. return JSON.parse(stringify(bson, options));
  168. }
  169. /**
  170. * Deserializes an Extended JSON object into a plain JavaScript object with native/BSON types
  171. *
  172. * @memberof EJSON
  173. * @param {object} ejson The Extended JSON object to deserialize
  174. * @param {object} [options] Optional settings passed to the parse method
  175. * @return {object}
  176. */
  177. function deserialize(ejson, options) {
  178. options = options || {};
  179. return parse(JSON.stringify(ejson), options);
  180. }
  181. function serializeArray(array, options) {
  182. return array.map(v => serializeValue(v, options));
  183. }
  184. function getISOString(date) {
  185. const isoStr = date.toISOString();
  186. // we should only show milliseconds in timestamp if they're non-zero
  187. return date.getUTCMilliseconds() !== 0 ? isoStr : isoStr.slice(0, -5) + 'Z';
  188. }
  189. function serializeValue(value, options) {
  190. if (Array.isArray(value)) return serializeArray(value, options);
  191. if (value === undefined) return null;
  192. if (value instanceof Date) {
  193. let dateNum = value.getTime(),
  194. // is it in year range 1970-9999?
  195. inRange = dateNum > -1 && dateNum < 253402318800000;
  196. return options.relaxed && inRange
  197. ? { $date: getISOString(value) }
  198. : { $date: { $numberLong: value.getTime().toString() } };
  199. }
  200. if (typeof value === 'number' && !options.relaxed) {
  201. // it's an integer
  202. if (Math.floor(value) === value) {
  203. let int32Range = value >= BSON_INT32_MIN && value <= BSON_INT32_MAX,
  204. int64Range = value >= BSON_INT64_MIN && value <= BSON_INT64_MAX;
  205. // interpret as being of the smallest BSON integer type that can represent the number exactly
  206. if (int32Range) return { $numberInt: value.toString() };
  207. if (int64Range) return { $numberLong: value.toString() };
  208. }
  209. return { $numberDouble: value.toString() };
  210. }
  211. if (value != null && typeof value === 'object') return serializeDocument(value, options);
  212. return value;
  213. }
  214. function serializeDocument(doc, options) {
  215. if (doc == null || typeof doc !== 'object') throw new Error('not an object instance');
  216. // the document itself is a BSON type
  217. if (doc._bsontype && typeof doc.toExtendedJSON === 'function') {
  218. if (doc._bsontype === 'Code' && doc.scope) {
  219. doc.scope = serializeDocument(doc.scope, options);
  220. } else if (doc._bsontype === 'DBRef' && doc.oid) {
  221. doc.oid = serializeDocument(doc.oid, options);
  222. }
  223. return doc.toExtendedJSON(options);
  224. }
  225. // the document is an object with nested BSON types
  226. const _doc = {};
  227. for (let name in doc) {
  228. let val = doc[name];
  229. if (Array.isArray(val)) {
  230. _doc[name] = serializeArray(val, options);
  231. } else if (val != null && typeof val.toExtendedJSON === 'function') {
  232. if (val._bsontype === 'Code' && val.scope) {
  233. val.scope = serializeDocument(val.scope, options);
  234. } else if (val._bsontype === 'DBRef' && val.oid) {
  235. val.oid = serializeDocument(val.oid, options);
  236. }
  237. _doc[name] = val.toExtendedJSON(options);
  238. } else if (val instanceof Date) {
  239. _doc[name] = serializeValue(val, options);
  240. } else if (val != null && typeof val === 'object') {
  241. _doc[name] = serializeDocument(val, options);
  242. }
  243. _doc[name] = serializeValue(val, options);
  244. if (val instanceof RegExp) {
  245. let flags = val.flags;
  246. if (flags === undefined) {
  247. flags = val.toString().match(/[gimuy]*$/)[0];
  248. }
  249. const rx = new BSONRegExp(val.source, flags);
  250. _doc[name] = rx.toExtendedJSON();
  251. }
  252. }
  253. return _doc;
  254. }
  255. module.exports = {
  256. parse,
  257. deserialize,
  258. serialize,
  259. stringify
  260. };