From fc6b77d98c314679c47026b2236bae1b2304f3c3 Mon Sep 17 00:00:00 2001
From: Tony Huang
Date: Wed, 8 May 2024 12:18:31 -0400
Subject: [PATCH 01/40] add fah tos check to backends:create (#7088)
* add initial tos requirement
* add tests
* comments
* fix test
* comments
---
src/commands/apphosting-backends-create.ts | 3 +
src/gcp/firedata.ts | 50 +++++++++++++++
src/requireTosAcceptance.ts | 35 ++++++++++
src/test/gcp/firedata.spec.ts | 73 +++++++++++++++++++++
src/test/requireTosAcceptance.spec.ts | 74 ++++++++++++++++++++++
5 files changed, 235 insertions(+)
create mode 100644 src/gcp/firedata.ts
create mode 100644 src/requireTosAcceptance.ts
create mode 100644 src/test/gcp/firedata.spec.ts
create mode 100644 src/test/requireTosAcceptance.spec.ts
diff --git a/src/commands/apphosting-backends-create.ts b/src/commands/apphosting-backends-create.ts
index 1a11b629e96..146f5dd20fc 100644
--- a/src/commands/apphosting-backends-create.ts
+++ b/src/commands/apphosting-backends-create.ts
@@ -4,6 +4,8 @@ import { needProjectId } from "../projectUtils";
import requireInteractive from "../requireInteractive";
import { doSetup } from "../apphosting";
import { ensureApiEnabled } from "../gcp/apphosting";
+import { APPHOSTING_TOS_ID } from "../gcp/firedata";
+import { requireTosAcceptance } from "../requireTosAcceptance";
export const command = new Command("apphosting:backends:create")
.description("create a Firebase App Hosting backend")
@@ -19,6 +21,7 @@ export const command = new Command("apphosting:backends:create")
)
.before(ensureApiEnabled)
.before(requireInteractive)
+ .before(requireTosAcceptance(APPHOSTING_TOS_ID))
.action(async (options: Options) => {
const projectId = needProjectId(options);
const webApp = options.app;
diff --git a/src/gcp/firedata.ts b/src/gcp/firedata.ts
new file mode 100644
index 00000000000..a226cc73cd0
--- /dev/null
+++ b/src/gcp/firedata.ts
@@ -0,0 +1,50 @@
+import { Client } from "../apiv2";
+import { firedataOrigin } from "../api";
+import { FirebaseError } from "../error";
+
+const client = new Client({ urlPrefix: firedataOrigin(), auth: true, apiVersion: "v1" });
+
+export const APPHOSTING_TOS_ID = "APP_HOSTING_TOS";
+export const APP_CHECK_TOS_ID = "APP_CHECK";
+
+export type TosId = typeof APPHOSTING_TOS_ID | typeof APP_CHECK_TOS_ID;
+
+export type AcceptanceStatus = null | "ACCEPTED" | "TERMS_UPDATED";
+
+export interface TosAcceptanceStatus {
+ status: AcceptanceStatus;
+}
+
+export interface ServiceTosStatus {
+ tosId: TosId;
+ serviceStatus: TosAcceptanceStatus;
+}
+
+export interface GetTosStatusResponse {
+ perServiceStatus: ServiceTosStatus[];
+}
+
+/**
+ * Fetches the Terms of Service status for the logged in user.
+ */
+export async function getTosStatus(): Promise {
+ const res = await client.get("accessmanagement/tos:getStatus");
+ return res.body;
+}
+
+/** Returns the AcceptanceStatus for a given product. */
+export function getAcceptanceStatus(
+ response: GetTosStatusResponse,
+ tosId: TosId,
+): AcceptanceStatus {
+ const perServiceStatus = response.perServiceStatus.find((tosStatus) => tosStatus.tosId === tosId);
+ if (perServiceStatus === undefined) {
+ throw new FirebaseError(`Missing terms of service status for product: ${tosId}`);
+ }
+ return perServiceStatus.serviceStatus.status;
+}
+
+/** Returns true if a product's ToS has been accepted. */
+export function isProductTosAccepted(response: GetTosStatusResponse, tosId: TosId): boolean {
+ return getAcceptanceStatus(response, tosId) === "ACCEPTED";
+}
diff --git a/src/requireTosAcceptance.ts b/src/requireTosAcceptance.ts
new file mode 100644
index 00000000000..210c131ca67
--- /dev/null
+++ b/src/requireTosAcceptance.ts
@@ -0,0 +1,35 @@
+import type { Options } from "./options";
+
+import { FirebaseError } from "./error";
+import { APPHOSTING_TOS_ID, TosId, getTosStatus, isProductTosAccepted } from "./gcp/firedata";
+import { consoleOrigin } from "./api";
+
+const consoleLandingPage = new Map([
+ [APPHOSTING_TOS_ID, `${consoleOrigin()}/project/_/apphosting`],
+]);
+
+/**
+ * Returns a function that checks product terms of service. Useful for Command `before` hooks.
+ *
+ * Example:
+ * new Command(...)
+ * .description(...)
+ * .before(requireTosAcceptance(APPHOSTING_TOS_ID)) ;
+ *
+ * Note: When supporting new products, be sure to update `consoleLandingPage` above to avoid surfacing
+ * generic ToS error messages.
+ **/
+export function requireTosAcceptance(tosId: TosId): (options: Options) => Promise {
+ return () => requireTos(tosId);
+}
+
+async function requireTos(tosId: TosId): Promise {
+ const res = await getTosStatus();
+ if (isProductTosAccepted(res, tosId)) {
+ return;
+ }
+ const console = consoleLandingPage.get(tosId) || consoleOrigin();
+ throw new FirebaseError(
+ `Your account has not accepted the required Terms of Service for this action. Please accept the Terms of Service and try again. ${console}`,
+ );
+}
diff --git a/src/test/gcp/firedata.spec.ts b/src/test/gcp/firedata.spec.ts
new file mode 100644
index 00000000000..e05a12c417a
--- /dev/null
+++ b/src/test/gcp/firedata.spec.ts
@@ -0,0 +1,73 @@
+import * as nock from "nock";
+import {
+ APPHOSTING_TOS_ID,
+ APP_CHECK_TOS_ID,
+ GetTosStatusResponse,
+ getAcceptanceStatus,
+ getTosStatus,
+ isProductTosAccepted,
+} from "../../gcp/firedata";
+import { expect } from "chai";
+
+const SAMPLE_RESPONSE = {
+ perServiceStatus: [
+ {
+ tosId: "APP_CHECK",
+ serviceStatus: {
+ tos: {
+ id: "app_check",
+ tosId: "APP_CHECK",
+ },
+ status: "ACCEPTED",
+ },
+ },
+ {
+ tosId: "APP_HOSTING_TOS",
+ serviceStatus: {
+ tos: {
+ id: "app_hosting",
+ tosId: "APP_HOSTING_TOS",
+ },
+ status: "TERMS_UPDATED",
+ },
+ },
+ ],
+};
+
+describe("firedata", () => {
+ before(() => {
+ nock.disableNetConnect();
+ });
+ after(() => {
+ nock.cleanAll();
+ nock.enableNetConnect();
+ });
+
+ describe("getTosStatus", () => {
+ it("should return parsed GetTosStatusResponse", async () => {
+ nock("https://mobilesdk-pa.googleapis.com")
+ .get("/v1/accessmanagement/tos:getStatus")
+ .reply(200, SAMPLE_RESPONSE);
+
+ await expect(getTosStatus()).to.eventually.deep.equal(
+ SAMPLE_RESPONSE as GetTosStatusResponse,
+ );
+ });
+ });
+
+ describe("getAcceptanceStatus", () => {
+ it("should return the status", () => {
+ const res = SAMPLE_RESPONSE as GetTosStatusResponse;
+ expect(getAcceptanceStatus(res, APP_CHECK_TOS_ID)).to.equal("ACCEPTED");
+ expect(getAcceptanceStatus(res, APPHOSTING_TOS_ID)).to.equal("TERMS_UPDATED");
+ });
+ });
+
+ describe("isProductTosAccepted", () => {
+ it("should determine whether tos is accepted", () => {
+ const res = SAMPLE_RESPONSE as GetTosStatusResponse;
+ expect(isProductTosAccepted(res, APP_CHECK_TOS_ID)).to.equal(true);
+ expect(isProductTosAccepted(res, APPHOSTING_TOS_ID)).to.equal(false);
+ });
+ });
+});
diff --git a/src/test/requireTosAcceptance.spec.ts b/src/test/requireTosAcceptance.spec.ts
new file mode 100644
index 00000000000..c36b12c1842
--- /dev/null
+++ b/src/test/requireTosAcceptance.spec.ts
@@ -0,0 +1,74 @@
+import * as nock from "nock";
+import { APPHOSTING_TOS_ID, APP_CHECK_TOS_ID } from "../gcp/firedata";
+import { requireTosAcceptance } from "../requireTosAcceptance";
+import { Options } from "../options";
+import { RC } from "../rc";
+import { expect } from "chai";
+
+const SAMPLE_OPTIONS: Options = {
+ cwd: "/",
+ configPath: "/",
+ /* eslint-disable-next-line */
+ config: {} as any,
+ only: "",
+ except: "",
+ nonInteractive: false,
+ json: false,
+ interactive: false,
+ debug: false,
+ force: false,
+ filteredTargets: [],
+ rc: new RC(),
+};
+
+const SAMPLE_RESPONSE = {
+ perServiceStatus: [
+ {
+ tosId: "APP_CHECK",
+ serviceStatus: {
+ tos: {
+ id: "app_check",
+ tosId: "APP_CHECK",
+ },
+ status: "ACCEPTED",
+ },
+ },
+ {
+ tosId: "APP_HOSTING_TOS",
+ serviceStatus: {
+ tos: {
+ id: "app_hosting",
+ tosId: "APP_HOSTING_TOS",
+ },
+ status: "TERMS_UPDATED",
+ },
+ },
+ ],
+};
+
+describe("requireTosAcceptance", () => {
+ before(() => {
+ nock.disableNetConnect();
+ });
+ after(() => {
+ nock.enableNetConnect();
+ });
+
+ it("should resolve for accepted terms of service", async () => {
+ nock("https://mobilesdk-pa.googleapis.com")
+ .get("/v1/accessmanagement/tos:getStatus")
+ .reply(200, SAMPLE_RESPONSE);
+
+ await requireTosAcceptance(APP_CHECK_TOS_ID)(SAMPLE_OPTIONS);
+ });
+
+ it("should throw error if not accepted", async () => {
+ nock("https://mobilesdk-pa.googleapis.com")
+ .get("/v1/accessmanagement/tos:getStatus")
+ .reply(200, SAMPLE_RESPONSE);
+
+ await expect(requireTosAcceptance(APPHOSTING_TOS_ID)(SAMPLE_OPTIONS)).to.be.rejectedWith(
+ "Terms of Service",
+ );
+ });
+});
From 8748b8087445c0c3226dec47765702ff279be2c3 Mon Sep 17 00:00:00 2001
From: Mathusan Selvarajah
Date: Wed, 8 May 2024 16:39:51 +0000
Subject: [PATCH 02/40] Improve App Hosting Web App selection UX (#7100)
---------
Co-authored-by: Mathusan Selvarajah
---
src/apphosting/app.ts | 128 +++++++++++---------------------
src/apphosting/index.ts | 2 +-
src/test/apphosting/app.spec.ts | 67 ++++++++---------
3 files changed, 73 insertions(+), 124 deletions(-)
diff --git a/src/apphosting/app.ts b/src/apphosting/app.ts
index 1694ccf07a9..85ce95e2cfb 100644
--- a/src/apphosting/app.ts
+++ b/src/apphosting/app.ts
@@ -1,8 +1,6 @@
-import * as fuzzy from "fuzzy";
-import * as inquirer from "inquirer";
-import { AppPlatform, WebAppMetadata, createWebApp, listFirebaseApps } from "../management/apps";
-import { promptOnce } from "../prompt";
+import { AppMetadata, AppPlatform, createWebApp, listFirebaseApps } from "../management/apps";
import { FirebaseError } from "../error";
+import { logWarning } from "../utils";
const CREATE_NEW_FIREBASE_WEB_APP = "CREATE_NEW_WEB_APP";
const CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP = "CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP";
@@ -11,7 +9,7 @@ export const webApps = {
CREATE_NEW_FIREBASE_WEB_APP,
CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP,
getOrCreateWebApp,
- promptFirebaseWebApp,
+ generateWebAppName,
};
type FirebaseWebApp = { name: string; id: string };
@@ -34,22 +32,7 @@ async function getOrCreateWebApp(
backendId: string,
): Promise {
const webAppsInProject = await listFirebaseApps(projectId, AppPlatform.WEB);
-
- if (webAppsInProject.length === 0) {
- // create a web app using backend id
- const { displayName, appId } = await createFirebaseWebApp(projectId, {
- displayName: backendId,
- });
- return { name: displayName, id: appId };
- }
-
- const existingUserProjectWebApps = new Map(
- webAppsInProject.map((obj) => [
- // displayName can be null, use app id instead if so. Example - displayName: "mathusan-web-app", appId: "1:461896338144:web:426291191cccce65fede85"
- obj.displayName ?? obj.appId,
- obj.appId,
- ]),
- );
+ const existingUserProjectWebApps = firebaseAppsToMap(webAppsInProject);
if (firebaseWebAppName) {
if (existingUserProjectWebApps.get(firebaseWebAppName) === undefined) {
@@ -58,79 +41,24 @@ async function getOrCreateWebApp(
);
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return { name: firebaseWebAppName, id: existingUserProjectWebApps.get(firebaseWebAppName)! };
- }
-
- return await webApps.promptFirebaseWebApp(projectId, backendId, existingUserProjectWebApps);
-}
-
-/**
- * Prompts the user for the web app that they would like to associate their backend with
- * @param projectId user's projectId
- * @param backendId user's backendId
- * @param existingUserProjectWebApps a map of a user's firebase web apps to their ids
- * @return the name and ID of a web app
- */
-async function promptFirebaseWebApp(
- projectId: string,
- backendId: string,
- existingUserProjectWebApps: Map,
-): Promise {
- const existingWebAppKeys = Array.from(existingUserProjectWebApps.keys());
-
- const firebaseWebAppName = await promptOnce({
- type: "autocomplete",
- name: "app",
- message:
- "Which of the following Firebase web apps would you like to associate your backend with?",
- source: (_: any, input = ""): Promise<(inquirer.DistinctChoice | inquirer.Separator)[]> => {
- return new Promise((resolve) =>
- resolve([
- new inquirer.Separator(),
- {
- name: "Create a new Firebase web app.",
- value: CREATE_NEW_FIREBASE_WEB_APP,
- },
- {
- name: "Continue without a Firebase web app.",
- value: CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP,
- },
- new inquirer.Separator(),
- ...fuzzy.filter(input, existingWebAppKeys).map((result) => {
- return result.original;
- }),
- ]),
- );
- },
- });
-
- if (firebaseWebAppName === CREATE_NEW_FIREBASE_WEB_APP) {
- const newFirebaseWebApp = await createFirebaseWebApp(projectId, { displayName: backendId });
- return { name: newFirebaseWebApp.displayName, id: newFirebaseWebApp.appId };
- } else if (firebaseWebAppName === CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP) {
- return;
+ return {
+ name: firebaseWebAppName,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ id: existingUserProjectWebApps.get(firebaseWebAppName)!,
+ };
}
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return { name: firebaseWebAppName, id: existingUserProjectWebApps.get(firebaseWebAppName)! };
-}
+ const webAppName = await generateWebAppName(projectId, backendId);
-/**
- * A wrapper for createWebApp to catch and log quota errors
- */
-async function createFirebaseWebApp(
- projectId: string,
- options: { displayName?: string },
-): Promise {
try {
- return await createWebApp(projectId, options);
+ const app = await createWebApp(projectId, { displayName: webAppName });
+ return { name: app.displayName, id: app.appId };
} catch (e) {
if (isQuotaError(e)) {
- throw new FirebaseError(
+ logWarning(
"Unable to create a new web app, the project has reached the quota for Firebase apps. Navigate to your Firebase console to manage or delete a Firebase app to continue. ",
- { original: e instanceof Error ? e : undefined },
);
+ return;
}
throw new FirebaseError("Unable to create a Firebase web app", {
@@ -139,6 +67,34 @@ async function createFirebaseWebApp(
}
}
+async function generateWebAppName(projectId: string, backendId: string): Promise {
+ const webAppsInProject = await listFirebaseApps(projectId, AppPlatform.WEB);
+ const appsMap = firebaseAppsToMap(webAppsInProject);
+ if (!appsMap.get(backendId)) {
+ return backendId;
+ }
+
+ let uniqueId = 1;
+ let webAppName = `${backendId}_${uniqueId}`;
+
+ while (appsMap.get(webAppName)) {
+ uniqueId += 1;
+ webAppName = `${backendId}_${uniqueId}`;
+ }
+
+ return webAppName;
+}
+
+function firebaseAppsToMap(apps: AppMetadata[]): Map {
+ return new Map(
+ apps.map((obj) => [
+ // displayName can be null, use app id instead if so. Example - displayName: "mathusan-web-app", appId: "1:461896338144:web:426291191cccce65fede85"
+ obj.displayName ?? obj.appId,
+ obj.appId,
+ ]),
+ );
+}
+
/**
* TODO: Make this generic to be re-used in other parts of the CLI
*/
diff --git a/src/apphosting/index.ts b/src/apphosting/index.ts
index fd485a6797e..e679828754f 100644
--- a/src/apphosting/index.ts
+++ b/src/apphosting/index.ts
@@ -107,7 +107,7 @@ export async function doSetup(
const webApp = await webApps.getOrCreateWebApp(projectId, webAppName, backendId);
if (webApp) {
- logSuccess(`Firebase web app set to ${webApp.name}.\n`);
+ logSuccess(`Created a new Firebase web app named "${webApp.name}"`);
} else {
logWarning(`Firebase web app not set`);
}
diff --git a/src/test/apphosting/app.spec.ts b/src/test/apphosting/app.spec.ts
index edbf2b06353..697c94cc7ce 100644
--- a/src/test/apphosting/app.spec.ts
+++ b/src/test/apphosting/app.spec.ts
@@ -1,6 +1,5 @@
import { webApps } from "../../apphosting/app";
import * as apps from "../../../src/management/apps";
-import * as prompts from "../../prompt";
import * as sinon from "sinon";
import { expect } from "chai";
import { FirebaseError } from "../../error";
@@ -9,21 +8,19 @@ describe("app", () => {
const projectId = "projectId";
const backendId = "backendId";
+ let listFirebaseApps: sinon.SinonStub;
+
describe("getOrCreateWebApp", () => {
let createWebApp: sinon.SinonStub;
- let listFirebaseApps: sinon.SinonStub;
- let promptFirebaseWebApp: sinon.SinonStub;
beforeEach(() => {
createWebApp = sinon.stub(apps, "createWebApp");
listFirebaseApps = sinon.stub(apps, "listFirebaseApps");
- promptFirebaseWebApp = sinon.stub(webApps, "promptFirebaseWebApp");
});
afterEach(() => {
createWebApp.restore();
listFirebaseApps.restore();
- promptFirebaseWebApp.restore();
});
it("should create an app with backendId if no web apps exist yet", async () => {
@@ -49,53 +46,49 @@ describe("app", () => {
);
});
- it("prompts user for a web app if none is provided", async () => {
- listFirebaseApps.returns(
- Promise.resolve([
- { displayName: "testWebApp1", appId: "testWebAppId1", platform: apps.AppPlatform.WEB },
- ]),
- );
-
- const userSelection = { name: "testWebApp2", id: "testWebAppId2" };
- promptFirebaseWebApp.returns(Promise.resolve(userSelection));
+ it("returns undefined if user has reached the app limit for their project", async () => {
+ listFirebaseApps.returns(Promise.resolve([]));
+ createWebApp.throws({ original: { status: 429 } });
- await expect(webApps.getOrCreateWebApp(projectId, null, backendId)).to.eventually.deep.equal(
- userSelection,
- );
- expect(promptFirebaseWebApp).to.be.called;
+ const app = await webApps.getOrCreateWebApp(projectId, null, backendId);
+ expect(app).equal(undefined);
});
});
- describe("promptFirebaseWebApp", () => {
- let promptOnce: sinon.SinonStub;
- let createWebApp: sinon.SinonStub;
-
+ describe("generateWebAppName", () => {
beforeEach(() => {
- promptOnce = sinon.stub(prompts, "promptOnce");
- createWebApp = sinon.stub(apps, "createWebApp");
+ listFirebaseApps = sinon.stub(apps, "listFirebaseApps");
});
afterEach(() => {
- promptOnce.restore();
- createWebApp.restore();
+ listFirebaseApps.restore();
});
- it("creates a new web app if user selects that option", async () => {
- promptOnce.returns(webApps.CREATE_NEW_FIREBASE_WEB_APP);
- createWebApp.returns({ displayName: backendId, appId: "appId" });
+ it("returns backendId if no such web app already exists", async () => {
+ listFirebaseApps.returns(Promise.resolve([]));
- await webApps.promptFirebaseWebApp(projectId, backendId, new Map([["app", "appId"]]));
- expect(createWebApp).to.be.calledWith(projectId, { displayName: backendId });
+ const appName = await webApps.generateWebAppName(projectId, backendId);
+ expect(appName).equal(backendId);
});
- it("skips a web selection if user selects that option", async () => {
- promptOnce.returns(webApps.CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP);
+ it("returns backendId as appName with a unique id if app with backendId already exists", async () => {
+ listFirebaseApps.returns(Promise.resolve([{ displayName: backendId, appId: "1234" }]));
- await expect(
- webApps.promptFirebaseWebApp(projectId, backendId, new Map([["app", "appId"]])),
- ).to.eventually.equal(undefined);
+ const appName = await webApps.generateWebAppName(projectId, backendId);
+ expect(appName).equal(`${backendId}_1`);
+ });
+
+ it("returns appropriate unique id if app with backendId already exists", async () => {
+ listFirebaseApps.returns(
+ Promise.resolve([
+ { displayName: backendId, appId: "1234" },
+ { displayName: `${backendId}_1`, appId: "1234" },
+ { displayName: `${backendId}_2`, appId: "1234" },
+ ]),
+ );
- expect(createWebApp).to.not.be.called;
+ const appName = await webApps.generateWebAppName(projectId, backendId);
+ expect(appName).equal(`${backendId}_3`);
});
});
});
From ca70c273fd70832b77b33a00ecc4edd19baac627 Mon Sep 17 00:00:00 2001
From: Thomas Bouldin
Date: Wed, 8 May 2024 10:21:03 -0700
Subject: [PATCH 03/40] Temporarily turn off reusable builds for CF3v2
---
src/deploy/functions/release/fabricator.ts | 9 +++++++--
src/experiments.ts | 12 +++++++-----
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/src/deploy/functions/release/fabricator.ts b/src/deploy/functions/release/fabricator.ts
index d03be4ce1ee..b755897285b 100644
--- a/src/deploy/functions/release/fabricator.ts
+++ b/src/deploy/functions/release/fabricator.ts
@@ -15,6 +15,7 @@ import * as deploymentTool from "../../../deploymentTool";
import * as gcf from "../../../gcp/cloudfunctions";
import * as gcfV2 from "../../../gcp/cloudfunctionsv2";
import * as eventarc from "../../../gcp/eventarc";
+import * as experiments from "../../../experiments";
import * as helper from "../functionsDeployHelper";
import * as planner from "./planner";
import * as poller from "../../../operation-poller";
@@ -364,7 +365,9 @@ export class Fabricator {
while (!resultFunction) {
resultFunction = await this.functionExecutor
.run(async () => {
- apiFunction.buildConfig.sourceToken = await scraper.getToken();
+ if (experiments.isEnabled("functionsv2deployoptimizations")) {
+ apiFunction.buildConfig.sourceToken = await scraper.getToken();
+ }
const op: { name: string } = await gcfV2.createFunction(apiFunction);
return await poller.pollOperation({
...gcfV2PollerOptions,
@@ -502,7 +505,9 @@ export class Fabricator {
const resultFunction = await this.functionExecutor
.run(
async () => {
- apiFunction.buildConfig.sourceToken = await scraper.getToken();
+ if (experiments.isEnabled("functionsv2deployoptimizations")) {
+ apiFunction.buildConfig.sourceToken = await scraper.getToken();
+ }
const op: { name: string } = await gcfV2.updateFunction(apiFunction);
return await poller.pollOperation({
...gcfV2PollerOptions,
diff --git a/src/experiments.ts b/src/experiments.ts
index 0a84f52d931..872c2bd6630 100644
--- a/src/experiments.ts
+++ b/src/experiments.ts
@@ -33,12 +33,14 @@ export const ALL_EXPERIMENTS = experiments({
shortDescription: "Use new endpoint to administer realtime database instances",
},
// Cloud Functions for Firebase experiments
- pythonfunctions: {
- shortDescription: "Python support for Cloud Functions for Firebase",
+ functionsv2deployoptimizations: {
+ shortDescription: "Optimize deployments of v2 firebase functions",
fullDescription:
- "Adds the ability to initializea and deploy Cloud " +
- "Functions for Firebase in Python. While this feature is experimental " +
- "breaking API changes are allowed in MINOR API revisions",
+ "Reuse build images across funtions to increase performance and reliaibility " +
+ "of deploys. This has been made an experiment due to backend bugs that are " +
+ "temporarily causing failures in some reginos with this optimization enabled",
+ public: true,
+ default: false,
},
deletegcfartifacts: {
shortDescription: `Add the ${bold(
From 16b1b83bcf3bcf37368ca94ac105ab2dbeb4f349 Mon Sep 17 00:00:00 2001
From: joehan
Date: Wed, 8 May 2024 14:08:00 -0400
Subject: [PATCH 04/40] Changelog for functions experiment (#7131)
---
CHANGELOG.md | 1 +
src/experiments.ts | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e695c9aa7f7..bec1b918412 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Temporarily disable performance improvements for second gen functions deploy to avoid a backend issue.
- Increased the timeout for waiting for emulators to start to 60s. (#7091)
- Fixes infinite loop when trying to create a Hosting site.
- Fix copied functions dist dir files for Next.js when source config ends with slash (#7099)
diff --git a/src/experiments.ts b/src/experiments.ts
index 872c2bd6630..fa52f6eafd6 100644
--- a/src/experiments.ts
+++ b/src/experiments.ts
@@ -38,7 +38,7 @@ export const ALL_EXPERIMENTS = experiments({
fullDescription:
"Reuse build images across funtions to increase performance and reliaibility " +
"of deploys. This has been made an experiment due to backend bugs that are " +
- "temporarily causing failures in some reginos with this optimization enabled",
+ "temporarily causing failures in some regions with this optimization enabled",
public: true,
default: false,
},
From e658645f62d4bf7aa12005e67798a66a20492b4a Mon Sep 17 00:00:00 2001
From: Google Open Source Bot
Date: Wed, 8 May 2024 18:27:36 +0000
Subject: [PATCH 05/40] 13.8.1
---
npm-shrinkwrap.json | 4 ++--
package.json | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index fa463144e96..65ee86a1b02 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1,12 +1,12 @@
{
"name": "firebase-tools",
- "version": "13.8.0",
+ "version": "13.8.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "firebase-tools",
- "version": "13.8.0",
+ "version": "13.8.1",
"license": "MIT",
"dependencies": {
"@google-cloud/cloud-sql-connector": "^1.2.3",
diff --git a/package.json b/package.json
index dfff3729196..f4dfcad1166 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "firebase-tools",
- "version": "13.8.0",
+ "version": "13.8.1",
"description": "Command-Line Interface for Firebase",
"main": "./lib/index.js",
"bin": {
From 3d7e7341b4c943491b0eb53d7ca3276100a3d68a Mon Sep 17 00:00:00 2001
From: Google Open Source Bot
Date: Wed, 8 May 2024 18:27:51 +0000
Subject: [PATCH 06/40] [firebase-release] Removed change log and reset repo
after 13.8.1 release
---
CHANGELOG.md | 4 ----
1 file changed, 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bec1b918412..e69de29bb2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +0,0 @@
-- Temporarily disable performance improvements for second gen functions deploy to avoid a backend issue.
-- Increased the timeout for waiting for emulators to start to 60s. (#7091)
-- Fixes infinite loop when trying to create a Hosting site.
-- Fix copied functions dist dir files for Next.js when source config ends with slash (#7099)
From a90a9db9c619c701c31601907bf43b8a9fe53a0e Mon Sep 17 00:00:00 2001
From: Mathusan Selvarajah
Date: Wed, 8 May 2024 19:01:39 +0000
Subject: [PATCH 07/40] log the selected location only when user is prompted
for one (#7114)
Co-authored-by: Mathusan Selvarajah
---
src/apphosting/index.ts | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/apphosting/index.ts b/src/apphosting/index.ts
index e679828754f..c964ac1bf37 100644
--- a/src/apphosting/index.ts
+++ b/src/apphosting/index.ts
@@ -96,7 +96,6 @@ export async function doSetup(
location =
location || (await promptLocation(projectId, "Select a location to host your backend:\n"));
- logSuccess(`Location set to ${location}.\n`);
const backendId = await promptNewBackendId(projectId, location, {
name: "backendId",
@@ -449,13 +448,17 @@ export async function promptLocation(
return allowedLocations[0];
}
- return (await promptOnce({
+ const location = (await promptOnce({
name: "location",
type: "list",
default: DEFAULT_LOCATION,
message: prompt,
choices: allowedLocations,
})) as string;
+
+ logSuccess(`Location set to ${location}.\n`);
+
+ return location;
}
/**
From 03aced19f5ad0a0a99d1e3cbd6d1e73663efb2a0 Mon Sep 17 00:00:00 2001
From: Mathusan Selvarajah
Date: Wed, 8 May 2024 19:22:51 +0000
Subject: [PATCH 08/40] App Hosting: Take optional web app Id instead of web
app name (#7129)
* take optional firebase web app Id instead of firebase web app name when creating a backend
---------
Co-authored-by: Mathusan Selvarajah
---
src/apphosting/app.ts | 29 ++++++++++------------
src/apphosting/index.ts | 4 +--
src/commands/apphosting-backends-create.ts | 8 +++---
3 files changed, 18 insertions(+), 23 deletions(-)
diff --git a/src/apphosting/app.ts b/src/apphosting/app.ts
index 85ce95e2cfb..c897cfaa854 100644
--- a/src/apphosting/app.ts
+++ b/src/apphosting/app.ts
@@ -1,6 +1,6 @@
import { AppMetadata, AppPlatform, createWebApp, listFirebaseApps } from "../management/apps";
import { FirebaseError } from "../error";
-import { logWarning } from "../utils";
+import { logSuccess, logWarning } from "../utils";
const CREATE_NEW_FIREBASE_WEB_APP = "CREATE_NEW_WEB_APP";
const CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP = "CONTINUE_WITHOUT_SELECTING_FIREBASE_WEB_APP";
@@ -15,36 +15,32 @@ export const webApps = {
type FirebaseWebApp = { name: string; id: string };
/**
- * If firebaseWebAppName is provided and a matching web app exists, it is
- * returned. If firebaseWebAppName is not provided then the user is prompted to
- * choose from one of their existing web apps or to create a new one or to skip
- * without selecting a web app. If user chooses to create a new web app,
- * a new web app with the given backendId is created. If user chooses to skip
- * without selecting a web app nothing is returned.
+ * If firebaseWebAppId is provided and a matching web app exists, it is
+ * returned. If firebaseWebAppId is not provided, a new web app with the given
+ * backendId is created.
* @param projectId user's projectId
- * @param firebaseWebAppName (optional) name of an existing Firebase web app
+ * @param firebaseWebAppId (optional) id of an existing Firebase web app
* @param backendId name of the app hosting backend
* @return app name and app id
*/
async function getOrCreateWebApp(
projectId: string,
- firebaseWebAppName: string | null,
+ firebaseWebAppId: string | null,
backendId: string,
): Promise {
const webAppsInProject = await listFirebaseApps(projectId, AppPlatform.WEB);
- const existingUserProjectWebApps = firebaseAppsToMap(webAppsInProject);
- if (firebaseWebAppName) {
- if (existingUserProjectWebApps.get(firebaseWebAppName) === undefined) {
+ if (firebaseWebAppId) {
+ const webApp = webAppsInProject.find((app) => app.appId === firebaseWebAppId);
+ if (webApp === undefined) {
throw new FirebaseError(
- `The web app '${firebaseWebAppName}' does not exist in project ${projectId}`,
+ `The web app '${firebaseWebAppId}' does not exist in project ${projectId}`,
);
}
return {
- name: firebaseWebAppName,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- id: existingUserProjectWebApps.get(firebaseWebAppName)!,
+ name: webApp.displayName ?? webApp.appId,
+ id: webApp.appId,
};
}
@@ -52,6 +48,7 @@ async function getOrCreateWebApp(
try {
const app = await createWebApp(projectId, { displayName: webAppName });
+ logSuccess(`Created a new Firebase web app named "${webAppName}"`);
return { name: app.displayName, id: app.appId };
} catch (e) {
if (isQuotaError(e)) {
diff --git a/src/apphosting/index.ts b/src/apphosting/index.ts
index c964ac1bf37..fcccea66158 100644
--- a/src/apphosting/index.ts
+++ b/src/apphosting/index.ts
@@ -105,9 +105,7 @@ export async function doSetup(
});
const webApp = await webApps.getOrCreateWebApp(projectId, webAppName, backendId);
- if (webApp) {
- logSuccess(`Created a new Firebase web app named "${webApp.name}"`);
- } else {
+ if (!webApp) {
logWarning(`Firebase web app not set`);
}
diff --git a/src/commands/apphosting-backends-create.ts b/src/commands/apphosting-backends-create.ts
index 146f5dd20fc..d855d76e535 100644
--- a/src/commands/apphosting-backends-create.ts
+++ b/src/commands/apphosting-backends-create.ts
@@ -10,8 +10,8 @@ import { requireTosAcceptance } from "../requireTosAcceptance";
export const command = new Command("apphosting:backends:create")
.description("create a Firebase App Hosting backend")
.option(
- "-a, --app ",
- "specify an existing Firebase web app to associate your App Hosting backend with",
+ "-a, --app ",
+ "specify an existing Firebase web app's ID to associate your App Hosting backend with",
)
.option("-l, --location ", "specify the location of the backend", "")
.option(
@@ -24,13 +24,13 @@ export const command = new Command("apphosting:backends:create")
.before(requireTosAcceptance(APPHOSTING_TOS_ID))
.action(async (options: Options) => {
const projectId = needProjectId(options);
- const webApp = options.app;
+ const webAppId = options.app;
const location = options.location;
const serviceAccount = options.serviceAccount;
await doSetup(
projectId,
- webApp as string | null,
+ webAppId as string | null,
location as string | null,
serviceAccount as string | null,
);
From d5a720f06951052c02ca69bce313307b4fc29e89 Mon Sep 17 00:00:00 2001
From: Remi Rousselet
Date: Wed, 8 May 2024 21:35:28 +0200
Subject: [PATCH 09/40] Fix first rendering of the result view (#7126)
Co-authored-by: Harold Shen
---
firebase-vscode/common/messaging/protocol.ts | 3 +
firebase-vscode/src/data-connect/execution.ts | 61 ++++++++++++-------
.../DataConnectExecutionResultsApp.tsx | 48 +++++++--------
3 files changed, 65 insertions(+), 47 deletions(-)
diff --git a/firebase-vscode/common/messaging/protocol.ts b/firebase-vscode/common/messaging/protocol.ts
index 9373788115d..837e81db46b 100644
--- a/firebase-vscode/common/messaging/protocol.ts
+++ b/firebase-vscode/common/messaging/protocol.ts
@@ -110,6 +110,9 @@ export interface WebviewToExtensionParamsMap {
/** Deploy all connectors/services to production */
"fdc.deploy-all": void;
+
+ // Initialize "result" tab.
+ getDataConnectResults: void;
}
export interface DataConnectResults {
diff --git a/firebase-vscode/src/data-connect/execution.ts b/firebase-vscode/src/data-connect/execution.ts
index 44a973cd08e..f8db0770be3 100644
--- a/firebase-vscode/src/data-connect/execution.ts
+++ b/firebase-vscode/src/data-connect/execution.ts
@@ -28,45 +28,56 @@ export function registerExecution(
context: ExtensionContext,
broker: ExtensionBrokerImpl,
dataConnectService: DataConnectService,
- emulatorsController: EmulatorsController
+ emulatorsController: EmulatorsController,
): Disposable {
const treeDataProvider = new ExecutionHistoryTreeDataProvider();
const executionHistoryTreeView = vscode.window.createTreeView(
"data-connect-execution-history",
{
treeDataProvider,
- }
+ },
);
// Select the corresponding tree-item when the selected-execution-id updates
- effect(() => {
+ const sub1 = effect(() => {
const id = selectedExecutionId.value;
const selectedItem = treeDataProvider.executionItems.find(
- ({ item }) => item.executionId === id
+ ({ item }) => item.executionId === id,
);
executionHistoryTreeView.reveal(selectedItem, { select: true });
});
+ function notifyDataConnectResults(item: ExecutionItem) {
+ broker.send("notifyDataConnectResults", {
+ args: item.args ?? "{}",
+ query: print(item.operation),
+ results:
+ item.results instanceof Error
+ ? toSerializedError(item.results)
+ : item.results,
+ displayName: item.operation.operation,
+ });
+ }
+
// Listen for changes to the selected-execution item
- effect(() => {
+ const sub2 = effect(() => {
const item = selectedExecution.value;
if (item) {
- broker.send("notifyDataConnectResults", {
- args: item.args ?? "{}",
- query: print(item.operation),
- results:
- item.results instanceof Error
- ? toSerializedError(item.results)
- : item.results,
- displayName: item.operation.operation,
- });
+ notifyDataConnectResults(item);
+ }
+ });
+
+ const sub3 = broker.on("getDataConnectResults", () => {
+ const item = selectedExecution.value;
+ if (item) {
+ notifyDataConnectResults(item);
}
});
async function executeOperation(
ast: OperationDefinitionNode,
{ document, documentPath, position }: OperationLocation,
- instance: InstanceType
+ instance: InstanceType,
) {
const configs = vscode.workspace.getConfiguration("firebase.dataConnect");
const alwaysExecuteMutationsInProduction =
@@ -84,7 +95,7 @@ export function registerExecution(
"Do you wish to start it?",
{ modal: true },
yes,
- always
+ always,
);
// If the user selects "always", we update User settings.
@@ -109,7 +120,7 @@ export function registerExecution(
"You are about to perform a mutation in production environment. Are you sure?",
{ modal: true },
yes,
- always
+ always,
);
if (result !== always && result !== yes) {
@@ -121,7 +132,7 @@ export function registerExecution(
configs.update(
alwaysExecuteMutationsInProduction,
true,
- ConfigurationTarget.Global
+ ConfigurationTarget.Global,
);
}
}
@@ -179,12 +190,16 @@ export function registerExecution(
}
}
- broker.on(
+ const sub4 = broker.on(
"definedDataConnectArgs",
- (value) => (executionArgsJSON.value = value)
+ (value) => (executionArgsJSON.value = value),
);
return Disposable.from(
+ { dispose: sub1 },
+ { dispose: sub2 },
+ { dispose: sub3 },
+ { dispose: sub4 },
registerWebview({
name: "data-connect-execution-configuration",
context,
@@ -198,13 +213,13 @@ export function registerExecution(
executionHistoryTreeView,
vscode.commands.registerCommand(
"firebase.dataConnect.executeOperation",
- executeOperation
+ executeOperation,
),
vscode.commands.registerCommand(
"firebase.dataConnect.selectExecutionResultToShow",
(executionId) => {
selectExecutionId(executionId);
- }
- )
+ },
+ ),
);
}
diff --git a/firebase-vscode/webviews/DataConnectExecutionResultsApp.tsx b/firebase-vscode/webviews/DataConnectExecutionResultsApp.tsx
index 72eac34ad3f..2b5bd7424c3 100644
--- a/firebase-vscode/webviews/DataConnectExecutionResultsApp.tsx
+++ b/firebase-vscode/webviews/DataConnectExecutionResultsApp.tsx
@@ -1,8 +1,7 @@
-import React, { useEffect, useState } from "react";
-import { broker } from "./globals/html-broker";
+import React from "react";
+import { useBroker } from "./globals/html-broker";
import { Label } from "./components/ui/Text";
import style from "./data-connect-execution-results.entry.scss";
-import { DataConnectResults } from "../common/messaging/protocol";
import { SerializedError } from "../common/error";
import { ExecutionResult, GraphQLError } from "graphql";
import { isExecutionResult } from "../common/graphql";
@@ -11,16 +10,15 @@ import { isExecutionResult } from "../common/graphql";
style;
export function DataConnectExecutionResultsApp() {
- const [dataConnectResults, setResults] = useState<
- DataConnectResults | undefined
- >(undefined);
+ const dataConnectResults = useBroker("notifyDataConnectResults", {
+ // Forcibly read the current execution results when the component mounts.
+ // This handles cases where the user navigates to the results view after
+ // an execution result has already been set.
+ initialRequest: "getDataConnectResults",
+ });
const results: ExecutionResult | SerializedError | undefined =
dataConnectResults?.results;
- useEffect(() => {
- broker.on("notifyDataConnectResults", setResults);
- }, []);
-
if (!dataConnectResults || !results) {
return null;
}
@@ -87,21 +85,23 @@ export function DataConnectExecutionResultsApp() {
*/
function InternalErrorView({ error }: { error: SerializedError }) {
return (
-
+ <>
- {
- // Stacktraces usually already include the message, so we only
- // display the message if there is no stacktrace.
- error.stack ? : error.message
- }
- {error.cause && (
- <>
-
-
Cause:
-
- >
- )}
-
+
+ {
+ // Stacktraces usually already include the message, so we only
+ // display the message if there is no stacktrace.
+ error.stack ? : error.message
+ }
+ {error.cause && (
+ <>
+
+
Cause:
+
+ >
+ )}
+
+ >
);
}
From 043f07ae573e2617343f417b8df604c518eff061 Mon Sep 17 00:00:00 2001
From: Remi Rousselet
Date: Wed, 8 May 2024 22:08:56 +0200
Subject: [PATCH 10/40] Add read-data lense (#7125)
Co-authored-by: Harold Shen
---
.../src/data-connect/ad-hoc-mutations.ts | 121 +++++++++++++++---
.../src/data-connect/code-lens-provider.ts | 9 ++
.../src/data-connect/file-utils.ts | 27 ++++
firebase-vscode/src/data-connect/index.ts | 2 +-
4 files changed, 142 insertions(+), 17 deletions(-)
diff --git a/firebase-vscode/src/data-connect/ad-hoc-mutations.ts b/firebase-vscode/src/data-connect/ad-hoc-mutations.ts
index dcb333590fc..383ac1dbaa5 100644
--- a/firebase-vscode/src/data-connect/ad-hoc-mutations.ts
+++ b/firebase-vscode/src/data-connect/ad-hoc-mutations.ts
@@ -1,13 +1,8 @@
-import vscode, { Disposable, ExtensionContext } from "vscode";
-import { ExtensionBrokerImpl } from "../extension-broker";
-import { ObjectTypeDefinitionNode } from "graphql";
-import { checkIfFileExists } from "./file-utils";
-
-export function registerAdHoc(
- context: ExtensionContext,
- broker: ExtensionBrokerImpl
-): Disposable {
- const pathSuffix = "_insert.gql";
+import vscode, { Disposable } from "vscode";
+import { DocumentNode, Kind, ObjectTypeDefinitionNode } from "graphql";
+import { checkIfFileExists, upsertFile } from "./file-utils";
+
+export function registerAdHoc(): Disposable {
const defaultScalarValues = {
Any: "{}",
AuthUID: '""',
@@ -25,15 +20,105 @@ export function registerAdHoc(
function isDataConnectScalarType(fieldType: string): boolean {
return fieldType in defaultScalarValues;
}
+
/**
* Creates a playground file with an ad-hoc mutation
* File will be created (unsaved) in operations/ folder, with an auto-generated named based on the schema type
* Mutation will be generated with all
* */
- async function schemaAddData(
+ async function schemaReadData(
+ document: DocumentNode,
ast: ObjectTypeDefinitionNode,
- { documentPath, position }
) {
+ // TODO(rrousselGit) - this is a temporary solution due to the lack of a "schema".
+ // As such, we hardcoded the list of allowed primitives.
+ // We should ideally refactor this to allow any scalar type.
+ const primitiveTypes = new Set([
+ "String",
+ "Int",
+ "Int64",
+ "Boolean",
+ "Date",
+ "Timestamp",
+ "Float",
+ "Any",
+ ]);
+
+ const basePath = vscode.workspace.rootPath + "/dataconnect/";
+ const filePath = vscode.Uri.file(`${basePath}${ast.name.value}_read.gql`);
+
+ // Recursively build a query for the object type.
+ // Returns undefined if the query is empty.
+ function buildRecursiveObjectQuery(
+ ast: ObjectTypeDefinitionNode,
+ level: number = 1,
+ ): string | undefined {
+ const indent = " ".repeat(level);
+
+ // Whether the query is non-empty. Used to determine whether to return undefined.
+ var hasField = false;
+ let query = "{\n";
+ for (const field of ast.fields) {
+ // We unwrap NonNullType to obtain the actual type
+ let fieldType = field.type;
+ if (fieldType.kind === Kind.NON_NULL_TYPE) {
+ fieldType = fieldType.type;
+ }
+
+ // Deference, for the sake of enabling TS to upcast to NamedType later
+ const targetType = fieldType;
+ if (targetType.kind === Kind.NAMED_TYPE) {
+ // Check if the type is a primitive type, such that no recursion is needed.
+ if (primitiveTypes.has(targetType.name.value)) {
+ query += ` ${indent}${field.name.value}\n`;
+ hasField = true;
+ continue;
+ }
+
+ // Check relational types.
+ // Since we lack a schema, we can only build queries for types that are defined in the same document.
+ const targetTypeDefinition = document.definitions.find(
+ (def) =>
+ def.kind === Kind.OBJECT_TYPE_DEFINITION &&
+ def.name.value === targetType.name.value,
+ ) as ObjectTypeDefinitionNode;
+
+ if (targetTypeDefinition) {
+ const subQuery = buildRecursiveObjectQuery(
+ targetTypeDefinition,
+ level + 1,
+ );
+ if (!subQuery) {
+ continue;
+ }
+ query += ` ${indent}${field.name.value} ${subQuery}\n`;
+ hasField = true;
+ }
+ }
+ }
+
+ query += `${indent}}`;
+ return query;
+ }
+
+ await upsertFile(filePath, () => {
+ const queryName = `${ast.name.value.charAt(0).toLowerCase()}${ast.name.value.slice(1)}s`;
+
+ return `
+# This is a file for you to write an un-named queries.
+# Only one un-named query is allowed per file.
+query {
+ ${queryName}${buildRecursiveObjectQuery(ast)!}
+}`;
+ });
+ }
+
+ /**
+ * Creates a playground file with an ad-hoc mutation
+ * File will be created (unsaved) in operations/ folder, with an auto-generated named based on the schema type
+ * Mutation will be generated with all
+ * */
+ async function schemaAddData(ast: ObjectTypeDefinitionNode) {
// generate content for the file
const preamble =
"# This is a file for you to write an un-named mutation. \n# Only one un-named mutation is allowed per file.";
@@ -41,7 +126,7 @@ export function registerAdHoc(
const content = [preamble, adhocMutation].join("\n");
const basePath = vscode.workspace.rootPath + "/dataconnect/";
- const filePath = vscode.Uri.file(basePath + ast.name.value + pathSuffix);
+ const filePath = vscode.Uri.file(`${basePath}${ast.name.value}_insert.gql`);
const doesFileExist = await checkIfFileExists(filePath);
if (!doesFileExist) {
@@ -86,7 +171,7 @@ export function registerAdHoc(
defaultValue = '""';
}
mutation.push(
- `${fieldSpacing}${fieldName}: ${defaultValue} # ${fieldTypeName}`
+ `${fieldSpacing}${fieldName}: ${defaultValue} # ${fieldTypeName}`,
); // field name + temp value + comment
}
mutation.push(`${functionSpacing}})`, "}"); // closing braces/paren
@@ -96,7 +181,11 @@ export function registerAdHoc(
return Disposable.from(
vscode.commands.registerCommand(
"firebase.dataConnect.schemaAddData",
- schemaAddData
- )
+ schemaAddData,
+ ),
+ vscode.commands.registerCommand(
+ "firebase.dataConnect.schemaReadData",
+ schemaReadData,
+ ),
);
}
diff --git a/firebase-vscode/src/data-connect/code-lens-provider.ts b/firebase-vscode/src/data-connect/code-lens-provider.ts
index 894295990bb..917818120f2 100644
--- a/firebase-vscode/src/data-connect/code-lens-provider.ts
+++ b/firebase-vscode/src/data-connect/code-lens-provider.ts
@@ -154,6 +154,15 @@ export class SchemaCodeLensProvider extends ComputedCodeLensProvider {
arguments: [x, schemaLocation],
})
);
+
+ codeLenses.push(
+ new vscode.CodeLens(range, {
+ title: `$(database) Read data`,
+ command: "firebase.dataConnect.schemaReadData",
+ tooltip: "Generate a query to read data of this type",
+ arguments: [documentNode, x],
+ })
+ );
}
}
diff --git a/firebase-vscode/src/data-connect/file-utils.ts b/firebase-vscode/src/data-connect/file-utils.ts
index 62e25cf7c35..386873010da 100644
--- a/firebase-vscode/src/data-connect/file-utils.ts
+++ b/firebase-vscode/src/data-connect/file-utils.ts
@@ -1,5 +1,6 @@
import vscode, { Uri } from "vscode";
import path from "path";
+
export async function checkIfFileExists(file: Uri) {
try {
await vscode.workspace.fs.stat(file);
@@ -13,3 +14,29 @@ export function isPathInside(childPath: string, parentPath: string): boolean {
const relative = path.relative(parentPath, childPath);
return !relative.startsWith("..") && !path.isAbsolute(relative);
}
+
+/** Opens a file in the editor. If the file is missing, opens an untitled file
+ * with the content provided by the `content` function.
+ */
+export async function upsertFile(
+ uri: vscode.Uri,
+ content: () => string | string,
+): Promise {
+ const doesFileExist = await checkIfFileExists(uri);
+
+ if (!doesFileExist) {
+ const doc = await vscode.workspace.openTextDocument(
+ uri.with({ scheme: "untitled" }),
+ );
+ const editor = await vscode.window.showTextDocument(doc);
+
+ await editor.edit((edit) =>
+ edit.insert(new vscode.Position(0, 0), content()),
+ );
+ return;
+ }
+
+ // Opens existing text document
+ const doc = await vscode.workspace.openTextDocument(uri);
+ await vscode.window.showTextDocument(doc);
+}
diff --git a/firebase-vscode/src/data-connect/index.ts b/firebase-vscode/src/data-connect/index.ts
index 4651fb553c9..fd2da913a0f 100644
--- a/firebase-vscode/src/data-connect/index.ts
+++ b/firebase-vscode/src/data-connect/index.ts
@@ -209,7 +209,7 @@ export function registerFdc(
registerExecution(context, broker, fdcService, emulatorController),
registerExplorer(context, broker, fdcService),
registerFirebaseDataConnectView(context, broker, emulatorController),
- registerAdHoc(context, broker),
+ registerAdHoc(),
registerConnectors(context, broker, fdcService),
registerFdcDeploy(broker),
operationCodeLensProvider,
From f42c02c33fe39b477eb75c4503dab41e6a7b7f97 Mon Sep 17 00:00:00 2001
From: joehan
Date: Wed, 8 May 2024 16:24:13 -0400
Subject: [PATCH 11/40] Handle invalid connector errors (#7119)
* Handle invalid connector errors
* clean up
* No longer tie to v1main errors
* Major refactor of behavior
* Also handle unconnected dbs
* PR fixes
---
src/commands/dataconnect-sql-migrate.ts | 9 +-
src/dataconnect/client.ts | 19 +-
src/dataconnect/errors.ts | 38 ++++
src/dataconnect/schemaMigration.ts | 225 +++++++++++++++++-------
4 files changed, 213 insertions(+), 78 deletions(-)
create mode 100644 src/dataconnect/errors.ts
diff --git a/src/commands/dataconnect-sql-migrate.ts b/src/commands/dataconnect-sql-migrate.ts
index f4d12fa3181..43df495bd72 100644
--- a/src/commands/dataconnect-sql-migrate.ts
+++ b/src/commands/dataconnect-sql-migrate.ts
@@ -2,12 +2,12 @@ import { Command } from "../command";
import { Options } from "../options";
import { needProjectId } from "../projectUtils";
import { pickService } from "../dataconnect/fileUtils";
-import { logger } from "../logger";
import { FirebaseError } from "../error";
import { migrateSchema } from "../dataconnect/schemaMigration";
import { requireAuth } from "../requireAuth";
import { requirePermissions } from "../requirePermissions";
import { ensureApis } from "../dataconnect/ensureApis";
+import { logLabeledSuccess } from "../utils";
export const command = new Command("dataconnect:sql:migrate [serviceId]")
.description("migrates your CloudSQL database's schema to match your local DataConnect schema")
@@ -38,11 +38,12 @@ export const command = new Command("dataconnect:sql:migrate [serviceId]")
validateOnly: true,
});
if (diffs.length) {
- logger.info(
- `Schema sucessfully migrated! Run 'firebase deploy' to deploy your new schema to your Data Connect service.`,
+ logLabeledSuccess(
+ "dataconnect",
+ `Database schema sucessfully migrated! Run 'firebase deploy' to deploy your new schema to your Data Connect service.`,
);
} else {
- logger.info("Schema was already up to date!");
+ logLabeledSuccess("dataconnect", "Database schema is already up to date!");
}
return { projectId, serviceId, diffs };
});
diff --git a/src/dataconnect/client.ts b/src/dataconnect/client.ts
index fb83e382910..1052bd5319c 100644
--- a/src/dataconnect/client.ts
+++ b/src/dataconnect/client.ts
@@ -27,14 +27,17 @@ export async function listLocations(projectId: string): Promise {
export async function listAllServices(projectId: string): Promise {
const locations = await listLocations(projectId);
let services: types.Service[] = [];
- for (const l of locations) {
- try {
- const locationServices = await listServices(projectId, l);
- services = services.concat(locationServices);
- } catch (err) {
- logger.debug(`Unable to listServices in ${l}: ${err}`);
- }
- }
+ await Promise.all(
+ locations.map(async (l) => {
+ try {
+ const locationServices = await listServices(projectId, l);
+ services = services.concat(locationServices);
+ } catch (err) {
+ logger.debug(`Unable to listServices in ${l}: ${err}`);
+ }
+ }),
+ );
+
return services;
}
diff --git a/src/dataconnect/errors.ts b/src/dataconnect/errors.ts
new file mode 100644
index 00000000000..5f6f9666580
--- /dev/null
+++ b/src/dataconnect/errors.ts
@@ -0,0 +1,38 @@
+import { IncompatibleSqlSchemaError } from "./types";
+
+const INCOMPATIBLE_SCHEMA_ERROR_TYPESTRING = "IncompatibleSqlSchemaError";
+const PRECONDITION_ERROR_TYPESTRING = "type.googleapis.com/google.rpc.PreconditionFailure";
+const INCOMPATIBLE_CONNECTOR_TYPE = "INCOMPATIBLE_CONNECTOR";
+
+export function getIncompatibleSchemaError(err: any): IncompatibleSqlSchemaError | undefined {
+ const original = err.context?.body?.error || err.orignal;
+ if (!original) {
+ // If we can't get the original, rethrow so we don't cover up the original error.
+ throw err;
+ }
+ const details: any[] = original.details;
+ const incompatibles = details.filter((d) =>
+ d["@type"]?.includes(INCOMPATIBLE_SCHEMA_ERROR_TYPESTRING),
+ );
+ // Should never get multiple incompatible schema errors
+ return incompatibles[0];
+}
+
+// Note - the backend just includes file name, not the name of the connector resource in the GQLerror extensions.
+// so we don't use this yet. Ideally, we'd just include connector name in the extensions.
+export function getInvalidConnectors(err: any): string[] {
+ const invalidConns: string[] = [];
+ const original = err.context?.body?.error || err?.orignal;
+ const details: any[] = original?.details;
+ const preconditionErrs = details?.filter((d) =>
+ d["@type"]?.includes(PRECONDITION_ERROR_TYPESTRING),
+ );
+ for (const preconditionErr of preconditionErrs) {
+ const incompatibleConnViolation = preconditionErr?.violations?.filter(
+ (v: { type: string }) => v.type === INCOMPATIBLE_CONNECTOR_TYPE,
+ );
+ const newConns = incompatibleConnViolation?.map((i: { subject: string }) => i.subject) ?? [];
+ invalidConns.push(...newConns);
+ }
+ return invalidConns;
+}
diff --git a/src/dataconnect/schemaMigration.ts b/src/dataconnect/schemaMigration.ts
index 4380d5c48a7..0f444547b7b 100644
--- a/src/dataconnect/schemaMigration.ts
+++ b/src/dataconnect/schemaMigration.ts
@@ -2,38 +2,34 @@ import * as clc from "colorette";
import { format } from "sql-formatter";
import { IncompatibleSqlSchemaError, Diff, SCHEMA_ID } from "./types";
-import { getSchema, upsertSchema } from "./client";
+import { getSchema, upsertSchema, deleteConnector } from "./client";
import { execute, firebaseowner, setupIAMUser } from "../gcp/cloudsql/connect";
-import { promptOnce } from "../prompt";
+import { promptOnce, confirm } from "../prompt";
import { logger } from "../logger";
import { Schema } from "./types";
import { Options } from "../options";
import { FirebaseError } from "../error";
import { needProjectId } from "../projectUtils";
-import { logLabeledWarning } from "../utils";
-
-const IMCOMPATIBLE_SCHEMA_ERROR_TYPESTRING =
- "type.googleapis.com/google.firebase.dataconnect.v1main.IncompatibleSqlSchemaError";
+import { logLabeledWarning, logLabeledSuccess } from "../utils";
+import * as errors from "./errors";
export async function diffSchema(schema: Schema): Promise {
- const dbName = schema.primaryDatasource.postgresql?.database;
- const instanceName = schema.primaryDatasource.postgresql?.cloudSql.instance;
- if (!instanceName || !dbName) {
- throw new FirebaseError(`tried to diff schema but ${instanceName} was undefined`);
- }
+ const { serviceName, instanceName, databaseId } = getIdentifiers(schema);
+ await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId);
try {
- const serviceName = schema.name.replace(`/schemas/${SCHEMA_ID}`, "");
- await ensureServiceIsConnectedToCloudSql(serviceName);
await upsertSchema(schema, /** validateOnly=*/ true);
} catch (err: any) {
- const incompatible = getIncompatibleSchemaError(err);
+ const invalidConnectors = errors.getInvalidConnectors(err);
+ if (invalidConnectors.length) {
+ displayInvalidConnectors(invalidConnectors);
+ }
+ const incompatible = errors.getIncompatibleSchemaError(err);
if (incompatible) {
displaySchemaChanges(incompatible);
return incompatible.diffs;
}
- throw err;
}
- logger.debug(`Schema was up to date for ${instanceName}:${dbName}`);
+ logLabeledSuccess("dataconnect", `Database schema is up to date.`);
return [];
}
@@ -43,41 +39,98 @@ export async function migrateSchema(args: {
allowNonInteractiveMigration: boolean;
validateOnly: boolean;
}): Promise {
- const { schema, validateOnly } = args;
+ const { options, schema, allowNonInteractiveMigration, validateOnly } = args;
- const databaseId = schema.primaryDatasource.postgresql?.database;
- if (!databaseId) {
- throw new FirebaseError(
- "Schema is missing primaryDatasource.postgresql?.database, cannot migrate",
- );
- }
- const instanceId = schema.primaryDatasource.postgresql?.cloudSql.instance.split("/").pop();
- if (!instanceId) {
- throw new FirebaseError(`tried to migrate schema but ${instanceId} was undefined`);
- }
+ const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema);
+ await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId);
try {
- const serviceName = schema.name.replace(`/schemas/${SCHEMA_ID}`, "");
- await ensureServiceIsConnectedToCloudSql(serviceName);
await upsertSchema(schema, validateOnly);
logger.debug(`Database schema was up to date for ${instanceId}:${databaseId}`);
- return [];
} catch (err: any) {
- const incompatible = getIncompatibleSchemaError(err);
- if (!incompatible) {
+ const incompatible = errors.getIncompatibleSchemaError(err);
+ const invalidConnectors = errors.getInvalidConnectors(err);
+ if (!incompatible && !invalidConnectors.length) {
// If we got a different type of error, throw it
throw err;
}
- // Try to migrate schema
- const diffs = await handleIncompatibleSchemaError({
- ...args,
- incompatibleSchemaError: incompatible,
- instanceId,
- databaseId,
- });
+ const shouldDeleteInvalidConnectors = await promptForInvalidConnectorError(
+ options,
+ invalidConnectors,
+ validateOnly,
+ );
+ if (!shouldDeleteInvalidConnectors && invalidConnectors.length) {
+ const cmd = suggestedCommand(serviceName, invalidConnectors);
+ throw new FirebaseError(
+ `Command aborted. Try deploying compatible connectors first with ${clc.bold(cmd)}`,
+ );
+ }
+ const migrationMode = incompatible
+ ? await promptForSchemaMigration(
+ options,
+ databaseId,
+ incompatible,
+ allowNonInteractiveMigration,
+ )
+ : "none";
+ // First, error out if we aren't making all changes
+ if (migrationMode === "none" && incompatible) {
+ throw new FirebaseError("Command aborted.");
+ }
+
+ let diffs: Diff[] = [];
+ if (incompatible) {
+ diffs = await handleIncompatibleSchemaError({
+ options,
+ databaseId,
+ instanceId,
+ incompatibleSchemaError: incompatible,
+ choice: migrationMode,
+ });
+ }
+
+ if (invalidConnectors.length) {
+ await deleteInvalidConnectors(invalidConnectors);
+ }
// Then, try to upsert schema again. If there still is an error, just throw it now
await upsertSchema(schema, validateOnly);
return diffs;
}
+ return [];
+}
+
+function getIdentifiers(schema: Schema): {
+ instanceName: string;
+ instanceId: string;
+ databaseId: string;
+ serviceName: string;
+} {
+ const databaseId = schema.primaryDatasource.postgresql?.database;
+ if (!databaseId) {
+ throw new FirebaseError(
+ "Schema is missing primaryDatasource.postgresql?.database, cannot migrate",
+ );
+ }
+ const instanceName = schema.primaryDatasource.postgresql?.cloudSql.instance;
+ if (!instanceName) {
+ throw new FirebaseError(
+ "tried to migrate schema but instance name was not provided in dataconnect.yaml",
+ );
+ }
+ const instanceId = instanceName.split("/").pop()!;
+ const serviceName = schema.name.replace(`/schemas/${SCHEMA_ID}`, "");
+ return {
+ databaseId,
+ instanceId,
+ instanceName,
+ serviceName,
+ };
+}
+
+function suggestedCommand(serviceName: string, invalidConnectorNames: string[]): string {
+ const serviceId = serviceName.split("/")[5];
+ const connectorIds = invalidConnectorNames.map((i) => i.split("/")[7]);
+ const onlys = connectorIds.map((c) => `dataconnect:${serviceId}:${c}`).join(",");
+ return `firebase deploy --only ${onlys}`;
}
async function handleIncompatibleSchemaError(args: {
@@ -85,18 +138,12 @@ async function handleIncompatibleSchemaError(args: {
options: Options;
instanceId: string;
databaseId: string;
- allowNonInteractiveMigration: boolean;
+ choice: "all" | "safe" | "none";
}): Promise {
- const { incompatibleSchemaError, options, instanceId, databaseId, allowNonInteractiveMigration } =
- args;
+ const { incompatibleSchemaError, options, instanceId, databaseId, choice } = args;
const projectId = needProjectId(options);
const iamUser = await setupIAMUser(instanceId, databaseId, options);
- const choice = await promptForSchemaMigration(
- options,
- databaseId,
- incompatibleSchemaError,
- allowNonInteractiveMigration,
- );
+
const commandsToExecute = incompatibleSchemaError.diffs
.filter((d) => {
switch (choice) {
@@ -173,26 +220,84 @@ async function promptForSchemaMigration(
}
}
+async function promptForInvalidConnectorError(
+ options: Options,
+ invalidConnectors: string[],
+ validateOnly: boolean,
+): Promise {
+ if (!invalidConnectors.length) {
+ return false;
+ }
+ displayInvalidConnectors(invalidConnectors);
+ if (validateOnly) {
+ return false;
+ } else if (
+ options.force ||
+ (!options.nonInteractive &&
+ (await confirm({
+ ...options,
+ message: "Would you like to delete and recreate these connectors?",
+ })))
+ ) {
+ return true;
+ }
+ return false;
+}
+
+async function deleteInvalidConnectors(invalidConnectors: string[]): Promise {
+ return Promise.all(invalidConnectors.map(deleteConnector));
+}
+
+function displayInvalidConnectors(invalidConnectors: string[]) {
+ const connectorIds = invalidConnectors.map((i) => i.split("/").pop()).join(", ");
+ logLabeledWarning(
+ "dataconnect",
+ `The schema you are deploying is incompatible with the following existing connectors: ${connectorIds}.`,
+ );
+ logLabeledWarning(
+ "dataconnect",
+ `This is a ${clc.red("breaking")} change and will cause a brief downtime.`,
+ );
+}
+
// If a service has never had a schema with schemaValidation=strict
// (ie when users create a service in console),
// the backend will not have the necesary permissions to check cSQL for differences.
// We fix this by upserting the currently deployed schema with schemaValidation=strict,
-async function ensureServiceIsConnectedToCloudSql(serviceName: string) {
- let currentSchema;
+async function ensureServiceIsConnectedToCloudSql(
+ serviceName: string,
+ instanceId: string,
+ databaseId: string,
+) {
+ let currentSchema: Schema;
try {
currentSchema = await getSchema(serviceName);
} catch (err: any) {
if (err.status === 404) {
- return;
+ // If no schema has been deployed yet, deploy an empty one to get connectivity.
+ currentSchema = {
+ name: `${serviceName}/schema/${SCHEMA_ID}`,
+ source: {
+ files: [],
+ },
+ primaryDatasource: {
+ postgresql: {
+ database: databaseId,
+ cloudSql: {
+ instance: instanceId,
+ },
+ },
+ },
+ };
+ } else {
+ throw err;
}
- throw err;
}
if (
!currentSchema.primaryDatasource.postgresql ||
currentSchema.primaryDatasource.postgresql.schemaValidation === "STRICT"
) {
- // Only want to do this coming from console half deployed state. If the current schema is "STRICT" mode,
- // or if there is not postgres attached, don't try this.
+ // If the current schema is "STRICT" mode, don't do this.
return;
}
currentSchema.primaryDatasource.postgresql.schemaValidation = "STRICT";
@@ -210,15 +315,3 @@ function displaySchemaChanges(error: IncompatibleSqlSchemaError) {
function toString(diff: Diff) {
return `\/** ${diff.destructive ? clc.red("Destructive: ") : ""}${diff.description}*\/\n${format(diff.sql, { language: "postgresql" })}`;
}
-
-function getIncompatibleSchemaError(err: any): IncompatibleSqlSchemaError | undefined {
- const original = err.context?.body.error || err.orignal;
- if (!original) {
- // If we can't get the original, rethrow so we don't cover up the original error.
- throw err;
- }
- const details: any[] = original.details;
- const incompatibles = details.filter((d) => d["@type"] === IMCOMPATIBLE_SCHEMA_ERROR_TYPESTRING);
- // Should never get multiple incompatible schema errors
- return incompatibles[0];
-}
From 89253352f0b0d475c3db14738061733179f83cd6 Mon Sep 17 00:00:00 2001
From: joehan
Date: Wed, 8 May 2024 16:59:29 -0400
Subject: [PATCH 12/40] Adding in data connect too (#7089)
* add initial tos requirement
* Adding in data connect!
* Small fixes
* add tests
* comments
* add initial tos requirement
* Adding in data connect!
* Small fixes
* add tests
* comments
* Merging
* revert
---------
Co-authored-by: Tony Huang
---
src/deploy/dataconnect/prepare.ts | 3 +++
src/gcp/firedata.ts | 5 +++--
src/requireTosAcceptance.ts | 11 +++++++++--
src/test/requireTosAcceptance.spec.ts | 4 ++++
4 files changed, 19 insertions(+), 4 deletions(-)
diff --git a/src/deploy/dataconnect/prepare.ts b/src/deploy/dataconnect/prepare.ts
index 34518b925dd..e7390c189b0 100644
--- a/src/deploy/dataconnect/prepare.ts
+++ b/src/deploy/dataconnect/prepare.ts
@@ -9,6 +9,8 @@ import { needProjectId } from "../../projectUtils";
import { getResourceFilters } from "../../dataconnect/filters";
import { build } from "../../dataconnect/build";
import { ensureApis } from "../../dataconnect/ensureApis";
+import { requireTosAcceptance } from "../../requireTosAcceptance";
+import { DATA_CONNECT_TOS_ID } from "../../gcp/firedata";
/**
* Prepares for a Firebase DataConnect deployment by loading schemas and connectors from file.
@@ -18,6 +20,7 @@ import { ensureApis } from "../../dataconnect/ensureApis";
export default async function (context: any, options: Options): Promise {
const projectId = needProjectId(options);
await ensureApis(projectId);
+ await requireTosAcceptance(DATA_CONNECT_TOS_ID)(options);
const serviceCfgs = readFirebaseJson(options.config);
utils.logLabeledBullet("dataconnect", `Preparing to deploy`);
const filters = getResourceFilters(options);
diff --git a/src/gcp/firedata.ts b/src/gcp/firedata.ts
index a226cc73cd0..f395708c68c 100644
--- a/src/gcp/firedata.ts
+++ b/src/gcp/firedata.ts
@@ -6,8 +6,9 @@ const client = new Client({ urlPrefix: firedataOrigin(), auth: true, apiVersion:
export const APPHOSTING_TOS_ID = "APP_HOSTING_TOS";
export const APP_CHECK_TOS_ID = "APP_CHECK";
+export const DATA_CONNECT_TOS_ID = "FIREBASE_DATA_CONNECT";
-export type TosId = typeof APPHOSTING_TOS_ID | typeof APP_CHECK_TOS_ID;
+export type TosId = typeof APPHOSTING_TOS_ID | typeof APP_CHECK_TOS_ID | typeof DATA_CONNECT_TOS_ID;
export type AcceptanceStatus = null | "ACCEPTED" | "TERMS_UPDATED";
@@ -39,7 +40,7 @@ export function getAcceptanceStatus(
): AcceptanceStatus {
const perServiceStatus = response.perServiceStatus.find((tosStatus) => tosStatus.tosId === tosId);
if (perServiceStatus === undefined) {
- throw new FirebaseError(`Missing terms of service status for product: ${tosId}`);
+ throw new FirebaseError(`Missing terms of service status for product: ${tosId}`);
}
return perServiceStatus.serviceStatus.status;
}
diff --git a/src/requireTosAcceptance.ts b/src/requireTosAcceptance.ts
index 210c131ca67..d6a5ef3280c 100644
--- a/src/requireTosAcceptance.ts
+++ b/src/requireTosAcceptance.ts
@@ -1,11 +1,18 @@
import type { Options } from "./options";
import { FirebaseError } from "./error";
-import { APPHOSTING_TOS_ID, TosId, getTosStatus, isProductTosAccepted } from "./gcp/firedata";
+import {
+ APPHOSTING_TOS_ID,
+ DATA_CONNECT_TOS_ID,
+ TosId,
+ getTosStatus,
+ isProductTosAccepted,
+} from "./gcp/firedata";
import { consoleOrigin } from "./api";
const consoleLandingPage = new Map([
[APPHOSTING_TOS_ID, `${consoleOrigin()}/project/_/apphosting`],
+ [DATA_CONNECT_TOS_ID, `${consoleOrigin()}/project/_/dataconnect`],
]);
/**
@@ -18,7 +25,7 @@ const consoleLandingPage = new Map([
*
* Note: When supporting new products, be sure to update `consoleLandingPage` above to avoid surfacing
* generic ToS error messages.
- **/
+ */
export function requireTosAcceptance(tosId: TosId): (options: Options) => Promise {
return () => requireTos(tosId);
}
diff --git a/src/test/requireTosAcceptance.spec.ts b/src/test/requireTosAcceptance.spec.ts
index c36b12c1842..7d805c145b9 100644
--- a/src/test/requireTosAcceptance.spec.ts
+++ b/src/test/requireTosAcceptance.spec.ts
@@ -60,6 +60,8 @@ describe("requireTosAcceptance", () => {
.reply(200, SAMPLE_RESPONSE);
await requireTosAcceptance(APP_CHECK_TOS_ID)(SAMPLE_OPTIONS);
+
+ expect(nock.isDone()).to.be.true;
});
it("should throw error if not accepted", async () => {
@@ -70,5 +72,7 @@ describe("requireTosAcceptance", () => {
await expect(requireTosAcceptance(APPHOSTING_TOS_ID)(SAMPLE_OPTIONS)).to.be.rejectedWith(
"Terms of Service",
);
+
+ expect(nock.isDone()).to.be.true;
});
});
From 55dd20a5a2c52238b83b88a21e6beb5e9faa2281 Mon Sep 17 00:00:00 2001
From: joehan
Date: Wed, 8 May 2024 17:56:50 -0400
Subject: [PATCH 13/40] Missing one very important character (#7138)
---
src/dataconnect/schemaMigration.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/dataconnect/schemaMigration.ts b/src/dataconnect/schemaMigration.ts
index 0f444547b7b..35d6f864982 100644
--- a/src/dataconnect/schemaMigration.ts
+++ b/src/dataconnect/schemaMigration.ts
@@ -276,7 +276,7 @@ async function ensureServiceIsConnectedToCloudSql(
if (err.status === 404) {
// If no schema has been deployed yet, deploy an empty one to get connectivity.
currentSchema = {
- name: `${serviceName}/schema/${SCHEMA_ID}`,
+ name: `${serviceName}/schemas/${SCHEMA_ID}`,
source: {
files: [],
},
From 02fddf7cfe36cd57432fea762f52defb0323f0ec Mon Sep 17 00:00:00 2001
From: Fred Zhang
Date: Wed, 8 May 2024 15:22:52 -0700
Subject: [PATCH 14/40] format GQL error in a way thats friendly to editor
(#7137)
---
src/dataconnect/graphqlError.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/dataconnect/graphqlError.ts b/src/dataconnect/graphqlError.ts
index a9bb83d31ac..feceacaf04b 100644
--- a/src/dataconnect/graphqlError.ts
+++ b/src/dataconnect/graphqlError.ts
@@ -3,8 +3,8 @@ import { GraphqlError } from "./types";
export function prettify(err: GraphqlError): string {
const message = err.message;
let header = err.extensions.file ?? "";
- for (const loc of err.locations) {
- header += `(${loc.line}, ${loc.column})`;
+ if (err.locations) {
+ header += `:${err.locations[0].line}`;
}
return header.length ? `${header}: ${message}` : message;
}
From 0731506c82a725825eb1770339efa313e7e0e9d4 Mon Sep 17 00:00:00 2001
From: Sam
Date: Wed, 8 May 2024 15:48:20 -0700
Subject: [PATCH 15/40] Firebase init updates (#7069)
* Update init
* Run functions config first
* Lint
* PR feedback
* Update genkit onboarding
* Time-gate
* Put behind an experiment
---
src/commands/init.ts | 8 +++
src/experiments.ts | 6 ++
src/init/features/functions/index.ts | 6 ++
.../features/functions/npm-dependencies.ts | 37 ++++--------
src/init/features/genkit.ts | 59 +++++++++++++++++++
src/init/features/index.ts | 1 +
src/init/index.ts | 1 +
src/init/spawn.ts | 22 +++++++
8 files changed, 114 insertions(+), 26 deletions(-)
create mode 100644 src/init/features/genkit.ts
create mode 100644 src/init/spawn.ts
diff --git a/src/commands/init.ts b/src/commands/init.ts
index 832f4ec1b89..b8ac3b5a6bf 100644
--- a/src/commands/init.ts
+++ b/src/commands/init.ts
@@ -73,6 +73,14 @@ const choices = [
},
];
+if (isEnabled("genkit")) {
+ choices.push({
+ value: "genkit",
+ name: "Genkit: Setup a new Genkit project with Firebase",
+ checked: false,
+ });
+}
+
if (isEnabled("apphosting")) {
choices.push({
value: "apphosting",
diff --git a/src/experiments.ts b/src/experiments.ts
index fa52f6eafd6..2d5eb3e75e4 100644
--- a/src/experiments.ts
+++ b/src/experiments.ts
@@ -126,6 +126,12 @@ export const ALL_EXPERIMENTS = experiments({
fullDescription: "Enable Data Connect related features.",
public: false,
},
+
+ genkit: {
+ shortDescription: "Enable Genkit related features.",
+ fullDescription: "Enable Genkit related features.",
+ public: false,
+ },
});
export type ExperimentName = keyof typeof ALL_EXPERIMENTS;
diff --git a/src/init/features/functions/index.ts b/src/init/features/functions/index.ts
index 9694f996760..3ac15033606 100644
--- a/src/init/features/functions/index.ts
+++ b/src/init/features/functions/index.ts
@@ -158,6 +158,11 @@ async function overwriteCodebase(setup: any, config: Config): Promise {
* User dialogue to set up configuration for functions codebase language choice.
*/
async function languageSetup(setup: any, config: Config): Promise {
+ // During genkit setup, always select TypeScript here.
+ if (setup.languageOverride) {
+ return require("./" + setup.languageOverride).setup(setup, config);
+ }
+
const choices = [
{
name: "JavaScript",
@@ -202,5 +207,6 @@ async function languageSetup(setup: any, config: Config): Promise {
cbconfig.ignore = ["venv", ".git", "firebase-debug.log", "firebase-debug.*.log", "*.local"];
break;
}
+ setup.functions.languageChoice = language;
return require("./" + language).setup(setup, config);
}
diff --git a/src/init/features/functions/npm-dependencies.ts b/src/init/features/functions/npm-dependencies.ts
index 078bd012d5b..fb7d7246123 100644
--- a/src/init/features/functions/npm-dependencies.ts
+++ b/src/init/features/functions/npm-dependencies.ts
@@ -1,37 +1,22 @@
-import * as spawn from "cross-spawn";
-
import { logger } from "../../../logger";
import { prompt } from "../../../prompt";
+import { wrapSpawn } from "../../spawn";
-export function askInstallDependencies(setup: any, config: any): Promise {
- return prompt(setup, [
+export async function askInstallDependencies(setup: any, config: any): Promise {
+ await prompt(setup, [
{
name: "npm",
type: "confirm",
message: "Do you want to install dependencies with npm now?",
default: true,
},
- ]).then(() => {
- if (setup.npm) {
- return new Promise((resolve) => {
- const installer = spawn("npm", ["install"], {
- cwd: config.projectDir + `/${setup.source}`,
- stdio: "inherit",
- });
-
- installer.on("error", (err: any) => {
- logger.debug(err.stack);
- });
-
- installer.on("close", (code) => {
- if (code === 0) {
- return resolve();
- }
- logger.info();
- logger.error("NPM install failed, continuing with Firebase initialization...");
- return resolve();
- });
- });
+ ]);
+ if (setup.npm) {
+ try {
+ await wrapSpawn("npm", ["install"], config.projectDir + `/${setup.source}`);
+ } catch (e) {
+ logger.info();
+ logger.error("NPM install failed, continuing with Firebase initialization...");
}
- });
+ }
}
diff --git a/src/init/features/genkit.ts b/src/init/features/genkit.ts
new file mode 100644
index 00000000000..a7c5148a89b
--- /dev/null
+++ b/src/init/features/genkit.ts
@@ -0,0 +1,59 @@
+import { logger } from "../../logger";
+import { doSetup as functionsSetup } from "./functions";
+import { Options } from "../../options";
+import { Config } from "../../config";
+import { promptOnce } from "../../prompt";
+import { wrapSpawn } from "../spawn";
+
+/**
+ * doSetup is the entry point for setting up the genkit suite.
+ */
+export async function doSetup(setup: any, config: Config, options: Options): Promise {
+ if (setup.functions?.languageChoice !== "typescript") {
+ const continueFunctions = await promptOnce({
+ type: "confirm",
+ message:
+ "Genkit's Firebase integration uses Cloud Functions for Firebase with TypeScript. Initialize Functions to continue?",
+ default: true,
+ });
+ if (!continueFunctions) {
+ logger.info("Stopped Genkit initialization");
+ return;
+ }
+
+ // Functions with genkit should always be typescript
+ setup.languageOverride = "typescript";
+ await functionsSetup(setup, config, options);
+ delete setup.languageOverride;
+ logger.info();
+ }
+
+ const projectDir: string = `${config.projectDir}/${setup.functions.source}`;
+
+ const installType = await promptOnce({
+ type: "list",
+ message: "Install the Genkit CLI globally or locally in this project?",
+ choices: [
+ { name: "Globally", value: "globally" },
+ { name: "Just this project", value: "project" },
+ ],
+ });
+
+ try {
+ logger.info("Installing Genkit CLI");
+ if (installType === "globally") {
+ await wrapSpawn("npm", ["install", "-g", "genkit"], projectDir);
+ await wrapSpawn("genkit", ["init", "-p", "firebase"], projectDir);
+ logger.info("Start the Genkit developer experience by running:");
+ logger.info(` cd ${setup.functions.source} && genkit start`);
+ } else {
+ await wrapSpawn("npm", ["install", "genkit", "--save-dev"], projectDir);
+ await wrapSpawn("npx", ["genkit", "init", "-p", "firebase"], projectDir);
+ logger.info("Start the Genkit developer experience by running:");
+ logger.info(` cd ${setup.functions.source} && npx genkit start`);
+ }
+ } catch (e) {
+ logger.error("Genkit initialization failed...");
+ return;
+ }
+}
diff --git a/src/init/features/index.ts b/src/init/features/index.ts
index 5e796ac288b..a2f4fc77246 100644
--- a/src/init/features/index.ts
+++ b/src/init/features/index.ts
@@ -12,3 +12,4 @@ export { doSetup as remoteconfig } from "./remoteconfig";
export { initGitHub as hostingGithub } from "./hosting/github";
export { doSetup as dataconnect } from "./dataconnect";
export { doSetup as apphosting } from "../../apphosting";
+export { doSetup as genkit } from "./genkit";
diff --git a/src/init/index.ts b/src/init/index.ts
index 09c6f54586e..04e3620ac98 100644
--- a/src/init/index.ts
+++ b/src/init/index.ts
@@ -29,6 +29,7 @@ const featureFns = new Map P
["project", features.project], // always runs, sets up .firebaserc
["remoteconfig", features.remoteconfig],
["hosting:github", features.hostingGithub],
+ ["genkit", features.genkit],
]);
export async function init(setup: Setup, config: any, options: any): Promise {
diff --git a/src/init/spawn.ts b/src/init/spawn.ts
new file mode 100644
index 00000000000..d44b0b60efe
--- /dev/null
+++ b/src/init/spawn.ts
@@ -0,0 +1,22 @@
+import * as spawn from "cross-spawn";
+import { logger } from "../logger";
+
+export function wrapSpawn(cmd: string, args: string[], projectDir: string): Promise {
+ return new Promise((resolve, reject) => {
+ const installer = spawn(cmd, args, {
+ cwd: projectDir,
+ stdio: "inherit",
+ });
+
+ installer.on("error", (err: any) => {
+ logger.debug(err.stack);
+ });
+
+ installer.on("close", (code) => {
+ if (code === 0) {
+ return resolve();
+ }
+ return reject();
+ });
+ });
+}
From 41fe55799c562b85ef7c356e99822a8e53e6fa47 Mon Sep 17 00:00:00 2001
From: joehan
Date: Wed, 8 May 2024 19:02:04 -0400
Subject: [PATCH 16/40] A bunch of FDC polish (#7133)
* Lots of data connect polish
* More polishing
* Add file
* spinners
---
scripts/dataconnect-test/tests.ts | 2 +-
...t-list.ts => dataconnect-services-list.ts} | 2 +-
src/commands/index.ts | 3 +-
src/dataconnect/freeTrial.ts | 3 +-
src/dataconnect/provisionCloudSql.ts | 27 ++++---
src/deploy/dataconnect/deploy.ts | 40 +++++-----
src/init/features/dataconnect/index.ts | 74 ++++++++++---------
templates/init/dataconnect/mutations.gql | 32 +++++++-
templates/init/dataconnect/queries.gql | 53 ++++++++++++-
templates/init/dataconnect/schema.gql | 33 ++++++---
10 files changed, 187 insertions(+), 82 deletions(-)
rename src/commands/{dataconnect-list.ts => dataconnect-services-list.ts} (97%)
diff --git a/scripts/dataconnect-test/tests.ts b/scripts/dataconnect-test/tests.ts
index 364e1473102..1a7be99f8a4 100644
--- a/scripts/dataconnect-test/tests.ts
+++ b/scripts/dataconnect-test/tests.ts
@@ -17,7 +17,7 @@ const expected = {
async function list() {
return await cli.exec(
- "dataconnect:list",
+ "dataconnect:services:list",
FIREBASE_PROJECT,
["--json"],
__dirname,
diff --git a/src/commands/dataconnect-list.ts b/src/commands/dataconnect-services-list.ts
similarity index 97%
rename from src/commands/dataconnect-list.ts
rename to src/commands/dataconnect-services-list.ts
index 29e62829a75..59f18a757a5 100644
--- a/src/commands/dataconnect-list.ts
+++ b/src/commands/dataconnect-services-list.ts
@@ -8,7 +8,7 @@ import { requirePermissions } from "../requirePermissions";
import { ensureApis } from "../dataconnect/ensureApis";
const Table = require("cli-table");
-export const command = new Command("dataconnect:list")
+export const command = new Command("dataconnect:services:list")
.description("list all deployed services in your Firebase project")
.before(requirePermissions, [
"dataconnect.services.list",
diff --git a/src/commands/index.ts b/src/commands/index.ts
index 70743eff4ea..be0c6ecda7e 100644
--- a/src/commands/index.ts
+++ b/src/commands/index.ts
@@ -208,7 +208,8 @@ export function load(client: any): any {
if (experiments.isEnabled("dataconnect")) {
client.dataconnect = {};
client.setup.emulators.dataconnect = loadCommand("setup-emulators-dataconnect");
- client.dataconnect.list = loadCommand("dataconnect-list");
+ client.dataconnect.services = {};
+ client.dataconnect.services.list = loadCommand("dataconnect-services-list");
client.dataconnect.sql = {};
client.dataconnect.sql.diff = loadCommand("dataconnect-sql-diff");
client.dataconnect.sql.migrate = loadCommand("dataconnect-sql-migrate");
diff --git a/src/dataconnect/freeTrial.ts b/src/dataconnect/freeTrial.ts
index c5b3528e61b..a01b1d052c6 100644
--- a/src/dataconnect/freeTrial.ts
+++ b/src/dataconnect/freeTrial.ts
@@ -2,8 +2,7 @@ import { listInstances } from "../gcp/cloudsql/cloudsqladmin";
import * as utils from "../utils";
export function freeTrialTermsLink(): string {
- // TODO: Link to the free trial terms here.
- return "";
+ return "https://firebase.google.com/pricing";
}
// Checks whether there is already a free trial instance on a project.
diff --git a/src/dataconnect/provisionCloudSql.ts b/src/dataconnect/provisionCloudSql.ts
index ac7764f1217..18d7a439f6e 100755
--- a/src/dataconnect/provisionCloudSql.ts
+++ b/src/dataconnect/provisionCloudSql.ts
@@ -2,6 +2,7 @@ import * as cloudSqlAdminClient from "../gcp/cloudsql/cloudsqladmin";
import * as utils from "../utils";
import { grantRolesToCloudSqlServiceAccount } from "./checkIam";
import { Instance } from "../gcp/cloudsql/types";
+import { promiseWithSpinner } from "../utils";
const GOOGLE_ML_INTEGRATION_ROLE = "roles/aiplatform.user";
@@ -31,12 +32,16 @@ export async function provisionCloudSql(args: {
silent ||
utils.logLabeledBullet(
"dataconnect",
- `Instance ${instanceId} settings not compatible with Firebase Data Connect.` +
+ `Instance ${instanceId} settings not compatible with Firebase Data Connect. ` +
`Updating instance to enable Cloud IAM authentication and public IP. This may take a few minutes...`,
);
- await cloudSqlAdminClient.updateInstanceForDataConnect(
- existingInstance,
- enableGoogleMlIntegration,
+ await promiseWithSpinner(
+ () =>
+ cloudSqlAdminClient.updateInstanceForDataConnect(
+ existingInstance,
+ enableGoogleMlIntegration,
+ ),
+ "Updating your instance...",
);
silent || utils.logLabeledBullet("dataconnect", "Instance updated");
}
@@ -56,11 +61,15 @@ export async function provisionCloudSql(args: {
`CloudSQL instance '${instanceId}' not found, creating it. This instance is provided under the terms of the Data Connect free trial ${freeTrialTermsLink()}`,
);
silent || utils.logLabeledBullet("dataconnect", `This may take while...`);
- const newInstance = await cloudSqlAdminClient.createInstance(
- projectId,
- locationId,
- instanceId,
- enableGoogleMlIntegration,
+ const newInstance = await promiseWithSpinner(
+ () =>
+ cloudSqlAdminClient.createInstance(
+ projectId,
+ locationId,
+ instanceId,
+ enableGoogleMlIntegration,
+ ),
+ "Creating your instance...",
);
silent || utils.logLabeledBullet("dataconnect", "Instance created");
connectionName = newInstance?.connectionName || "";
diff --git a/src/deploy/dataconnect/deploy.ts b/src/deploy/dataconnect/deploy.ts
index 511afd7dd8e..3146e059ae1 100644
--- a/src/deploy/dataconnect/deploy.ts
+++ b/src/deploy/dataconnect/deploy.ts
@@ -5,7 +5,6 @@ import { Service, ServiceInfo, requiresVector } from "../../dataconnect/types";
import { needProjectId } from "../../projectUtils";
import { provisionCloudSql } from "../../dataconnect/provisionCloudSql";
import { parseServiceName } from "../../dataconnect/names";
-import { confirm } from "../../prompt";
import { ResourceFilter } from "../../dataconnect/filters";
import { vertexAIOrigin } from "../../api";
import * as ensureApiEnabled from "../../ensureApiEnabled";
@@ -56,23 +55,28 @@ export default async function (
);
if (servicesToDelete.length) {
- if (
- await confirm({
- force: options.force,
- nonInteractive: options.nonInteractive,
- message: `The following services exist on ${projectId} but are not listed in your 'firebase.json'\n${servicesToDelete
- .map((s) => s.name)
- .join("\n")}\nWould you like to delete these services?`,
- })
- ) {
- await Promise.all(
- servicesToDelete.map(async (s) => {
- const { projectId, locationId, serviceId } = splitName(s.name);
- await client.deleteService(projectId, locationId, serviceId);
- utils.logLabeledSuccess("dataconnect", `Deleted service ${s.name}`);
- }),
- );
- }
+ const warning = `The following services exist on ${projectId} but are not listed in your 'firebase.json'\n${servicesToDelete
+ .map((s) => s.name)
+ .join("\n")}\nConsider deleting these via the Firebase console if they are no longer needed.`;
+ utils.logLabeledWarning("dataconnect", warning);
+ // TODO: Switch this back to prompting for deletion.
+ // if (
+ // await confirm({
+ // force: options.force,
+ // nonInteractive: options.nonInteractive,
+ // message: `The following services exist on ${projectId} but are not listed in your 'firebase.json'\n${servicesToDelete
+ // .map((s) => s.name)
+ // .join("\n")}\nWould you like to delete these services?`,
+ // })
+ // ) {
+ // await Promise.all(
+ // servicesToDelete.map(async (s) => {
+ // const { projectId, locationId, serviceId } = splitName(s.name);
+ // await client.deleteService(projectId, locationId, serviceId);
+ // utils.logLabeledSuccess("dataconnect", `Deleted service ${s.name}`);
+ // }),
+ // );
+ // }
}
// Provision CloudSQL resources
diff --git a/src/init/features/dataconnect/index.ts b/src/init/features/dataconnect/index.ts
index e7afdc8fc0d..86aceac0b8d 100644
--- a/src/init/features/dataconnect/index.ts
+++ b/src/init/features/dataconnect/index.ts
@@ -4,6 +4,7 @@ import { readFileSync } from "fs";
import { Config } from "../../../config";
import { Setup } from "../..";
import { provisionCloudSql } from "../../../dataconnect/provisionCloudSql";
+import { checkForFreeTrialInstance } from "../../../dataconnect/freeTrial";
import * as cloudsql from "../../../gcp/cloudsql/cloudsqladmin";
import { ensureApis } from "../../../dataconnect/ensureApis";
import { listLocations } from "../../../dataconnect/client";
@@ -25,63 +26,70 @@ export async function doSetup(setup: Setup, config: Config): Promise {
type: "input",
default: "dataconnect",
});
- // Hardcoded locations for when there is no project set up.
- let locationOptions = [
- { name: "us-central1", value: "us-central1" },
- { name: "europe-north1", value: "europe-north1" },
- { name: "europe-central2", value: "europe-central2" },
- { name: "europe-west1", value: "europe-west1" },
- { name: "southamerica-west1", value: "southamerica-west1" },
- { name: "us-east4", value: "us-east4" },
- { name: "us-west1", value: "us-west1" },
- { name: "asia-southeast1", value: "asia-southeast1" },
- ];
- if (setup.projectId) {
- const locations = await listLocations(setup.projectId);
- locationOptions = locations.map((l) => {
- return { name: l, value: l };
- });
- }
- const locationId = await promptOnce({
- message: "What location would you like to deploy this service into?",
- type: "list",
- choices: locationOptions,
- });
// TODO: Guided prompts to set up connector auth mode and generate
const connectorId = await promptOnce({
message: "What ID would you like to use for your connector?",
type: "input",
default: "my-connector",
});
- const dir: string = config.get("dataconnect.source") || "dataconnect";
- if (!config.has("dataconnect")) {
- config.set("dataconnect.source", dir);
- config.set("dataconnect.location", locationId);
- }
+
let cloudSqlInstanceId = "";
let newInstance = false;
+ let locationId = "";
if (setup.projectId) {
const instances = await cloudsql.listInstances(setup.projectId);
- const instancesInLocation = instances.filter((i) => i.region === locationId);
- const choices = instancesInLocation.map((i) => {
- return { name: i.name, value: i.name };
+ const choices = instances.map((i) => {
+ return { name: i.name, value: i.name, location: i.region };
});
- choices.push({ name: "Create a new instance", value: "" });
- if (instancesInLocation.length) {
+
+ const freeTrialInstanceId = await checkForFreeTrialInstance(setup.projectId);
+ if (!freeTrialInstanceId) {
+ choices.push({ name: "Create a new instance", value: "", location: "" });
+ }
+ if (instances.length) {
cloudSqlInstanceId = await promptOnce({
- message: `Which CloudSSQL in ${locationId} would you like to use?`,
+ message: `Which CloudSSQL instance would you like to use?`,
type: "list",
choices,
});
}
+ locationId = choices.find((c) => c.value === cloudSqlInstanceId)!.location;
}
if (cloudSqlInstanceId === "") {
+ // Hardcoded locations for when there is no project set up.
+ let locationOptions = [
+ { name: "us-central1", value: "us-central1" },
+ { name: "europe-north1", value: "europe-north1" },
+ { name: "europe-central2", value: "europe-central2" },
+ { name: "europe-west1", value: "europe-west1" },
+ { name: "southamerica-west1", value: "southamerica-west1" },
+ { name: "us-east4", value: "us-east4" },
+ { name: "us-west1", value: "us-west1" },
+ { name: "asia-southeast1", value: "asia-southeast1" },
+ ];
+ if (setup.projectId) {
+ const locations = await listLocations(setup.projectId);
+ locationOptions = locations.map((l) => {
+ return { name: l, value: l };
+ });
+ }
+
newInstance = true;
cloudSqlInstanceId = await promptOnce({
message: `What ID would you like to use for your new CloudSQL instance?`,
type: "input",
default: `dataconnect-test`,
});
+ locationId = await promptOnce({
+ message: "What location would you use for this instance?",
+ type: "list",
+ choices: locationOptions,
+ });
+ }
+ const dir: string = config.get("dataconnect.source") || "dataconnect";
+ if (!config.has("dataconnect")) {
+ config.set("dataconnect.source", dir);
+ config.set("dataconnect.location", locationId);
}
let cloudSqlDatabase = "";
let newDB = false;
diff --git a/templates/init/dataconnect/mutations.gql b/templates/init/dataconnect/mutations.gql
index 190335fcff5..9338e0665d8 100644
--- a/templates/init/dataconnect/mutations.gql
+++ b/templates/init/dataconnect/mutations.gql
@@ -1,4 +1,30 @@
-# # Example mutations
-# mutation createOrder($name: String!) {
-# order_insert(data : {name: $name})
+# # Example mutations for a simple email app
+
+# mutation CreateUser($uid: String, $name: String, $address: String) @auth(level: NO_ACCESS) {
+## _insert lets you create a new row in your table.
+# user_insert(data: {
+# uid: $uid,
+# name: $name,
+# address: $address
+# })
+# }
+# mutation CreateEmail($content: String, $subject: String, $fromUid: String) @auth(level: PUBLIC) {
+# email_insert(data: {
+# text: $content,
+# subject: $subject,
+# fromUid: $fromUid,
+## Server values let your service populate data for you
+## Here, we use sent_date: { today: true } to set 'sent' to today's date.
+# sent_date: { today: true }
+# })
+# }
+# mutation CreateRecipient($emailId: UUID, $uid: String) @auth(level: PUBLIC) {
+# recipient_insert(data: {
+# emailId: $emailId,
+# userUid: $uid
+# })
+# }
+# mutation DeleteEmail($emailId: UUID, $uid: String) @auth(level: PUBLIC) {
+## _ delete lets you delete rows from your table.
+# recipient_delete(key: {emailId: $emailId, userUid: $uid})
# }
diff --git a/templates/init/dataconnect/queries.gql b/templates/init/dataconnect/queries.gql
index f44f4731e27..fa8fea0531c 100644
--- a/templates/init/dataconnect/queries.gql
+++ b/templates/init/dataconnect/queries.gql
@@ -1,6 +1,51 @@
-# # Example query
-# query listOrders {
-# orders {
-# name
+# # Example queries for a simple email app.
+
+## @auth() directives control who can call each operation.
+## Only admins should be able to list all users, so we use NO_ACCESS
+# query ListUsers @auth(level: NO_ACCESS) {
+# users { uid, name, email: address }
+# }
+
+## Everyone should be able to see their inbox though, so we use PUBLIC
+# query ListInbox(
+# $uid: String
+# ) @auth(level: PUBLIC) {
+## where allows you to filter lists
+## Here, we use it to filter to only emails where this user is one of the recipients.
+# emails(where: {
+# users_via_Recipient: {
+# exist: { uid: { eq: $uid }
+# }}
+# }) {
+# id subject sent
+# content: text
+# sender: from { name email: address uid }
+
+## _on_ makes it easy to grab info from another table
+## Here, we use it to grab all the recipients of the email.
+# to: recipients_on_email {
+# user { name email: address uid }
+# }
+# }
+# }
+
+# query GetUidByEmail($emails: [String!]) @auth(level: PUBLIC) {
+# users(where: { address: { in: $emails } }) {
+# uid email: address
+# }
+# }
+
+# query ListSent(
+# $uid: String
+# ) @auth(level: PUBLIC) {
+# emails(where: {
+# fromUid: { eq: $uid }
+# }) {
+# id subject sent
+# content: text
+# sender: from { name email: address uid }
+# to: recipients_on_email {
+# user { name email: address uid }
+# }
# }
# }
diff --git a/templates/init/dataconnect/schema.gql b/templates/init/dataconnect/schema.gql
index ff4ebf066b8..aad46137d4f 100644
--- a/templates/init/dataconnect/schema.gql
+++ b/templates/init/dataconnect/schema.gql
@@ -1,15 +1,28 @@
-# # Example schema
-# type Product @table {
-# name: String!
-# price: Int!
+## Example schema for simple email app
+# type User @table(key: "uid") {
+# uid: String!
+# name: String!
+# address: String!
# }
-# type Order @table {
-# name: String!
+# type Email @table {
+# subject: String!
+# sent: Date!
+# text: String!
+# from: User!
# }
-# type OrderItem @table(key: ["order", "product"]) {
-# order: Order!
-# product: Product!
-# quantity: Int!
+# type Recipient @table(key: ["email", "user"]) {
+# email: Email!
+# user: User!
+# }
+
+# type EmailMeta @table(key: ["user", "email"]) {
+# user: User!
+# email: Email!
+# labels: [String]
+# read: Boolean!
+# starred: Boolean!
+# muted: Boolean!
+# snoozed: Date
# }
From c94b5ab2d4c1568c45c6d32646ae8bdeea6ddc81 Mon Sep 17 00:00:00 2001
From: Harold Shen
Date: Wed, 8 May 2024 19:25:15 -0400
Subject: [PATCH 17/40] Fix emulator issues stream (#7134)
* small fix
* rename
---
firebase-vscode/CHANGELOG.md | 1 +
firebase-vscode/src/data-connect/emulator-stream.ts | 6 +++---
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md
index 26f525bff34..64bed97af45 100644
--- a/firebase-vscode/CHANGELOG.md
+++ b/firebase-vscode/CHANGELOG.md
@@ -4,6 +4,7 @@
- Update Extensions page Logo
- Update README for Extensions page
+- Surface emulator issues as notifications
- Emulator Bump 1.1.15
## 0.1.7
diff --git a/firebase-vscode/src/data-connect/emulator-stream.ts b/firebase-vscode/src/data-connect/emulator-stream.ts
index 77c2950903c..5d9f3ba77d5 100644
--- a/firebase-vscode/src/data-connect/emulator-stream.ts
+++ b/firebase-vscode/src/data-connect/emulator-stream.ts
@@ -37,9 +37,9 @@ export async function runEmulatorIssuesStream(
) {
const obsErrors = await getEmulatorIssuesStream(configs, fdcEndpoint);
const obsConverter = {
- next(nextCompilerResponse: CompilerResponse) {
- if (nextCompilerResponse.result?.issues?.length) {
- for (const issue of nextCompilerResponse.result.issues) {
+ next(nextResponse: EmulatorIssueResponse) {
+ if (nextResponse.result?.issues?.length) {
+ for (const issue of nextResponse.result.issues) {
displayIssue(issue);
}
}
From 30ec0bc11d0d061cfa62485fe3f339b37f530c7e Mon Sep 17 00:00:00 2001
From: Harold Shen
Date: Wed, 8 May 2024 20:26:39 -0400
Subject: [PATCH 18/40] update extension dependencies (#7136)
Co-authored-by: joehan
---
firebase-vscode/package.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/firebase-vscode/package.json b/firebase-vscode/package.json
index fca655268f3..1af9605296b 100644
--- a/firebase-vscode/package.json
+++ b/firebase-vscode/package.json
@@ -13,6 +13,7 @@
"categories": [
"Other"
],
+ "extensionDependencies": ["graphql.vscode-graphql-syntax"],
"activationEvents": [
"onStartupFinished",
"onLanguage:graphql",
From 58800d9fa59fcbd6e259a253c1537272511412bb Mon Sep 17 00:00:00 2001
From: Yuchen Shi
Date: Wed, 8 May 2024 17:50:10 -0700
Subject: [PATCH 19/40] Use relative path for deploying schema and operations.
(#7139)
---
src/dataconnect/fileUtils.ts | 18 +++++++++++-------
1 file changed, 11 insertions(+), 7 deletions(-)
diff --git a/src/dataconnect/fileUtils.ts b/src/dataconnect/fileUtils.ts
index 5c1f84fa2b2..e087b9dfa39 100644
--- a/src/dataconnect/fileUtils.ts
+++ b/src/dataconnect/fileUtils.ts
@@ -59,17 +59,21 @@ function validateConnectorYaml(unvalidated: any): ConnectorYaml {
export async function readGQLFiles(sourceDir: string): Promise {
const files = await fs.readdir(sourceDir);
- return files.filter((f) => f.endsWith(".gql")).map((f) => toFile(path.join(sourceDir, f)));
+ // TODO: Handle files in subdirectories such as `foo/a.gql` and `bar/baz/b.gql`.
+ return files
+ .filter((f) => f.endsWith(".gql") || f.endsWith(".graphql"))
+ .map((f) => toFile(sourceDir, f));
}
-function toFile(path: string): File {
- if (!fs.existsSync(path)) {
- throw new FirebaseError(`file ${path} not found`);
+function toFile(sourceDir: string, relPath: string): File {
+ const fullPath = path.join(sourceDir, relPath);
+ if (!fs.existsSync(fullPath)) {
+ throw new FirebaseError(`file ${fullPath} not found`);
}
- const file = fs.readFileSync(path).toString();
+ const content = fs.readFileSync(fullPath).toString();
return {
- path: path,
- content: file,
+ path: relPath,
+ content,
};
}
From a9160e4f6f1a06f8d489903bb2a9344918fcd811 Mon Sep 17 00:00:00 2001
From: Harold Shen
Date: Wed, 8 May 2024 22:26:35 -0400
Subject: [PATCH 20/40] Generate .graphqlrc on startup (#7140)
* first attempt at generating config
* generate yaml file for ls
* Move generated config file to .firebase/.graphqlrc.
* Add read-data lense (#7125)
Co-authored-by: Harold Shen
* Handle invalid connector errors (#7119)
* Handle invalid connector errors
* clean up
* No longer tie to v1main errors
* Major refactor of behavior
* Also handle unconnected dbs
* PR fixes
* Adding in data connect too (#7089)
* add initial tos requirement
* Adding in data connect!
* Small fixes
* add tests
* comments
* add initial tos requirement
* Adding in data connect!
* Small fixes
* add tests
* comments
* Merging
* revert
---------
Co-authored-by: Tony Huang
* Missing one very important character (#7138)
* format GQL error in a way thats friendly to editor (#7137)
* Firebase init updates (#7069)
* Update init
* Run functions config first
* Lint
* PR feedback
* Update genkit onboarding
* Time-gate
* Put behind an experiment
* A bunch of FDC polish (#7133)
* Lots of data connect polish
* More polishing
* Add file
* spinners
* Fix emulator issues stream (#7134)
* small fix
* rename
* update extension dependencies (#7136)
Co-authored-by: joehan
* Use relative path for deploying schema and operations. (#7139)
* update changelog
* check configs > 0
* close lsp client
---------
Co-authored-by: Yuchen Shi
Co-authored-by: Remi Rousselet
Co-authored-by: joehan
Co-authored-by: Tony Huang
Co-authored-by: Fred Zhang
Co-authored-by: Sam
---
firebase-vscode/CHANGELOG.md | 1 +
firebase-vscode/src/data-connect/config.ts | 20 ++++++
firebase-vscode/src/data-connect/index.ts | 23 ++++--
.../src/data-connect/language-client.ts | 71 ++++++++++++++-----
.../src/data-connect/language-server.ts | 6 +-
5 files changed, 94 insertions(+), 27 deletions(-)
diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md
index 64bed97af45..d494e229dce 100644
--- a/firebase-vscode/CHANGELOG.md
+++ b/firebase-vscode/CHANGELOG.md
@@ -5,6 +5,7 @@
- Update Extensions page Logo
- Update README for Extensions page
- Surface emulator issues as notifications
+- Generate .graphqlrc automatically
- Emulator Bump 1.1.15
## 0.1.7
diff --git a/firebase-vscode/src/data-connect/config.ts b/firebase-vscode/src/data-connect/config.ts
index 6233aa2c680..2821ebd1a18 100644
--- a/firebase-vscode/src/data-connect/config.ts
+++ b/firebase-vscode/src/data-connect/config.ts
@@ -144,6 +144,26 @@ export class ResolvedDataConnectConfig {
return result;
}
+ get connectorDirs(): string[] {
+ return this.value.connectorDirs;
+ }
+
+ get schemaDir(): string {
+ return this.value.schema.source;
+ }
+
+ get relativePath(): string {
+ return this.path.split("/").pop();
+ }
+
+ get relativeSchemaPath(): string {
+ return this.schemaDir.replace(".", this.relativePath);
+ }
+
+ get relativeConnectorPaths(): string[] {
+ return this.connectorDirs.map((connectorDir) => connectorDir.replace(".", this.relativePath));
+ }
+
containsPath(path: string) {
return isPathInside(path, this.path);
}
diff --git a/firebase-vscode/src/data-connect/index.ts b/firebase-vscode/src/data-connect/index.ts
index fd2da913a0f..b8755760806 100644
--- a/firebase-vscode/src/data-connect/index.ts
+++ b/firebase-vscode/src/data-connect/index.ts
@@ -20,16 +20,14 @@ import { registerFdcDeploy } from "./deploy";
import * as graphql from "graphql";
import {
ResolvedDataConnectConfigs,
- VSCODE_ENV_VARS,
dataConnectConfigs,
registerDataConnectConfigs,
} from "./config";
import { locationToRange } from "../utils/graphql";
import { runDataConnectCompiler } from "./core-compiler";
-import { setVSCodeEnvVars } from "../../../src/utils";
import { Result } from "../result";
-import { setTerminalEnvVars } from "./terminal";
import { runEmulatorIssuesStream } from "./emulator-stream";
+import { LanguageClient } from "vscode-languageclient/node";
class CodeActionsProvider implements vscode.CodeActionProvider {
constructor(
@@ -155,18 +153,29 @@ export function registerFdc(
);
const schemaCodeLensProvider = new SchemaCodeLensProvider(emulatorController);
- const client = setupLanguageClient(context);
- client.start();
+ let client: LanguageClient;
+ // setup new language client on config change
+ context.subscriptions.push({
+ dispose: effect(() => {
+ const configs = dataConnectConfigs.value?.tryReadValue;
+ if (client) client.stop();
+ if (configs && configs.values.length > 0) {
+ client = setupLanguageClient(
+ context,
+ configs,
+ );
+ vscode.commands.executeCommand("fdc-graphql.start");
+ }
+ }),
+ });
// Perform some side-effects when the endpoint changes
context.subscriptions.push({
dispose: effect(() => {
const configs = dataConnectConfigs.value?.tryReadValue;
-
if (configs && fdcService.localEndpoint.value) {
// TODO move to client.start or setupLanguageClient
vscode.commands.executeCommand("fdc-graphql.restart");
-
vscode.commands.executeCommand(
"firebase.dataConnect.executeIntrospection",
);
diff --git a/firebase-vscode/src/data-connect/language-client.ts b/firebase-vscode/src/data-connect/language-client.ts
index 1008dad7f90..c4ca40e4222 100644
--- a/firebase-vscode/src/data-connect/language-client.ts
+++ b/firebase-vscode/src/data-connect/language-client.ts
@@ -7,12 +7,15 @@ import {
LanguageClient,
} from "vscode-languageclient/node";
import * as path from "node:path";
-import { Signal } from "@preact/signals-core";
+import { ResolvedDataConnectConfigs } from "./config";
-export function setupLanguageClient(context) {
+export function setupLanguageClient(
+ context,
+ configs: ResolvedDataConnectConfigs,
+) {
// activate language client/serer
const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel(
- "Firebase GraphQL Language Server"
+ "Firebase GraphQL Language Server",
);
const serverPath = path.join("dist", "server.js");
@@ -47,12 +50,12 @@ export function setupLanguageClient(context) {
// also, it makes sense that it should only re-load on file save, but we need to document that.
// TODO: perhaps we can intercept change events, and remind the user
// to save for the changes to take effect
- true
+ true,
),
// TODO: load ignore file
// These ignore node_modules and .git by default
vscode.workspace.createFileSystemWatcher(
- "**/{*.graphql,*.graphqls,*.gql,*.js,*.mjs,*.cjs,*.esm,*.es,*.es6,*.jsx,*.ts,*.tsx,*.vue,*.svelte,*.cts,*.mts}"
+ "**/{*.graphql,*.graphqls,*.gql,*.js,*.mjs,*.cjs,*.esm,*.es,*.es6,*.jsx,*.ts,*.tsx,*.vue,*.svelte,*.cts,*.mts}",
),
],
},
@@ -75,38 +78,70 @@ export function setupLanguageClient(context) {
"graphQLlanguageServer",
"GraphQL Language Server",
serverOptions,
- clientOptions
+ clientOptions,
);
// register commands
const commandShowOutputChannel = vscode.commands.registerCommand(
"fdc-graphql.showOutputChannel",
- () => outputChannel.show()
+ () => outputChannel.show(),
);
context.subscriptions.push(commandShowOutputChannel);
+ const generateYamlFile = async () => {
+ const basePath = vscode.workspace.rootPath;
+ const filePath = ".firebase/.graphqlrc";
+ const fileUri = vscode.Uri.file(`${basePath}/${filePath}`);
+ const folderPath = ".firebase";
+ const folderUri = vscode.Uri.file(`${basePath}/${folderPath}`);
+
+ // TODO: Expand to multiple services
+ const config = configs.values[0];
+ const generatedPath = ".dataconnect";
+ const schemaPaths = [
+ `../${config.relativeSchemaPath}/**/*.gql`,
+ `../${config.relativePath}/${generatedPath}/**/*.gql`,
+ ];
+ const documentPaths = config.relativeConnectorPaths.map(
+ (connectorPath) => `../${connectorPath}/**/*.gql`,
+ );
+
+ const yamlJson = JSON.stringify({
+ schema: schemaPaths,
+ document: documentPaths,
+ });
+ // create folder if needed
+ if (!vscode.workspace.getWorkspaceFolder(folderUri)) {
+ vscode.workspace.fs.createDirectory(folderUri);
+ }
+ vscode.workspace.fs.writeFile(fileUri, Buffer.from(yamlJson));
+ };
+
vscode.commands.registerCommand("fdc-graphql.restart", async () => {
outputChannel.appendLine("Stopping Firebase GraphQL Language Server");
await client.stop();
-
+ await generateYamlFile();
outputChannel.appendLine("Restarting Firebase GraphQL Language Server");
await client.start();
outputChannel.appendLine("Firebase GraphQL Language Server restarted");
});
- const restartGraphqlLSP = () => {
- vscode.commands.executeCommand("fdc-graphql.restart");
- };
+ vscode.commands.registerCommand("fdc-graphql.start", async () => {
+ await generateYamlFile();
+ await client.start();
+ outputChannel.appendLine("Firebase GraphQL Language Server restarted");
+ });
+ // ** DISABLED FOR NOW WHILE WE TEST GENERATED YAML **
// restart server whenever config file changes
- const watcher = vscode.workspace.createFileSystemWatcher(
- "**/.graphqlrc.*", // TODO: extend to schema files, and other config types
- false,
- false,
- false
- );
- watcher.onDidChange(() => restartGraphqlLSP());
+ // const watcher = vscode.workspace.createFileSystemWatcher(
+ // "**/.graphqlrc.*", // TODO: extend to schema files, and other config types
+ // false,
+ // false,
+ // false,
+ // );
+ // watcher.onDidChange(() => restartGraphqlLSP());
return client;
}
diff --git a/firebase-vscode/src/data-connect/language-server.ts b/firebase-vscode/src/data-connect/language-server.ts
index 9658c8f53e3..bd2c94e9b3d 100644
--- a/firebase-vscode/src/data-connect/language-server.ts
+++ b/firebase-vscode/src/data-connect/language-server.ts
@@ -1,11 +1,13 @@
import { startServer } from "graphql-language-service-server";
-
// The npm scripts are configured to only build this once before
// watching the extension, so please restart the extension debugger for changes!
async function start() {
try {
- await startServer({ method: "node" });
+ await startServer({
+ method: "node",
+ loadConfigOptions: { rootDir: ".firebase" },
+ });
// eslint-disable-next-line no-console
console.log("Firebase GraphQL Language Server started!");
} catch (err) {
From 0d16fa0a4343ee0aba7a20a85a64486dc1de45b3 Mon Sep 17 00:00:00 2001
From: abhis3
Date: Thu, 9 May 2024 01:41:07 -0400
Subject: [PATCH 21/40] rebase master (#7102)
---
src/commands/init.ts | 8 --------
1 file changed, 8 deletions(-)
diff --git a/src/commands/init.ts b/src/commands/init.ts
index b8ac3b5a6bf..01e1ffdc218 100644
--- a/src/commands/init.ts
+++ b/src/commands/init.ts
@@ -81,14 +81,6 @@ if (isEnabled("genkit")) {
});
}
-if (isEnabled("apphosting")) {
- choices.push({
- value: "apphosting",
- name: "App Hosting: Get started with App Hosting projects.",
- checked: false,
- });
-}
-
if (isEnabled("dataconnect")) {
choices.push({
value: "dataconnect",
From 27e93f9ce42de8ede8cbe24b1a0142cde27c1ffb Mon Sep 17 00:00:00 2001
From: Harold Shen
Date: Thu, 9 May 2024 11:21:33 -0400
Subject: [PATCH 22/40] update dc emulator (#7141)
---
src/emulator/downloadableEmulators.ts | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts
index 237f1b01386..362c45391d7 100755
--- a/src/emulator/downloadableEmulators.ts
+++ b/src/emulator/downloadableEmulators.ts
@@ -57,14 +57,14 @@ const EMULATOR_UPDATE_DETAILS: { [s in DownloadableEmulators]: EmulatorUpdateDet
dataconnect:
process.platform === "darwin"
? {
- version: "1.1.15",
- expectedSize: 25600896,
- expectedChecksum: "36dcf9be7273b9ba6052faf0b3c0347f",
+ version: "1.1.16",
+ expectedSize: 25628592,
+ expectedChecksum: "e6ec9eecfe3e3721a5e19949447da530",
}
: {
- version: "1.1.15",
- expectedSize: 23036688,
- expectedChecksum: "e42203947bf984993f295976ee3ba2be",
+ version: "1.1.16",
+ expectedSize: 23061488,
+ expectedChecksum: "a255eaad9f943925002d738c7ab1252a",
},
};
From fad7f6f752d6e71ef1599ab0255c5d1ae829a074 Mon Sep 17 00:00:00 2001
From: sijinli <148275505+sjjj986@users.noreply.github.com>
Date: Thu, 9 May 2024 12:18:37 -0400
Subject: [PATCH 23/40] App Hosting: Reorder cli prompt to match console
experience (#7130)
* change prompt order
---
src/apphosting/githubConnections.ts | 14 ++++----
src/apphosting/index.ts | 51 +++++++++++++++--------------
src/apphosting/repo.ts | 4 +--
3 files changed, 35 insertions(+), 34 deletions(-)
diff --git a/src/apphosting/githubConnections.ts b/src/apphosting/githubConnections.ts
index 83d851e6b00..d04afcad0ae 100644
--- a/src/apphosting/githubConnections.ts
+++ b/src/apphosting/githubConnections.ts
@@ -95,7 +95,7 @@ export async function linkGitHubRepository(
projectId: string,
location: string,
): Promise {
- utils.logBullet(clc.bold(`${clc.yellow("===")} Set up a GitHub connection`));
+ utils.logBullet(clc.bold(`${clc.yellow("===")} Import a GitHub repository`));
// Fetch the sentinel Oauth connection first which is needed to create further GitHub connections.
const oauthConn = await getOrCreateOauthConnection(projectId, location);
const existingConns = await listAppHostingConnections(projectId);
@@ -134,8 +134,6 @@ export async function linkGitHubRepository(
});
const repo = await getOrCreateRepository(projectId, location, connectionId, repoCloneUri);
- utils.logSuccess(`Successfully linked GitHub repository at remote URI`);
- utils.logSuccess(`\t${repo.cloneUri}\n`);
return repo;
}
@@ -203,8 +201,7 @@ export async function getOrCreateOauthConnection(
}
while (conn.installationState.stage === "PENDING_USER_OAUTH") {
- utils.logBullet("You must authorize the Firebase GitHub app.");
- utils.logBullet("Sign in to GitHub and authorize Firebase GitHub app:");
+ utils.logBullet("Please authorize the Firebase GitHub app by visiting this url:");
const { url, cleanup } = await utils.openInBrowserPopup(
conn.installationState.actionUri,
"Authorize the GitHub app",
@@ -212,12 +209,13 @@ export async function getOrCreateOauthConnection(
utils.logBullet(`\t${url}`);
await promptOnce({
type: "input",
- message: "Press Enter once you have authorized the app",
+ message: "Press Enter once you have authorized the GitHub App.",
});
cleanup();
const { projectId, location, id } = parseConnectionName(conn.name)!;
conn = await devConnect.getConnection(projectId, location, id);
}
+ utils.logSuccess("Connected with GitHub successfully\n");
return conn;
}
@@ -230,7 +228,7 @@ async function promptCloneUri(
const cloneUri = await promptOnce({
type: "autocomplete",
name: "cloneUri",
- message: "Which repository would you like to deploy?",
+ message: "Which GitHub repo do you want to deploy?",
source: (_: any, input = ""): Promise<(inquirer.DistinctChoice | inquirer.Separator)[]> => {
return new Promise((resolve) =>
resolve([
@@ -317,7 +315,7 @@ export async function ensureSecretManagerAdminGrant(projectId: string): Promise<
}
utils.logSuccess(
- "Successfully granted the required role to the Developer Connect Service Agent!",
+ "Successfully granted the required role to the Developer Connect Service Agent!\n",
);
}
diff --git a/src/apphosting/index.ts b/src/apphosting/index.ts
index fcccea66158..5608a8b1249 100644
--- a/src/apphosting/index.ts
+++ b/src/apphosting/index.ts
@@ -1,3 +1,4 @@
+import * as clc from "colorette";
import * as poller from "../operation-poller";
import * as apphosting from "../gcp/apphosting";
import * as githubConnections from "./githubConnections";
@@ -79,7 +80,6 @@ export async function doSetup(
ensure(projectId, artifactRegistryDomain(), "apphosting", true),
ensure(projectId, iamOrigin(), "apphosting", true),
]);
- logBullet("First we need a few details to create your backend.\n");
// Hack: Because IAM can take ~45 seconds to propagate, we provision the service account as soon as
// possible to reduce the likelihood that the subsequent Cloud Build fails. See b/336862200.
@@ -97,18 +97,6 @@ export async function doSetup(
location =
location || (await promptLocation(projectId, "Select a location to host your backend:\n"));
- const backendId = await promptNewBackendId(projectId, location, {
- name: "backendId",
- type: "input",
- default: "my-web-app",
- message: "Create a name for your backend [1-30 characters]",
- });
-
- const webApp = await webApps.getOrCreateWebApp(projectId, webAppName, backendId);
- if (!webApp) {
- logWarning(`Firebase web app not set`);
- }
-
const gitRepositoryConnection: GitRepositoryLink = await githubConnections.linkGitHubRepository(
projectId,
location,
@@ -121,6 +109,31 @@ export async function doSetup(
message: "Specify your app's root directory relative to your repository",
});
+ // TODO: Once tag patterns are implemented, prompt which method the user
+ // prefers. We could reduce the number of questions asked by letting people
+ // enter tag:?
+ const branch = await promptOnce({
+ name: "branch",
+ type: "input",
+ default: "main",
+ message: "Pick a branch for continuous deployment",
+ });
+ logSuccess(`Repo linked successfully!\n`);
+
+ logBullet(`${clc.yellow("===")} Set up your backend`);
+ const backendId = await promptNewBackendId(projectId, location, {
+ name: "backendId",
+ type: "input",
+ default: "my-web-app",
+ message: "Provide a name for your backend [1-30 characters]",
+ });
+ logSuccess(`Name set to ${backendId}\n`);
+
+ const webApp = await webApps.getOrCreateWebApp(projectId, webAppName, backendId);
+ if (!webApp) {
+ logWarning(`Firebase web app not set`);
+ }
+
const createBackendSpinner = ora("Creating your new backend...").start();
const backend = await createBackend(
projectId,
@@ -131,17 +144,7 @@ export async function doSetup(
webApp?.id,
rootDir,
);
- createBackendSpinner.succeed(`Successfully created backend:\n\t${backend.name}\n`);
-
- // TODO: Once tag patterns are implemented, prompt which method the user
- // prefers. We could reduce the number of questions asked by letting people
- // enter tag:?
- const branch = await promptOnce({
- name: "branch",
- type: "input",
- default: "main",
- message: "Pick a branch for continuous deployment",
- });
+ createBackendSpinner.succeed(`Successfully created backend!\n\t${backend.name}\n`);
await setDefaultTrafficPolicy(projectId, location, backendId, branch);
diff --git a/src/apphosting/repo.ts b/src/apphosting/repo.ts
index 00619c806d3..5fd913960e7 100644
--- a/src/apphosting/repo.ts
+++ b/src/apphosting/repo.ts
@@ -90,7 +90,7 @@ export async function linkGitHubRepository(
projectId: string,
location: string,
): Promise {
- utils.logBullet(clc.bold(`${clc.yellow("===")} Set up a GitHub connection`));
+ utils.logBullet(clc.bold(`${clc.yellow("===")} Import a GitHub repository`));
// Fetch the sentinel Oauth connection first which is needed to create further GitHub connections.
const oauthConn = await getOrCreateOauthConnection(projectId, location);
const existingConns = await listAppHostingConnections(projectId);
@@ -214,7 +214,7 @@ async function promptRepositoryUri(
const remoteUri = await promptOnce({
type: "autocomplete",
name: "remoteUri",
- message: "Which repository would you like to deploy?",
+ message: "Which GitHub repo do you want to deploy?",
source: (_: any, input = ""): Promise<(inquirer.DistinctChoice | inquirer.Separator)[]> => {
return new Promise((resolve) =>
resolve([
From cf04642088e6c5d8feac43ea154d4ece3f82ff78 Mon Sep 17 00:00:00 2001
From: harshyyy21
Date: Thu, 9 May 2024 12:22:27 -0500
Subject: [PATCH 24/40] Release Firestore Emulator v1.19.6 (#7132)
* Release Firestore Emulator v1.19.6
* Update CHANGELOG.md
* fix formating
---------
Co-authored-by: joehan
---
CHANGELOG.md | 1 +
src/emulator/downloadableEmulators.ts | 6 +++---
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e69de29bb2d..12bbbf80bf0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -0,0 +1 @@
+- Release Firestore Emulator version 1.19.6 which fixes a few Datastore Mode bugs regarding transactions (#7132).
diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts
index 362c45391d7..e627e803518 100755
--- a/src/emulator/downloadableEmulators.ts
+++ b/src/emulator/downloadableEmulators.ts
@@ -33,9 +33,9 @@ const EMULATOR_UPDATE_DETAILS: { [s in DownloadableEmulators]: EmulatorUpdateDet
expectedChecksum: "2fd771101c0e1f7898c04c9204f2ce63",
},
firestore: {
- version: "1.19.5",
- expectedSize: 66204670,
- expectedChecksum: "6d9fb826605701668af722f25048ad95",
+ version: "1.19.6",
+ expectedSize: 66349770,
+ expectedChecksum: "2eaabbe3cdb4867df585b7ec5505bad7",
},
storage: {
version: "1.1.3",
From e06ae7b1b00a3f131803c20b57337cdc510cea9f Mon Sep 17 00:00:00 2001
From: joehan
Date: Thu, 9 May 2024 14:10:12 -0400
Subject: [PATCH 25/40] Fix init shape (#7143)
* Fix init shape
* Use correct string too
* format
---
src/init/features/dataconnect/index.ts | 4 ++--
src/init/features/emulators.ts | 22 +++++++++++-----------
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/init/features/dataconnect/index.ts b/src/init/features/dataconnect/index.ts
index 86aceac0b8d..620cb80ec70 100644
--- a/src/init/features/dataconnect/index.ts
+++ b/src/init/features/dataconnect/index.ts
@@ -8,6 +8,7 @@ import { checkForFreeTrialInstance } from "../../../dataconnect/freeTrial";
import * as cloudsql from "../../../gcp/cloudsql/cloudsqladmin";
import { ensureApis } from "../../../dataconnect/ensureApis";
import { listLocations } from "../../../dataconnect/client";
+import { DEFAULT_POSTGRES_CONNECTION } from "../emulators";
const TEMPLATE_ROOT = resolve(__dirname, "../../../../templates/init/dataconnect/");
@@ -116,10 +117,9 @@ export async function doSetup(setup: Setup, config: Config): Promise {
});
}
- // postgresql://localhost:5432 is a default out of the box value for most installations of Postgres
const defaultConnectionString =
setup.rcfile.dataconnectEmulatorConfig?.postgres?.localConnectionString ??
- "postgresql://localhost:5432?sslmode=disable";
+ DEFAULT_POSTGRES_CONNECTION;
// TODO: Download Postgres
const localConnectionString = await promptOnce({
type: "input",
diff --git a/src/init/features/emulators.ts b/src/init/features/emulators.ts
index da8c17dec24..87ea621b629 100644
--- a/src/init/features/emulators.ts
+++ b/src/init/features/emulators.ts
@@ -1,7 +1,7 @@
import * as clc from "colorette";
import * as _ from "lodash";
import * as utils from "../../utils";
-import { prompt } from "../../prompt";
+import { prompt, promptOnce } from "../../prompt";
import { Emulators, ALL_SERVICE_EMULATORS, isDownloadableEmulator } from "../../emulator/types";
import { Constants } from "../../emulator/constants";
import { downloadIfNecessary } from "../../emulator/downloadableEmulators";
@@ -12,6 +12,9 @@ interface EmulatorsInitSelections {
download?: boolean;
}
+// postgresql://localhost:5432 is a default out of the box value for most installations of Postgres
+export const DEFAULT_POSTGRES_CONNECTION = "postgresql://localhost:5432?sslmode=disable";
+
export async function doSetup(setup: Setup, config: any) {
const choices = ALL_SERVICE_EMULATORS.map((e) => {
return {
@@ -92,19 +95,16 @@ export async function doSetup(setup: Setup, config: any) {
}
if (selections.emulators.includes(Emulators.DATACONNECT)) {
- // postgresql://localhost:5432 is a default out of the box value for most installations of Postgres
const defaultConnectionString =
setup.rcfile.dataconnectEmulatorConfig?.postgres?.localConnectionString ??
- "postgresql://localhost:5432";
+ DEFAULT_POSTGRES_CONNECTION;
// TODO: Download Postgres
- const localConnectionString = await prompt(setup.config.emulators[Emulators.DATACONNECT], [
- {
- type: "input",
- name: "localConnectionString",
- message: `What is the connection string of the local Postgres instance you would like to use with the Data Connect emulator?`,
- default: defaultConnectionString,
- },
- ]);
+ const localConnectionString = await promptOnce({
+ type: "input",
+ name: "localConnectionString",
+ message: `What is the connection string of the local Postgres instance you would like to use with the Data Connect emulator?`,
+ default: defaultConnectionString,
+ });
setup.rcfile.dataconnectEmulatorConfig = { postgres: { localConnectionString } };
}
From 143ffd014c249cf0223c93e71c1c4b3aeb972265 Mon Sep 17 00:00:00 2001
From: Harold Shen
Date: Thu, 9 May 2024 15:06:31 -0400
Subject: [PATCH 26/40] update doc links, bump 0.1.8 (#7142)
---
firebase-vscode/CHANGELOG.md | 2 +-
firebase-vscode/package-lock.json | 4 ++--
firebase-vscode/package.json | 2 +-
firebase-vscode/webviews/data-connect.entry.tsx | 4 ++--
4 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/firebase-vscode/CHANGELOG.md b/firebase-vscode/CHANGELOG.md
index d494e229dce..433b61787ce 100644
--- a/firebase-vscode/CHANGELOG.md
+++ b/firebase-vscode/CHANGELOG.md
@@ -6,7 +6,7 @@
- Update README for Extensions page
- Surface emulator issues as notifications
- Generate .graphqlrc automatically
-- Emulator Bump 1.1.15
+- Emulator Bump 1.1.16
## 0.1.7
diff --git a/firebase-vscode/package-lock.json b/firebase-vscode/package-lock.json
index be29ee2817f..b77275038d3 100644
--- a/firebase-vscode/package-lock.json
+++ b/firebase-vscode/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "firebase-vscode",
- "version": "0.1.6",
+ "version": "0.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "firebase-vscode",
- "version": "0.1.6",
+ "version": "0.1.8",
"dependencies": {
"@preact/signals-core": "^1.4.0",
"@preact/signals-react": "1.3.6",
diff --git a/firebase-vscode/package.json b/firebase-vscode/package.json
index 1af9605296b..6376d3b52cb 100644
--- a/firebase-vscode/package.json
+++ b/firebase-vscode/package.json
@@ -4,7 +4,7 @@
"publisher": "firebase",
"icon": "./resources/firebase_logo.png",
"description": "VSCode Extension for Firebase",
- "version": "0.1.6",
+ "version": "0.1.8",
"engines": {
"vscode": "^1.69.0"
},
diff --git a/firebase-vscode/webviews/data-connect.entry.tsx b/firebase-vscode/webviews/data-connect.entry.tsx
index e8b43ee9d5f..00004cd78ab 100644
--- a/firebase-vscode/webviews/data-connect.entry.tsx
+++ b/firebase-vscode/webviews/data-connect.entry.tsx
@@ -25,7 +25,7 @@ function DataConnect() {