diff --git a/src/deploy/functions/backend.ts b/src/deploy/functions/backend.ts index 411ed90d466..cdca11775af 100644 --- a/src/deploy/functions/backend.ts +++ b/src/deploy/functions/backend.ts @@ -43,6 +43,16 @@ export interface HttpsTriggered { httpsTrigger: HttpsTrigger; } +/** API agnostic version of a Firebase Data Connect HTTPS trigger. */ +export interface DataConnectGraphqlTrigger { + invoker?: string[] | null; +} + +/** Something that has a Data Connect HTTPS trigger */ +export interface DataConnectGraphqlTriggered { + dataConnectGraphqlTrigger: DataConnectGraphqlTrigger; +} + /** API agnostic version of a Firebase callable function. */ export type CallableTrigger = { genkitAction?: string; @@ -151,6 +161,8 @@ export function endpointTriggerType(endpoint: Endpoint): string { return "scheduled"; } else if (isHttpsTriggered(endpoint)) { return "https"; + } else if (isDataConnectGraphqlTriggered(endpoint)) { + return "dataConnectGraphql"; } else if (isCallableTriggered(endpoint)) { return "callable"; } else if (isEventTriggered(endpoint)) { @@ -305,6 +317,7 @@ export type FunctionsPlatform = (typeof AllFunctionsPlatforms)[number]; export type Triggered = | HttpsTriggered + | DataConnectGraphqlTriggered | CallableTriggered | EventTriggered | ScheduleTriggered @@ -316,6 +329,13 @@ export function isHttpsTriggered(triggered: Triggered): triggered is HttpsTrigge return {}.hasOwnProperty.call(triggered, "httpsTrigger"); } +/** Whether something has a DataConnectGraphqlTrigger */ +export function isDataConnectGraphqlTriggered( + triggered: Triggered, +): triggered is DataConnectGraphqlTriggered { + return {}.hasOwnProperty.call(triggered, "dataConnectGraphqlTrigger"); +} + /** Whether something has a CallableTrigger */ export function isCallableTriggered(triggered: Triggered): triggered is CallableTriggered { return {}.hasOwnProperty.call(triggered, "callableTrigger"); diff --git a/src/deploy/functions/build.spec.ts b/src/deploy/functions/build.spec.ts index 64b604aa164..1eba1fdb9a0 100644 --- a/src/deploy/functions/build.spec.ts +++ b/src/deploy/functions/build.spec.ts @@ -75,7 +75,7 @@ describe("toBackend", () => { expect(Object.keys(backend.endpoints).length).to.equal(0); }); - it("populates multiple specified invokers correctly", () => { + it("populates multiple specified https invokers correctly", () => { const desiredBuild: build.Build = build.of({ func: { platform: "gcfv1", @@ -113,6 +113,46 @@ describe("toBackend", () => { } }); + it("populates multiple specified data connect https invokers correctly", () => { + const desiredBuild: build.Build = build.of({ + func: { + platform: "gcfv2", + region: ["us-central1"], + project: "project", + runtime: "nodejs16", + entryPoint: "func", + maxInstances: 42, + minInstances: 1, + serviceAccount: "service-account-1@", + vpc: { + connector: "projects/project/locations/region/connectors/connector", + egressSettings: "PRIVATE_RANGES_ONLY", + }, + ingressSettings: "ALLOW_ALL", + labels: { + test: "testing", + }, + dataConnectGraphqlTrigger: { + invoker: ["service-account-1@", "service-account-2@"], + }, + }, + }); + const backend = build.toBackend(desiredBuild, {}); + expect(Object.keys(backend.endpoints).length).to.equal(1); + const endpointDef = Object.values(backend.endpoints)[0]; + expect(endpointDef).to.not.equal(undefined); + if (endpointDef) { + expect(endpointDef.func.id).to.equal("func"); + expect(endpointDef.func.project).to.equal("project"); + expect(endpointDef.func.region).to.equal("us-central1"); + expect( + "dataConnectGraphqlTrigger" in endpointDef.func + ? endpointDef.func.dataConnectGraphqlTrigger.invoker + : [], + ).to.have.members(["service-account-1@", "service-account-2@"]); + } + }); + it("populates multiple param values", () => { const desiredBuild: build.Build = build.of({ func: { diff --git a/src/deploy/functions/build.ts b/src/deploy/functions/build.ts index c786a22c907..20c361b8f1c 100644 --- a/src/deploy/functions/build.ts +++ b/src/deploy/functions/build.ts @@ -71,6 +71,12 @@ export interface HttpsTrigger { invoker?: Array> | null; } +export interface DataConnectGraphqlTrigger { + // Which service account should be able to trigger this function. No value means that only the + // Firebase Data Connect P4SA can trigger this function. For more, see go/cf3-http-access-control + invoker?: Array> | null; +} + // Trigger definitions for RPCs servers using the HTTP protocol defined at // https://firebase.google.com/docs/functions/callable-reference interface CallableTrigger { @@ -150,6 +156,7 @@ export interface ScheduleTrigger { } export type HttpsTriggered = { httpsTrigger: HttpsTrigger }; +export type DataConnectGraphqlTriggered = { dataConnectGraphqlTrigger: DataConnectGraphqlTrigger }; export type CallableTriggered = { callableTrigger: CallableTrigger }; export type BlockingTriggered = { blockingTrigger: BlockingTrigger }; export type EventTriggered = { eventTrigger: EventTrigger }; @@ -157,6 +164,7 @@ export type ScheduleTriggered = { scheduleTrigger: ScheduleTrigger }; export type TaskQueueTriggered = { taskQueueTrigger: TaskQueueTrigger }; export type Triggered = | HttpsTriggered + | DataConnectGraphqlTriggered | CallableTriggered | BlockingTriggered | EventTriggered @@ -168,6 +176,13 @@ export function isHttpsTriggered(triggered: Triggered): triggered is HttpsTrigge return {}.hasOwnProperty.call(triggered, "httpsTrigger"); } +/** Whether something has a DataConnectGraphqlTrigger */ +export function isDataConnectGraphqlTriggered( + triggered: Triggered, +): triggered is DataConnectGraphqlTriggered { + return {}.hasOwnProperty.call(triggered, "dataConnectGraphqlTrigger"); +} + /** Whether something has a CallableTrigger */ export function isCallableTriggered(triggered: Triggered): triggered is CallableTriggered { return {}.hasOwnProperty.call(triggered, "callableTrigger"); @@ -559,6 +574,16 @@ function discoverTrigger(endpoint: Endpoint, region: string, r: Resolver): backe httpsTrigger.invoker = endpoint.httpsTrigger.invoker.map(r.resolveString); } return { httpsTrigger }; + } else if (isDataConnectGraphqlTriggered(endpoint)) { + const dataConnectGraphqlTrigger: backend.DataConnectGraphqlTrigger = {}; + if (endpoint.dataConnectGraphqlTrigger.invoker === null) { + dataConnectGraphqlTrigger.invoker = null; + } else if (typeof endpoint.dataConnectGraphqlTrigger.invoker !== "undefined") { + dataConnectGraphqlTrigger.invoker = endpoint.dataConnectGraphqlTrigger.invoker.map( + r.resolveString, + ); + } + return { dataConnectGraphqlTrigger }; } else if (isCallableTriggered(endpoint)) { const trigger: CallableTriggered = { callableTrigger: {} }; proto.copyIfPresent(trigger.callableTrigger, endpoint.callableTrigger, "genkitAction"); diff --git a/src/deploy/functions/checkIam.ts b/src/deploy/functions/checkIam.ts index 211aa1a440c..2d1c4607eaf 100644 --- a/src/deploy/functions/checkIam.ts +++ b/src/deploy/functions/checkIam.ts @@ -77,7 +77,7 @@ export async function checkHttpIam( const filters = context.filters || getEndpointFilters(options, context.config!); const wantBackends = Object.values(payload.functions).map(({ wantBackend }) => wantBackend); const httpEndpoints = [...flattenArray(wantBackends.map((b) => backend.allEndpoints(b)))] - .filter(backend.isHttpsTriggered) + .filter(backend.isHttpsTriggered || backend.isDataConnectGraphqlTriggered) .filter((f) => endpointMatchesAnyFilter(f, filters)); const existing = await backend.existingBackend(context); diff --git a/src/deploy/functions/prepare.ts b/src/deploy/functions/prepare.ts index 0cc513dda0b..a5dc7da34ed 100644 --- a/src/deploy/functions/prepare.ts +++ b/src/deploy/functions/prepare.ts @@ -112,6 +112,7 @@ export async function prepare( // This drives GA4 metric `has_runtime_config` in the functions deploy reporter. context.hasRuntimeConfig = Object.keys(runtimeConfig).some((k) => k !== "firebase"); + // TODO: Modify to also load dataconnect schema if `onGraphRequest` is used with `schemaFilePath`. const wantBuilds = await loadCodebases( context.config, options, diff --git a/src/deploy/functions/release/fabricator.spec.ts b/src/deploy/functions/release/fabricator.spec.ts index 01ff014b849..3525821ae1a 100644 --- a/src/deploy/functions/release/fabricator.spec.ts +++ b/src/deploy/functions/release/fabricator.spec.ts @@ -708,6 +708,48 @@ describe("Fabricator", () => { }); }); + describe("dataConnectGraphqlTrigger", () => { + it("doesn't set invoker by default", async () => { + gcfv2.createFunction.resolves({ name: "op", done: false }); + poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); + run.setInvokerCreate.resolves(); + const ep = endpoint({ dataConnectGraphqlTrigger: {} }, { platform: "gcfv2" }); + + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); + expect(run.setInvokerCreate).to.not.have.been.called; + }); + + it("sets explicit invoker", async () => { + gcfv2.createFunction.resolves({ name: "op", done: false }); + poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); + run.setInvokerCreate.resolves(); + const ep = endpoint( + { + dataConnectGraphqlTrigger: { + invoker: ["custom@"], + }, + }, + { platform: "gcfv2" }, + ); + + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); + expect(run.setInvokerCreate).to.have.been.calledWith(ep.project, "service", ["custom@"]); + }); + + it("doesn't set private invoker on create", async () => { + gcfv2.createFunction.resolves({ name: "op", done: false }); + poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); + run.setInvokerCreate.resolves(); + const ep = endpoint( + { dataConnectGraphqlTrigger: { invoker: ["private"] } }, + { platform: "gcfv2" }, + ); + + await fab.createV2Function(ep, new scraper.SourceTokenScraper()); + expect(run.setInvokerCreate).to.not.have.been.called; + }); + }); + describe("callableTrigger", () => { it("always sets invoker to public", async () => { gcfv2.createFunction.resolves({ name: "op", done: false }); @@ -854,6 +896,23 @@ describe("Fabricator", () => { expect(run.setInvokerUpdate).to.have.been.calledWith(ep.project, "service", ["custom@"]); }); + it("sets explicit invoker on dataConnectGraphqlTrigger", async () => { + gcfv2.updateFunction.resolves({ name: "op", done: false }); + poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); + run.setInvokerUpdate.resolves(); + const ep = endpoint( + { + dataConnectGraphqlTrigger: { + invoker: ["custom@"], + }, + }, + { platform: "gcfv2" }, + ); + + await fab.updateV2Function(ep, new scraper.SourceTokenScraper()); + expect(run.setInvokerUpdate).to.have.been.calledWith(ep.project, "service", ["custom@"]); + }); + it("sets explicit invoker on taskQueueTrigger", async () => { gcfv2.updateFunction.resolves({ name: "op", done: false }); poller.pollOperation.resolves({ serviceConfig: { service: "service" } }); diff --git a/src/deploy/functions/release/fabricator.ts b/src/deploy/functions/release/fabricator.ts index d0c7d5e1ce3..001ab0cfaf9 100644 --- a/src/deploy/functions/release/fabricator.ts +++ b/src/deploy/functions/release/fabricator.ts @@ -421,6 +421,13 @@ export class Fabricator { .run(() => run.setInvokerCreate(endpoint.project, serviceName, invoker)) .catch(rethrowAs(endpoint, "set invoker")); } + } else if (backend.isDataConnectGraphqlTriggered(endpoint)) { + const invoker = endpoint.dataConnectGraphqlTrigger.invoker; + if (invoker && !invoker.includes("private")) { + await this.executor + .run(() => run.setInvokerCreate(endpoint.project, serviceName, invoker)) + .catch(rethrowAs(endpoint, "set invoker")); + } } else if (backend.isCallableTriggered(endpoint)) { // Callable functions should always be public await this.executor @@ -547,6 +554,11 @@ export class Fabricator { let invoker: string[] | undefined; if (backend.isHttpsTriggered(endpoint)) { invoker = endpoint.httpsTrigger.invoker === null ? ["public"] : endpoint.httpsTrigger.invoker; + } else if (backend.isDataConnectGraphqlTriggered(endpoint)) { + invoker = + endpoint.dataConnectGraphqlTrigger.invoker === null + ? undefined + : endpoint.dataConnectGraphqlTrigger.invoker; } else if (backend.isTaskQueueTriggered(endpoint)) { invoker = endpoint.taskQueueTrigger.invoker === null ? [] : endpoint.taskQueueTrigger.invoker; } else if ( diff --git a/src/deploy/functions/release/index.ts b/src/deploy/functions/release/index.ts index 97705a8cdb2..d3ff18f22fc 100644 --- a/src/deploy/functions/release/index.ts +++ b/src/deploy/functions/release/index.ts @@ -127,7 +127,9 @@ export async function release( * has updated the URI of endpoints after deploy. */ export function printTriggerUrls(results: backend.Backend): void { - const httpsFunctions = backend.allEndpoints(results).filter(backend.isHttpsTriggered); + const httpsFunctions = backend + .allEndpoints(results) + .filter(backend.isHttpsTriggered || backend.isDataConnectGraphqlTriggered); if (httpsFunctions.length === 0) { return; } diff --git a/src/deploy/functions/release/planner.ts b/src/deploy/functions/release/planner.ts index c125c183bec..b14b155f6ef 100644 --- a/src/deploy/functions/release/planner.ts +++ b/src/deploy/functions/release/planner.ts @@ -269,6 +269,8 @@ export function checkForIllegalUpdate(want: backend.Endpoint, have: backend.Endp const triggerType = (e: backend.Endpoint): string => { if (backend.isHttpsTriggered(e)) { return "an HTTPS"; + } else if (backend.isDataConnectGraphqlTriggered(e)) { + return "a Data Connect HTTPS"; } else if (backend.isCallableTriggered(e)) { return "a callable"; } else if (backend.isEventTriggered(e)) { diff --git a/src/deploy/functions/release/reporter.ts b/src/deploy/functions/release/reporter.ts index b3e8fc827dc..fa395bc9dab 100644 --- a/src/deploy/functions/release/reporter.ts +++ b/src/deploy/functions/release/reporter.ts @@ -265,6 +265,10 @@ export function triggerTag(endpoint: backend.Endpoint): string { return `${prefix}.https`; } + if (backend.isDataConnectGraphqlTriggered(endpoint)) { + return `${prefix}.dataConnectGraphql`; + } + if (backend.isBlockingTriggered(endpoint)) { return `${prefix}.blocking`; } diff --git a/src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts b/src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts index 5aece13566a..5c56a22e72b 100644 --- a/src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts +++ b/src/deploy/functions/runtimes/discovery/v1alpha1.spec.ts @@ -162,6 +162,33 @@ describe("buildFromV1Alpha", () => { }); }); + describe("dataConnectGraphqlTriggers", () => { + it("invalid value for Data Connect https trigger key invoker", () => { + assertParserError({ + endpoints: { + func: { + ...MIN_ENDPOINT, + dataConnectGraphqlTrigger: { invoker: 42 }, + }, + }, + }); + }); + + it("cannot be used with 1st gen", () => { + assertParserError({ + endpoints: { + func: { + ...MIN_ENDPOINT, + platform: "gcfv1", + dataConnectGraphqlTrigger: { + invoker: "custom@", + }, + }, + }, + }); + }); + }); + describe("genkitTriggers", () => { it("fails with invalid fields", () => { assertParserError({ diff --git a/src/deploy/functions/runtimes/discovery/v1alpha1.ts b/src/deploy/functions/runtimes/discovery/v1alpha1.ts index 8afaeb1f016..7c9ddfa0c27 100644 --- a/src/deploy/functions/runtimes/discovery/v1alpha1.ts +++ b/src/deploy/functions/runtimes/discovery/v1alpha1.ts @@ -40,6 +40,7 @@ type WireEventTrigger = build.EventTrigger & { export type WireEndpoint = build.Triggered & Partial & + Partial & Partial & Partial<{ eventTrigger: WireEventTrigger }> & Partial & @@ -158,6 +159,7 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void { environmentVariables: "object?", secretEnvironmentVariables: "array?", httpsTrigger: "object", + dataConnectGraphqlTrigger: "object", callableTrigger: "object", eventTrigger: "object", scheduleTrigger: "object", @@ -176,6 +178,9 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void { if (ep.httpsTrigger) { triggerCount++; } + if (ep.dataConnectGraphqlTrigger) { + triggerCount++; + } if (ep.callableTrigger) { triggerCount++; } @@ -213,6 +218,10 @@ function assertBuildEndpoint(ep: WireEndpoint, id: string): void { assertKeyTypes(prefix + ".httpsTrigger", ep.httpsTrigger, { invoker: "array?", }); + } else if (build.isDataConnectGraphqlTriggered(ep)) { + assertKeyTypes(prefix + ".dataConnectGraphqlTrigger", ep.dataConnectGraphqlTrigger, { + invoker: "array?", + }); } else if (build.isCallableTriggered(ep)) { assertKeyTypes(prefix + ".callableTrigger", ep.callableTrigger, { genkitAction: "string?", @@ -311,6 +320,9 @@ function parseEndpointForBuild( } else if (build.isHttpsTriggered(ep)) { triggered = { httpsTrigger: {} }; copyIfPresent(triggered.httpsTrigger, ep.httpsTrigger, "invoker"); + } else if (build.isDataConnectGraphqlTriggered(ep)) { + triggered = { dataConnectGraphqlTrigger: {} }; + copyIfPresent(triggered.dataConnectGraphqlTrigger, ep.dataConnectGraphqlTrigger, "invoker"); } else if (build.isCallableTriggered(ep)) { triggered = { callableTrigger: {} }; copyIfPresent(triggered.callableTrigger, ep.callableTrigger, "genkitAction"); diff --git a/src/deploy/functions/services/dataconnect.spec.ts b/src/deploy/functions/services/dataconnect.spec.ts index 359efe80cba..20ce378edde 100644 --- a/src/deploy/functions/services/dataconnect.spec.ts +++ b/src/deploy/functions/services/dataconnect.spec.ts @@ -46,3 +46,57 @@ describe("ensureDatabaseTriggerRegion", () => { ); }); }); + +describe("obtainDataConnectBindings", () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("should return the correct binding for autopush", async () => { + process.env.FIREBASE_DATACONNECT_URL = + "https://autopush-firebasedataconnect.sandbox.googleapis.com"; + + const bindings = await dataconnect.obtainDataConnectBindings(projectNumber); + + expect(bindings.length).to.equal(1); + expect(bindings[0]).to.deep.equal({ + role: "roles/run.invoker", + members: [ + `serviceAccount:service-${projectNumber}@gcp-sa-autopush-dataconnect.iam.gserviceaccount.com`, + ], + }); + }); + + it("should return the correct binding for staging", async () => { + process.env.FIREBASE_DATACONNECT_URL = + "https://staging-firebasedataconnect.sandbox.googleapis.com"; + + const bindings = await dataconnect.obtainDataConnectBindings(projectNumber); + + expect(bindings.length).to.equal(1); + expect(bindings[0]).to.deep.equal({ + role: "roles/run.invoker", + members: [ + `serviceAccount:service-${projectNumber}@gcp-sa-staging-dataconnect.iam.gserviceaccount.com`, + ], + }); + }); + + it("should return the correct binding for prod", async () => { + const bindings = await dataconnect.obtainDataConnectBindings(projectNumber); + + expect(bindings.length).to.equal(1); + expect(bindings[0]).to.deep.equal({ + role: "roles/run.invoker", + members: [ + `serviceAccount:service-${projectNumber}@gcp-sa-firebasedataconnect.iam.gserviceaccount.com`, + ], + }); + }); +}); diff --git a/src/deploy/functions/services/dataconnect.ts b/src/deploy/functions/services/dataconnect.ts index 9ef1042798e..f44ca3d704f 100644 --- a/src/deploy/functions/services/dataconnect.ts +++ b/src/deploy/functions/services/dataconnect.ts @@ -1,5 +1,12 @@ import * as backend from "../backend"; +import { dataconnectOrigin } from "../../../api"; import { FirebaseError } from "../../../error"; +import { iam } from "../../../gcp"; + +const CLOUD_RUN_INVOKER_ROLE = "roles/run.invoker"; +const AUTOPUSH_DATACONNECT_SA_DOMAIN = "gcp-sa-autopush-dataconnect.iam.gserviceaccount.com"; +const STAGING_DATACONNECT_SA_DOMAIN = "gcp-sa-staging-dataconnect.iam.gserviceaccount.com"; +const PROD_DATACONNECT_SA_DOMAIN = "gcp-sa-firebasedataconnect.iam.gserviceaccount.com"; /** * Sets a Firebase Data Connect event trigger's region to the function region. @@ -18,3 +25,27 @@ export function ensureDataConnectTriggerRegion( } return Promise.resolve(); } + +function getServiceAccount(projectNumber: string): string { + const origin = dataconnectOrigin(); + if (origin.includes("autopush")) { + return `service-${projectNumber}@${AUTOPUSH_DATACONNECT_SA_DOMAIN}`; + } + if (origin.includes("staging")) { + return `service-${projectNumber}@${STAGING_DATACONNECT_SA_DOMAIN}`; + } + return `service-${projectNumber}@${PROD_DATACONNECT_SA_DOMAIN}`; +} + +/** + * Finds the required project level IAM bindings for the Firebase Data Connect service agent + * @param projectNumber project identifier + */ +export function obtainDataConnectBindings(projectNumber: string): Promise> { + const dataConnectServiceAgent = `serviceAccount:${getServiceAccount(projectNumber)}`; + const cloudRunInvokerBinding = { + role: CLOUD_RUN_INVOKER_ROLE, + members: [dataConnectServiceAgent], + }; + return Promise.resolve([cloudRunInvokerBinding]); +} diff --git a/src/deploy/functions/services/index.ts b/src/deploy/functions/services/index.ts index 9b2dbbdc4e7..24ef6d0fc24 100644 --- a/src/deploy/functions/services/index.ts +++ b/src/deploy/functions/services/index.ts @@ -8,7 +8,7 @@ import { ensureDatabaseTriggerRegion } from "./database"; import { ensureRemoteConfigTriggerRegion } from "./remoteConfig"; import { ensureTestLabTriggerRegion } from "./testLab"; import { ensureFirestoreTriggerRegion } from "./firestore"; -import { ensureDataConnectTriggerRegion } from "./dataconnect"; +import { ensureDataConnectTriggerRegion, obtainDataConnectBindings } from "./dataconnect"; /** A standard void No Op */ export const noop = (): Promise => Promise.resolve(); @@ -136,7 +136,7 @@ const firestoreService: Service = { const dataconnectService: Service = { name: "dataconnect", api: "firebasedataconnect.googleapis.com", - requiredProjectBindings: noopProjectBindings, + requiredProjectBindings: obtainDataConnectBindings, ensureTriggerRegion: ensureDataConnectTriggerRegion, validateTrigger: noop, registerTrigger: noop, @@ -186,5 +186,9 @@ export function serviceForEndpoint(endpoint: backend.Endpoint): Service { return EVENT_SERVICE_MAPPING[endpoint.blockingTrigger.eventType as events.Event] || noOpService; } + if (backend.isDataConnectGraphqlTriggered(endpoint)) { + return dataconnectService; + } + return noOpService; } diff --git a/src/deploy/functions/validate.ts b/src/deploy/functions/validate.ts index ddf0d172787..ec571b4b8e3 100644 --- a/src/deploy/functions/validate.ts +++ b/src/deploy/functions/validate.ts @@ -222,7 +222,11 @@ export function validateTimeoutConfig(endpoints: backend.Endpoint[]): void { limit = MAX_V2_SCHEDULE_TIMEOUT_SECONDS; } else if (backend.isTaskQueueTriggered(ep)) { limit = MAX_V2_TASK_QUEUE_TIMEOUT_SECONDS; - } else if (backend.isHttpsTriggered(ep) || backend.isCallableTriggered(ep)) { + } else if ( + backend.isHttpsTriggered(ep) || + backend.isCallableTriggered(ep) || + backend.isDataConnectGraphqlTriggered(ep) + ) { limit = MAX_V2_HTTP_TIMEOUT_SECONDS; } diff --git a/src/emulator/functionsEmulatorShared.ts b/src/emulator/functionsEmulatorShared.ts index dfddd8a0b58..852b8d23857 100644 --- a/src/emulator/functionsEmulatorShared.ts +++ b/src/emulator/functionsEmulatorShared.ts @@ -199,6 +199,8 @@ export function emulatedFunctionsFromEndpoints( // process requires it in this form. Need to work in Firestore emulator for a proper fix... if (backend.isHttpsTriggered(endpoint)) { def.httpsTrigger = endpoint.httpsTrigger; + } else if (backend.isDataConnectGraphqlTriggered(endpoint)) { + def.httpsTrigger = endpoint.dataConnectGraphqlTrigger; } else if (backend.isCallableTriggered(endpoint)) { def.httpsTrigger = {}; def.labels = { ...def.labels, "deployment-callable": "true" };