Skip to content

Commit 6e000fb

Browse files
Fixing firestore deploy on fresh projects (#9220)
* Fixing firestore deploy on fresh projects * Update src/deploy/firestore/prepare.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent fc531f0 commit 6e000fb

File tree

4 files changed

+72
-62
lines changed

4 files changed

+72
-62
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
- Fixed a bug when `firebase init dataconnect` failed to create a React app when launched from VS Code extension (#9171).
66
- Improved the clarity of the `firebase apptesting:execute` command when you have zero or multiple apps.
77
- `firebase dataconnect:sql:migrate` now supports Cloud SQL instances with only private IPs. The command must be run in the same VPC of the instance to work. (##9200)
8+
- Fixed an issue where `firebase deploy --only firestore` would fail with 403 errors on projects that never had a database created.

src/deploy/firestore/deploy.ts

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

33
import { FirestoreApi } from "../../firestore/api";
4-
import * as types from "../../firestore/api-types";
54
import { logger } from "../../logger";
65
import * as utils from "../../utils";
76
import { RulesDeploy, RulesetServiceType } from "../../rulesDeploy";
87
import { IndexContext } from "./prepare";
9-
import { FirestoreConfig } from "../../firebaseConfig";
108
import { sleep } from "../../utils";
119
import { Options } from "../../options";
12-
import { FirebaseError } from "../../error";
13-
14-
async function createDatabase(context: any, options: Options): Promise<void> {
15-
let firestoreCfg: FirestoreConfig = options.config.data.firestore;
16-
if (Array.isArray(firestoreCfg)) {
17-
firestoreCfg = firestoreCfg[0];
18-
}
19-
if (!options.projectId) {
20-
throw new FirebaseError("Project ID is required to create a Firestore database.");
21-
}
22-
if (!firestoreCfg) {
23-
throw new FirebaseError("Firestore database configuration not found in firebase.json.");
24-
}
25-
if (!firestoreCfg.database) {
26-
firestoreCfg.database = "(default)";
27-
}
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-
43-
const api = new FirestoreApi();
44-
try {
45-
await api.getDatabase(options.projectId, firestoreCfg.database);
46-
} catch (e: any) {
47-
if (e.status === 404) {
48-
// Database is not found. Let's create it.
49-
utils.logLabeledBullet(
50-
"firetore",
51-
`Creating the new Firestore database ${firestoreCfg.database}...`,
52-
);
53-
const createDatabaseReq: types.CreateDatabaseReq = {
54-
project: options.projectId,
55-
databaseId: firestoreCfg.database,
56-
locationId: firestoreCfg.location || "nam5", // Default to 'nam5' if location is not specified
57-
type: types.DatabaseType.FIRESTORE_NATIVE,
58-
databaseEdition: edition,
59-
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
60-
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
61-
};
62-
await api.createDatabase(createDatabaseReq);
63-
}
64-
}
65-
}
6610

6711
/**
6812
* Deploys Firestore Rules.
@@ -129,7 +73,6 @@ async function deployIndexes(context: any, options: any): Promise<void> {
12973
* @param options The CLI options object.
13074
*/
13175
export default async function (context: any, options: Options): Promise<void> {
132-
await createDatabase(context, options);
13376
await deployRules(context);
13477
await deployIndexes(context, options);
13578
}

src/deploy/firestore/prepare.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { logger } from "../../logger";
99
import { DeployOptions } from "..";
1010
import { ensure } from "../../ensureApiEnabled";
1111
import { firestoreOrigin } from "../../api";
12+
import { FirebaseError } from "../../error";
13+
import * as types from "../../firestore/api-types";
14+
import { FirestoreConfig } from "../../firebaseConfig";
15+
import { FirestoreApi } from "../../firestore/api";
1216

1317
export interface RulesContext {
1418
databaseId: string;
@@ -67,13 +71,66 @@ function prepareIndexes(
6771
indexesRawSpec,
6872
} as IndexContext);
6973
}
74+
async function createDatabase(context: any, options: Options): Promise<void> {
75+
let firestoreCfg: FirestoreConfig = options.config.data.firestore;
76+
if (Array.isArray(firestoreCfg)) {
77+
firestoreCfg = firestoreCfg[0];
78+
}
79+
if (!options.projectId) {
80+
throw new FirebaseError("Project ID is required to create a Firestore database.");
81+
}
82+
if (!firestoreCfg) {
83+
throw new FirebaseError("Firestore database configuration not found in firebase.json.");
84+
}
85+
if (!firestoreCfg.database) {
86+
firestoreCfg.database = "(default)";
87+
}
88+
89+
let edition: types.DatabaseEdition = types.DatabaseEdition.STANDARD;
90+
if (firestoreCfg.edition) {
91+
const upperEdition = firestoreCfg.edition.toUpperCase();
92+
if (
93+
upperEdition !== types.DatabaseEdition.STANDARD &&
94+
upperEdition !== types.DatabaseEdition.ENTERPRISE
95+
) {
96+
throw new FirebaseError(
97+
`Invalid edition specified for database in firebase.json: ${firestoreCfg.edition}`,
98+
);
99+
}
100+
edition = upperEdition as types.DatabaseEdition;
101+
}
102+
103+
const api = new FirestoreApi();
104+
try {
105+
await api.getDatabase(options.projectId, firestoreCfg.database);
106+
} catch (e: any) {
107+
if (e.status === 404) {
108+
// Database is not found. Let's create it.
109+
utils.logLabeledBullet(
110+
"firestore",
111+
`Creating the new Firestore database ${firestoreCfg.database}...`,
112+
);
113+
const createDatabaseReq: types.CreateDatabaseReq = {
114+
project: options.projectId,
115+
databaseId: firestoreCfg.database,
116+
locationId: firestoreCfg.location || "nam5", // Default to 'nam5' if location is not specified
117+
type: types.DatabaseType.FIRESTORE_NATIVE,
118+
databaseEdition: edition,
119+
deleteProtectionState: types.DatabaseDeleteProtectionState.DISABLED,
120+
pointInTimeRecoveryEnablement: types.PointInTimeRecoveryEnablement.DISABLED,
121+
};
122+
await api.createDatabase(createDatabaseReq);
123+
}
124+
}
125+
}
70126

71127
/**
72128
* Prepares Firestore deploys.
73129
* @param context The deploy context.
74130
* @param options The CLI options object.
75131
*/
76132
export default async function (context: any, options: DeployOptions): Promise<void> {
133+
await ensure(context.projectId, firestoreOrigin(), "firestore");
77134
await ensure(context.projectId, firestoreOrigin(), "firestore");
78135
if (options.only) {
79136
const targets = options.only.split(",");
@@ -111,6 +168,9 @@ export default async function (context: any, options: DeployOptions): Promise<vo
111168
const rulesDeploy: RulesDeploy = new RulesDeploy(options, RulesetServiceType.CLOUD_FIRESTORE);
112169
context.firestore.rulesDeploy = rulesDeploy;
113170

171+
// We need to create the DB first if it doesn't exist
172+
// Otherwise, prepare rules will fail when it calls the :test endpoint
173+
await createDatabase(context, options);
114174
for (const firestoreConfig of firestoreConfigs) {
115175
if (firestoreConfig.indexes) {
116176
prepareIndexes(context, options, firestoreConfig.database, firestoreConfig.indexes);

src/firestore/api.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { FirebaseError } from "../error";
1515
import { Client } from "../apiv2";
1616
import { PrettyPrint } from "./pretty-print";
1717
import { optionalValueMatches } from "../functional";
18+
import { pollOperation } from "../operation-poller";
1819

1920
export class FirestoreApi {
2021
apiClient = new Client({ urlPrefix: firestoreOrigin(), apiVersion: "v1" });
@@ -779,11 +780,16 @@ export class FirestoreApi {
779780
cmekConfig: req.cmekConfig,
780781
};
781782
const options = { queryParams: { databaseId: req.databaseId } };
782-
const res = await this.apiClient.post<types.DatabaseReq, { response?: types.DatabaseResp }>(
783-
url,
784-
payload,
785-
options,
786-
);
783+
const res = await this.apiClient.post<
784+
types.DatabaseReq,
785+
{ name: string; response?: types.DatabaseResp }
786+
>(url, payload, options);
787+
await pollOperation({
788+
apiOrigin: firestoreOrigin(),
789+
apiVersion: "v1",
790+
operationResourceName: res.body.name,
791+
masterTimeout: 600000,
792+
});
787793
const database = res.body.response;
788794
if (!database) {
789795
throw new FirebaseError("Not found");

0 commit comments

Comments
 (0)