Skip to content

Commit f5786fd

Browse files
authored
Firebase CLI Supports Firestore Provisioning API (#5616)
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
1 parent 4cc49d4 commit f5786fd

18 files changed

+593
-47
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1+
- Adds new commands for provisioning and managing Firestore databases: (#5616)
2+
- firestore:databases:list
3+
- firestore:databases:create
4+
- firestore:databases:get
5+
- firestore:databases:update
6+
- firestore:databases:delete
7+
- firestore:locations
18
- Relaxed repo URI validation in ext:dev:publish (#5698).
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Command } from "../command";
2+
import * as clc from "colorette";
3+
import * as fsi from "../firestore/api";
4+
import * as types from "../firestore/api-types";
5+
import { logger } from "../logger";
6+
import { requirePermissions } from "../requirePermissions";
7+
import { Emulators } from "../emulator/types";
8+
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
9+
import { FirestoreOptions } from "../firestore/options";
10+
11+
export const command = new Command("firestore:databases:create <database>")
12+
.description("Create a database in your Firebase project.")
13+
.option(
14+
"--location <locationId>",
15+
"Region to create database, for example 'nam5'. Run 'firebase firestore:locations' to get a list of eligible locations. (required)"
16+
)
17+
.option(
18+
"--delete-protection <deleteProtectionState>",
19+
"Whether or not to prevent deletion of database, for example 'ENABLED' or 'DISABLED'. Default is 'DISABLED'"
20+
)
21+
.before(requirePermissions, ["datastore.databases.create"])
22+
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
23+
.action(async (database: string, options: FirestoreOptions) => {
24+
const api = new fsi.FirestoreApi();
25+
if (!options.location) {
26+
logger.error(
27+
"Missing required flag --location. See firebase firestore:databases:create --help for more info."
28+
);
29+
return;
30+
}
31+
// Type is always Firestore Native since Firebase does not support Datastore Mode
32+
const type: types.DatabaseType = types.DatabaseType.FIRESTORE_NATIVE;
33+
if (
34+
options.deleteProtection &&
35+
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.ENABLED &&
36+
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.DISABLED
37+
) {
38+
logger.error(
39+
"Invalid value for flag --delete-protection. See firebase firestore:databases:create --help for more info."
40+
);
41+
return;
42+
}
43+
const deleteProtectionState: types.DatabaseDeleteProtectionState =
44+
options.deleteProtection === types.DatabaseDeleteProtectionStateOption.ENABLED
45+
? types.DatabaseDeleteProtectionState.ENABLED
46+
: types.DatabaseDeleteProtectionState.DISABLED;
47+
48+
const databaseResp: types.DatabaseResp = await api.createDatabase(
49+
options.project,
50+
database,
51+
options.location,
52+
type,
53+
deleteProtectionState
54+
);
55+
56+
if (options.json) {
57+
logger.info(JSON.stringify(databaseResp, undefined, 2));
58+
} else {
59+
logger.info(clc.bold(`Successfully created ${api.prettyDatabaseString(databaseResp)}`));
60+
logger.info(
61+
"Please be sure to configure Firebase rules in your Firebase config file for\n" +
62+
"the new database. By default, created databases will have closed rules that\n" +
63+
"block any incoming third-party traffic."
64+
);
65+
}
66+
67+
return databaseResp;
68+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Command } from "../command";
2+
import * as clc from "colorette";
3+
import * as fsi from "../firestore/api";
4+
import * as types from "../firestore/api-types";
5+
import { promptOnce } from "../prompt";
6+
import { logger } from "../logger";
7+
import { requirePermissions } from "../requirePermissions";
8+
import { Emulators } from "../emulator/types";
9+
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
10+
import { FirestoreOptions } from "../firestore/options";
11+
import { FirebaseError } from "../error";
12+
13+
export const command = new Command("firestore:databases:delete <database>")
14+
.description(
15+
"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"
16+
)
17+
.option("--force", "Attempt to delete database without prompting for confirmation.")
18+
.before(requirePermissions, ["datastore.databases.delete"])
19+
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
20+
.action(async (database: string, options: FirestoreOptions) => {
21+
const api = new fsi.FirestoreApi();
22+
23+
if (!options.force) {
24+
const confirmMessage = `You are about to delete projects/${options.project}/databases/${database}. Do you wish to continue?`;
25+
const consent = await promptOnce({
26+
type: "confirm",
27+
message: confirmMessage,
28+
default: false,
29+
});
30+
if (!consent) {
31+
throw new FirebaseError("Delete database canceled.");
32+
}
33+
}
34+
35+
const databaseResp: types.DatabaseResp = await api.deleteDatabase(options.project, database);
36+
37+
if (options.json) {
38+
logger.info(JSON.stringify(databaseResp, undefined, 2));
39+
} else {
40+
logger.info(clc.bold(`Successfully deleted ${api.prettyDatabaseString(databaseResp)}`));
41+
}
42+
43+
return databaseResp;
44+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Command } from "../command";
2+
import * as fsi from "../firestore/api";
3+
import * as types from "../firestore/api-types";
4+
import { logger } from "../logger";
5+
import { requirePermissions } from "../requirePermissions";
6+
import { Emulators } from "../emulator/types";
7+
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
8+
import { FirestoreOptions } from "../firestore/options";
9+
10+
export const command = new Command("firestore:databases:get [database]")
11+
.description("Get database in your Cloud Firestore project.")
12+
.before(requirePermissions, ["datastore.databases.get"])
13+
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
14+
.action(async (database: string, options: FirestoreOptions) => {
15+
const api = new fsi.FirestoreApi();
16+
17+
const databaseId = database || "(default)";
18+
const databaseResp: types.DatabaseResp = await api.getDatabase(options.project, databaseId);
19+
20+
if (options.json) {
21+
logger.info(JSON.stringify(databaseResp, undefined, 2));
22+
} else {
23+
api.prettyPrintDatabase(databaseResp);
24+
}
25+
26+
return databaseResp;
27+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Command } from "../command";
2+
import * as fsi from "../firestore/api";
3+
import * as types from "../firestore/api-types";
4+
import { logger } from "../logger";
5+
import { requirePermissions } from "../requirePermissions";
6+
import { Emulators } from "../emulator/types";
7+
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
8+
import { FirestoreOptions } from "../firestore/options";
9+
10+
export const command = new Command("firestore:databases:list")
11+
.description("List databases in your Cloud Firestore project.")
12+
.before(requirePermissions, ["datastore.databases.list"])
13+
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
14+
.action(async (options: FirestoreOptions) => {
15+
const api = new fsi.FirestoreApi();
16+
17+
const databases: types.DatabaseResp[] = await api.listDatabases(options.project);
18+
19+
if (options.json) {
20+
logger.info(JSON.stringify(databases, undefined, 2));
21+
} else {
22+
api.prettyPrintDatabases(databases);
23+
}
24+
25+
return databases;
26+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Command } from "../command";
2+
import * as clc from "colorette";
3+
import * as fsi from "../firestore/api";
4+
import * as types from "../firestore/api-types";
5+
import { logger } from "../logger";
6+
import { requirePermissions } from "../requirePermissions";
7+
import { Emulators } from "../emulator/types";
8+
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
9+
import { FirestoreOptions } from "../firestore/options";
10+
11+
export const command = new Command("firestore:databases:update <database>")
12+
.description(
13+
"Update a database in your Firebase project. Must specify at least one property to update."
14+
)
15+
.option("--json", "Prints raw json response of the create API call if specified")
16+
.option(
17+
"--delete-protection <deleteProtectionState>",
18+
"Whether or not to prevent deletion of database, for example 'ENABLED' or 'DISABLED'. Default is 'DISABLED'"
19+
)
20+
.before(requirePermissions, ["datastore.databases.update"])
21+
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
22+
.action(async (database: string, options: FirestoreOptions) => {
23+
const api = new fsi.FirestoreApi();
24+
25+
if (!options.type && !options.deleteProtection) {
26+
logger.error(
27+
"Missing properties to update. See firebase firestore:databases:update --help for more info."
28+
);
29+
return;
30+
}
31+
const type: types.DatabaseType = types.DatabaseType.FIRESTORE_NATIVE;
32+
if (
33+
options.deleteProtection &&
34+
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.ENABLED &&
35+
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.DISABLED
36+
) {
37+
logger.error(
38+
"Invalid value for flag --delete-protection. See firebase firestore:databases:update --help for more info."
39+
);
40+
return;
41+
}
42+
const deleteProtectionState: types.DatabaseDeleteProtectionState =
43+
options.deleteProtection === types.DatabaseDeleteProtectionStateOption.ENABLED
44+
? types.DatabaseDeleteProtectionState.ENABLED
45+
: types.DatabaseDeleteProtectionState.DISABLED;
46+
47+
const databaseResp: types.DatabaseResp = await api.updateDatabase(
48+
options.project,
49+
database,
50+
type,
51+
deleteProtectionState
52+
);
53+
54+
if (options.json) {
55+
logger.info(JSON.stringify(databaseResp, undefined, 2));
56+
} else {
57+
logger.info(clc.bold(`Successfully updated ${api.prettyDatabaseString(databaseResp)}`));
58+
}
59+
60+
return databaseResp;
61+
});

src/commands/firestore-indexes-list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Command } from "../command";
22
import * as clc from "colorette";
3-
import * as fsi from "../firestore/indexes";
3+
import * as fsi from "../firestore/api";
44
import { logger } from "../logger";
55
import { requirePermissions } from "../requirePermissions";
66
import { Emulators } from "../emulator/types";
@@ -21,7 +21,7 @@ export const command = new Command("firestore:indexes")
2121
.before(requirePermissions, ["datastore.indexes.list"])
2222
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
2323
.action(async (options: FirestoreOptions) => {
24-
const indexApi = new fsi.FirestoreIndexes();
24+
const indexApi = new fsi.FirestoreApi();
2525

2626
const databaseId = options.database ?? "(default)";
2727
const indexes = await indexApi.listIndexes(options.project, databaseId);

src/commands/firestore-locations.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Command } from "../command";
2+
import * as fsi from "../firestore/api";
3+
import * as types from "../firestore/api-types";
4+
import { logger } from "../logger";
5+
import { requirePermissions } from "../requirePermissions";
6+
import { Emulators } from "../emulator/types";
7+
import { warnEmulatorNotSupported } from "../emulator/commandUtils";
8+
import { FirestoreOptions } from "../firestore/options";
9+
10+
export const command = new Command("firestore:locations")
11+
.description("List possible locations for your Cloud Firestore project.")
12+
.before(requirePermissions, ["datastore.locations.list"])
13+
.before(warnEmulatorNotSupported, Emulators.FIRESTORE)
14+
.action(async (options: FirestoreOptions) => {
15+
const api = new fsi.FirestoreApi();
16+
17+
const locations: types.Location[] = await api.locations(options.project);
18+
19+
if (options.json) {
20+
logger.info(JSON.stringify(locations, undefined, 2));
21+
} else {
22+
api.prettyPrintLocations(locations);
23+
}
24+
25+
return locations;
26+
});

src/commands/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ export function load(client: any): any {
108108
client.firestore = {};
109109
client.firestore.delete = loadCommand("firestore-delete");
110110
client.firestore.indexes = loadCommand("firestore-indexes-list");
111+
client.firestore.locations = loadCommand("firestore-locations");
112+
client.firestore.databases = {};
113+
client.firestore.databases.list = loadCommand("firestore-databases-list");
114+
client.firestore.databases.get = loadCommand("firestore-databases-get");
115+
client.firestore.databases.create = loadCommand("firestore-databases-create");
116+
client.firestore.databases.update = loadCommand("firestore-databases-update");
117+
client.firestore.databases.delete = loadCommand("firestore-databases-delete");
111118
client.functions = {};
112119
client.functions.config = {};
113120
client.functions.config.clone = loadCommand("functions-config-clone");

src/commands/open.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ const LINKS: Link[] = [
3030
{ name: "Firestore: Data", arg: "firestore", consolePath: "/firestore/data" },
3131
{ name: "Firestore: Rules", arg: "firestore:rules", consolePath: "/firestore/rules" },
3232
{ name: "Firestore: Indexes", arg: "firestore:indexes", consolePath: "/firestore/indexes" },
33+
{
34+
name: "Firestore: Databases List",
35+
arg: "firestore:databases:list",
36+
consolePath: "/firestore/databases/list",
37+
},
38+
{
39+
name: "Firestore: Locations",
40+
arg: "firestore:locations",
41+
consolePath: "/firestore/locations",
42+
},
3343
{ name: "Firestore: Usage", arg: "firestore:usage", consolePath: "/firestore/usage" },
3444
{ name: "Functions", arg: "functions", consolePath: "/functions/list" },
3545
{ name: "Functions Log", arg: "functions:log" } /* Special Case */,

src/deploy/firestore/deploy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as clc from "colorette";
22

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

3232
utils.logBullet(clc.bold(clc.cyan("firestore: ")) + "deploying indexes...");
33-
const firestoreIndexes = new FirestoreIndexes();
33+
const firestoreIndexes = new FirestoreApi();
3434
await Promise.all(
3535
indexesContext.map(async (indexContext: IndexContext): Promise<void> => {
3636
const { databaseId, indexesFileName, indexesRawSpec } = indexContext;

src/firestore/indexes-sort.ts renamed to src/firestore/api-sort.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import * as API from "./indexes-api";
2-
import * as Spec from "./indexes-spec";
1+
import * as API from "./api-types";
2+
import * as Spec from "./api-spec";
33
import * as util from "./util";
44

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

62+
/**
63+
* Compare two Database api entries for sorting.
64+
*
65+
* Comparisons:
66+
* 1) The databaseId (name)
67+
*/
68+
export function compareApiDatabase(a: API.DatabaseResp, b: API.DatabaseResp): number {
69+
// Name should always be unique and present
70+
return a.name > b.name ? 1 : -1;
71+
}
72+
73+
/**
74+
* Compare two Location api entries for sorting.
75+
*
76+
* Comparisons:
77+
* 1) The locationId.
78+
*/
79+
export function compareLocation(a: API.Location, b: API.Location): number {
80+
// LocationId should always be unique and present
81+
return a.locationId > b.locationId ? 1 : -1;
82+
}
83+
6284
/**
6385
* Compare two Field api entries for sorting.
6486
*

src/firestore/indexes-spec.ts renamed to src/firestore/api-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Please review and update the README as needed and notify [email protected].
55
*/
66

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

99
/**
1010
* An entry specifying a compound or other non-default index.

0 commit comments

Comments
 (0)