Skip to content

Commit 3a663a1

Browse files
authored
[FDC] Super charge firebase init dataconnect with Gemini (#8988)
1 parent 49bd4ef commit 3a663a1

File tree

10 files changed

+476
-321
lines changed

10 files changed

+476
-321
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
- Fixed a bug when `firebase deploy --only dataconnect` doesn't include GQL in nested folders (#8981)
33
- Make it possible to init a dataconnect project in non interactive mode (#8993)
44
- Added 2 new MCP tools for crashlytics `get_sample_crash_for_issue` and `get_issue_details` (#8995)
5+
- Use Gemini to generate schema and seed_data.gql in `firebase init dataconnect` (#8988)

src/dataconnect/client.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,31 @@ export async function createService(
4141
projectId: string,
4242
locationId: string,
4343
serviceId: string,
44-
): Promise<types.Service> {
45-
const op = await dataconnectClient().post<types.Service, types.Service>(
46-
`/projects/${projectId}/locations/${locationId}/services`,
47-
{
48-
name: `projects/${projectId}/locations/${locationId}/services/${serviceId}`,
49-
},
50-
{
51-
queryParams: {
52-
service_id: serviceId,
44+
): Promise<types.Service | undefined> {
45+
try {
46+
const op = await dataconnectClient().post<types.Service, types.Service>(
47+
`/projects/${projectId}/locations/${locationId}/services`,
48+
{
49+
name: `projects/${projectId}/locations/${locationId}/services/${serviceId}`,
5350
},
54-
},
55-
);
56-
const pollRes = await operationPoller.pollOperation<types.Service>({
57-
apiOrigin: dataconnectOrigin(),
58-
apiVersion: DATACONNECT_API_VERSION,
59-
operationResourceName: op.body.name,
60-
});
61-
return pollRes;
51+
{
52+
queryParams: {
53+
service_id: serviceId,
54+
},
55+
},
56+
);
57+
const pollRes = await operationPoller.pollOperation<types.Service>({
58+
apiOrigin: dataconnectOrigin(),
59+
apiVersion: DATACONNECT_API_VERSION,
60+
operationResourceName: op.body.name,
61+
});
62+
return pollRes;
63+
} catch (err: any) {
64+
if (err.status !== 409) {
65+
throw err;
66+
}
67+
return undefined; // Service already exists
68+
}
6269
}
6370

6471
export async function deleteService(serviceName: string): Promise<types.Service> {

src/dataconnect/ensureApis.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
import * as api from "../api";
2-
import { check, ensure } from "../ensureApiEnabled";
2+
import { ensure } from "../ensureApiEnabled";
33

44
const prefix = "dataconnect";
55

6-
export async function isApiEnabled(projectId: string): Promise<boolean> {
7-
return await check(projectId, api.dataconnectOrigin(), prefix);
8-
}
9-
106
export async function ensureApis(projectId: string): Promise<void> {
11-
await ensure(projectId, api.dataconnectOrigin(), prefix);
12-
await ensure(projectId, api.cloudSQLAdminOrigin(), prefix);
13-
}
14-
15-
export async function ensureSparkApis(projectId: string): Promise<void> {
16-
// These are the APIs that can be enabled without a billing account.
17-
await ensure(projectId, api.cloudSQLAdminOrigin(), prefix);
18-
await ensure(projectId, api.dataconnectOrigin(), prefix);
7+
await Promise.all([
8+
ensure(projectId, api.dataconnectOrigin(), prefix),
9+
ensure(projectId, api.cloudSQLAdminOrigin(), prefix),
10+
]);
1911
}
2012

2113
export async function ensureGIFApis(projectId: string): Promise<void> {

src/dataconnect/freeTrial.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,32 @@ const FREE_TRIAL_METRIC = "sqladmin.googleapis.com/fdc_lifetime_free_trial_per_p
1212

1313
// Checks whether there is already a free trial instance on a project.
1414
export async function checkFreeTrialInstanceUsed(projectId: string): Promise<boolean> {
15-
utils.logLabeledBullet("dataconnect", "Checking Cloud SQL no cost trial eligibility...");
1615
const past7d = new Date();
1716
past7d.setDate(past7d.getDate() - 7);
1817
const query: CmQuery = {
1918
filter: `metric.type="serviceruntime.googleapis.com/quota/allocation/usage" AND metric.label.quota_metric = "${FREE_TRIAL_METRIC}"`,
2019
"interval.endTime": new Date().toJSON(),
2120
"interval.startTime": past7d.toJSON(),
2221
};
22+
let used = true;
2323
try {
2424
const ts = await queryTimeSeries(query, projectId);
25-
let used = true;
2625
if (ts.length) {
2726
used = ts[0].points.some((p) => p.value.int64Value);
2827
}
29-
if (used) {
30-
utils.logLabeledWarning(
31-
"dataconnect",
32-
"CloudSQL no cost trial has already been used on this project.",
33-
);
34-
}
35-
return used;
3628
} catch (err: any) {
3729
// If the metric doesn't exist, free trial is not used.
30+
used = false;
31+
}
32+
if (used) {
33+
utils.logLabeledWarning(
34+
"dataconnect",
35+
"CloudSQL no cost trial has already been used on this project.",
36+
);
37+
} else {
3838
utils.logLabeledSuccess("dataconnect", "CloudSQL no cost trial available!");
39-
return false;
4039
}
40+
return used;
4141
}
4242

4343
export async function getFreeTrialInstanceId(projectId: string): Promise<string | undefined> {
@@ -84,9 +84,11 @@ export function printFreeTrialUnavailable(
8484
}
8585

8686
export function upgradeInstructions(projectId: string): string {
87-
return `If you'd like to provision a CloudSQL Postgres instance on the Firebase Data Connect no-cost trial:
88-
1. Please upgrade to the pay-as-you-go (Blaze) billing plan. Visit the following page:
89-
https://console.firebase.google.com/project/${projectId}/usage/details
90-
2. Run ${clc.bold("firebase init dataconnect")} again to configure the Cloud SQL instance.
91-
3. Run ${clc.bold("firebase deploy --only dataconnect")} to deploy your Data Connect service.`;
87+
return `To provision a CloudSQL Postgres instance on the Firebase Data Connect no-cost trial:
88+
89+
1. Please upgrade to the pay-as-you-go (Blaze) billing plan. Visit the following page:
90+
91+
https://console.firebase.google.com/project/${projectId}/usage/details
92+
93+
2. Run ${clc.bold("firebase deploy --only dataconnect")} to deploy your Data Connect service.`;
9294
}

src/dataconnect/provisionCloudSql.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export async function provisionCloudSql(args: {
3232
} = args;
3333
try {
3434
const existingInstance = await cloudSqlAdminClient.getInstance(projectId, instanceId);
35-
silent || utils.logLabeledBullet("dataconnect", `Found existing instance ${instanceId}.`);
35+
silent ||
36+
utils.logLabeledBullet("dataconnect", `Found existing Cloud SQL instance ${instanceId}.`);
3637
connectionName = existingInstance?.connectionName || "";
3738
const why = getUpdateReason(existingInstance, enableGoogleMlIntegration);
3839
if (why) {
@@ -61,7 +62,6 @@ export async function provisionCloudSql(args: {
6162
if (err.status !== 404) {
6263
throw err;
6364
}
64-
cmekWarning();
6565
const cta = dryRun ? "It will be created on your next deploy" : "Creating it now.";
6666
const freeTrialUsed = await checkFreeTrialInstanceUsed(projectId);
6767
silent ||
@@ -95,7 +95,7 @@ export async function provisionCloudSql(args: {
9595
silent ||
9696
utils.logLabeledBullet(
9797
"dataconnect",
98-
"Cloud SQL instance creation started - it should be ready shortly. Database and users will be created on your next deploy.",
98+
"Cloud SQL instance creation started. While it is being set up, your data will be saved in a temporary database. When it is ready, your data will be migrated.",
9999
);
100100
return connectionName;
101101
}
@@ -111,17 +111,11 @@ export async function provisionCloudSql(args: {
111111
silent ||
112112
utils.logLabeledBullet(
113113
"dataconnect",
114-
`Database ${databaseId} not found. It will be created on your next deploy.`,
114+
`Postgres database ${databaseId} not found. It will be created on your next deploy.`,
115115
);
116116
} else {
117-
// Create the database if not found.
118-
silent ||
119-
utils.logLabeledBullet(
120-
"dataconnect",
121-
`Database ${databaseId} not found, creating it now...`,
122-
);
123117
await cloudSqlAdminClient.createDatabase(projectId, instanceId, databaseId);
124-
silent || utils.logLabeledBullet("dataconnect", `Database ${databaseId} created.`);
118+
silent || utils.logLabeledBullet("dataconnect", `Postgres database ${databaseId} created.`);
125119
}
126120
} else {
127121
// Skip it if the database is not accessible.
@@ -171,11 +165,3 @@ export function getUpdateReason(instance: Instance, requireGoogleMlIntegration:
171165

172166
return reason;
173167
}
174-
175-
function cmekWarning() {
176-
const message =
177-
"Cloud SQL instances created via the Firebase CLI do not support customer managed encryption keys.\n" +
178-
"If you'd like to use a CMEK to encrypt your data, first create a CMEK encrypted instance (https://cloud.google.com/sql/docs/postgres/configure-cmek#createcmekinstance).\n" +
179-
"Then, edit your `dataconnect.yaml` file to use the encrypted instance and redeploy.";
180-
utils.logLabeledWarning("dataconnect", message);
181-
}

src/dataconnect/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface PostgreSql {
3535
database?: string;
3636
cloudSql?: CloudSqlInstance;
3737
schemaValidation?: SchemaValidation | "NONE" | "SQL_SCHEMA_VALIDATION_UNSPECIFIED";
38+
schemaMigration?: "MIGRATE_COMPATIBLE";
3839
}
3940

4041
export interface CloudSqlInstance {

src/gemini/fdcExperience.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ const OPERATION_GENERATION_EXPERIENCE = "/appeco/firebase/fdc-query-generator";
1616
const FIREBASE_CHAT_REQUEST_CONTEXT_TYPE_NAME =
1717
"type.googleapis.com/google.cloud.cloudaicompanion.v1main.FirebaseChatRequestContext";
1818

19+
export const PROMPT_GENERATE_CONNECTOR =
20+
"Create 4 operations for an app using the instance schema with proper authentication.";
21+
22+
export const PROMPT_GENERATE_SEED_DATA =
23+
"Create a mutation to populate the database with some seed data.";
24+
1925
/**
2026
* generateSchema generates a schema based on the users app design prompt.
2127
* @param prompt description of the app the user would like to generate.
@@ -36,7 +42,7 @@ export async function generateSchema(
3642
},
3743
},
3844
);
39-
return res.body.output.messages[0].content;
45+
return extractCodeBlock(res.body.output.messages[0].content);
4046
}
4147

4248
/**
@@ -90,7 +96,7 @@ export async function generateOperation(
9096
},
9197
},
9298
);
93-
return res.body.output.messages[0].content;
99+
return extractCodeBlock(res.body.output.messages[0].content);
94100
}
95101

96102
/**

0 commit comments

Comments
 (0)