Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
- [Added] Support for creating Firestore Enterprise databases using `firestore:databases:create --edition enterprise`. (#8952)
- [Added] Support for Firestore Enterprise database index configurations. (#8939)
3 changes: 3 additions & 0 deletions schema/firebase-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
"database": {
"type": "string"
},
"edition": {
"type": "string"
},
"indexes": {
"type": "string"
},
Expand Down
259 changes: 259 additions & 0 deletions src/commands/firestore-databases-create.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import * as sinon from "sinon";
import { expect } from "chai";
import { Command } from "../command";
import { command as firestoreDatabasesCreate } from "./firestore-databases-create";
import * as fsi from "../firestore/api";
import * as types from "../firestore/api-types";
import { FirebaseError } from "../error";
import * as requireAuthModule from "../requireAuth";

describe("firestore:databases:create", () => {
const PROJECT = "test-project";
const DATABASE = "test-database";
const LOCATION = "nam5";

let command: Command;
let firestoreApiStub: sinon.SinonStubbedInstance<fsi.FirestoreApi>;
let requireAuthStub: sinon.SinonStub;

beforeEach(() => {
command = firestoreDatabasesCreate;
firestoreApiStub = sinon.createStubInstance(fsi.FirestoreApi);
requireAuthStub = sinon.stub(requireAuthModule, "requireAuth");
sinon.stub(fsi, "FirestoreApi").returns(firestoreApiStub);
requireAuthStub.resolves("a@b.com");
});

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

const mockDatabaseResp = (overrides: Partial<types.DatabaseResp>): types.DatabaseResp => {
return {
name: `projects/${PROJECT}/databases/${DATABASE}`,
uid: "test-uid",
createTime: "2025-07-28T12:00:00Z",
updateTime: "2025-07-28T12:00:00Z",
locationId: LOCATION,
type: types.DatabaseType.FIRESTORE_NATIVE,
databaseEdition: types.DatabaseEdition.STANDARD,
concurrencyMode: "OPTIMISTIC",
appEngineIntegrationMode: "DISABLED",
keyPrefix: `projects/${PROJECT}/databases/${DATABASE}`,
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
etag: "test-etag",
versionRetentionPeriod: "1h",
earliestVersionTime: "2025-07-28T11:00:00Z",
...overrides,
};
};

it("should create a new database with the correct parameters", async () => {
const options = {
project: PROJECT,
location: LOCATION,
json: true,
};
const expectedDatabase = mockDatabaseResp({});
firestoreApiStub.createDatabase.resolves(expectedDatabase);

const result = await command.runner()(DATABASE, options);

Check warning on line 61 in src/commands/firestore-databases-create.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

expect(result).to.deep.equal(expectedDatabase);
expect(
firestoreApiStub.createDatabase.calledOnceWith({
project: PROJECT,
databaseId: DATABASE,
locationId: LOCATION,
type: types.DatabaseType.FIRESTORE_NATIVE,
databaseEdition: types.DatabaseEdition.STANDARD,
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
cmekConfig: undefined,
}),
).to.be.true;
});

it("should throw an error if location is not provided", async () => {
const options = {
project: PROJECT,
};

await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(
FirebaseError,
"Missing required flag --location",
);
});

it("should throw an error for invalid delete protection option", async () => {
const options = {
project: PROJECT,
location: LOCATION,
deleteProtection: "INVALID",
};

await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(
FirebaseError,
"Invalid value for flag --delete-protection",
);
});

it("should throw an error for invalid point-in-time recovery option", async () => {
const options = {
project: PROJECT,
location: LOCATION,
pointInTimeRecovery: "INVALID",
};

await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(
FirebaseError,
"Invalid value for flag --point-in-time-recovery",
);
});

it("should throw an error for invalid edition option", async () => {
const options = {
project: PROJECT,
location: LOCATION,
edition: "INVALID",
};

await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(
FirebaseError,
"Invalid value for flag --edition",
);
});

it("should create a database with enterprise edition", async () => {
const options = {
project: PROJECT,
location: LOCATION,
edition: "enterprise",
json: true,
};
const expectedDatabase = mockDatabaseResp({
databaseEdition: types.DatabaseEdition.ENTERPRISE,
});
firestoreApiStub.createDatabase.resolves(expectedDatabase);

const result = await command.runner()(DATABASE, options);

Check warning on line 140 in src/commands/firestore-databases-create.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

expect(result).to.deep.equal(expectedDatabase);
expect(
firestoreApiStub.createDatabase.calledOnceWith({
project: PROJECT,
databaseId: DATABASE,
locationId: LOCATION,
type: types.DatabaseType.FIRESTORE_NATIVE,
databaseEdition: types.DatabaseEdition.ENTERPRISE,
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
cmekConfig: undefined,
}),
).to.be.true;
});

it("should create a database with delete protection enabled", async () => {
const options = {
project: PROJECT,
location: LOCATION,
deleteProtection: "ENABLED",
json: true,
};
const expectedDatabase = mockDatabaseResp({
deleteProtectionState: types.DatabaseDeleteProtectionState.ENABLED,
});
firestoreApiStub.createDatabase.resolves(expectedDatabase);

const result = await command.runner()(DATABASE, options);

Check warning on line 169 in src/commands/firestore-databases-create.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

expect(result).to.deep.equal(expectedDatabase);
expect(
firestoreApiStub.createDatabase.calledOnceWith({
project: PROJECT,
databaseId: DATABASE,
locationId: LOCATION,
type: types.DatabaseType.FIRESTORE_NATIVE,
databaseEdition: types.DatabaseEdition.STANDARD,
deleteProtectionState: types.DatabaseDeleteProtectionState.ENABLED,
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
cmekConfig: undefined,
}),
).to.be.true;
});

it("should create a database with point-in-time recovery enabled", async () => {
const options = {
project: PROJECT,
location: LOCATION,
pointInTimeRecovery: "ENABLED",
json: true,
};
const expectedDatabase = mockDatabaseResp({
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.ENABLED,
});
firestoreApiStub.createDatabase.resolves(expectedDatabase);

const result = await command.runner()(DATABASE, options);

Check warning on line 198 in src/commands/firestore-databases-create.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

expect(result).to.deep.equal(expectedDatabase);
expect(
firestoreApiStub.createDatabase.calledOnceWith({
project: PROJECT,
databaseId: DATABASE,
locationId: LOCATION,
type: types.DatabaseType.FIRESTORE_NATIVE,
databaseEdition: types.DatabaseEdition.STANDARD,
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.ENABLED,
cmekConfig: undefined,
}),
).to.be.true;
});

it("should create a database with a KMS key", async () => {
const KMS_KEY = "test-kms-key";
const options = {
project: PROJECT,
location: LOCATION,
kmsKeyName: KMS_KEY,
json: true,
};
const expectedDatabase = mockDatabaseResp({
cmekConfig: {
kmsKeyName: KMS_KEY,
},
});
firestoreApiStub.createDatabase.resolves(expectedDatabase);

const result = await command.runner()(DATABASE, options);

Check warning on line 230 in src/commands/firestore-databases-create.spec.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value

expect(result).to.deep.equal(expectedDatabase);
expect(
firestoreApiStub.createDatabase.calledOnceWith({
project: PROJECT,
databaseId: DATABASE,
locationId: LOCATION,
type: types.DatabaseType.FIRESTORE_NATIVE,
databaseEdition: types.DatabaseEdition.STANDARD,
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
cmekConfig: {
kmsKeyName: KMS_KEY,
},
}),
).to.be.true;
});

it("should throw an error if the API call fails", async () => {
const options = {
project: PROJECT,
location: LOCATION,
};
const apiError = new Error("API Error");
firestoreApiStub.createDatabase.rejects(apiError);

await expect(command.runner()(DATABASE, options)).to.be.rejectedWith(apiError);
});
});
19 changes: 19 additions & 0 deletions src/commands/firestore-databases-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export const command = new Command("firestore:databases:create <database>")
"--location <locationId>",
"region to create database, for example 'nam5'. Run 'firebase firestore:locations' to get a list of eligible locations (required)",
)
.option(
"--edition <edition>",
"the edition of the database to create, for example 'standard' or 'enterprise'. If not provided, 'standard' is used as a default.",
)
.option(
"--delete-protection <deleteProtectionState>",
"whether or not to prevent deletion of database, for example 'ENABLED' or 'DISABLED'. Default is 'DISABLED'",
Expand Down Expand Up @@ -44,6 +48,20 @@ export const command = new Command("firestore:databases:create <database>")
}
// Type is always Firestore Native since Firebase does not support Datastore Mode
const type: types.DatabaseType = types.DatabaseType.FIRESTORE_NATIVE;

// Figure out the database edition.
let databaseEdition: types.DatabaseEdition = types.DatabaseEdition.STANDARD;
if (options.edition) {
const edition = options.edition.toUpperCase();
if (
edition !== types.DatabaseEdition.STANDARD &&
edition !== types.DatabaseEdition.ENTERPRISE
) {
throw new FirebaseError(`Invalid value for flag --edition. ${helpCommandText}`);
}
databaseEdition = edition as types.DatabaseEdition;
}

if (
options.deleteProtection &&
options.deleteProtection !== types.DatabaseDeleteProtectionStateOption.ENABLED &&
Expand Down Expand Up @@ -82,6 +100,7 @@ export const command = new Command("firestore:databases:create <database>")
databaseId: database,
locationId: options.location,
type,
databaseEdition,
deleteProtectionState,
pointInTimeRecoveryEnablement,
cmekConfig,
Expand Down
16 changes: 16 additions & 0 deletions src/deploy/firestore/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import { Options } from "../../options";
import { FirebaseError } from "../../error";

async function createDatabase(context: any, options: Options): Promise<void> {

Check warning on line 14 in src/deploy/firestore/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
let firestoreCfg: FirestoreConfig = options.config.data.firestore;

Check warning on line 15 in src/deploy/firestore/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .firestore on an `any` value

Check warning on line 15 in src/deploy/firestore/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe assignment of an `any` value
if (Array.isArray(firestoreCfg)) {
firestoreCfg = firestoreCfg[0];
}
Expand All @@ -25,11 +25,26 @@
if (!firestoreCfg.database) {
firestoreCfg.database = "(default)";
}

let edition: types.DatabaseEdition = types.DatabaseEdition.STANDARD;
if (firestoreCfg.edition) {
const upperEdition = firestoreCfg.edition.toUpperCase();
if (
upperEdition !== types.DatabaseEdition.STANDARD &&
upperEdition !== types.DatabaseEdition.ENTERPRISE
) {
throw new FirebaseError(
`Invalid edition specified for database in firebase.json: ${firestoreCfg.edition}`,
);
}
edition = upperEdition as types.DatabaseEdition;
}

const api = new FirestoreApi();
try {
await api.getDatabase(options.projectId, firestoreCfg.database);
} catch (e: any) {

Check warning on line 46 in src/deploy/firestore/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unexpected any. Specify a different type
if (e.status === 404) {

Check warning on line 47 in src/deploy/firestore/deploy.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Unsafe member access .status on an `any` value
// Database is not found. Let's create it.
utils.logLabeledBullet(
"firetore",
Expand All @@ -40,6 +55,7 @@
databaseId: firestoreCfg.database,
locationId: firestoreCfg.location || "nam5", // Default to 'nam5' if location is not specified
type: types.DatabaseType.FIRESTORE_NATIVE,
databaseEdition: edition,
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
};
Expand Down
1 change: 1 addition & 0 deletions src/firebaseConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type DatabaseMultiple = ({
type FirestoreSingle = {
database?: string;
location?: string;
edition?: string;
rules?: string;
indexes?: string;
} & Deployable;
Expand Down
2 changes: 2 additions & 0 deletions src/firestore/api-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ export enum DatabaseEdition {
export interface DatabaseReq {
locationId?: string;
type?: DatabaseType;
databaseEdition?: DatabaseEdition;
deleteProtectionState?: DatabaseDeleteProtectionState;
pointInTimeRecoveryEnablement?: PointInTimeRecoveryEnablement;
cmekConfig?: CmekConfig;
Expand All @@ -157,6 +158,7 @@ export interface CreateDatabaseReq {
databaseId: string;
locationId: string;
type: DatabaseType;
databaseEdition?: DatabaseEdition;
deleteProtectionState: DatabaseDeleteProtectionState;
pointInTimeRecoveryEnablement: PointInTimeRecoveryEnablement;
cmekConfig?: CmekConfig;
Expand Down
1 change: 1 addition & 0 deletions src/firestore/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,7 @@ export class FirestoreApi {
const payload: types.DatabaseReq = {
locationId: req.locationId,
type: req.type,
databaseEdition: req.databaseEdition,
deleteProtectionState: req.deleteProtectionState,
pointInTimeRecoveryEnablement: req.pointInTimeRecoveryEnablement,
cmekConfig: req.cmekConfig,
Expand Down
1 change: 1 addition & 0 deletions src/firestore/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface FirestoreOptions extends Options {
type?: types.DatabaseType;
deleteProtection?: types.DatabaseDeleteProtectionStateOption;
pointInTimeRecoveryEnablement?: types.PointInTimeRecoveryEnablementOption;
edition?: string;

// backup schedules
backupSchedule?: string;
Expand Down
Loading