responses.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.ClientBulkWriteCursorResponse = exports.ExplainedCursorResponse = exports.CursorResponse = exports.MongoDBResponse = void 0;
  4. exports.isErrorResponse = isErrorResponse;
  5. const bson_1 = require("../../bson");
  6. const error_1 = require("../../error");
  7. const utils_1 = require("../../utils");
  8. const document_1 = require("./on_demand/document");
  9. const BSONElementOffset = {
  10. type: 0,
  11. nameOffset: 1,
  12. nameLength: 2,
  13. offset: 3,
  14. length: 4
  15. };
  16. /**
  17. * Accepts a BSON payload and checks for na "ok: 0" element.
  18. * This utility is intended to prevent calling response class constructors
  19. * that expect the result to be a success and demand certain properties to exist.
  20. *
  21. * For example, a cursor response always expects a cursor embedded document.
  22. * In order to write the class such that the properties reflect that assertion (non-null)
  23. * we cannot invoke the subclass constructor if the BSON represents an error.
  24. *
  25. * @param bytes - BSON document returned from the server
  26. */
  27. function isErrorResponse(bson, elements) {
  28. for (let eIdx = 0; eIdx < elements.length; eIdx++) {
  29. const element = elements[eIdx];
  30. if (element[BSONElementOffset.nameLength] === 2) {
  31. const nameOffset = element[BSONElementOffset.nameOffset];
  32. // 111 == "o", 107 == "k"
  33. if (bson[nameOffset] === 111 && bson[nameOffset + 1] === 107) {
  34. const valueOffset = element[BSONElementOffset.offset];
  35. const valueLength = element[BSONElementOffset.length];
  36. // If any byte in the length of the ok number (works for any type) is non zero,
  37. // then it is considered "ok: 1"
  38. for (let i = valueOffset; i < valueOffset + valueLength; i++) {
  39. if (bson[i] !== 0x00)
  40. return false;
  41. }
  42. return true;
  43. }
  44. }
  45. }
  46. return true;
  47. }
  48. /** @internal */
  49. class MongoDBResponse extends document_1.OnDemandDocument {
  50. get(name, as, required) {
  51. try {
  52. return super.get(name, as, required);
  53. }
  54. catch (cause) {
  55. throw new error_1.MongoUnexpectedServerResponseError(cause.message, { cause });
  56. }
  57. }
  58. static is(value) {
  59. return value instanceof MongoDBResponse;
  60. }
  61. static make(bson) {
  62. const elements = (0, bson_1.parseToElementsToArray)(bson, 0);
  63. const isError = isErrorResponse(bson, elements);
  64. return isError
  65. ? new MongoDBResponse(bson, 0, false, elements)
  66. : new this(bson, 0, false, elements);
  67. }
  68. /**
  69. * Returns true iff:
  70. * - ok is 0 and the top-level code === 50
  71. * - ok is 1 and the writeErrors array contains a code === 50
  72. * - ok is 1 and the writeConcern object contains a code === 50
  73. */
  74. get isMaxTimeExpiredError() {
  75. // {ok: 0, code: 50 ... }
  76. const isTopLevel = this.ok === 0 && this.code === error_1.MONGODB_ERROR_CODES.MaxTimeMSExpired;
  77. if (isTopLevel)
  78. return true;
  79. if (this.ok === 0)
  80. return false;
  81. // {ok: 1, writeConcernError: {code: 50 ... }}
  82. const isWriteConcern = this.get('writeConcernError', bson_1.BSONType.object)?.getNumber('code') ===
  83. error_1.MONGODB_ERROR_CODES.MaxTimeMSExpired;
  84. if (isWriteConcern)
  85. return true;
  86. const writeErrors = this.get('writeErrors', bson_1.BSONType.array);
  87. if (writeErrors?.size()) {
  88. for (let i = 0; i < writeErrors.size(); i++) {
  89. const isWriteError = writeErrors.get(i, bson_1.BSONType.object)?.getNumber('code') ===
  90. error_1.MONGODB_ERROR_CODES.MaxTimeMSExpired;
  91. // {ok: 1, writeErrors: [{code: 50 ... }]}
  92. if (isWriteError)
  93. return true;
  94. }
  95. }
  96. return false;
  97. }
  98. /**
  99. * Drivers can safely assume that the `recoveryToken` field is always a BSON document but drivers MUST NOT modify the
  100. * contents of the document.
  101. */
  102. get recoveryToken() {
  103. return (this.get('recoveryToken', bson_1.BSONType.object)?.toObject({
  104. promoteValues: false,
  105. promoteLongs: false,
  106. promoteBuffers: false,
  107. validation: { utf8: true }
  108. }) ?? null);
  109. }
  110. /**
  111. * The server creates a cursor in response to a snapshot find/aggregate command and reports atClusterTime within the cursor field in the response.
  112. * For the distinct command the server adds a top-level atClusterTime field to the response.
  113. * The atClusterTime field represents the timestamp of the read and is guaranteed to be majority committed.
  114. */
  115. get atClusterTime() {
  116. return (this.get('cursor', bson_1.BSONType.object)?.get('atClusterTime', bson_1.BSONType.timestamp) ??
  117. this.get('atClusterTime', bson_1.BSONType.timestamp));
  118. }
  119. get operationTime() {
  120. return this.get('operationTime', bson_1.BSONType.timestamp);
  121. }
  122. /** Normalizes whatever BSON value is "ok" to a JS number 1 or 0. */
  123. get ok() {
  124. return this.getNumber('ok') ? 1 : 0;
  125. }
  126. get $err() {
  127. return this.get('$err', bson_1.BSONType.string);
  128. }
  129. get errmsg() {
  130. return this.get('errmsg', bson_1.BSONType.string);
  131. }
  132. get code() {
  133. return this.getNumber('code');
  134. }
  135. get $clusterTime() {
  136. if (!('clusterTime' in this)) {
  137. const clusterTimeDoc = this.get('$clusterTime', bson_1.BSONType.object);
  138. if (clusterTimeDoc == null) {
  139. this.clusterTime = null;
  140. return null;
  141. }
  142. const clusterTime = clusterTimeDoc.get('clusterTime', bson_1.BSONType.timestamp, true);
  143. const signature = clusterTimeDoc.get('signature', bson_1.BSONType.object)?.toObject();
  144. // @ts-expect-error: `signature` is incorrectly typed. It is public API.
  145. this.clusterTime = { clusterTime, signature };
  146. }
  147. return this.clusterTime ?? null;
  148. }
  149. toObject(options) {
  150. const exactBSONOptions = {
  151. ...(0, bson_1.pluckBSONSerializeOptions)(options ?? {}),
  152. validation: (0, bson_1.parseUtf8ValidationOption)(options)
  153. };
  154. return super.toObject(exactBSONOptions);
  155. }
  156. }
  157. exports.MongoDBResponse = MongoDBResponse;
  158. // {ok:1}
  159. MongoDBResponse.empty = new MongoDBResponse(new Uint8Array([13, 0, 0, 0, 16, 111, 107, 0, 1, 0, 0, 0, 0]));
  160. /** @internal */
  161. class CursorResponse extends MongoDBResponse {
  162. constructor() {
  163. super(...arguments);
  164. this._batch = null;
  165. this.iterated = 0;
  166. this._encryptedBatch = null;
  167. }
  168. /**
  169. * This supports a feature of the FindCursor.
  170. * It is an optimization to avoid an extra getMore when the limit has been reached
  171. */
  172. static get emptyGetMore() {
  173. return new CursorResponse((0, bson_1.serialize)({ ok: 1, cursor: { id: 0n, nextBatch: [] } }));
  174. }
  175. static is(value) {
  176. return value instanceof CursorResponse || value === CursorResponse.emptyGetMore;
  177. }
  178. get cursor() {
  179. return this.get('cursor', bson_1.BSONType.object, true);
  180. }
  181. get id() {
  182. try {
  183. return bson_1.Long.fromBigInt(this.cursor.get('id', bson_1.BSONType.long, true));
  184. }
  185. catch (cause) {
  186. throw new error_1.MongoUnexpectedServerResponseError(cause.message, { cause });
  187. }
  188. }
  189. get ns() {
  190. const namespace = this.cursor.get('ns', bson_1.BSONType.string);
  191. if (namespace != null)
  192. return (0, utils_1.ns)(namespace);
  193. return null;
  194. }
  195. get length() {
  196. return Math.max(this.batchSize - this.iterated, 0);
  197. }
  198. get encryptedBatch() {
  199. if (this.encryptedResponse == null)
  200. return null;
  201. if (this._encryptedBatch != null)
  202. return this._encryptedBatch;
  203. const cursor = this.encryptedResponse?.get('cursor', bson_1.BSONType.object);
  204. if (cursor?.has('firstBatch'))
  205. this._encryptedBatch = cursor.get('firstBatch', bson_1.BSONType.array, true);
  206. else if (cursor?.has('nextBatch'))
  207. this._encryptedBatch = cursor.get('nextBatch', bson_1.BSONType.array, true);
  208. else
  209. throw new error_1.MongoUnexpectedServerResponseError('Cursor document did not contain a batch');
  210. return this._encryptedBatch;
  211. }
  212. get batch() {
  213. if (this._batch != null)
  214. return this._batch;
  215. const cursor = this.cursor;
  216. if (cursor.has('firstBatch'))
  217. this._batch = cursor.get('firstBatch', bson_1.BSONType.array, true);
  218. else if (cursor.has('nextBatch'))
  219. this._batch = cursor.get('nextBatch', bson_1.BSONType.array, true);
  220. else
  221. throw new error_1.MongoUnexpectedServerResponseError('Cursor document did not contain a batch');
  222. return this._batch;
  223. }
  224. get batchSize() {
  225. return this.batch?.size();
  226. }
  227. get postBatchResumeToken() {
  228. return (this.cursor.get('postBatchResumeToken', bson_1.BSONType.object)?.toObject({
  229. promoteValues: false,
  230. promoteLongs: false,
  231. promoteBuffers: false,
  232. validation: { utf8: true }
  233. }) ?? null);
  234. }
  235. shift(options) {
  236. if (this.iterated >= this.batchSize) {
  237. return null;
  238. }
  239. const result = this.batch.get(this.iterated, bson_1.BSONType.object, true) ?? null;
  240. const encryptedResult = this.encryptedBatch?.get(this.iterated, bson_1.BSONType.object, true) ?? null;
  241. this.iterated += 1;
  242. if (options?.raw) {
  243. return result.toBytes();
  244. }
  245. else {
  246. const object = result.toObject(options);
  247. if (encryptedResult) {
  248. (0, utils_1.decorateDecryptionResult)(object, encryptedResult.toObject(options), true);
  249. }
  250. return object;
  251. }
  252. }
  253. clear() {
  254. this.iterated = this.batchSize;
  255. }
  256. }
  257. exports.CursorResponse = CursorResponse;
  258. /**
  259. * Explain responses have nothing to do with cursor responses
  260. * This class serves to temporarily avoid refactoring how cursors handle
  261. * explain responses which is to detect that the response is not cursor-like and return the explain
  262. * result as the "first and only" document in the "batch" and end the "cursor"
  263. */
  264. class ExplainedCursorResponse extends CursorResponse {
  265. constructor() {
  266. super(...arguments);
  267. this.isExplain = true;
  268. this._length = 1;
  269. }
  270. get id() {
  271. return bson_1.Long.fromBigInt(0n);
  272. }
  273. get batchSize() {
  274. return 0;
  275. }
  276. get ns() {
  277. return null;
  278. }
  279. get length() {
  280. return this._length;
  281. }
  282. shift(options) {
  283. if (this._length === 0)
  284. return null;
  285. this._length -= 1;
  286. return this.toObject(options);
  287. }
  288. }
  289. exports.ExplainedCursorResponse = ExplainedCursorResponse;
  290. /**
  291. * Client bulk writes have some extra metadata at the top level that needs to be
  292. * included in the result returned to the user.
  293. */
  294. class ClientBulkWriteCursorResponse extends CursorResponse {
  295. get insertedCount() {
  296. return this.get('nInserted', bson_1.BSONType.int, true);
  297. }
  298. get upsertedCount() {
  299. return this.get('nUpserted', bson_1.BSONType.int, true);
  300. }
  301. get matchedCount() {
  302. return this.get('nMatched', bson_1.BSONType.int, true);
  303. }
  304. get modifiedCount() {
  305. return this.get('nModified', bson_1.BSONType.int, true);
  306. }
  307. get deletedCount() {
  308. return this.get('nDeleted', bson_1.BSONType.int, true);
  309. }
  310. get writeConcernError() {
  311. return this.get('writeConcernError', bson_1.BSONType.object, false);
  312. }
  313. }
  314. exports.ClientBulkWriteCursorResponse = ClientBulkWriteCursorResponse;
  315. //# sourceMappingURL=responses.js.map