Skip to content

feat: Add enterprise edition support to firestore:databases:create #8952

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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("[email protected]");
});

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