Skip to content
Merged
96 changes: 96 additions & 0 deletions src/commands/firestore-operations-cancel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { expect } from "chai";
import * as sinon from "sinon";
import { command } from "./firestore-operations-cancel";
import * as fsi from "../firestore/api";
import * as prompt from "../prompt";
import * as utils from "../utils";
import { logger } from "../logger";

describe("firestore:operations:cancel", () => {
const sandbox = sinon.createSandbox();
let firestoreApiStub: sinon.SinonStubbedInstance<fsi.FirestoreApi>;
let confirmStub: sinon.SinonStub;
let logSuccessStub: sinon.SinonStub;
let logWarningStub: sinon.SinonStub;
let loggerInfoStub: sinon.SinonStub;

beforeEach(() => {
firestoreApiStub = sandbox.createStubInstance(fsi.FirestoreApi);
sandbox.stub(fsi, "FirestoreApi").returns(firestoreApiStub);
confirmStub = sandbox.stub(prompt, "confirm");
logSuccessStub = sandbox.stub(utils, "logSuccess");
logWarningStub = sandbox.stub(utils, "logWarning");
loggerInfoStub = sandbox.stub(logger, "info");
});

afterEach(() => {
sandbox.restore();
});

it("should call the Firestore API with the correct parameters with --force", async () => {
const options = { project: "test-project", database: "test-db", force: true };
const operationName = "test-operation";
firestoreApiStub.cancelOperation.resolves({ success: true });

await command.runner()(operationName, options);

expect(firestoreApiStub.cancelOperation).to.be.calledOnceWith(
"test-project",
"test-db",
operationName,
);
expect(confirmStub).to.not.be.called;
});

it("should prompt for confirmation and continue if confirmed", async () => {
const options = { project: "test-project", database: "test-db", force: false };
const operationName = "test-operation";
confirmStub.resolves(true);
firestoreApiStub.cancelOperation.resolves({ success: true });

await command.runner()(operationName, options);

expect(confirmStub).to.be.calledOnce;
expect(firestoreApiStub.cancelOperation).to.be.calledOnceWith(
"test-project",
"test-db",
operationName,
);
expect(logSuccessStub).to.be.calledOnceWith("Operation cancelled successfully.");
});

it("should not cancel the operation if not confirmed", async () => {
const options = { project: "test-project", database: "test-db", force: false };
const operationName = "test-operation";
confirmStub.resolves(false);

await expect(command.runner()(operationName, options)).to.be.rejectedWith("Command aborted.");

expect(confirmStub).to.be.calledOnce;
expect(firestoreApiStub.cancelOperation).to.not.be.called;
});

it("should log a warning if operation cancellation fails", async () => {
const options = { project: "test-project", database: "test-db", force: true };
const operationName = "test-operation";
firestoreApiStub.cancelOperation.resolves({ success: false });

await command.runner()(operationName, options);

expect(firestoreApiStub.cancelOperation).to.be.calledOnce;
expect(logWarningStub).to.be.calledOnceWith("Canceling the operation failed.");
});

it("should print status in JSON format when --json is specified", async () => {
const options = { project: "test-project", database: "test-db", force: true, json: true };
const operationName = "test-operation";
const status = { success: true };
firestoreApiStub.cancelOperation.resolves(status);

await command.runner()(operationName, options);

expect(loggerInfoStub).to.be.calledOnceWith(JSON.stringify(status, undefined, 2));
expect(logSuccessStub).to.not.be.called;
expect(logWarningStub).to.not.be.called;
});
});
48 changes: 48 additions & 0 deletions src/commands/firestore-operations-cancel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Command } from "../command";
import * as fsi from "../firestore/api";
import { Emulators } from "../emulator/types";
import { errorMissingProject, warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";
import { getShortOperationName } from "./firestore-utils";
import { confirm } from "../prompt";
import * as clc from "colorette";
import * as utils from "../utils";
import { logger } from "../logger";

export const command = new Command("firestore:operations:cancel <operationName>")
.description("cancels a long-running Cloud Firestore admin operation")
.option(
"--database <databaseName>",
'Database ID for which the operation is running. "(default)" if none is provided.',
)
.option("--force", "Forces the operation cancellation without asking for confirmation")
.before(errorMissingProject)
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (operationName: string, options: FirestoreOptions) => {
const databaseId = options.database || "(default)";
operationName = getShortOperationName(operationName);

if (!options.force) {
const fullName = `/projects/${options.project}/databases/${databaseId}/operations/${operationName}`;
const confirmMessage = `You are about to cancel the operation: ${clc.bold(clc.yellow(clc.underline(fullName)))}. Do you wish to continue?`;
const consent = await confirm(confirmMessage);
if (!consent) {
return utils.reject("Command aborted.", { exit: 1 });
}
}

const api = new fsi.FirestoreApi();
const status = await api.cancelOperation(options.project, databaseId, operationName);

if (options.json) {
logger.info(JSON.stringify(status, undefined, 2));
} else {
if (status.success) {
utils.logSuccess("Operation cancelled successfully.");
} else {
utils.logWarning("Canceling the operation failed.");
}
}

return status;
});
104 changes: 104 additions & 0 deletions src/commands/firestore-operations-describe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { expect } from "chai";
import * as sinon from "sinon";
import { command } from "./firestore-operations-describe";
import * as fsi from "../firestore/api";
import { logger } from "../logger";
import { PrettyPrint } from "../firestore/pretty-print";
import { FirebaseError } from "../error";

describe("firestore:operations:describe", () => {
const sandbox = sinon.createSandbox();
let firestoreApiStub: sinon.SinonStubbedInstance<fsi.FirestoreApi>;
let loggerInfoStub: sinon.SinonStub;
let prettyPrintStub: sinon.SinonStub;

beforeEach(() => {
firestoreApiStub = sandbox.createStubInstance(fsi.FirestoreApi);
sandbox.stub(fsi, "FirestoreApi").returns(firestoreApiStub);
loggerInfoStub = sandbox.stub(logger, "info");
prettyPrintStub = sandbox.stub(PrettyPrint.prototype, "prettyPrintOperation");
});

afterEach(() => {
sandbox.restore();
});

it("should call the Firestore API with the correct parameters", async () => {
const options = { project: "test-project", database: "test-db" };
const operationName = "test-operation";
firestoreApiStub.describeOperation.resolves({ name: "op1", done: false, metadata: {} });

await command.runner()(operationName, options);

expect(firestoreApiStub.describeOperation).to.be.calledOnceWith(
"test-project",
"test-db",
operationName,
);
});

it("should use default values for database if not provided", async () => {
const options = { project: "test-project" };
const operationName = "test-operation";
firestoreApiStub.describeOperation.resolves({ name: "op1", done: false, metadata: {} });

await command.runner()(operationName, options);

expect(firestoreApiStub.describeOperation).to.be.calledOnceWith(
"test-project",
"(default)",
operationName,
);
});

it("should print the operation in JSON format when --json is specified", async () => {
const options = { project: "test-project", json: true };
const operationName = "test-operation";
const operation = { name: "op1", done: false, metadata: {} };
firestoreApiStub.describeOperation.resolves(operation);

await command.runner()(operationName, options);

expect(loggerInfoStub).to.be.calledOnceWith(JSON.stringify(operation, undefined, 2));
expect(prettyPrintStub).to.not.be.called;
});

it("should pretty-print the operation when --json is not specified", async () => {
const options = { project: "test-project" };
const operationName = "test-operation";
const operation = { name: "op1", done: false, metadata: {} };
firestoreApiStub.describeOperation.resolves(operation);

await command.runner()(operationName, options);

expect(prettyPrintStub).to.be.calledOnceWith(operation);
expect(loggerInfoStub).to.not.be.called;
});

it("should throw a FirebaseError if project is not defined", async () => {
const options = {};
const operationName = "test-operation";
await expect(command.runner()(operationName, options)).to.be.rejectedWith(
FirebaseError,
"Project is not defined. Either use `--project` or use `firebase use` to set your active project.",
);
});

it("should throw a FirebaseError if operation name is invalid", async () => {
const options = { project: "test-project" };
await expect(command.runner()("", options)).to.be.rejectedWith(
FirebaseError,
'"" is not a valid operation name.',
);
await expect(command.runner()("projects/p/databases/d", options)).to.be.rejectedWith(
FirebaseError,
'"projects/p/databases/d" is not a valid operation name.',
);
await expect(
command.runner()("projects/p/databases/d/operations/", options),
).to.be.rejectedWith(
FirebaseError,
'"projects/p/databases/d/operations/" is not a valid operation name.',
);
});
});
32 changes: 32 additions & 0 deletions src/commands/firestore-operations-describe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Command } from "../command";
import * as fsi from "../firestore/api";
import { logger } from "../logger";
import { Emulators } from "../emulator/types";
import { errorMissingProject, warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";
import { PrettyPrint } from "../firestore/pretty-print";
import { getShortOperationName } from "./firestore-utils";

export const command = new Command("firestore:operations:describe <operationName>")
.description("retrieves information about a Cloud Firestore admin operation")
.option(
"--database <databaseName>",
'Database ID for which the operation is running. "(default)" if none is provided.',
)
.before(errorMissingProject)
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (operationName: string, options: FirestoreOptions) => {
const databaseId = options.database || "(default)";
operationName = getShortOperationName(operationName);
const api = new fsi.FirestoreApi();
const operation = await api.describeOperation(options.project, databaseId, operationName);

if (options.json) {
logger.info(JSON.stringify(operation, undefined, 2));
} else {
const printer = new PrettyPrint();
printer.prettyPrintOperation(operation);
}

return operation;
});
79 changes: 79 additions & 0 deletions src/commands/firestore-operations-list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { expect } from "chai";
import * as sinon from "sinon";
import { command } from "./firestore-operations-list";
import * as fsi from "../firestore/api";
import { logger } from "../logger";
import { PrettyPrint } from "../firestore/pretty-print";
import { FirebaseError } from "../error";

describe("firestore:operations:list", () => {
const sandbox = sinon.createSandbox();
let firestoreApiStub: sinon.SinonStubbedInstance<fsi.FirestoreApi>;
let loggerInfoStub: sinon.SinonStub;
let prettyPrintStub: sinon.SinonStub;

beforeEach(() => {
firestoreApiStub = sandbox.createStubInstance(fsi.FirestoreApi);
sandbox.stub(fsi, "FirestoreApi").returns(firestoreApiStub);
loggerInfoStub = sandbox.stub(logger, "info");
prettyPrintStub = sandbox.stub(PrettyPrint.prototype, "prettyPrintOperations");
});

afterEach(() => {
sandbox.restore();
});

it("should call the Firestore API with the correct parameters", async () => {
const options = { project: "test-project", database: "test-db", limit: 50 };
firestoreApiStub.listOperations.resolves({ operations: [] });

await command.runner()(options);

expect(firestoreApiStub.listOperations).to.be.calledOnceWith("test-project", "test-db", 50);
});

it("should use default values for database and limit if not provided", async () => {
const options = { project: "test-project" };
firestoreApiStub.listOperations.resolves({ operations: [] });

await command.runner()(options);

expect(firestoreApiStub.listOperations).to.be.calledOnceWith("test-project", "(default)", 100);
});

it("should print operations in JSON format when --json is specified", async () => {
const options = { project: "test-project", json: true };
const operations = [
{ name: "op1", done: false, metadata: {} },
{ name: "op2", done: true, metadata: {} },
];
firestoreApiStub.listOperations.resolves({ operations });

await command.runner()(options);

expect(loggerInfoStub).to.be.calledOnceWith(JSON.stringify(operations, undefined, 2));
expect(prettyPrintStub).to.not.be.called;
});

it("should pretty-print operations when --json is not specified", async () => {
const options = { project: "test-project" };
const operations = [
{ name: "op1", done: false, metadata: {} },
{ name: "op2", done: true, metadata: {} },
];
firestoreApiStub.listOperations.resolves({ operations });

await command.runner()(options);

expect(prettyPrintStub).to.be.calledOnceWith(operations);
expect(loggerInfoStub).to.not.be.called;
});

it("should throw a FirebaseError if project is not defined", async () => {
const options = {};
await expect(command.runner()(options)).to.be.rejectedWith(
FirebaseError,
"Project is not defined. Either use `--project` or use `firebase use` to set your active project.",
);
});
});
33 changes: 33 additions & 0 deletions src/commands/firestore-operations-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Command } from "../command";
import * as fsi from "../firestore/api";
import { logger } from "../logger";
import { Emulators } from "../emulator/types";
import { errorMissingProject, warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";
import { PrettyPrint } from "../firestore/pretty-print";

export const command = new Command("firestore:operations:list")
.description("list pending Cloud Firestore admin operations and their status")
.option(
"--database <databaseName>",
'Database ID for database to list operations for. "(default)" if none is provided.',
)
.option("--limit <number>", "The maximum number of operations to list. Uses 100 by default.")
.before(errorMissingProject)
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (options: FirestoreOptions) => {
const databaseId = options.database || "(default)";
const limit = options.limit === undefined ? 100 : Number(options.limit);

const api = new fsi.FirestoreApi();
const { operations } = await api.listOperations(options.project, databaseId, limit);

if (options.json) {
logger.info(JSON.stringify(operations, undefined, 2));
} else {
const printer = new PrettyPrint();
printer.prettyPrintOperations(operations);
}

return operations;
});
Loading
Loading