251 lines
9.8 KiB
JavaScript
251 lines
9.8 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.RecursiveDelete = exports.RECURSIVE_DELETE_MIN_PENDING_OPS = exports.RECURSIVE_DELETE_MAX_PENDING_OPS = exports.REFERENCE_NAME_MIN_ID = void 0;
|
|
const assert = require("assert");
|
|
const _1 = require(".");
|
|
const util_1 = require("./util");
|
|
const query_options_1 = require("./reference/query-options");
|
|
/*!
|
|
* Datastore allowed numeric IDs where Firestore only allows strings. Numeric
|
|
* IDs are exposed to Firestore as __idNUM__, so this is the lowest possible
|
|
* negative numeric value expressed in that format.
|
|
*
|
|
* This constant is used to specify startAt/endAt values when querying for all
|
|
* descendants in a single collection.
|
|
*/
|
|
exports.REFERENCE_NAME_MIN_ID = '__id-9223372036854775808__';
|
|
/*!
|
|
* The query limit used for recursive deletes when fetching all descendants of
|
|
* the specified reference to delete. This is done to prevent the query stream
|
|
* from streaming documents faster than Firestore can delete.
|
|
*/
|
|
// Visible for testing.
|
|
exports.RECURSIVE_DELETE_MAX_PENDING_OPS = 5000;
|
|
/*!
|
|
* The number of pending BulkWriter operations at which RecursiveDelete
|
|
* starts the next limit query to fetch descendants. By starting the query
|
|
* while there are pending operations, Firestore can improve BulkWriter
|
|
* throughput. This helps prevent BulkWriter from idling while Firestore
|
|
* fetches the next query.
|
|
*/
|
|
exports.RECURSIVE_DELETE_MIN_PENDING_OPS = 1000;
|
|
/**
|
|
* Class used to store state required for running a recursive delete operation.
|
|
* Each recursive delete call should use a new instance of the class.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
class RecursiveDelete {
|
|
/**
|
|
*
|
|
* @param firestore The Firestore instance to use.
|
|
* @param writer The BulkWriter instance to use for delete operations.
|
|
* @param ref The document or collection reference to recursively delete.
|
|
* @param maxLimit The query limit to use when fetching descendants
|
|
* @param minLimit The number of pending BulkWriter operations at which
|
|
* RecursiveDelete starts the next limit query to fetch descendants.
|
|
*/
|
|
constructor(firestore, writer, ref, maxLimit, minLimit) {
|
|
this.firestore = firestore;
|
|
this.writer = writer;
|
|
this.ref = ref;
|
|
this.maxLimit = maxLimit;
|
|
this.minLimit = minLimit;
|
|
/**
|
|
* The number of deletes that failed with a permanent error.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
this.errorCount = 0;
|
|
/**
|
|
* Whether there are still documents to delete that still need to be fetched.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
this.documentsPending = true;
|
|
/**
|
|
* Whether run() has been called.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
this.started = false;
|
|
/**
|
|
* A deferred promise that resolves when the recursive delete operation
|
|
* is completed.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
this.completionDeferred = new util_1.Deferred();
|
|
/**
|
|
* Whether a query stream is currently in progress. Only one stream can be
|
|
* run at a time.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
this.streamInProgress = false;
|
|
/**
|
|
* The number of pending BulkWriter operations. Used to determine when the
|
|
* next query can be run.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
this.pendingOpsCount = 0;
|
|
this.errorStack = '';
|
|
this.maxPendingOps = maxLimit;
|
|
this.minPendingOps = minLimit;
|
|
}
|
|
/**
|
|
* Recursively deletes the reference provided in the class constructor.
|
|
* Returns a promise that resolves when all descendants have been deleted, or
|
|
* if an error occurs.
|
|
*/
|
|
run() {
|
|
assert(!this.started, 'RecursiveDelete.run() should only be called once.');
|
|
// Capture the error stack to preserve stack tracing across async calls.
|
|
this.errorStack = Error().stack;
|
|
this.writer._verifyNotClosed();
|
|
this.setupStream();
|
|
return this.completionDeferred.promise;
|
|
}
|
|
/**
|
|
* Creates a query stream and attaches event handlers to it.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
setupStream() {
|
|
const stream = this.getAllDescendants(this.ref instanceof _1.CollectionReference
|
|
? this.ref
|
|
: this.ref);
|
|
this.streamInProgress = true;
|
|
let streamedDocsCount = 0;
|
|
stream
|
|
.on('error', err => {
|
|
err.code = 14 /* StatusCode.UNAVAILABLE */;
|
|
err.stack = 'Failed to fetch children documents: ' + err.stack;
|
|
this.lastError = err;
|
|
this.onQueryEnd();
|
|
})
|
|
.on('data', (snap) => {
|
|
streamedDocsCount++;
|
|
this.lastDocumentSnap = snap;
|
|
this.deleteRef(snap.ref);
|
|
})
|
|
.on('end', () => {
|
|
this.streamInProgress = false;
|
|
// If there are fewer than the number of documents specified in the
|
|
// limit() field, we know that the query is complete.
|
|
if (streamedDocsCount < this.minPendingOps) {
|
|
this.onQueryEnd();
|
|
}
|
|
else if (this.pendingOpsCount === 0) {
|
|
this.setupStream();
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Retrieves all descendant documents nested under the provided reference.
|
|
* @param ref The reference to fetch all descendants for.
|
|
* @private
|
|
* @internal
|
|
* @return {Stream<QueryDocumentSnapshot>} Stream of descendant documents.
|
|
*/
|
|
getAllDescendants(ref) {
|
|
// The parent is the closest ancestor document to the location we're
|
|
// deleting. If we are deleting a document, the parent is the path of that
|
|
// document. If we are deleting a collection, the parent is the path of the
|
|
// document containing that collection (or the database root, if it is a
|
|
// root collection).
|
|
let parentPath = ref._resourcePath;
|
|
if (ref instanceof _1.CollectionReference) {
|
|
parentPath = parentPath.popLast();
|
|
}
|
|
const collectionId = ref instanceof _1.CollectionReference
|
|
? ref.id
|
|
: ref.parent.id;
|
|
let query = new _1.Query(this.firestore, query_options_1.QueryOptions.forKindlessAllDescendants(parentPath, collectionId,
|
|
/* requireConsistency= */ false));
|
|
// Query for names only to fetch empty snapshots.
|
|
query = query.select(_1.FieldPath.documentId()).limit(this.maxPendingOps);
|
|
if (ref instanceof _1.CollectionReference) {
|
|
// To find all descendants of a collection reference, we need to use a
|
|
// composite filter that captures all documents that start with the
|
|
// collection prefix. The MIN_KEY constant represents the minimum key in
|
|
// this collection, and a null byte + the MIN_KEY represents the minimum
|
|
// key is the next possible collection.
|
|
const nullChar = String.fromCharCode(0);
|
|
const startAt = collectionId + '/' + exports.REFERENCE_NAME_MIN_ID;
|
|
const endAt = collectionId + nullChar + '/' + exports.REFERENCE_NAME_MIN_ID;
|
|
query = query
|
|
.where(_1.FieldPath.documentId(), '>=', startAt)
|
|
.where(_1.FieldPath.documentId(), '<', endAt);
|
|
}
|
|
if (this.lastDocumentSnap) {
|
|
query = query.startAfter(this.lastDocumentSnap);
|
|
}
|
|
return query.stream();
|
|
}
|
|
/**
|
|
* Called when all descendants of the provided reference have been streamed
|
|
* or if a permanent error occurs during the stream. Deletes the developer
|
|
* provided reference and wraps any errors that occurred.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
onQueryEnd() {
|
|
this.documentsPending = false;
|
|
if (this.ref instanceof _1.DocumentReference) {
|
|
this.writer.delete(this.ref).catch(err => this.incrementErrorCount(err));
|
|
}
|
|
this.writer.flush().then(async () => {
|
|
var _a;
|
|
if (this.lastError === undefined) {
|
|
this.completionDeferred.resolve();
|
|
}
|
|
else {
|
|
let error = new (require('google-gax/build/src/fallback').GoogleError)(`${this.errorCount} ` +
|
|
`${this.errorCount !== 1 ? 'deletes' : 'delete'} ` +
|
|
'failed. The last delete failed with: ');
|
|
if (this.lastError.code !== undefined) {
|
|
error.code = this.lastError.code;
|
|
}
|
|
error = (0, util_1.wrapError)(error, this.errorStack);
|
|
// Wrap the BulkWriter error last to provide the full stack trace.
|
|
this.completionDeferred.reject(this.lastError.stack
|
|
? (0, util_1.wrapError)(error, (_a = this.lastError.stack) !== null && _a !== void 0 ? _a : '')
|
|
: error);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Deletes the provided reference and starts the next stream if conditions
|
|
* are met.
|
|
* @private
|
|
* @internal
|
|
*/
|
|
deleteRef(docRef) {
|
|
this.pendingOpsCount++;
|
|
this.writer
|
|
.delete(docRef)
|
|
.catch(err => {
|
|
this.incrementErrorCount(err);
|
|
})
|
|
.then(() => {
|
|
this.pendingOpsCount--;
|
|
// We wait until the previous stream has ended in order to sure the
|
|
// startAfter document is correct. Starting the next stream while
|
|
// there are pending operations allows Firestore to maximize
|
|
// BulkWriter throughput.
|
|
if (this.documentsPending &&
|
|
!this.streamInProgress &&
|
|
this.pendingOpsCount < this.minPendingOps) {
|
|
this.setupStream();
|
|
}
|
|
});
|
|
}
|
|
incrementErrorCount(err) {
|
|
this.errorCount++;
|
|
this.lastError = err;
|
|
}
|
|
}
|
|
exports.RecursiveDelete = RecursiveDelete;
|
|
//# sourceMappingURL=recursive-delete.js.map
|