Skip to content

Commit

Permalink
Firebase CLI Supports Firestore Provisioning API (#5616)
Browse files Browse the repository at this point in the history
b/267473272 - Add support for Firestore Provisioning API. This feature is experimental and may not yet be enabled on all projects. It introduces 6 new commands to the Firebase CLI
  • Loading branch information
VicVer09 committed Apr 17, 2023
1 parent 4cc49d4 commit f5786fd
Show file tree
Hide file tree
Showing 18 changed files with 593 additions and 47 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
- Adds new commands for provisioning and managing Firestore databases: (#5616)
- firestore:databases:list
- firestore:databases:create
- firestore:databases:get
- firestore:databases:update
- firestore:databases:delete
- firestore:locations
- Relaxed repo URI validation in ext:dev:publish (#5698).
68 changes: 68 additions & 0 deletions src/commands/firestore-databases-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Command } from "../command";
import * as clc from "colorette";
import * as fsi from "../firestore/api";
import * as types from "../firestore/api-types";
import { logger } from "../logger";
import { requirePermissions } from "../requirePermissions";
import { Emulators } from "../emulator/types";
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";

export const command = new Command("firestore:databases:create <database>")
.description("Create a database in your Firebase project.")
.option(
"--location <locationId>",
"Region to create database, for example 'nam5'. Run 'firebase firestore:locations' to get a list of eligible locations. (required)"
)
.option(
"--delete-protection <deleteProtectionState>",
"Whether or not to prevent deletion of database, for example 'ENABLED' or 'DISABLED'. Default is 'DISABLED'"
)
.before(requirePermissions, ["datastore.databases.create"])
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (database: string, options: FirestoreOptions) => {
const api = new fsi.FirestoreApi();
if (!options.location) {
logger.error(
"Missing required flag --location. See firebase firestore:databases:create --help for more info."
);
return;
}
// Type is always Firestore Native since Firebase does not support Datastore Mode
const type: types.DatabaseType = types.DatabaseType.FIRESTORE_NATIVE;
if (
options.deleteProtection &&
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.ENABLED &&
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.DISABLED
) {
logger.error(
"Invalid value for flag --delete-protection. See firebase firestore:databases:create --help for more info."
);
return;
}
const deleteProtectionState: types.DatabaseDeleteProtectionState =
options.deleteProtection === types.DatabaseDeleteProtectionStateOption.ENABLED
? types.DatabaseDeleteProtectionState.ENABLED
: types.DatabaseDeleteProtectionState.DISABLED;

const databaseResp: types.DatabaseResp = await api.createDatabase(
options.project,
database,
options.location,
type,
deleteProtectionState
);

if (options.json) {
logger.info(JSON.stringify(databaseResp, undefined, 2));
} else {
logger.info(clc.bold(`Successfully created ${api.prettyDatabaseString(databaseResp)}`));
logger.info(
"Please be sure to configure Firebase rules in your Firebase config file for\n" +
"the new database. By default, created databases will have closed rules that\n" +
"block any incoming third-party traffic."
);
}

return databaseResp;
});
44 changes: 44 additions & 0 deletions src/commands/firestore-databases-delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Command } from "../command";
import * as clc from "colorette";
import * as fsi from "../firestore/api";
import * as types from "../firestore/api-types";
import { promptOnce } from "../prompt";
import { logger } from "../logger";
import { requirePermissions } from "../requirePermissions";
import { Emulators } from "../emulator/types";
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";
import { FirebaseError } from "../error";

export const command = new Command("firestore:databases:delete <database>")
.description(
"Delete a database in your Cloud Firestore project. Database delete protection state must be disabled. To do so, use the update command: firebase firestore:databases:update <database> --delete-protection DISABLED"
)
.option("--force", "Attempt to delete database without prompting for confirmation.")
.before(requirePermissions, ["datastore.databases.delete"])
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (database: string, options: FirestoreOptions) => {
const api = new fsi.FirestoreApi();

if (!options.force) {
const confirmMessage = `You are about to delete projects/${options.project}/databases/${database}. Do you wish to continue?`;
const consent = await promptOnce({
type: "confirm",
message: confirmMessage,
default: false,
});
if (!consent) {
throw new FirebaseError("Delete database canceled.");
}
}

const databaseResp: types.DatabaseResp = await api.deleteDatabase(options.project, database);

if (options.json) {
logger.info(JSON.stringify(databaseResp, undefined, 2));
} else {
logger.info(clc.bold(`Successfully deleted ${api.prettyDatabaseString(databaseResp)}`));
}

return databaseResp;
});
27 changes: 27 additions & 0 deletions src/commands/firestore-databases-get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Command } from "../command";
import * as fsi from "../firestore/api";
import * as types from "../firestore/api-types";
import { logger } from "../logger";
import { requirePermissions } from "../requirePermissions";
import { Emulators } from "../emulator/types";
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";

export const command = new Command("firestore:databases:get [database]")
.description("Get database in your Cloud Firestore project.")
.before(requirePermissions, ["datastore.databases.get"])
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (database: string, options: FirestoreOptions) => {
const api = new fsi.FirestoreApi();

const databaseId = database || "(default)";
const databaseResp: types.DatabaseResp = await api.getDatabase(options.project, databaseId);

if (options.json) {
logger.info(JSON.stringify(databaseResp, undefined, 2));
} else {
api.prettyPrintDatabase(databaseResp);
}

return databaseResp;
});
26 changes: 26 additions & 0 deletions src/commands/firestore-databases-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Command } from "../command";
import * as fsi from "../firestore/api";
import * as types from "../firestore/api-types";
import { logger } from "../logger";
import { requirePermissions } from "../requirePermissions";
import { Emulators } from "../emulator/types";
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";

export const command = new Command("firestore:databases:list")
.description("List databases in your Cloud Firestore project.")
.before(requirePermissions, ["datastore.databases.list"])
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (options: FirestoreOptions) => {
const api = new fsi.FirestoreApi();

const databases: types.DatabaseResp[] = await api.listDatabases(options.project);

if (options.json) {
logger.info(JSON.stringify(databases, undefined, 2));
} else {
api.prettyPrintDatabases(databases);
}

return databases;
});
61 changes: 61 additions & 0 deletions src/commands/firestore-databases-update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Command } from "../command";
import * as clc from "colorette";
import * as fsi from "../firestore/api";
import * as types from "../firestore/api-types";
import { logger } from "../logger";
import { requirePermissions } from "../requirePermissions";
import { Emulators } from "../emulator/types";
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";

export const command = new Command("firestore:databases:update <database>")
.description(
"Update a database in your Firebase project. Must specify at least one property to update."
)
.option("--json", "Prints raw json response of the create API call if specified")
.option(
"--delete-protection <deleteProtectionState>",
"Whether or not to prevent deletion of database, for example 'ENABLED' or 'DISABLED'. Default is 'DISABLED'"
)
.before(requirePermissions, ["datastore.databases.update"])
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (database: string, options: FirestoreOptions) => {
const api = new fsi.FirestoreApi();

if (!options.type && !options.deleteProtection) {
logger.error(
"Missing properties to update. See firebase firestore:databases:update --help for more info."
);
return;
}
const type: types.DatabaseType = types.DatabaseType.FIRESTORE_NATIVE;
if (
options.deleteProtection &&
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.ENABLED &&
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.DISABLED
) {
logger.error(
"Invalid value for flag --delete-protection. See firebase firestore:databases:update --help for more info."
);
return;
}
const deleteProtectionState: types.DatabaseDeleteProtectionState =
options.deleteProtection === types.DatabaseDeleteProtectionStateOption.ENABLED
? types.DatabaseDeleteProtectionState.ENABLED
: types.DatabaseDeleteProtectionState.DISABLED;

const databaseResp: types.DatabaseResp = await api.updateDatabase(
options.project,
database,
type,
deleteProtectionState
);

if (options.json) {
logger.info(JSON.stringify(databaseResp, undefined, 2));
} else {
logger.info(clc.bold(`Successfully updated ${api.prettyDatabaseString(databaseResp)}`));
}

return databaseResp;
});
4 changes: 2 additions & 2 deletions src/commands/firestore-indexes-list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Command } from "../command";
import * as clc from "colorette";
import * as fsi from "../firestore/indexes";
import * as fsi from "../firestore/api";
import { logger } from "../logger";
import { requirePermissions } from "../requirePermissions";
import { Emulators } from "../emulator/types";
Expand All @@ -21,7 +21,7 @@ export const command = new Command("firestore:indexes")
.before(requirePermissions, ["datastore.indexes.list"])
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (options: FirestoreOptions) => {
const indexApi = new fsi.FirestoreIndexes();
const indexApi = new fsi.FirestoreApi();

const databaseId = options.database ?? "(default)";
const indexes = await indexApi.listIndexes(options.project, databaseId);
Expand Down
26 changes: 26 additions & 0 deletions src/commands/firestore-locations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Command } from "../command";
import * as fsi from "../firestore/api";
import * as types from "../firestore/api-types";
import { logger } from "../logger";
import { requirePermissions } from "../requirePermissions";
import { Emulators } from "../emulator/types";
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
import { FirestoreOptions } from "../firestore/options";

export const command = new Command("firestore:locations")
.description("List possible locations for your Cloud Firestore project.")
.before(requirePermissions, ["datastore.locations.list"])
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
.action(async (options: FirestoreOptions) => {
const api = new fsi.FirestoreApi();

const locations: types.Location[] = await api.locations(options.project);

if (options.json) {
logger.info(JSON.stringify(locations, undefined, 2));
} else {
api.prettyPrintLocations(locations);
}

return locations;
});
7 changes: 7 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ export function load(client: any): any {
client.firestore = {};
client.firestore.delete = loadCommand("firestore-delete");
client.firestore.indexes = loadCommand("firestore-indexes-list");
client.firestore.locations = loadCommand("firestore-locations");
client.firestore.databases = {};
client.firestore.databases.list = loadCommand("firestore-databases-list");
client.firestore.databases.get = loadCommand("firestore-databases-get");
client.firestore.databases.create = loadCommand("firestore-databases-create");
client.firestore.databases.update = loadCommand("firestore-databases-update");
client.firestore.databases.delete = loadCommand("firestore-databases-delete");
client.functions = {};
client.functions.config = {};
client.functions.config.clone = loadCommand("functions-config-clone");
Expand Down
10 changes: 10 additions & 0 deletions src/commands/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ const LINKS: Link[] = [
{ name: "Firestore: Data", arg: "firestore", consolePath: "/firestore/data" },
{ name: "Firestore: Rules", arg: "firestore:rules", consolePath: "/firestore/rules" },
{ name: "Firestore: Indexes", arg: "firestore:indexes", consolePath: "/firestore/indexes" },
{
name: "Firestore: Databases List",
arg: "firestore:databases:list",
consolePath: "/firestore/databases/list",
},
{
name: "Firestore: Locations",
arg: "firestore:locations",
consolePath: "/firestore/locations",
},
{ name: "Firestore: Usage", arg: "firestore:usage", consolePath: "/firestore/usage" },
{ name: "Functions", arg: "functions", consolePath: "/functions/list" },
{ name: "Functions Log", arg: "functions:log" } /* Special Case */,
Expand Down
4 changes: 2 additions & 2 deletions src/deploy/firestore/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as clc from "colorette";

import { FirestoreIndexes } from "../../firestore/indexes";
import { FirestoreApi } from "../../firestore/api";
import { logger } from "../../logger";
import * as utils from "../../utils";
import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy";
Expand Down Expand Up @@ -30,7 +30,7 @@ async function deployIndexes(context: any, options: any): Promise<void> {
const indexesContext: IndexContext[] = context?.firestore?.indexes;

utils.logBullet(clc.bold(clc.cyan("firestore: ")) + "deploying indexes...");
const firestoreIndexes = new FirestoreIndexes();
const firestoreIndexes = new FirestoreApi();
await Promise.all(
indexesContext.map(async (indexContext: IndexContext): Promise<void> => {
const { databaseId, indexesFileName, indexesRawSpec } = indexContext;
Expand Down
26 changes: 24 additions & 2 deletions src/firestore/indexes-sort.ts → src/firestore/api-sort.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as API from "./indexes-api";
import * as Spec from "./indexes-spec";
import * as API from "./api-types";
import * as Spec from "./api-spec";
import * as util from "./util";

const QUERY_SCOPE_SEQUENCE = [
Expand Down Expand Up @@ -59,6 +59,28 @@ export function compareApiIndex(a: API.Index, b: API.Index): number {
return compareArrays(a.fields, b.fields, compareIndexField);
}

/**
* Compare two Database api entries for sorting.
*
* Comparisons:
* 1) The databaseId (name)
*/
export function compareApiDatabase(a: API.DatabaseResp, b: API.DatabaseResp): number {
// Name should always be unique and present
return a.name > b.name ? 1 : -1;
}

/**
* Compare two Location api entries for sorting.
*
* Comparisons:
* 1) The locationId.
*/
export function compareLocation(a: API.Location, b: API.Location): number {
// LocationId should always be unique and present
return a.locationId > b.locationId ? 1 : -1;
}

/**
* Compare two Field api entries for sorting.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Please review and update the README as needed and notify firebase-docs@google.com.
*/

import * as API from "./indexes-api";
import * as API from "./api-types";

/**
* An entry specifying a compound or other non-default index.
Expand Down

0 comments on commit f5786fd

Please sign in to comment.