Skip to content

Commit d4dcf16

Browse files
authored
[FDC] Generate schema during init dataconnect flow (#8596)
1 parent 609bb02 commit d4dcf16

File tree

6 files changed

+122
-11
lines changed

6 files changed

+122
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Add `generate_dataconnect_schema`, `dataconnect_generate_operation`, `firebase_consult_assistant` MCP tools. (#8647)
2+
- `firebase init dataconnect` is now integrated with Gemini in Firebase API to generate Schema based on description. (#8596)

src/api.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@ export const functionsDefaultRegion = () =>
9191

9292
export const cloudbuildOrigin = () =>
9393
utils.envOverride("FIREBASE_CLOUDBUILD_URL", "https://cloudbuild.googleapis.com");
94-
export const cloudCompanionOrigin = () =>
95-
utils.envOverride("CLOUD_COMPANION_URL", "https://cloudaicompanion.googleapis.com");
9694
export const cloudschedulerOrigin = () =>
9795
utils.envOverride("FIREBASE_CLOUDSCHEDULER_URL", "https://cloudscheduler.googleapis.com");
9896
export const cloudTasksOrigin = () =>

src/dataconnect/ensureApis.ts

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

4+
const prefix = "dataconnect";
5+
46
export async function ensureApis(projectId: string): Promise<void> {
5-
const prefix = "dataconnect";
67
await ensure(projectId, api.dataconnectOrigin(), prefix);
78
await ensure(projectId, api.cloudSQLAdminOrigin(), prefix);
89
await ensure(projectId, api.computeOrigin(), prefix);
910
}
1011

1112
export async function ensureSparkApis(projectId: string): Promise<void> {
12-
const prefix = "dataconnect";
1313
// These are the APIs that can be enabled without a billing account.
1414
await ensure(projectId, api.cloudSQLAdminOrigin(), prefix);
1515
}
16+
17+
export async function ensureGIFApis(projectId: string): Promise<void> {
18+
await ensure(projectId, api.cloudAiCompanionOrigin(), prefix);
19+
}

src/gemini/fdcExperience.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { expect } from "chai";
2+
import { extractCodeBlock } from "./fdcExperience";
3+
4+
describe("extractCodeBlock", () => {
5+
it("should extract a basic GraphQL query block", () => {
6+
const text =
7+
'Here is a GraphQL query:\n```graphql\nquery GetUser { user(id: "1") { name email } }\n```\nThanks!';
8+
const expected = 'query GetUser { user(id: "1") { name email } }';
9+
expect(extractCodeBlock(text)).to.eq(expected);
10+
});
11+
12+
it("should extract a multi-line GraphQL mutation block", () => {
13+
const text = `
14+
Some preamble.
15+
\`\`\`graphql
16+
mutation CreatePost($title: String!, $content: String!) {
17+
createPost(title: $title, content: $content) {
18+
id
19+
title
20+
}
21+
}
22+
\`\`\`
23+
Followed by some description.
24+
`;
25+
const expected = `mutation CreatePost($title: String!, $content: String!) {
26+
createPost(title: $title, content: $content) {
27+
id
28+
title
29+
}
30+
}`;
31+
expect(extractCodeBlock(text)).to.eq(expected);
32+
});
33+
34+
it("should extract a GraphQL fragment block", () => {
35+
const text = "```graphql\nfragment UserFields on User { id name }\n```";
36+
const expected = "fragment UserFields on User { id name }";
37+
expect(extractCodeBlock(text)).to.eq(expected);
38+
});
39+
40+
it("should extract an empty GraphQL code block", () => {
41+
const text = "```graphql\n\n```";
42+
const expected = "";
43+
expect(extractCodeBlock(text)).to.eq(expected);
44+
});
45+
46+
it("should extract a GraphQL schema definition block", () => {
47+
const text = `
48+
\`\`\`graphql
49+
type Query {
50+
hello: String
51+
}
52+
schema {
53+
query: Query
54+
}
55+
\`\`\`
56+
`;
57+
const expected = `type Query {
58+
hello: String
59+
}
60+
schema {
61+
query: Query
62+
}`;
63+
expect(extractCodeBlock(text)).to.eq(expected);
64+
});
65+
});

src/gemini/fdcExperience.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { Client } from "../apiv2";
2-
import { cloudCompanionOrigin } from "../api";
2+
import { cloudAiCompanionOrigin } from "../api";
33
import {
44
ChatExperienceResponse,
55
CloudAICompanionMessage,
66
CloudAICompanionRequest,
77
GenerateOperationResponse,
88
GenerateSchemaResponse,
99
} from "./types";
10+
import { FirebaseError } from "../error";
1011

11-
const apiClient = new Client({ urlPrefix: cloudCompanionOrigin(), auth: true });
12+
const apiClient = new Client({ urlPrefix: cloudAiCompanionOrigin(), auth: true });
1213
const SCHEMA_GENERATOR_EXPERIENCE = "/appeco/firebase/fdc-schema-generator";
1314
const GEMINI_IN_FIREBASE_EXPERIENCE = "/appeco/firebase/firebase-chat/free";
1415
const OPERATION_GENERATION_EXPERIENCE = "/appeco/firebase/fdc-query-generator";
@@ -91,3 +92,17 @@ export async function generateOperation(
9192
);
9293
return res.body.output.messages[0].content;
9394
}
95+
96+
/**
97+
* extractCodeBlock extracts the code block from the generated response.
98+
* @param text the generated response from the service.
99+
* @return the code block from the generated response.
100+
*/
101+
export function extractCodeBlock(text: string): string {
102+
const regex = /```(?:[a-z]+\n)?([\s\S]*?)```/m;
103+
const match = text.match(regex);
104+
if (match && match[1]) {
105+
return match[1].trim();
106+
}
107+
throw new FirebaseError(`No code block found in the generated response: ${text}`);
108+
}

src/init/features/dataconnect/index.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Setup } from "../..";
88
import { provisionCloudSql } from "../../../dataconnect/provisionCloudSql";
99
import { checkFreeTrialInstanceUsed, upgradeInstructions } from "../../../dataconnect/freeTrial";
1010
import * as cloudsql from "../../../gcp/cloudsql/cloudsqladmin";
11-
import { ensureApis, ensureSparkApis } from "../../../dataconnect/ensureApis";
11+
import { ensureApis, ensureGIFApis, ensureSparkApis } from "../../../dataconnect/ensureApis";
1212
import {
1313
listLocations,
1414
listAllServices,
@@ -19,10 +19,12 @@ import { Schema, Service, File, Platform } from "../../../dataconnect/types";
1919
import { parseCloudSQLInstanceName, parseServiceName } from "../../../dataconnect/names";
2020
import { logger } from "../../../logger";
2121
import { readTemplateSync } from "../../../templates";
22-
import { logBullet, logWarning, envOverride } from "../../../utils";
22+
import { logBullet, logWarning, envOverride, promiseWithSpinner } from "../../../utils";
2323
import { isBillingEnabled } from "../../../gcp/cloudbilling";
2424
import * as sdk from "./sdk";
2525
import { getPlatformFromFolder } from "../../../dataconnect/fileUtils";
26+
import { extractCodeBlock, generateSchema } from "../../../gemini/fdcExperience";
27+
import { configstore } from "../../../configstore";
2628

2729
const DATACONNECT_YAML_TEMPLATE = readTemplateSync("init/dataconnect/dataconnect.yaml");
2830
const CONNECTOR_YAML_TEMPLATE = readTemplateSync("init/dataconnect/connector.yaml");
@@ -103,8 +105,7 @@ export async function askQuestions(setup: Setup): Promise<void> {
103105
default: true,
104106
}));
105107
if (shouldConfigureBackend) {
106-
// TODO: Prompt for app idea and use GiF backend to generate them.
107-
info = await promptForService(info);
108+
info = await promptForSchema(setup, info);
108109
info = await promptForCloudSQL(setup, info);
109110

110111
info.shouldProvisionCSQL = !!(
@@ -444,12 +445,38 @@ async function promptForCloudSQL(setup: Setup, info: RequiredInfo): Promise<Requ
444445
return info;
445446
}
446447

447-
async function promptForService(info: RequiredInfo): Promise<RequiredInfo> {
448+
async function promptForSchema(setup: Setup, info: RequiredInfo): Promise<RequiredInfo> {
448449
if (info.serviceId === "") {
449450
info.serviceId = await input({
450451
message: "What ID would you like to use for this service?",
451452
default: basename(process.cwd()),
452453
});
454+
if (setup.projectId) {
455+
if (!configstore.get("gemini")) {
456+
logBullet(
457+
"Learn more about Gemini in Firebase and how it uses your data: https://firebase.google.com/docs/gemini-in-firebase#how-gemini-in-firebase-uses-your-data",
458+
);
459+
}
460+
if (
461+
await confirm({
462+
message: `Do you want Gemini in Firebase to help generate a schema for your service?`,
463+
default: false,
464+
})
465+
) {
466+
configstore.set("gemini", true);
467+
await ensureGIFApis(setup.projectId);
468+
const prompt = await input({
469+
message: "Describe the app you are building:",
470+
default: "movie rating app",
471+
});
472+
const schema = await promiseWithSpinner(
473+
() => generateSchema(prompt, setup.projectId!),
474+
"Generating the Data Connect Schema...",
475+
);
476+
info.schemaGql = [{ path: "schema.gql", content: extractCodeBlock(schema) }];
477+
info.connectors = [emptyConnector];
478+
}
479+
}
453480
}
454481
return info;
455482
}

0 commit comments

Comments
 (0)