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() {

Start the FDC emulator. See also:{" "} - + Working with the emulator

@@ -50,7 +50,7 @@ function DataConnect() {

Deploy FDC services and connectors to production. See also:{" "} - Deploying + Deploying

broker.send("fdc.deploy")}> From 77eedb14250637959ca6af323b1f778f8d8bb10f Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 9 May 2024 16:05:19 -0400 Subject: [PATCH 27/40] Handle no schema cases (#7145) --- src/commands/dataconnect-services-list.ts | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/commands/dataconnect-services-list.ts b/src/commands/dataconnect-services-list.ts index 59f18a757a5..53a02480150 100644 --- a/src/commands/dataconnect-services-list.ts +++ b/src/commands/dataconnect-services-list.ts @@ -6,6 +6,7 @@ import * as client from "../dataconnect/client"; import { logger } from "../logger"; import { requirePermissions } from "../requirePermissions"; import { ensureApis } from "../dataconnect/ensureApis"; +import { Schema } from "../dataconnect/types"; const Table = require("cli-table"); export const command = new Command("dataconnect:services:list") @@ -32,14 +33,30 @@ export const command = new Command("dataconnect:services:list") }); const jsonOutput: { services: Record[] } = { services: [] }; for (const service of services) { - const schema = await client.getSchema(service.name); + let schema: Schema = { + name: "", + primaryDatasource: {}, + source: { files: [] }, + }; + try { + schema = await client.getSchema(service.name); + } catch (err) { + logger.debug(`Error fetching schema: ${err}`); + } const connectors = await client.listConnectors(service.name); const serviceName = names.parseServiceName(service.name); const instanceName = schema?.primaryDatasource.postgresql?.cloudSql.instance ?? ""; const instanceId = instanceName.split("/").pop(); const dbId = schema?.primaryDatasource.postgresql?.database ?? ""; - const dbName = `CloudSQL Instance: ${instanceId} Database:${dbId}`; - table.push([serviceName.serviceId, serviceName.location, dbName, schema?.updateTime, "", ""]); + const dbName = `CloudSQL Instance: ${instanceId}\nDatabase:${dbId}`; + table.push([ + serviceName.serviceId, + serviceName.location, + dbName, + schema?.updateTime ?? "", + "", + "", + ]); const serviceJson = { serviceId: serviceName.serviceId, location: serviceName.location, From de9cbdefb2c1bd6d0070f1a801161f436292a583 Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 9 May 2024 16:33:45 -0400 Subject: [PATCH 28/40] Block safe migrations because they are exteremely unsafe (#7144) --- src/dataconnect/schemaMigration.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dataconnect/schemaMigration.ts b/src/dataconnect/schemaMigration.ts index 35d6f864982..46df2dcc1b9 100644 --- a/src/dataconnect/schemaMigration.ts +++ b/src/dataconnect/schemaMigration.ts @@ -42,8 +42,8 @@ export async function migrateSchema(args: { const { options, schema, allowNonInteractiveMigration, validateOnly } = args; const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema); - await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId); try { + await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId); await upsertSchema(schema, validateOnly); logger.debug(`Database schema was up to date for ${instanceId}:${databaseId}`); } catch (err: any) { @@ -141,6 +141,11 @@ async function handleIncompatibleSchemaError(args: { choice: "all" | "safe" | "none"; }): Promise { const { incompatibleSchemaError, options, instanceId, databaseId, choice } = args; + if (incompatibleSchemaError.destructive && choice === "safe") { + throw new FirebaseError( + "This schema migration includes potentially destructive changes. If you'd like to execute it anyway, rerun this command with --force", + ); + } const projectId = needProjectId(options); const iamUser = await setupIAMUser(instanceId, databaseId, options); @@ -187,7 +192,6 @@ async function promptForSchemaMigration( const choices = err.destructive ? [ { name: "Execute all changes (including destructive changes)", value: "all" }, - { name: "Execute only safe changes", value: "safe" }, { name: "Abort changes", value: "none" }, ] : [ @@ -297,7 +301,6 @@ async function ensureServiceIsConnectedToCloudSql( !currentSchema.primaryDatasource.postgresql || currentSchema.primaryDatasource.postgresql.schemaValidation === "STRICT" ) { - // If the current schema is "STRICT" mode, don't do this. return; } currentSchema.primaryDatasource.postgresql.schemaValidation = "STRICT"; From d30d96ca86d3f5600053a63df983af208943cd03 Mon Sep 17 00:00:00 2001 From: Harold Shen Date: Thu, 9 May 2024 16:58:18 -0400 Subject: [PATCH 29/40] fix non-null type support in generated mutations (#7147) * fix non-null type support in generated mutations * remove console log --- firebase-vscode/src/data-connect/ad-hoc-mutations.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/firebase-vscode/src/data-connect/ad-hoc-mutations.ts b/firebase-vscode/src/data-connect/ad-hoc-mutations.ts index 383ac1dbaa5..519d13cd297 100644 --- a/firebase-vscode/src/data-connect/ad-hoc-mutations.ts +++ b/firebase-vscode/src/data-connect/ad-hoc-mutations.ts @@ -160,11 +160,14 @@ query { mutation.push(`${functionSpacing}${name}_insert(data: {`); // insert function for (const field of ast.fields) { // necessary to avoid type error - const fieldType: any = field.type; - let fieldTypeName: string = fieldType.type.name.value; + let fieldType: any = field.type; + // We unwrap NonNullType to obtain the actual type + if (fieldType.kind === Kind.NON_NULL_TYPE) { + fieldType = fieldType.type; + } + let fieldTypeName: string = fieldType.name.value; let fieldName: string = field.name.value; let defaultValue = defaultScalarValues[fieldTypeName] as string; - if (!isDataConnectScalarType(fieldTypeName)) { fieldTypeName += "Id"; fieldName += "Id"; From 2361e0d1e3fd60d8c4094b512aa561de135941d2 Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 9 May 2024 17:12:31 -0400 Subject: [PATCH 30/40] Retry user creation when role is not ready (#7096) Co-authored-by: Harold Shen --- src/gcp/cloudsql/cloudsqladmin.ts | 66 ++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/src/gcp/cloudsql/cloudsqladmin.ts b/src/gcp/cloudsql/cloudsqladmin.ts index 9c8846b202d..c61c0aaeb86 100755 --- a/src/gcp/cloudsql/cloudsqladmin.ts +++ b/src/gcp/cloudsql/cloudsqladmin.ts @@ -162,28 +162,50 @@ export async function createUser( username: string, password?: string, ): Promise { - const op = await client.post( - `projects/${projectId}/instances/${instanceId}/users`, - { - name: username, - instance: instanceId, - project: projectId, - password: password, - sqlserverUserDetails: { - disabled: false, - serverRoles: ["cloudsqlsuperuser"], - }, - type, - }, - ); - const opName = `projects/${projectId}/operations/${op.body.name}`; - const pollRes = await operationPoller.pollOperation({ - apiOrigin: cloudSQLAdminOrigin(), - apiVersion: API_VERSION, - operationResourceName: opName, - doneFn: (op: Operation) => op.status === "DONE", - }); - return pollRes; + const maxRetries = 3; + let retries = 0; + while (true) { + try { + const op = await client.post( + `projects/${projectId}/instances/${instanceId}/users`, + { + name: username, + instance: instanceId, + project: projectId, + password: password, + sqlserverUserDetails: { + disabled: false, + serverRoles: ["cloudsqlsuperuser"], + }, + type, + }, + ); + const opName = `projects/${projectId}/operations/${op.body.name}`; + const pollRes = await operationPoller.pollOperation({ + apiOrigin: cloudSQLAdminOrigin(), + apiVersion: API_VERSION, + operationResourceName: opName, + doneFn: (op: Operation) => op.status === "DONE", + }); + return pollRes; + } catch (err: any) { + if (builtinRoleNotReady(err.message) && retries < maxRetries) { + retries++; + await new Promise((resolve) => { + setTimeout(resolve, 1000 * retries); + }); + } else { + throw err; + } + } + } +} + +// CloudSQL built in roles get created _after_ the operation is complete. +// This means that we occasionally bump into cases where we try to create the user +// before the role required for IAM users exists. +function builtinRoleNotReady(message: string): boolean { + return message.includes("cloudsqliamuser"); } export async function getUser( From 5d2bc85844ad73cf6232283e8404b808fa11d463 Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 9 May 2024 17:34:11 -0400 Subject: [PATCH 31/40] Using android/ios default ports (#7149) * Using android/ios default ports * format --- src/emulator/constants.ts | 2 +- src/emulator/dataconnectEmulator.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index 5731ae2cb72..c738c5d272f 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -13,7 +13,7 @@ export const DEFAULT_PORTS: { [s in Emulators]: number } = { auth: 9099, storage: 9199, eventarc: 9299, - dataconnect: 9399, + dataconnect: 9509, }; export const FIND_AVAILBLE_PORT_BY_DEFAULT: Record = { diff --git a/src/emulator/dataconnectEmulator.ts b/src/emulator/dataconnectEmulator.ts index 94dde153d17..1fc919a9af6 100644 --- a/src/emulator/dataconnectEmulator.ts +++ b/src/emulator/dataconnectEmulator.ts @@ -19,6 +19,8 @@ export interface DataConnectEmulatorArgs { rc: RC; } +const grpcDefaultPort = 9510; + export class DataConnectEmulator implements EmulatorInstance { constructor(private args: DataConnectEmulatorArgs) {} private logger = EmulatorLogger.forEmulator(Emulators.DATACONNECT); @@ -45,7 +47,7 @@ export class DataConnectEmulator implements EmulatorInstance { return start(Emulators.DATACONNECT, { ...this.args, http_port: port, - grpc_port: port + 1, + grpc_port: grpcDefaultPort, config_dir: this.args.configDir, local_connection_string: this.getLocalConectionString(), project_id: this.args.projectId, From 2164cb3bdeb8d1ca3f8e04ccd40330bb2d75f3f1 Mon Sep 17 00:00:00 2001 From: joehan Date: Thu, 9 May 2024 19:14:44 -0400 Subject: [PATCH 32/40] Only throw 5xx errors on ensure (#7151) --- src/dataconnect/schemaMigration.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/dataconnect/schemaMigration.ts b/src/dataconnect/schemaMigration.ts index 46df2dcc1b9..385dc2eb9d4 100644 --- a/src/dataconnect/schemaMigration.ts +++ b/src/dataconnect/schemaMigration.ts @@ -42,8 +42,8 @@ export async function migrateSchema(args: { const { options, schema, allowNonInteractiveMigration, validateOnly } = args; const { serviceName, instanceId, instanceName, databaseId } = getIdentifiers(schema); + await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId); try { - await ensureServiceIsConnectedToCloudSql(serviceName, instanceName, databaseId); await upsertSchema(schema, validateOnly); logger.debug(`Database schema was up to date for ${instanceId}:${databaseId}`); } catch (err: any) { @@ -304,7 +304,14 @@ async function ensureServiceIsConnectedToCloudSql( return; } currentSchema.primaryDatasource.postgresql.schemaValidation = "STRICT"; - await upsertSchema(currentSchema, /** validateOnly=*/ false); + try { + await upsertSchema(currentSchema, /** validateOnly=*/ false); + } catch (err: any) { + if (err.status >= 500) { + throw err; + } + logger.debug(err); + } } function displaySchemaChanges(error: IncompatibleSqlSchemaError) { From ddd1c56dc2ca1586455d9bb166468a2a23fd4508 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 9 May 2024 17:11:56 -0700 Subject: [PATCH 33/40] Cut a new release of the emulator (#7153) --- 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 e627e803518..234d04c59ab 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.16", - expectedSize: 25628592, - expectedChecksum: "e6ec9eecfe3e3721a5e19949447da530", + version: "1.1.17", + expectedSize: 25602224, + expectedChecksum: "1f9e3dd040a0ac4d1cb4d9dde4a3c0b0", } : { - version: "1.1.16", - expectedSize: 23061488, - expectedChecksum: "a255eaad9f943925002d738c7ab1252a", + version: "1.1.17", + expectedSize: 23036912, + expectedChecksum: "a0ec0517108f842ed06fea14fe7c7e56", }, }; From c9bd8a406f0291fa77706f718377bf43fd64d502 Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 10 May 2024 00:24:55 +0000 Subject: [PATCH 34/40] 13.8.2 --- 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 65ee86a1b02..de90c1756ba 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "13.8.1", + "version": "13.8.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "13.8.1", + "version": "13.8.2", "license": "MIT", "dependencies": { "@google-cloud/cloud-sql-connector": "^1.2.3", diff --git a/package.json b/package.json index f4dfcad1166..0e9af1be1f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "13.8.1", + "version": "13.8.2", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From d2e11729eb9b79880f1bf249f65b1309d4cc8c6d Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 10 May 2024 00:25:10 +0000 Subject: [PATCH 35/40] [firebase-release] Removed change log and reset repo after 13.8.2 release --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12bbbf80bf0..e69de29bb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ -- Release Firestore Emulator version 1.19.6 which fixes a few Datastore Mode bugs regarding transactions (#7132). From 03176c5940aa62d1bee7f64f3c78f3a6036a21e5 Mon Sep 17 00:00:00 2001 From: Harold Shen Date: Fri, 10 May 2024 12:48:33 -0400 Subject: [PATCH 36/40] only create one output channel for the language server (#7160) --- firebase-vscode/src/data-connect/index.ts | 14 ++++++++------ .../src/data-connect/language-client.ts | 6 +----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/firebase-vscode/src/data-connect/index.ts b/firebase-vscode/src/data-connect/index.ts index b8755760806..f3fdf8febd0 100644 --- a/firebase-vscode/src/data-connect/index.ts +++ b/firebase-vscode/src/data-connect/index.ts @@ -153,17 +153,19 @@ export function registerFdc( ); const schemaCodeLensProvider = new SchemaCodeLensProvider(emulatorController); + // activate language client/serer let client: LanguageClient; + const lsOutputChannel: vscode.OutputChannel = vscode.window.createOutputChannel( + "Firebase GraphQL Language Server", + ); + // 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, - ); + client = setupLanguageClient(context, configs, lsOutputChannel); vscode.commands.executeCommand("fdc-graphql.start"); } }), @@ -179,7 +181,7 @@ export function registerFdc( vscode.commands.executeCommand( "firebase.dataConnect.executeIntrospection", ); - runEmulatorIssuesStream(configs,fdcService.localEndpoint.value); + runEmulatorIssuesStream(configs, fdcService.localEndpoint.value); runDataConnectCompiler(configs, fdcService.localEndpoint.value); } }), @@ -205,7 +207,7 @@ export function registerFdc( return Disposable.from( codeActions, selectedProjectStatus, - {dispose: sub1}, + { dispose: sub1 }, { dispose: effect(() => { selectedProjectStatus.text = `$(mono-firebase) ${ diff --git a/firebase-vscode/src/data-connect/language-client.ts b/firebase-vscode/src/data-connect/language-client.ts index c4ca40e4222..32882606257 100644 --- a/firebase-vscode/src/data-connect/language-client.ts +++ b/firebase-vscode/src/data-connect/language-client.ts @@ -12,12 +12,8 @@ import { ResolvedDataConnectConfigs } from "./config"; export function setupLanguageClient( context, configs: ResolvedDataConnectConfigs, + outputChannel: vscode.OutputChannel, ) { - // activate language client/serer - const outputChannel: vscode.OutputChannel = vscode.window.createOutputChannel( - "Firebase GraphQL Language Server", - ); - const serverPath = path.join("dist", "server.js"); const serverModule = context.asAbsolutePath(serverPath); From 71ded1ce22ea022ab1cd28c5a5bc6db5a6e033a9 Mon Sep 17 00:00:00 2001 From: joehan Date: Fri, 10 May 2024 17:29:22 -0400 Subject: [PATCH 37/40] Readd platform specific dependencies to npm-shrinkwrap.json (#7162) --- npm-shrinkwrap.json | 483 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 483 insertions(+) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index de90c1756ba..1188a292365 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -980,6 +980,54 @@ "node": ">=16" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.16.tgz", + "integrity": "sha512-baLqRpLe4JnKrUXLJChoTN0iXZH7El/mu58GE3WIA6/H834k0XWvLRmGLG8y8arTRS9hJJibPnF0tiGhmWeZgw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.16.tgz", + "integrity": "sha512-QX48qmsEZW+gcHgTmAj+x21mwTz8MlYQBnzF6861cNdQGvj2jzzFjqH0EBabrIa/WVZ2CHolwMoqxVryqKt8+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.16.tgz", + "integrity": "sha512-G4wfHhrrz99XJgHnzFvB4UwwPxAWZaZBOFXh+JH1Duf1I4vIVfuYY9uVLpx4eiV2D/Jix8LJY+TAdZ3i40tDow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.17.16", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.16.tgz", @@ -996,6 +1044,294 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.16.tgz", + "integrity": "sha512-SzBQtCV3Pdc9kyizh36Ol+dNVhkDyIrGb/JXZqFq8WL37LIyrXU0gUpADcNV311sCOhvY+f2ivMhb5Tuv8nMOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.16.tgz", + "integrity": "sha512-ZqftdfS1UlLiH1DnS2u3It7l4Bc3AskKeu+paJSfk7RNOMrOxmeFDhLTMQqMxycP1C3oj8vgkAT6xfAuq7ZPRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.16.tgz", + "integrity": "sha512-rHV6zNWW1tjgsu0dKQTX9L0ByiJHHLvQKrWtnz8r0YYJI27FU3Xu48gpK2IBj1uCSYhJ+pEk6Y0Um7U3rIvV8g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.16.tgz", + "integrity": "sha512-n4O8oVxbn7nl4+m+ISb0a68/lcJClIbaGAoXwqeubj/D1/oMMuaAXmJVfFlRjJLu/ZvHkxoiFJnmbfp4n8cdSw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.16.tgz", + "integrity": "sha512-8yoZhGkU6aHu38WpaM4HrRLTFc7/VVD9Q2SvPcmIQIipQt2I/GMTZNdEHXoypbbGao5kggLcxg0iBKjo0SQYKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.16.tgz", + "integrity": "sha512-9ZBjlkdaVYxPNO8a7OmzDbOH9FMQ1a58j7Xb21UfRU29KcEEU3VTHk+Cvrft/BNv0gpWJMiiZ/f4w0TqSP0gLA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.16.tgz", + "integrity": "sha512-TIZTRojVBBzdgChY3UOG7BlPhqJz08AL7jdgeeu+kiObWMFzGnQD7BgBBkWRwOtKR1i2TNlO7YK6m4zxVjjPRQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.16.tgz", + "integrity": "sha512-UPeRuFKCCJYpBbIdczKyHLAIU31GEm0dZl1eMrdYeXDH+SJZh/i+2cAmD3A1Wip9pIc5Sc6Kc5cFUrPXtR0XHA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.16.tgz", + "integrity": "sha512-io6yShgIEgVUhExJejJ21xvO5QtrbiSeI7vYUnr7l+v/O9t6IowyhdiYnyivX2X5ysOVHAuyHW+Wyi7DNhdw6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.16.tgz", + "integrity": "sha512-WhlGeAHNbSdG/I2gqX2RK2gfgSNwyJuCiFHMc8s3GNEMMHUI109+VMBfhVqRb0ZGzEeRiibi8dItR3ws3Lk+cA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.16.tgz", + "integrity": "sha512-gHRReYsJtViir63bXKoFaQ4pgTyah4ruiMRQ6im9YZuv+gp3UFJkNTY4sFA73YDynmXZA6hi45en4BGhNOJUsw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.16.tgz", + "integrity": "sha512-mfiiBkxEbUHvi+v0P+TS7UnA9TeGXR48aK4XHkTj0ZwOijxexgMF01UDFaBX7Q6CQsB0d+MFNv9IiXbIHTNd4g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.16.tgz", + "integrity": "sha512-n8zK1YRDGLRZfVcswcDMDM0j2xKYLNXqei217a4GyBxHIuPMGrrVuJ+Ijfpr0Kufcm7C1k/qaIrGy6eG7wvgmA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.16.tgz", + "integrity": "sha512-lEEfkfsUbo0xC47eSTBqsItXDSzwzwhKUSsVaVjVji07t8+6KA5INp2rN890dHZeueXJAI8q0tEIfbwVRYf6Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.16.tgz", + "integrity": "sha512-jlRjsuvG1fgGwnE8Afs7xYDnGz0dBgTNZfgCK6TlvPH3Z13/P5pi6I57vyLE8qZYLrGVtwcm9UbUx1/mZ8Ukag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.16.tgz", + "integrity": "sha512-TzoU2qwVe2boOHl/3KNBUv2PNUc38U0TNnzqOAcgPiD/EZxT2s736xfC2dYQbszAwo4MKzzwBV0iHjhfjxMimg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.16.tgz", + "integrity": "sha512-B8b7W+oo2yb/3xmwk9Vc99hC9bNolvqjaTZYEfMQhzdpBsjTvZBlXQ/teUE55Ww6sg//wlcDjOaqldOKyigWdA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.16.tgz", + "integrity": "sha512-xJ7OH/nanouJO9pf03YsL9NAFQBHd8AqfrQd7Pf5laGyyTt/gToul6QYOA/i5i/q8y9iaM5DQFNTgpi995VkOg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -19582,6 +19918,27 @@ "jsdoc-type-pratt-parser": "~4.0.0" } }, + "@esbuild/android-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.16.tgz", + "integrity": "sha512-baLqRpLe4JnKrUXLJChoTN0iXZH7El/mu58GE3WIA6/H834k0XWvLRmGLG8y8arTRS9hJJibPnF0tiGhmWeZgw==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.16.tgz", + "integrity": "sha512-QX48qmsEZW+gcHgTmAj+x21mwTz8MlYQBnzF6861cNdQGvj2jzzFjqH0EBabrIa/WVZ2CHolwMoqxVryqKt8+Q==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.16.tgz", + "integrity": "sha512-G4wfHhrrz99XJgHnzFvB4UwwPxAWZaZBOFXh+JH1Duf1I4vIVfuYY9uVLpx4eiV2D/Jix8LJY+TAdZ3i40tDow==", + "dev": true, + "optional": true + }, "@esbuild/darwin-arm64": { "version": "0.17.16", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.16.tgz", @@ -19589,6 +19946,132 @@ "dev": true, "optional": true }, + "@esbuild/darwin-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.16.tgz", + "integrity": "sha512-SzBQtCV3Pdc9kyizh36Ol+dNVhkDyIrGb/JXZqFq8WL37LIyrXU0gUpADcNV311sCOhvY+f2ivMhb5Tuv8nMOQ==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.16.tgz", + "integrity": "sha512-ZqftdfS1UlLiH1DnS2u3It7l4Bc3AskKeu+paJSfk7RNOMrOxmeFDhLTMQqMxycP1C3oj8vgkAT6xfAuq7ZPRA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.16.tgz", + "integrity": "sha512-rHV6zNWW1tjgsu0dKQTX9L0ByiJHHLvQKrWtnz8r0YYJI27FU3Xu48gpK2IBj1uCSYhJ+pEk6Y0Um7U3rIvV8g==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.16.tgz", + "integrity": "sha512-n4O8oVxbn7nl4+m+ISb0a68/lcJClIbaGAoXwqeubj/D1/oMMuaAXmJVfFlRjJLu/ZvHkxoiFJnmbfp4n8cdSw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.16.tgz", + "integrity": "sha512-8yoZhGkU6aHu38WpaM4HrRLTFc7/VVD9Q2SvPcmIQIipQt2I/GMTZNdEHXoypbbGao5kggLcxg0iBKjo0SQYKA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.16.tgz", + "integrity": "sha512-9ZBjlkdaVYxPNO8a7OmzDbOH9FMQ1a58j7Xb21UfRU29KcEEU3VTHk+Cvrft/BNv0gpWJMiiZ/f4w0TqSP0gLA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.16.tgz", + "integrity": "sha512-TIZTRojVBBzdgChY3UOG7BlPhqJz08AL7jdgeeu+kiObWMFzGnQD7BgBBkWRwOtKR1i2TNlO7YK6m4zxVjjPRQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.16.tgz", + "integrity": "sha512-UPeRuFKCCJYpBbIdczKyHLAIU31GEm0dZl1eMrdYeXDH+SJZh/i+2cAmD3A1Wip9pIc5Sc6Kc5cFUrPXtR0XHA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.16.tgz", + "integrity": "sha512-io6yShgIEgVUhExJejJ21xvO5QtrbiSeI7vYUnr7l+v/O9t6IowyhdiYnyivX2X5ysOVHAuyHW+Wyi7DNhdw6Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.16.tgz", + "integrity": "sha512-WhlGeAHNbSdG/I2gqX2RK2gfgSNwyJuCiFHMc8s3GNEMMHUI109+VMBfhVqRb0ZGzEeRiibi8dItR3ws3Lk+cA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.16.tgz", + "integrity": "sha512-gHRReYsJtViir63bXKoFaQ4pgTyah4ruiMRQ6im9YZuv+gp3UFJkNTY4sFA73YDynmXZA6hi45en4BGhNOJUsw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.16.tgz", + "integrity": "sha512-mfiiBkxEbUHvi+v0P+TS7UnA9TeGXR48aK4XHkTj0ZwOijxexgMF01UDFaBX7Q6CQsB0d+MFNv9IiXbIHTNd4g==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.16.tgz", + "integrity": "sha512-n8zK1YRDGLRZfVcswcDMDM0j2xKYLNXqei217a4GyBxHIuPMGrrVuJ+Ijfpr0Kufcm7C1k/qaIrGy6eG7wvgmA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.16.tgz", + "integrity": "sha512-lEEfkfsUbo0xC47eSTBqsItXDSzwzwhKUSsVaVjVji07t8+6KA5INp2rN890dHZeueXJAI8q0tEIfbwVRYf6Ew==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.16.tgz", + "integrity": "sha512-jlRjsuvG1fgGwnE8Afs7xYDnGz0dBgTNZfgCK6TlvPH3Z13/P5pi6I57vyLE8qZYLrGVtwcm9UbUx1/mZ8Ukag==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.16.tgz", + "integrity": "sha512-TzoU2qwVe2boOHl/3KNBUv2PNUc38U0TNnzqOAcgPiD/EZxT2s736xfC2dYQbszAwo4MKzzwBV0iHjhfjxMimg==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.16.tgz", + "integrity": "sha512-B8b7W+oo2yb/3xmwk9Vc99hC9bNolvqjaTZYEfMQhzdpBsjTvZBlXQ/teUE55Ww6sg//wlcDjOaqldOKyigWdA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.17.16", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.16.tgz", + "integrity": "sha512-xJ7OH/nanouJO9pf03YsL9NAFQBHd8AqfrQd7Pf5laGyyTt/gToul6QYOA/i5i/q8y9iaM5DQFNTgpi995VkOg==", + "dev": true, + "optional": true + }, "@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", From b998cbab232b8682845bf1e43d41ee186558ab10 Mon Sep 17 00:00:00 2001 From: joehan Date: Fri, 10 May 2024 18:52:12 -0400 Subject: [PATCH 38/40] Change default dataconnect port back to 9399. (#7163) * Back to 9399 * changelog --- CHANGELOG.md | 1 + src/emulator/constants.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb2d..c1068b55926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Internal bug fixes. diff --git a/src/emulator/constants.ts b/src/emulator/constants.ts index c738c5d272f..5731ae2cb72 100644 --- a/src/emulator/constants.ts +++ b/src/emulator/constants.ts @@ -13,7 +13,7 @@ export const DEFAULT_PORTS: { [s in Emulators]: number } = { auth: 9099, storage: 9199, eventarc: 9299, - dataconnect: 9509, + dataconnect: 9399, }; export const FIND_AVAILBLE_PORT_BY_DEFAULT: Record = { From 98456ce5ed01dc7334e1f125172c9d7ed4b8448e Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 10 May 2024 23:03:51 +0000 Subject: [PATCH 39/40] 13.8.3 --- 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 1188a292365..8f95235881a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "13.8.2", + "version": "13.8.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "13.8.2", + "version": "13.8.3", "license": "MIT", "dependencies": { "@google-cloud/cloud-sql-connector": "^1.2.3", diff --git a/package.json b/package.json index 0e9af1be1f2..d20dccfcddd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "13.8.2", + "version": "13.8.3", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { From 24b60413b1d6408893452d9cd9b4a0e06a0d861b Mon Sep 17 00:00:00 2001 From: Google Open Source Bot Date: Fri, 10 May 2024 23:04:05 +0000 Subject: [PATCH 40/40] [firebase-release] Removed change log and reset repo after 13.8.3 release --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1068b55926..e69de29bb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +0,0 @@ -- Internal bug fixes.