bundle.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. "use strict";
  2. // Copyright 2020 Google LLC
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // https://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. Object.defineProperty(exports, "__esModule", { value: true });
  16. exports.BundleBuilder = void 0;
  17. const document_1 = require("./document");
  18. const query_snapshot_1 = require("./reference/query-snapshot");
  19. const timestamp_1 = require("./timestamp");
  20. const validate_1 = require("./validate");
  21. const BUNDLE_VERSION = 1;
  22. /**
  23. * Builds a Firestore data bundle with results from the given document and query snapshots.
  24. */
  25. class BundleBuilder {
  26. constructor(bundleId) {
  27. this.bundleId = bundleId;
  28. // Resulting documents for the bundle, keyed by full document path.
  29. this.documents = new Map();
  30. // Named queries saved in the bundle, keyed by query name.
  31. this.namedQueries = new Map();
  32. // The latest read time among all bundled documents and queries.
  33. this.latestReadTime = new timestamp_1.Timestamp(0, 0);
  34. }
  35. /**
  36. * Adds a Firestore document snapshot or query snapshot to the bundle.
  37. * Both the documents data and the query read time will be included in the bundle.
  38. *
  39. * @param {DocumentSnapshot | string} documentOrName A document snapshot to add or a name of a query.
  40. * @param {Query=} querySnapshot A query snapshot to add to the bundle, if provided.
  41. * @returns {BundleBuilder} This instance.
  42. *
  43. * @example
  44. * ```
  45. * const bundle = firestore.bundle('data-bundle');
  46. * const docSnapshot = await firestore.doc('abc/123').get();
  47. * const querySnapshot = await firestore.collection('coll').get();
  48. *
  49. * const bundleBuffer = bundle.add(docSnapshot) // Add a document
  50. * .add('coll-query', querySnapshot) // Add a named query.
  51. * .build()
  52. * // Save `bundleBuffer` to CDN or stream it to clients.
  53. * ```
  54. */
  55. add(documentOrName, querySnapshot) {
  56. // eslint-disable-next-line prefer-rest-params
  57. (0, validate_1.validateMinNumberOfArguments)('BundleBuilder.add', arguments, 1);
  58. // eslint-disable-next-line prefer-rest-params
  59. (0, validate_1.validateMaxNumberOfArguments)('BundleBuilder.add', arguments, 2);
  60. if (arguments.length === 1) {
  61. validateDocumentSnapshot('documentOrName', documentOrName);
  62. this.addBundledDocument(documentOrName);
  63. }
  64. else {
  65. (0, validate_1.validateString)('documentOrName', documentOrName);
  66. validateQuerySnapshot('querySnapshot', querySnapshot);
  67. this.addNamedQuery(documentOrName, querySnapshot);
  68. }
  69. return this;
  70. }
  71. addBundledDocument(snap, queryName) {
  72. const originalDocument = this.documents.get(snap.ref.path);
  73. const originalQueries = originalDocument === null || originalDocument === void 0 ? void 0 : originalDocument.metadata.queries;
  74. // Update with document built from `snap` because it is newer.
  75. if (!originalDocument ||
  76. timestamp_1.Timestamp.fromProto(originalDocument.metadata.readTime) < snap.readTime) {
  77. const docProto = snap.toDocumentProto();
  78. this.documents.set(snap.ref.path, {
  79. document: snap.exists ? docProto : undefined,
  80. metadata: {
  81. name: docProto.name,
  82. readTime: snap.readTime.toProto().timestampValue,
  83. exists: snap.exists,
  84. },
  85. });
  86. }
  87. // Update `queries` to include both original and `queryName`.
  88. const newDocument = this.documents.get(snap.ref.path);
  89. newDocument.metadata.queries = originalQueries || [];
  90. if (queryName) {
  91. newDocument.metadata.queries.push(queryName);
  92. }
  93. if (snap.readTime > this.latestReadTime) {
  94. this.latestReadTime = snap.readTime;
  95. }
  96. }
  97. addNamedQuery(name, querySnap) {
  98. if (this.namedQueries.has(name)) {
  99. throw new Error(`Query name conflict: ${name} has already been added.`);
  100. }
  101. this.namedQueries.set(name, {
  102. name,
  103. bundledQuery: querySnap.query._toBundledQuery(),
  104. readTime: querySnap.readTime.toProto().timestampValue,
  105. });
  106. for (const snap of querySnap.docs) {
  107. this.addBundledDocument(snap, name);
  108. }
  109. if (querySnap.readTime > this.latestReadTime) {
  110. this.latestReadTime = querySnap.readTime;
  111. }
  112. }
  113. /**
  114. * Converts a IBundleElement to a Buffer whose content is the length prefixed JSON representation
  115. * of the element.
  116. * @private
  117. * @internal
  118. */
  119. elementToLengthPrefixedBuffer(bundleElement) {
  120. // Convert to a valid proto message object then take its JSON representation.
  121. // This take cares of stuff like converting internal byte array fields
  122. // to Base64 encodings.
  123. // We lazy-load the Proto file to reduce cold-start times.
  124. const message = require('../protos/firestore_v1_proto_api')
  125. .firestore.BundleElement.fromObject(bundleElement)
  126. .toJSON();
  127. const buffer = Buffer.from(JSON.stringify(message), 'utf-8');
  128. const lengthBuffer = Buffer.from(buffer.length.toString());
  129. return Buffer.concat([lengthBuffer, buffer]);
  130. }
  131. build() {
  132. let bundleBuffer = Buffer.alloc(0);
  133. for (const namedQuery of this.namedQueries.values()) {
  134. bundleBuffer = Buffer.concat([
  135. bundleBuffer,
  136. this.elementToLengthPrefixedBuffer({ namedQuery }),
  137. ]);
  138. }
  139. for (const bundledDocument of this.documents.values()) {
  140. const documentMetadata = bundledDocument.metadata;
  141. bundleBuffer = Buffer.concat([
  142. bundleBuffer,
  143. this.elementToLengthPrefixedBuffer({ documentMetadata }),
  144. ]);
  145. // Write to the bundle if document exists.
  146. const document = bundledDocument.document;
  147. if (document) {
  148. bundleBuffer = Buffer.concat([
  149. bundleBuffer,
  150. this.elementToLengthPrefixedBuffer({ document }),
  151. ]);
  152. }
  153. }
  154. const metadata = {
  155. id: this.bundleId,
  156. createTime: this.latestReadTime.toProto().timestampValue,
  157. version: BUNDLE_VERSION,
  158. totalDocuments: this.documents.size,
  159. totalBytes: bundleBuffer.length,
  160. };
  161. // Prepends the metadata element to the bundleBuffer: `bundleBuffer` is the second argument to `Buffer.concat`.
  162. bundleBuffer = Buffer.concat([
  163. this.elementToLengthPrefixedBuffer({ metadata }),
  164. bundleBuffer,
  165. ]);
  166. return bundleBuffer;
  167. }
  168. }
  169. exports.BundleBuilder = BundleBuilder;
  170. /**
  171. * Convenient class to hold both the metadata and the actual content of a document to be bundled.
  172. * @private
  173. * @internal
  174. */
  175. class BundledDocument {
  176. constructor(metadata, document) {
  177. this.metadata = metadata;
  178. this.document = document;
  179. }
  180. }
  181. /**
  182. * Validates that 'value' is DocumentSnapshot.
  183. *
  184. * @private
  185. * @internal
  186. * @param arg The argument name or argument index (for varargs methods).
  187. * @param value The input to validate.
  188. */
  189. function validateDocumentSnapshot(arg, value) {
  190. if (!(value instanceof document_1.DocumentSnapshot)) {
  191. throw new Error((0, validate_1.invalidArgumentMessage)(arg, 'DocumentSnapshot'));
  192. }
  193. }
  194. /**
  195. * Validates that 'value' is QuerySnapshot.
  196. *
  197. * @private
  198. * @internal
  199. * @param arg The argument name or argument index (for varargs methods).
  200. * @param value The input to validate.
  201. */
  202. function validateQuerySnapshot(arg, value) {
  203. if (!(value instanceof query_snapshot_1.QuerySnapshot)) {
  204. throw new Error((0, validate_1.invalidArgumentMessage)(arg, 'QuerySnapshot'));
  205. }
  206. }
  207. //# sourceMappingURL=bundle.js.map