GSI - Employe Self Service Mobile
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

342 lines
13 KiB

2 months ago
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.FirestoreDelete = void 0;
  4. const clc = require("colorette");
  5. const ProgressBar = require("progress");
  6. const apiv2 = require("../apiv2");
  7. const firestore = require("../gcp/firestore");
  8. const error_1 = require("../error");
  9. const logger_1 = require("../logger");
  10. const utils = require("../utils");
  11. const api_1 = require("../api");
  12. const MIN_ID = "__id-9223372036854775808__";
  13. class FirestoreDelete {
  14. constructor(project, path, options) {
  15. this.project = project;
  16. this.path = path || "";
  17. this.recursive = Boolean(options.recursive);
  18. this.shallow = Boolean(options.shallow);
  19. this.allCollections = Boolean(options.allCollections);
  20. this.readBatchSize = 7500;
  21. this.maxPendingDeletes = 15;
  22. this.deleteBatchSize = 250;
  23. this.maxQueueSize = this.deleteBatchSize * this.maxPendingDeletes * 2;
  24. this.path = this.path.replace(/(^\/+|\/+$)/g, "");
  25. this.allDescendants = this.recursive;
  26. this.root = "projects/" + project + "/databases/(default)/documents";
  27. const segments = this.path.split("/");
  28. this.isDocumentPath = segments.length % 2 === 0;
  29. this.isCollectionPath = !this.isDocumentPath;
  30. this.parent = this.root;
  31. if (this.isCollectionPath) {
  32. segments.pop();
  33. }
  34. if (segments.length > 0) {
  35. this.parent += "/" + segments.join("/");
  36. }
  37. if (!options.allCollections) {
  38. this.validateOptions();
  39. }
  40. this.apiClient = new apiv2.Client({
  41. auth: true,
  42. apiVersion: "v1",
  43. urlPrefix: api_1.firestoreOriginOrEmulator,
  44. });
  45. }
  46. setDeleteBatchSize(size) {
  47. this.deleteBatchSize = size;
  48. this.maxQueueSize = this.deleteBatchSize * this.maxPendingDeletes * 2;
  49. }
  50. validateOptions() {
  51. if (this.recursive && this.shallow) {
  52. throw new error_1.FirebaseError("Cannot pass recursive and shallow options together.");
  53. }
  54. if (this.isCollectionPath && !this.recursive && !this.shallow) {
  55. throw new error_1.FirebaseError("Must pass recursive or shallow option when deleting a collection.");
  56. }
  57. const pieces = this.path.split("/");
  58. if (pieces.length === 0) {
  59. throw new error_1.FirebaseError("Path length must be greater than zero.");
  60. }
  61. const hasEmptySegment = pieces.some((piece) => {
  62. return piece.length === 0;
  63. });
  64. if (hasEmptySegment) {
  65. throw new error_1.FirebaseError("Path must not have any empty segments.");
  66. }
  67. }
  68. collectionDescendantsQuery(allDescendants, batchSize, startAfter) {
  69. const nullChar = String.fromCharCode(0);
  70. const startAt = this.root + "/" + this.path + "/" + MIN_ID;
  71. const endAt = this.root + "/" + this.path + nullChar + "/" + MIN_ID;
  72. const where = {
  73. compositeFilter: {
  74. op: "AND",
  75. filters: [
  76. {
  77. fieldFilter: {
  78. field: {
  79. fieldPath: "__name__",
  80. },
  81. op: "GREATER_THAN_OR_EQUAL",
  82. value: {
  83. referenceValue: startAt,
  84. },
  85. },
  86. },
  87. {
  88. fieldFilter: {
  89. field: {
  90. fieldPath: "__name__",
  91. },
  92. op: "LESS_THAN",
  93. value: {
  94. referenceValue: endAt,
  95. },
  96. },
  97. },
  98. ],
  99. },
  100. };
  101. const query = {
  102. structuredQuery: {
  103. where: where,
  104. limit: batchSize,
  105. from: [
  106. {
  107. allDescendants: allDescendants,
  108. },
  109. ],
  110. select: {
  111. fields: [{ fieldPath: "__name__" }],
  112. },
  113. orderBy: [{ field: { fieldPath: "__name__" } }],
  114. },
  115. };
  116. if (startAfter) {
  117. query.structuredQuery.startAt = {
  118. values: [{ referenceValue: startAfter }],
  119. before: false,
  120. };
  121. }
  122. return query;
  123. }
  124. docDescendantsQuery(allDescendants, batchSize, startAfter) {
  125. const query = {
  126. structuredQuery: {
  127. limit: batchSize,
  128. from: [
  129. {
  130. allDescendants: allDescendants,
  131. },
  132. ],
  133. select: {
  134. fields: [{ fieldPath: "__name__" }],
  135. },
  136. orderBy: [{ field: { fieldPath: "__name__" } }],
  137. },
  138. };
  139. if (startAfter) {
  140. query.structuredQuery.startAt = {
  141. values: [{ referenceValue: startAfter }],
  142. before: false,
  143. };
  144. }
  145. return query;
  146. }
  147. getDescendantBatch(allDescendants, batchSize, startAfter) {
  148. const url = this.parent + ":runQuery";
  149. const body = this.isDocumentPath
  150. ? this.docDescendantsQuery(allDescendants, batchSize, startAfter)
  151. : this.collectionDescendantsQuery(allDescendants, batchSize, startAfter);
  152. return this.apiClient.post(url, body).then((res) => {
  153. const docs = [];
  154. for (const x of res.body) {
  155. if (x.document) {
  156. docs.push(x.document);
  157. }
  158. }
  159. return docs;
  160. });
  161. }
  162. recursiveBatchDelete() {
  163. let queue = [];
  164. let numDocsDeleted = 0;
  165. let numPendingDeletes = 0;
  166. let pagesRemaining = true;
  167. let pageIncoming = false;
  168. let lastDocName = undefined;
  169. const retried = {};
  170. const failures = [];
  171. let fetchFailures = 0;
  172. const queueLoop = () => {
  173. if (queue.length === 0 && numPendingDeletes === 0 && !pagesRemaining) {
  174. return true;
  175. }
  176. if (failures.length > 0) {
  177. logger_1.logger.debug("Found " + failures.length + " failed operations, failing.");
  178. return true;
  179. }
  180. if (queue.length <= this.maxQueueSize && pagesRemaining && !pageIncoming) {
  181. pageIncoming = true;
  182. this.getDescendantBatch(this.allDescendants, this.readBatchSize, lastDocName)
  183. .then((docs) => {
  184. fetchFailures = 0;
  185. pageIncoming = false;
  186. if (docs.length === 0) {
  187. pagesRemaining = false;
  188. return;
  189. }
  190. queue = queue.concat(docs);
  191. lastDocName = docs[docs.length - 1].name;
  192. })
  193. .catch((e) => {
  194. logger_1.logger.debug("Failed to fetch page after " + lastDocName, e);
  195. pageIncoming = false;
  196. fetchFailures++;
  197. if (fetchFailures >= 3) {
  198. failures.push("Failed to fetch documents to delete >= 3 times.");
  199. }
  200. });
  201. }
  202. if (numDocsDeleted === 0 && numPendingDeletes >= 1) {
  203. return false;
  204. }
  205. if (numPendingDeletes > this.maxPendingDeletes) {
  206. return false;
  207. }
  208. if (queue.length === 0) {
  209. return false;
  210. }
  211. const toDelete = [];
  212. const numToDelete = Math.min(this.deleteBatchSize, queue.length);
  213. for (let i = 0; i < numToDelete; i++) {
  214. const d = queue.shift();
  215. if (d) {
  216. toDelete.push(d);
  217. }
  218. }
  219. numPendingDeletes++;
  220. firestore
  221. .deleteDocuments(this.project, toDelete)
  222. .then((numDeleted) => {
  223. FirestoreDelete.progressBar.tick(numDeleted);
  224. numDocsDeleted += numDeleted;
  225. numPendingDeletes--;
  226. })
  227. .catch((e) => {
  228. if (e.status === 400 &&
  229. e.message.includes("Transaction too big") &&
  230. this.deleteBatchSize >= 2) {
  231. logger_1.logger.debug("Transaction too big error deleting doc batch", e);
  232. const newBatchSize = Math.floor(toDelete.length / 10);
  233. if (newBatchSize < this.deleteBatchSize) {
  234. utils.logLabeledWarning("firestore", `delete transaction too large, reducing batch size from ${this.deleteBatchSize} to ${newBatchSize}`);
  235. this.setDeleteBatchSize(newBatchSize);
  236. }
  237. queue.unshift(...toDelete);
  238. }
  239. else if (e.status >= 500 && e.status < 600) {
  240. logger_1.logger.debug("Server error deleting doc batch", e);
  241. toDelete.forEach((doc) => {
  242. if (retried[doc.name]) {
  243. const message = `Failed to delete doc ${doc.name} multiple times.`;
  244. logger_1.logger.debug(message);
  245. failures.push(message);
  246. }
  247. else {
  248. retried[doc.name] = true;
  249. queue.push(doc);
  250. }
  251. });
  252. }
  253. else {
  254. const docIds = toDelete.map((d) => d.name).join(", ");
  255. const msg = `Fatal error deleting docs ${docIds}`;
  256. logger_1.logger.debug(msg, e);
  257. failures.push(msg);
  258. }
  259. numPendingDeletes--;
  260. });
  261. return false;
  262. };
  263. return new Promise((resolve, reject) => {
  264. const intervalId = setInterval(() => {
  265. if (queueLoop()) {
  266. clearInterval(intervalId);
  267. if (failures.length === 0) {
  268. resolve();
  269. }
  270. else {
  271. const errorDescription = failures.join(", ");
  272. reject(new error_1.FirebaseError(`Deletion failed. Errors: ${errorDescription}.`, { exit: 1 }));
  273. }
  274. }
  275. }, 0);
  276. });
  277. }
  278. deletePath() {
  279. let initialDelete;
  280. if (this.isDocumentPath) {
  281. const doc = { name: this.root + "/" + this.path };
  282. initialDelete = firestore.deleteDocument(doc).catch((err) => {
  283. logger_1.logger.debug("deletePath:initialDelete:error", err);
  284. if (this.allDescendants) {
  285. return Promise.resolve();
  286. }
  287. return utils.reject("Unable to delete " + clc.cyan(this.path));
  288. });
  289. }
  290. else {
  291. initialDelete = Promise.resolve();
  292. }
  293. return initialDelete.then(() => {
  294. return this.recursiveBatchDelete();
  295. });
  296. }
  297. deleteDatabase() {
  298. return firestore
  299. .listCollectionIds(this.project)
  300. .catch((err) => {
  301. logger_1.logger.debug("deleteDatabase:listCollectionIds:error", err);
  302. return utils.reject("Unable to list collection IDs");
  303. })
  304. .then((collectionIds) => {
  305. const promises = [];
  306. logger_1.logger.info("Deleting the following collections: " + clc.cyan(collectionIds.join(", ")));
  307. for (let i = 0; i < collectionIds.length; i++) {
  308. const collectionId = collectionIds[i];
  309. const deleteOp = new FirestoreDelete(this.project, collectionId, {
  310. recursive: true,
  311. });
  312. promises.push(deleteOp.execute());
  313. }
  314. return Promise.all(promises);
  315. });
  316. }
  317. checkHasChildren() {
  318. return this.getDescendantBatch(true, 1).then((docs) => {
  319. return docs.length > 0;
  320. });
  321. }
  322. execute() {
  323. let verifyRecurseSafe;
  324. if (this.isDocumentPath && !this.recursive && !this.shallow) {
  325. verifyRecurseSafe = this.checkHasChildren().then((multiple) => {
  326. if (multiple) {
  327. return utils.reject("Document has children, must specify -r or --shallow.", { exit: 1 });
  328. }
  329. });
  330. }
  331. else {
  332. verifyRecurseSafe = Promise.resolve();
  333. }
  334. return verifyRecurseSafe.then(() => {
  335. return this.deletePath();
  336. });
  337. }
  338. }
  339. exports.FirestoreDelete = FirestoreDelete;
  340. FirestoreDelete.progressBar = new ProgressBar("Deleted :current docs (:rate docs/s)\n", {
  341. total: Number.MAX_SAFE_INTEGER,
  342. });