diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f98ea1c0f7..e69de29bb2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +0,0 @@ -- Changes default CF3 runtime to nodejs22 (#8037) -- Fixed an issue where `--import` would error for the Data Connect emulator if `dataDir` was also set. -- Fixed an issue where `firebase init dataconnect` errored when importing a schema with no GQL files. diff --git a/firebase-vscode/src/logger-wrapper.ts b/firebase-vscode/src/logger-wrapper.ts index 4b453b8fbb4..20cd3c11023 100644 --- a/firebase-vscode/src/logger-wrapper.ts +++ b/firebase-vscode/src/logger-wrapper.ts @@ -44,28 +44,37 @@ export function logSetup() { // Log to file // Only log to file if firebase.debug extension setting is true. - // Re-implement file logger call from ../../src/bin/firebase.ts to not bring - // in the entire firebase.ts file - const rootFolders = getRootFolders(); - // Default to a central path, but write files to a local path if we're in a Firebase directory. - let filePath = path.join(os.homedir(), ".cache", "firebase", "logs", "vsce-debug.log"); - if (fs.existsSync(path.join(rootFolders[0], "firebase.json"))) { - filePath = path.join(rootFolders[0], ".firebase", "logs", "vsce-debug.log"); - } - pluginLogger.info("Logging to path", filePath); - cliLogger.add( - new transports.File({ - level: "debug", - filename: filePath, - format: format.printf((info) => { - const segments = [info.message, ...(info[SPLAT] || [])].map( - tryStringify, - ); - return `[${info.level}] ${stripVTControlCharacters(segments.join(" "))}`; - }), + // Re-implement file logger call from ../../src/bin/firebase.ts to not bring + // in the entire firebase.ts file + const rootFolders = getRootFolders(); + // Default to a central path, but write files to a local path if we're in a Firebase directory. + let filePath = path.join( + os.homedir(), + ".cache", + "firebase", + "logs", + "vsce-debug.log", + ); + if ( + rootFolders.length > 0 && + fs.existsSync(path.join(rootFolders[0], "firebase.json")) + ) { + filePath = path.join(rootFolders[0], ".firebase", "logs", "vsce-debug.log"); + } + pluginLogger.info("Logging to path", filePath); + cliLogger.add( + new transports.File({ + level: "debug", + filename: filePath, + format: format.printf((info) => { + const segments = [info.message, ...(info[SPLAT] || [])].map( + tryStringify, + ); + return `[${info.level}] ${stripVTControlCharacters(segments.join(" "))}`; }), - ); - cliLogger.add(new VSCodeOutputTransport({ level: "info" })); + }), + ); + cliLogger.add(new VSCodeOutputTransport({ level: "info" })); } /** diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 77d3aa60b92..bb54c19f7e9 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,12 +1,12 @@ { "name": "firebase-tools", - "version": "13.28.0", + "version": "13.29.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "firebase-tools", - "version": "13.28.0", + "version": "13.29.0", "license": "MIT", "dependencies": { "@electric-sql/pglite": "^0.2.0", diff --git a/package.json b/package.json index e87e0efa6f5..abeaa3c8af3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "firebase-tools", - "version": "13.28.0", + "version": "13.29.0", "description": "Command-Line Interface for Firebase", "main": "./lib/index.js", "bin": { diff --git a/src/deploy/functions/backend.ts b/src/deploy/functions/backend.ts index 0b5e4cd041a..113d1ac09dd 100644 --- a/src/deploy/functions/backend.ts +++ b/src/deploy/functions/backend.ts @@ -4,7 +4,7 @@ import * as utils from "../../utils"; import { Runtime } from "./runtimes/supported"; import { FirebaseError } from "../../error"; import { Context } from "./args"; -import { flattenArray } from "../../functional"; +import { assertExhaustive, flattenArray } from "../../functional"; /** Retry settings for a ScheduleSpec. */ export interface ScheduleRetryConfig { @@ -41,7 +41,9 @@ export interface HttpsTriggered { } /** API agnostic version of a Firebase callable function. */ -export type CallableTrigger = Record; +export type CallableTrigger = { + genkitAction?: string; +}; /** Something that has a callable trigger */ export interface CallableTriggered { @@ -135,6 +137,7 @@ export interface BlockingTrigger { eventType: string; options?: Record; } + export interface BlockingTriggered { blockingTrigger: BlockingTrigger; } @@ -153,9 +156,8 @@ export function endpointTriggerType(endpoint: Endpoint): string { return "taskQueue"; } else if (isBlockingTriggered(endpoint)) { return endpoint.blockingTrigger.eventType; - } else { - throw new Error("Unexpected trigger type for endpoint " + JSON.stringify(endpoint)); } + assertExhaustive(endpoint); } // TODO(inlined): Enum types should be singularly named diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index 6dc379c545d..4e5a0e5f6ec 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -74,8 +74,9 @@ export interface HttpsTrigger { // Trigger definitions for RPCs servers using the HTTP protocol defined at // https://firebase.google.com/docs/functions/callable-reference -// eslint-disable-next-line -interface CallableTrigger {} +interface CallableTrigger { + genkitAction?: string; +} // Trigger definitions for endpoints that should be called as a delegate for other operations. // For example, before user login. @@ -568,7 +569,9 @@ function discoverTrigger(endpoint: Endpoint, region: string, r: Resolver): backe } return { httpsTrigger }; } else if (isCallableTriggered(endpoint)) { - return { callableTrigger: {} }; + const trigger: CallableTriggered = { callableTrigger: {} }; + proto.copyIfPresent(trigger.callableTrigger, endpoint.callableTrigger, "genkitAction"); + return trigger; } else if (isBlockingTriggered(endpoint)) { return { blockingTrigger: endpoint.blockingTrigger }; } else if (isEventTriggered(endpoint)) { diff --git a/src/deploy/functions/release/planner.spec.ts b/src/deploy/functions/release/planner.spec.ts index 0e8de3127a3..80be985ef9f 100644 --- a/src/deploy/functions/release/planner.spec.ts +++ b/src/deploy/functions/release/planner.spec.ts @@ -46,6 +46,16 @@ describe("planner", () => { expect(() => planner.calculateUpdate(httpsFunc, scheduleFunc)).to.throw(); }); + it("allows upgrades of genkit functions from the genkit plugin to firebase-functions SDK", () => { + const httpsFunc = func("a", "b", { httpsTrigger: {} }); + const genkitFunc = func("a", "b", { callableTrigger: { genkitAction: "flows/flow" } }); + expect(planner.calculateUpdate(genkitFunc, httpsFunc)).to.deep.equal({ + // Missing: deleteAndRecreate + endpoint: genkitFunc, + unsafe: false, + }); + }); + it("knows to delete & recreate for v2 topic changes", () => { const original: backend.Endpoint = { ...func("a", "b", { diff --git a/src/deploy/functions/release/planner.ts b/src/deploy/functions/release/planner.ts index 74a137b24d0..9ae8872e215 100644 --- a/src/deploy/functions/release/planner.ts +++ b/src/deploy/functions/release/planner.ts @@ -8,10 +8,6 @@ import { FirebaseError } from "../../../error"; import * as utils from "../../../utils"; import * as backend from "../backend"; import * as v2events from "../../../functions/events/v2"; -import { - FIRESTORE_EVENT_REGEX, - FIRESTORE_EVENT_WITH_AUTH_CONTEXT_REGEX, -} from "../../../functions/events/v2"; export interface EndpointUpdate { endpoint: backend.Endpoint; @@ -261,9 +257,9 @@ export function upgradedScheduleFromV1ToV2( export function checkForUnsafeUpdate(want: backend.Endpoint, have: backend.Endpoint): boolean { return ( backend.isEventTriggered(want) && - FIRESTORE_EVENT_WITH_AUTH_CONTEXT_REGEX.test(want.eventTrigger.eventType) && backend.isEventTriggered(have) && - FIRESTORE_EVENT_REGEX.test(have.eventTrigger.eventType) + want.eventTrigger.eventType === + v2events.CONVERTABLE_EVENTS[have.eventTrigger.eventType as v2events.Event] ); } @@ -289,7 +285,12 @@ export function checkForIllegalUpdate(want: backend.Endpoint, have: backend.Endp }; const wantType = triggerType(want); const haveType = triggerType(have); - if (wantType !== haveType) { + + // Originally, @genkit-ai/firebase/functions defined onFlow which created an HTTPS trigger that implemented the streaming callable protocol for the Flow. + // The new version is firebase-functions/https which defines onCallFlow + const upgradingHttpsFunction = + backend.isHttpsTriggered(have) && backend.isCallableTriggered(want); + if (wantType !== haveType && !upgradingHttpsFunction) { throw new FirebaseError( `[${getFunctionLabel( want, diff --git a/src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts b/src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts index bba3b9b0484..f66349b0699 100644 --- a/src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts +++ b/src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts @@ -162,6 +162,35 @@ describe("buildFromV1Alpha", () => { }); }); + describe("genkitTriggers", () => { + it("fails with invalid fields", () => { + assertParserError({ + endpoints: { + func: { + ...MIN_ENDPOINT, + genkitTrigger: { + tool: "tools are not supported", + }, + }, + }, + }); + }); + + it("cannot be used with 1st gen", () => { + assertParserError({ + endpoints: { + func: { + ...MIN_ENDPOINT, + platform: "gcfv1", + genkitTrigger: { + flow: "agent", + }, + }, + }, + }); + }); + }); + describe("scheduleTriggers", () => { const validTrigger: build.ScheduleTrigger = { schedule: "every 5 minutes", diff --git a/src/deploy/functions/runtimes/discovery/v1alpha1.ts b/src/deploy/functions/runtimes/discovery/v1alpha1.ts index cc3a6e00c42..0436335d364 100644 --- a/src/deploy/functions/runtimes/discovery/v1alpha1.ts +++ b/src/deploy/functions/runtimes/discovery/v1alpha1.ts @@ -214,7 +214,9 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void { invoker: "array?", }); } else if (build.isCallableTriggered(ep)) { - // no-op + assertKeyTypes(prefix + ".callableTrigger", ep.callableTrigger, { + genkitAction: "string?", + }); } else if (build.isScheduleTriggered(ep)) { assertKeyTypes(prefix + ".scheduleTrigger", ep.scheduleTrigger, { schedule: "Field", @@ -263,6 +265,7 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void { options: "object", }); } else { + // TODO: Replace with assertExhaustive, which needs some type magic here because we have an any throw new FirebaseError( `Do not recognize trigger type for endpoint ${id}. Try upgrading ` + "firebase-tools with npm install -g firebase-tools@latest", @@ -310,6 +313,7 @@ function parseEndpointForBuild( copyIfPresent(triggered.httpsTrigger, ep.httpsTrigger, "invoker"); } else if (build.isCallableTriggered(ep)) { triggered = { callableTrigger: {} }; + copyIfPresent(triggered.callableTrigger, ep.callableTrigger, "genkitAction"); } else if (build.isScheduleTriggered(ep)) { const st: build.ScheduleTrigger = { // TODO: consider adding validation for fields like this that reject diff --git a/src/emulator/dataconnect/pgliteServer.ts b/src/emulator/dataconnect/pgliteServer.ts index 1aee1c2272c..8e6ce558252 100644 --- a/src/emulator/dataconnect/pgliteServer.ts +++ b/src/emulator/dataconnect/pgliteServer.ts @@ -1,6 +1,6 @@ // https://github.com/supabase-community/pg-gateway -import { PGlite, PGliteOptions } from "@electric-sql/pglite"; +import { DebugLevel, PGlite, PGliteOptions } from "@electric-sql/pglite"; // Unfortunately, we need to dynamically import the Postgres extensions. // They are only available as ESM, and if we import them normally, // our tsconfig will convert them to requires, which will cause errors @@ -18,6 +18,7 @@ import { import { fromNodeSocket } from "./pg-gateway/platforms/node"; import { logger } from "../../logger"; import { hasMessage } from "../../error"; + export const TRUNCATE_TABLES_SQL = ` DO $do$ BEGIN @@ -35,8 +36,11 @@ export class PostgresServer { private database: string; private dataDirectory?: string; private importPath?: string; + private debug: DebugLevel; public db: PGlite | undefined = undefined; + private server: net.Server | undefined = undefined; + public async createPGServer(host: string = "127.0.0.1", port: number): Promise { const getDb = this.getDb.bind(this); @@ -67,6 +71,7 @@ export class PostgresServer { server.emit("error", err); }); }); + this.server = server; const listeningPromise = new Promise((resolve) => { server.listen(port, host, () => { @@ -86,7 +91,7 @@ export class PostgresServer { const pgliteArgs: PGliteOptions = { username: this.username, database: this.database, - debug: 0, + debug: this.debug, extensions: { vector, uuidOssp, @@ -132,11 +137,28 @@ export class PostgresServer { } } - constructor(database: string, username: string, dataDirectory?: string, importPath?: string) { - this.username = username; - this.database = database; - this.dataDirectory = dataDirectory; - this.importPath = importPath; + public async stop(): Promise { + if (this.db) { + await this.db.close(); + } + if (this.server) { + this.server.close(); + } + return; + } + + constructor(args: { + database: string; + username: string; + dataDirectory?: string; + importPath?: string; + debug?: boolean; + }) { + this.username = args.username; + this.database = args.database; + this.dataDirectory = args.dataDirectory; + this.importPath = args.importPath; + this.debug = args.debug ? 5 : 0; } } diff --git a/src/emulator/dataconnectEmulator.ts b/src/emulator/dataconnectEmulator.ts index a1e83910bc8..243229c833a 100644 --- a/src/emulator/dataconnectEmulator.ts +++ b/src/emulator/dataconnectEmulator.ts @@ -40,6 +40,7 @@ export interface DataConnectEmulatorArgs { enable_output_schema_extensions: boolean; enable_output_generated_sdk: boolean; importPath?: string; + debug?: boolean; } export interface DataConnectGenerateArgs { @@ -116,7 +117,13 @@ export class DataConnectEmulator implements EmulatorInstance { const postgresDumpPath = this.args.importPath ? path.join(this.args.importPath, "postgres.tar.gz") : undefined; - this.postgresServer = new PostgresServer(dbId, "postgres", dataDirectory, postgresDumpPath); + this.postgresServer = new PostgresServer({ + database: dbId, + username: "fdc", + dataDirectory, + importPath: postgresDumpPath, + debug: this.args.debug, + }); const server = await this.postgresServer.createPGServer(pgHost, pgPort); const connectableHost = connectableHostname(pgHost); connStr = `postgres://${connectableHost}:${pgPort}/${dbId}?sslmode=disable`; @@ -166,6 +173,9 @@ export class DataConnectEmulator implements EmulatorInstance { ); return; } + if (this.postgresServer) { + await this.postgresServer.stop(); + } return stop(Emulators.DATACONNECT); } diff --git a/src/emulator/downloadableEmulators.ts b/src/emulator/downloadableEmulators.ts index 51431926b66..b9be3704f2b 100755 --- a/src/emulator/downloadableEmulators.ts +++ b/src/emulator/downloadableEmulators.ts @@ -59,20 +59,20 @@ const EMULATOR_UPDATE_DETAILS: { [s in DownloadableEmulators]: EmulatorUpdateDet dataconnect: process.platform === "darwin" ? { - version: "1.7.4", - expectedSize: 25277184, - expectedChecksum: "74f6b66c79a8a903132c7ab26c644593", + version: "1.7.5", + expectedSize: 25281280, + expectedChecksum: "85d0de96b5c08b553fd8506a2bc381bb", } : process.platform === "win32" ? { - version: "1.7.4", - expectedSize: 25707520, - expectedChecksum: "66eec92e2d57ae42a8b58f33b65b4184", + version: "1.7.5", + expectedSize: 25711616, + expectedChecksum: "c99d67fa8e74d41760b96122b055b8e2", } : { - version: "1.7.4", + version: "1.7.5", expectedSize: 25190552, - expectedChecksum: "acb7be487020afa6e1a597ceb8c6e862", + expectedChecksum: "61d966b781e6f2887f8b38ec271b54e2", }, }; diff --git a/src/functions/events/v2.ts b/src/functions/events/v2.ts index 15132856a93..cd897d9a5ee 100644 --- a/src/functions/events/v2.ts +++ b/src/functions/events/v2.ts @@ -33,10 +33,6 @@ export const FIRESTORE_EVENTS = [ export const FIREALERTS_EVENT = "google.firebase.firebasealerts.alerts.v1.published"; -export const FIRESTORE_EVENT_REGEX = /^google\.cloud\.firestore\.document\.v1\.[^\.]*$/; -export const FIRESTORE_EVENT_WITH_AUTH_CONTEXT_REGEX = - /^google\.cloud\.firestore\.document\.v1\..*\.withAuthContext$/; - export type Event = | typeof PUBSUB_PUBLISH_EVENT | (typeof STORAGE_EVENTS)[number] @@ -46,3 +42,17 @@ export type Event = | typeof TEST_LAB_EVENT | (typeof FIRESTORE_EVENTS)[number] | typeof FIREALERTS_EVENT; + +// Why can't auth context be removed? This is map was added to correct a bug where a regex +// allowed any non-auth type to be converted to any auth type, but we should follow up for why +// a functon can't opt into reducing PII. +export const CONVERTABLE_EVENTS: Partial> = { + "google.cloud.firestore.document.v1.created": + "google.cloud.firestore.document.v1.created.withAuthContext", + "google.cloud.firestore.document.v1.updated": + "google.cloud.firestore.document.v1.updated.withAuthContext", + "google.cloud.firestore.document.v1.deleted": + "google.cloud.firestore.document.v1.deleted.withAuthContext", + "google.cloud.firestore.document.v1.written": + "google.cloud.firestore.document.v1.written.withAuthContext", +}; diff --git a/src/gcp/cloudfunctionsv2.spec.ts b/src/gcp/cloudfunctionsv2.spec.ts index 4d3b89eab35..ef9486f9b9f 100644 --- a/src/gcp/cloudfunctionsv2.spec.ts +++ b/src/gcp/cloudfunctionsv2.spec.ts @@ -221,6 +221,23 @@ describe("cloudfunctionsv2", () => { [BLOCKING_LABEL]: "before-sign-in", }, }); + + expect( + cloudfunctionsv2.functionFromEndpoint({ + ...ENDPOINT, + platform: "gcfv2", + callableTrigger: { + genkitAction: "flows/flow", + }, + }), + ).to.deep.equal({ + ...CLOUD_FUNCTION_V2, + labels: { + ...CLOUD_FUNCTION_V2.labels, + "deployment-callable": "true", + "genkit-action": "flows/flow", + }, + }); }); it("should copy trival fields", () => { @@ -637,6 +654,29 @@ describe("cloudfunctionsv2", () => { }); }); + it("should translate genkit callables", () => { + expect( + cloudfunctionsv2.endpointFromFunction({ + ...HAVE_CLOUD_FUNCTION_V2, + labels: { + "deployment-callable": "true", + "genkit-action": "flows/flow", + }, + }), + ).to.deep.equal({ + ...ENDPOINT, + callableTrigger: { + genkitAction: "flows/flow", + }, + platform: "gcfv2", + uri: GCF_URL, + labels: { + "deployment-callable": "true", + "genkit-action": "flows/flow", + }, + }); + }); + it("should copy optional fields", () => { const extraFields: backend.ServiceConfiguration = { ingressSettings: "ALLOW_ALL", diff --git a/src/gcp/cloudfunctionsv2.ts b/src/gcp/cloudfunctionsv2.ts index af1cb92984a..6d17c607bbc 100644 --- a/src/gcp/cloudfunctionsv2.ts +++ b/src/gcp/cloudfunctionsv2.ts @@ -609,6 +609,9 @@ export function functionFromEndpoint(endpoint: backend.Endpoint): InputCloudFunc gcfFunction.labels = { ...gcfFunction.labels, "deployment-taskqueue": "true" }; } else if (backend.isCallableTriggered(endpoint)) { gcfFunction.labels = { ...gcfFunction.labels, "deployment-callable": "true" }; + if (endpoint.callableTrigger.genkitAction) { + gcfFunction.labels["genkit-action"] = endpoint.callableTrigger.genkitAction; + } } else if (backend.isBlockingTriggered(endpoint)) { gcfFunction.labels = { ...gcfFunction.labels, @@ -654,6 +657,9 @@ export function endpointFromFunction(gcfFunction: OutputCloudFunction): backend. trigger = { callableTrigger: {}, }; + if (gcfFunction.labels["genkit-action"]) { + trigger.callableTrigger.genkitAction = gcfFunction.labels["genkit-action"]; + } } else if (gcfFunction.labels?.[BLOCKING_LABEL]) { trigger = { blockingTrigger: { diff --git a/src/management/projects.ts b/src/management/projects.ts index 93076da1699..9f25bc4552b 100644 --- a/src/management/projects.ts +++ b/src/management/projects.ts @@ -33,12 +33,30 @@ export const PROJECTS_CREATE_QUESTIONS: Question[] = [ message: "Please specify a unique project id " + `(${clc.yellow("warning")}: cannot be modified afterward) [6-30 characters]:\n`, + validate: (projectId: string) => { + if (projectId.length < 6) { + return "Project ID must be at least 6 characters long"; + } else if (projectId.length > 30) { + return "Project ID cannot be longer than 30 characters"; + } else { + return true; + } + }, }, { type: "input", name: "displayName", - default: "", + default: (answers: any) => answers.projectId, message: "What would you like to call your project? (defaults to your project ID)", + validate: (displayName: string) => { + if (displayName.length < 4) { + return "Project name must be at least 4 characters long"; + } else if (displayName.length > 30) { + return "Project name cannot be longer than 30 characters"; + } else { + return true; + } + }, }, ]; diff --git a/templates/init/functions/typescript/tsconfig.json b/templates/init/functions/typescript/tsconfig.json index 7ce05d039d6..57b915f3cc9 100644 --- a/templates/init/functions/typescript/tsconfig.json +++ b/templates/init/functions/typescript/tsconfig.json @@ -1,6 +1,8 @@ { "compilerOptions": { - "module": "commonjs", + "module": "NodeNext", + "esModuleInterop": true, + "moduleResolution": "nodenext", "noImplicitReturns": true, "noUnusedLocals": true, "outDir": "lib",