Skip to content

Commit 254a69c

Browse files
ehsannasjoehan
andauthored
feat: Add enterprise edition support to firestore:databases:create (#8952)
* Good progress on enterprise edition from Gemini plus a helping hand * Added unit test, updated flag to an enum * minor fixups. * Pick up the edition from firebase.json as well. * Add the database edition to firebase-config.json * requireAuthStub in test. * fix merge conflicts. * Address feedback. * Add changelog. * Prettier. --------- Co-authored-by: Joe Hanley <[email protected]>
1 parent 4f22156 commit 254a69c

File tree

9 files changed

+303
-0
lines changed

9 files changed

+303
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
- [Added] Support for creating Firestore Enterprise databases using `firestore:databases:create --edition enterprise`. (#8952)
12
- [Added] Support for Firestore Enterprise database index configurations. (#8939)

schema/firebase-config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@
8888
"database": {
8989
"type": "string"
9090
},
91+
"edition": {
92+
"type": "string"
93+
},
9194
"indexes": {
9295
"type": "string"
9396
},
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import * as sinon from "sinon";
2+
import { expect } from "chai";
3+
import { Command } from "../command";
4+
import { command as firestoreDatabasesCreate } from "./firestore-databases-create";
5+
import * as fsi from "../firestore/api";
6+
import * as types from "../firestore/api-types";
7+
import { FirebaseError } from "../error";
8+
import * as requireAuthModule from "../requireAuth";
9+
10+
describe("firestore:databases:create", () => {
11+
const PROJECT = "test-project";
12+
const DATABASE = "test-database";
13+
const LOCATION = "nam5";
14+
15+
let command: Command;
16+
let firestoreApiStub: sinon.SinonStubbedInstance<fsi.FirestoreApi>;
17+
let requireAuthStub: sinon.SinonStub;
18+
19+
beforeEach(() => {
20+
command = firestoreDatabasesCreate;
21+
firestoreApiStub = sinon.createStubInstance(fsi.FirestoreApi);
22+
requireAuthStub = sinon.stub(requireAuthModule, "requireAuth");
23+
sinon.stub(fsi, "FirestoreApi").returns(firestoreApiStub);
24+
requireAuthStub.resolves("[email protected]");
25+
});
26+
27+
afterEach(() => {
28+
sinon.restore();
29+
});
30+
31+
const mockDatabaseResp = (overrides: Partial<types.DatabaseResp>): types.DatabaseResp => {
32+
return {
33+
name: `projects/${PROJECT}/databases/${DATABASE}`,
34+
uid: "test-uid",
35+
createTime: "2025-07-28T12:00:00Z",
36+
updateTime: "2025-07-28T12:00:00Z",
37+
locationId: LOCATION,
38+
type: types.DatabaseType.FIRESTORE_NATIVE,
39+
databaseEdition: types.DatabaseEdition.STANDARD,
40+
concurrencyMode: "OPTIMISTIC",
41+
appEngineIntegrationMode: "DISABLED",
42+
keyPrefix: `projects/${PROJECT}/databases/${DATABASE}`,
43+
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
44+
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
45+
etag: "test-etag",
46+
versionRetentionPeriod: "1h",
47+
earliestVersionTime: "2025-07-28T11:00:00Z",
48+
...overrides,
49+
};
50+
};
51+
52+
it("should create a new database with the correct parameters", async () => {
53+
const options = {
54+
project: PROJECT,
55+
location: LOCATION,
56+
json: true,
57+
};
58+
const expectedDatabase = mockDatabaseResp({});
59+
firestoreApiStub.createDatabase.resolves(expectedDatabase);
60+
61+
const result = await command.runner()(DATABASE, options);
62+
63+
expect(result).to.deep.equal(expectedDatabase);
64+
expect(
65+
firestoreApiStub.createDatabase.calledOnceWith({
66+
project: PROJECT,
67+
databaseId: DATABASE,
68+
locationId: LOCATION,
69+
type: types.DatabaseType.FIRESTORE_NATIVE,
70+
databaseEdition: types.DatabaseEdition.STANDARD,
71+
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
72+
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
73+
cmekConfig: undefined,
74+
}),
75+
).to.be.true;
76+
});
77+
78+
it("should throw an error if location is not provided", async () => {
79+
const options = {
80+
project: PROJECT,
81+
};
82+
83+
await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(
84+
FirebaseError,
85+
"Missing required flag --location",
86+
);
87+
});
88+
89+
it("should throw an error for invalid delete protection option", async () => {
90+
const options = {
91+
project: PROJECT,
92+
location: LOCATION,
93+
deleteProtection: "INVALID",
94+
};
95+
96+
await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(
97+
FirebaseError,
98+
"Invalid value for flag --delete-protection",
99+
);
100+
});
101+
102+
it("should throw an error for invalid point-in-time recovery option", async () => {
103+
const options = {
104+
project: PROJECT,
105+
location: LOCATION,
106+
pointInTimeRecovery: "INVALID",
107+
};
108+
109+
await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(
110+
FirebaseError,
111+
"Invalid value for flag --point-in-time-recovery",
112+
);
113+
});
114+
115+
it("should throw an error for invalid edition option", async () => {
116+
const options = {
117+
project: PROJECT,
118+
location: LOCATION,
119+
edition: "INVALID",
120+
};
121+
122+
await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(
123+
FirebaseError,
124+
"Invalid value for flag --edition",
125+
);
126+
});
127+
128+
it("should create a database with enterprise edition", async () => {
129+
const options = {
130+
project: PROJECT,
131+
location: LOCATION,
132+
edition: "enterprise",
133+
json: true,
134+
};
135+
const expectedDatabase = mockDatabaseResp({
136+
databaseEdition: types.DatabaseEdition.ENTERPRISE,
137+
});
138+
firestoreApiStub.createDatabase.resolves(expectedDatabase);
139+
140+
const result = await command.runner()(DATABASE, options);
141+
142+
expect(result).to.deep.equal(expectedDatabase);
143+
expect(
144+
firestoreApiStub.createDatabase.calledOnceWith({
145+
project: PROJECT,
146+
databaseId: DATABASE,
147+
locationId: LOCATION,
148+
type: types.DatabaseType.FIRESTORE_NATIVE,
149+
databaseEdition: types.DatabaseEdition.ENTERPRISE,
150+
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
151+
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
152+
cmekConfig: undefined,
153+
}),
154+
).to.be.true;
155+
});
156+
157+
it("should create a database with delete protection enabled", async () => {
158+
const options = {
159+
project: PROJECT,
160+
location: LOCATION,
161+
deleteProtection: "ENABLED",
162+
json: true,
163+
};
164+
const expectedDatabase = mockDatabaseResp({
165+
deleteProtectionState: types.DatabaseDeleteProtectionState.ENABLED,
166+
});
167+
firestoreApiStub.createDatabase.resolves(expectedDatabase);
168+
169+
const result = await command.runner()(DATABASE, options);
170+
171+
expect(result).to.deep.equal(expectedDatabase);
172+
expect(
173+
firestoreApiStub.createDatabase.calledOnceWith({
174+
project: PROJECT,
175+
databaseId: DATABASE,
176+
locationId: LOCATION,
177+
type: types.DatabaseType.FIRESTORE_NATIVE,
178+
databaseEdition: types.DatabaseEdition.STANDARD,
179+
deleteProtectionState: types.DatabaseDeleteProtectionState.ENABLED,
180+
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
181+
cmekConfig: undefined,
182+
}),
183+
).to.be.true;
184+
});
185+
186+
it("should create a database with point-in-time recovery enabled", async () => {
187+
const options = {
188+
project: PROJECT,
189+
location: LOCATION,
190+
pointInTimeRecovery: "ENABLED",
191+
json: true,
192+
};
193+
const expectedDatabase = mockDatabaseResp({
194+
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.ENABLED,
195+
});
196+
firestoreApiStub.createDatabase.resolves(expectedDatabase);
197+
198+
const result = await command.runner()(DATABASE, options);
199+
200+
expect(result).to.deep.equal(expectedDatabase);
201+
expect(
202+
firestoreApiStub.createDatabase.calledOnceWith({
203+
project: PROJECT,
204+
databaseId: DATABASE,
205+
locationId: LOCATION,
206+
type: types.DatabaseType.FIRESTORE_NATIVE,
207+
databaseEdition: types.DatabaseEdition.STANDARD,
208+
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
209+
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.ENABLED,
210+
cmekConfig: undefined,
211+
}),
212+
).to.be.true;
213+
});
214+
215+
it("should create a database with a KMS key", async () => {
216+
const KMS_KEY = "test-kms-key";
217+
const options = {
218+
project: PROJECT,
219+
location: LOCATION,
220+
kmsKeyName: KMS_KEY,
221+
json: true,
222+
};
223+
const expectedDatabase = mockDatabaseResp({
224+
cmekConfig: {
225+
kmsKeyName: KMS_KEY,
226+
},
227+
});
228+
firestoreApiStub.createDatabase.resolves(expectedDatabase);
229+
230+
const result = await command.runner()(DATABASE, options);
231+
232+
expect(result).to.deep.equal(expectedDatabase);
233+
expect(
234+
firestoreApiStub.createDatabase.calledOnceWith({
235+
project: PROJECT,
236+
databaseId: DATABASE,
237+
locationId: LOCATION,
238+
type: types.DatabaseType.FIRESTORE_NATIVE,
239+
databaseEdition: types.DatabaseEdition.STANDARD,
240+
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
241+
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
242+
cmekConfig: {
243+
kmsKeyName: KMS_KEY,
244+
},
245+
}),
246+
).to.be.true;
247+
});
248+
249+
it("should throw an error if the API call fails", async () => {
250+
const options = {
251+
project: PROJECT,
252+
location: LOCATION,
253+
};
254+
const apiError = new Error("API Error");
255+
firestoreApiStub.createDatabase.rejects(apiError);
256+
257+
await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(apiError);
258+
});
259+
});

src/commands/firestore-databases-create.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export const command = new Command("firestore:databases:create <database>")
1717
"--location <locationId>",
1818
"region to create database, for example 'nam5'. Run 'firebase firestore:locations' to get a list of eligible locations (required)",
1919
)
20+
.option(
21+
"--edition <edition>",
22+
"the edition of the database to create, for example 'standard' or 'enterprise'. If not provided, 'standard' is used as a default.",
23+
)
2024
.option(
2125
"--delete-protection <deleteProtectionState>",
2226
"whether or not to prevent deletion of database, for example 'ENABLED' or 'DISABLED'. Default is 'DISABLED'",
@@ -44,6 +48,20 @@ export const command = new Command("firestore:databases:create <database>")
4448
}
4549
// Type is always Firestore Native since Firebase does not support Datastore Mode
4650
const type: types.DatabaseType = types.DatabaseType.FIRESTORE_NATIVE;
51+
52+
// Figure out the database edition.
53+
let databaseEdition: types.DatabaseEdition = types.DatabaseEdition.STANDARD;
54+
if (options.edition) {
55+
const edition = options.edition.toUpperCase();
56+
if (
57+
edition !== types.DatabaseEdition.STANDARD &&
58+
edition !== types.DatabaseEdition.ENTERPRISE
59+
) {
60+
throw new FirebaseError(`Invalid value for flag --edition. ${helpCommandText}`);
61+
}
62+
databaseEdition = edition as types.DatabaseEdition;
63+
}
64+
4765
if (
4866
options.deleteProtection &&
4967
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.ENABLED &&
@@ -82,6 +100,7 @@ export const command = new Command("firestore:databases:create <database>")
82100
databaseId: database,
83101
locationId: options.location,
84102
type,
103+
databaseEdition,
85104
deleteProtectionState,
86105
pointInTimeRecoveryEnablement,
87106
cmekConfig,

src/deploy/firestore/deploy.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,21 @@ async function createDatabase(context: any, options: Options): Promise<void> {
2525
if (!firestoreCfg.database) {
2626
firestoreCfg.database = "(default)";
2727
}
28+
29+
let edition: types.DatabaseEdition = types.DatabaseEdition.STANDARD;
30+
if (firestoreCfg.edition) {
31+
const upperEdition = firestoreCfg.edition.toUpperCase();
32+
if (
33+
upperEdition !== types.DatabaseEdition.STANDARD &&
34+
upperEdition !== types.DatabaseEdition.ENTERPRISE
35+
) {
36+
throw new FirebaseError(
37+
`Invalid edition specified for database in firebase.json: ${firestoreCfg.edition}`,
38+
);
39+
}
40+
edition = upperEdition as types.DatabaseEdition;
41+
}
42+
2843
const api = new FirestoreApi();
2944
try {
3045
await api.getDatabase(options.projectId, firestoreCfg.database);
@@ -40,6 +55,7 @@ async function createDatabase(context: any, options: Options): Promise<void> {
4055
databaseId: firestoreCfg.database,
4156
locationId: firestoreCfg.location || "nam5", // Default to 'nam5' if location is not specified
4257
type: types.DatabaseType.FIRESTORE_NATIVE,
58+
databaseEdition: edition,
4359
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
4460
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
4561
};

src/firebaseConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type DatabaseMultiple = ({
3939
type FirestoreSingle = {
4040
database?: string;
4141
location?: string;
42+
edition?: string;
4243
rules?: string;
4344
indexes?: string;
4445
} & Deployable;

src/firestore/api-types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ export enum DatabaseEdition {
147147
export interface DatabaseReq {
148148
locationId?: string;
149149
type?: DatabaseType;
150+
databaseEdition?: DatabaseEdition;
150151
deleteProtectionState?: DatabaseDeleteProtectionState;
151152
pointInTimeRecoveryEnablement?: PointInTimeRecoveryEnablement;
152153
cmekConfig?: CmekConfig;
@@ -157,6 +158,7 @@ export interface CreateDatabaseReq {
157158
databaseId: string;
158159
locationId: string;
159160
type: DatabaseType;
161+
databaseEdition?: DatabaseEdition;
160162
deleteProtectionState: DatabaseDeleteProtectionState;
161163
pointInTimeRecoveryEnablement: PointInTimeRecoveryEnablement;
162164
cmekConfig?: CmekConfig;

src/firestore/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,7 @@ export class FirestoreApi {
718718
const payload: types.DatabaseReq = {
719719
locationId: req.locationId,
720720
type: req.type,
721+
databaseEdition: req.databaseEdition,
721722
deleteProtectionState: req.deleteProtectionState,
722723
pointInTimeRecoveryEnablement: req.pointInTimeRecoveryEnablement,
723724
cmekConfig: req.cmekConfig,

src/firestore/options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface FirestoreOptions extends Options {
1818
type?: types.DatabaseType;
1919
deleteProtection?: types.DatabaseDeleteProtectionStateOption;
2020
pointInTimeRecoveryEnablement?: types.PointInTimeRecoveryEnablementOption;
21+
edition?: string;
2122

2223
// backup schedules
2324
backupSchedule?: string;

0 commit comments

Comments
 (0)