From 39137fe21c7b5b3681187d0906232190364540a7 Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Wed, 27 Aug 2025 14:10:23 -0700 Subject: [PATCH 01/11] Add Firebase Data Connect v2 support --- spec/v2/providers/dataconnect.spec.ts | 447 ++++++++++++++++++++++++++ src/common/params.ts | 2 +- src/v2/providers/dataconnect.ts | 281 ++++++++++++++++ 3 files changed, 729 insertions(+), 1 deletion(-) create mode 100644 spec/v2/providers/dataconnect.spec.ts create mode 100644 src/v2/providers/dataconnect.ts diff --git a/spec/v2/providers/dataconnect.spec.ts b/spec/v2/providers/dataconnect.spec.ts new file mode 100644 index 000000000..ad3c910f7 --- /dev/null +++ b/spec/v2/providers/dataconnect.spec.ts @@ -0,0 +1,447 @@ +// The MIT License (MIT) +// +// Copyright (c) 2023 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { expect } from "chai"; +import * as dataconnect from "../../../src/v2/providers/dataconnect"; +import { CloudEvent } from "../../../src/v2"; +import { onInit } from "../../../src/v2/core"; + +const expectedEndpointBase = { + platform: "gcfv2", + availableMemoryMb: {}, + concurrency: {}, + ingressSettings: {}, + maxInstances: {}, + minInstances: {}, + serviceAccountEmail: {}, + timeoutSeconds: {}, + vpc: {}, + labels: {}, +}; + +function makeExpectedEndpoint(eventType: string, eventFilters, eventFilterPathPatterns) { + return { + ...expectedEndpointBase, + eventTrigger: { + eventType, + eventFilters, + eventFilterPathPatterns, + retry: false, + }, + }; +} + +describe("dataconnect", () => { + describe("onMutationExecuted", () => { + it("should create a func", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + connector: "my-connector", + operation: "my-operation", + }, + {} + ); + + const func = dataconnect.onMutationExecuted( + "services/my-service/connectors/my-connector/operations/my-operation", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + connector: "my-connector", + operation: "my-operation", + }, + {} + ); + + const func = dataconnect.onMutationExecuted( + { + service: "my-service", + connector: "my-connector", + operation: "my-operation", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func with a service path pattern", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + connector: "my-connector", + operation: "my-operation", + }, + { + service: "{service}", + } + ); + + const func = dataconnect.onMutationExecuted( + "services/{service}/connectors/my-connector/operations/my-operation", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts with a service path pattern", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + connector: "my-connector", + operation: "my-operation", + }, + { + service: "{service}", + } + ); + + const func = dataconnect.onMutationExecuted( + { + service: "{service}", + connector: "my-connector", + operation: "my-operation", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func with a connector path pattern", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + operation: "my-operation", + }, + { + connector: "{connector}", + } + ); + + const func = dataconnect.onMutationExecuted( + "services/my-service/connectors/{connector}/operations/my-operation", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts with a connector path pattern", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + operation: "my-operation", + }, + { + connector: "{connector}", + } + ); + + const func = dataconnect.onMutationExecuted( + { + service: "my-service", + connector: "{connector}", + operation: "my-operation", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func with an operation path pattern", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + connector: "my-connector", + }, + { + operation: "{operation}", + } + ); + + const func = dataconnect.onMutationExecuted( + "services/my-service/connectors/my-connector/operations/{operation}", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts with an operation path pattern", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + connector: "my-connector", + }, + { + operation: "{operation}", + } + ); + + const func = dataconnect.onMutationExecuted( + { + service: "my-service", + connector: "my-connector", + operation: "{operation}", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func with path patterns", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + {}, + { + service: "{service}", + connector: "{connector}", + operation: "{operation}", + } + ); + + const func = dataconnect.onMutationExecuted( + "services/{service}/connectors/{connector}/operations/{operation}", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts with path patterns", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + {}, + { + service: "{service}", + connector: "{connector}", + operation: "{operation}", + } + ); + + const func = dataconnect.onMutationExecuted( + { + service: "{service}", + connector: "{connector}", + operation: "{operation}", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func with a service wildcard", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + connector: "my-connector", + operation: "my-operation", + }, + { + service: "*", + } + ); + + const func = dataconnect.onMutationExecuted( + "services/*/connectors/my-connector/operations/my-operation", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts with a service wildcard", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + connector: "my-connector", + operation: "my-operation", + }, + { + service: "*", + } + ); + + const func = dataconnect.onMutationExecuted( + { + service: "*", + connector: "my-connector", + operation: "my-operation", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func with a connector wildcard", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + operation: "my-operation", + }, + { + connector: "*", + } + ); + + const func = dataconnect.onMutationExecuted( + "services/my-service/connectors/*/operations/my-operation", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts with a connector wildcard", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + operation: "my-operation", + }, + { + connector: "*", + } + ); + + const func = dataconnect.onMutationExecuted( + { + service: "my-service", + connector: "*", + operation: "my-operation", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func with an operation wildcard", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + connector: "my-connector", + }, + { + operation: "*", + } + ); + + const func = dataconnect.onMutationExecuted( + "services/my-service/connectors/my-connector/operations/*", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts with an operation wildcard", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + service: "my-service", + connector: "my-connector", + }, + { + operation: "*", + } + ); + + const func = dataconnect.onMutationExecuted( + { + service: "my-service", + connector: "my-connector", + operation: "*", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func with wildcards", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + {}, + { + service: "*", + connector: "*", + operation: "*", + } + ); + + const func = dataconnect.onMutationExecuted( + "services/*/connectors/*/operations/*", + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("should create a func using param opts with wildcards", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + {}, + { + service: "*", + connector: "*", + operation: "*", + } + ); + + const func = dataconnect.onMutationExecuted( + { + service: "*", + connector: "*", + operation: "*", + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + + it("calls init function", async () => { + const event: CloudEvent = { + specversion: "1.0", + id: "id", + source: "google.firebase.dataconnect.connector.v1.mutationExecuted", + type: "type", + time: "time", + data: "data", + }; + + let hello; + onInit(() => (hello = "world")); + expect(hello).to.be.undefined; + await dataconnect.onMutationExecuted( + "services/*/connectors/*/operations/*", + () => null + )(event); + expect(hello).to.equal("world"); + }); + }); +}); diff --git a/src/common/params.ts b/src/common/params.ts index e0b0b8537..b6b225cb1 100644 --- a/src/common/params.ts +++ b/src/common/params.ts @@ -73,7 +73,7 @@ export type Extract = Part extends `{${infer Param}=**}` : never; /** - * A type that maps all parameter capture gropus into keys of a record. + * A type that maps all parameter capture groups into keys of a record. * For example, ParamsOf<"users/{uid}"> is { uid: string } * ParamsOf<"users/{uid}/logs/{log}"> is { uid: string; log: string } * ParamsOf<"some/static/data"> is {} diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts new file mode 100644 index 000000000..8e5c8d26f --- /dev/null +++ b/src/v2/providers/dataconnect.ts @@ -0,0 +1,281 @@ +// The MIT License (MIT) +// +// Copyright (c) 2023 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { CloudEvent, CloudFunction } from "../core"; +import { ParamsOf } from "../../common/params"; +import { EventHandlerOptions, getGlobalOptions, optionsToEndpoint } from "../options"; +import { normalizePath } from "../../common/utilities/path"; +import { wrapTraceContext } from "../trace"; +import { withInit } from "../../common/onInit"; +import { initV2Endpoint, ManifestEndpoint } from "../../runtime/manifest"; +import { PathPattern } from "../../common/utilities/path-pattern"; + +/** @internal */ +export const mutationExecutedEventType = + "google.firebase.dataconnect.connector.v1.mutationExecuted"; + +/** @hidden */ +export interface SourceLocation { + line: number; + column: number; +} + +/** @hidden */ +export interface GraphqlErrorExtensions { + file: string; + code: string; + debugDetails: string; +} + +/** @hidden */ +export interface GraphqlError { + message: string; + locations: Array; + path: Array; + extensions: GraphqlErrorExtensions; +} + +/** @hidden */ +export interface RawMutation { + data: R; + variables: V; + errors: Array; +} + +/** @hidden */ +export interface MutationEventData { + ["@type"]: "type.googleapis.com/google.events.firebase.dataconnect.v1.MutationEventData"; + payload: RawMutation; +} + +/** @hidden */ +export interface RawDataConnectEvent extends CloudEvent { + project: string; + location: string; + service: string; + schema: string; + connector: string; + operation: string; +} + +/** OperationOptions extend EventHandlerOptions with a provided service, connector, and operation. */ +export interface OperationOptions extends EventHandlerOptions { + /** Firebase Data Connect service ID */ + service: string; + /** Firebase Data Connect connector ID */ + connector: string; + /** Name of the operation */ + operation: string; +} + +export interface DataConnectEvent> extends CloudEvent { + /** The location of the Firebase Data Connect instance */ + location: string; + /** The project identifier */ + project: string; + /** + * An object containing the values of the path patterns. + * Only named capture groups will be populated - {key}, {key=*}, {key=**}. + */ + params: Params; +} + +/** + * Event handler that triggers when a mutation is executed in Firebase Data Connect. + * + * @param mutation - The mutation path to trigger on. + * @param handler - Event handler which is run every time a mutation is executed. + */ +export function onMutationExecuted< + Mutation extends string, + Variables = unknown, + ResponseData = unknown +>( + mutation: Mutation, + handler: ( + event: DataConnectEvent, ParamsOf> + ) => unknown | Promise +): CloudFunction, ParamsOf>>; + +/** + * Event handler that triggers when a mutation is executed in Firebase Data Connect. + * + * @param opts - Options that can be set on an individual event-handling function. + * @param handler - Event handler which is run every time a mutation is executed. + */ +export function onMutationExecuted< + Mutation extends string, + Variables = unknown, + ResponseData = unknown +>( + opts: OperationOptions, + handler: ( + event: DataConnectEvent, ParamsOf> + ) => unknown | Promise +): CloudFunction, ParamsOf>>; + +/** + * Event handler that triggers when a mutation is executed in Firebase Data Connect. + * + * @param mutationOrOpts - Options or string mutation path. + * @param handler - Event handler which is run every time a mutation is executed. + */ +export function onMutationExecuted< + Mutation extends string, + Variables = unknown, + ResponseData = unknown +>( + mutationOrOpts: Mutation | OperationOptions, + handler: ( + event: DataConnectEvent, ParamsOf> + ) => unknown | Promise +): CloudFunction, ParamsOf>> { + return onOperation(mutationExecutedEventType, mutationOrOpts, handler); +} + +function getOpts(mutationOrOpts: string | OperationOptions) { + const operationRegex = new RegExp("services/([^/]+)/connectors/([^/]+)/operations/([^/]+)"); + + let service: string; + let connector: string; + let operation: string; + let opts: EventHandlerOptions; + if (typeof mutationOrOpts === "string") { + const path = normalizePath(mutationOrOpts); + const match = path.match(operationRegex); + if (!match) { + throw new Error(`Invalid operation path: ${path}`); + } + + service = match[1]; + connector = match[2]; + operation = match[3]; + opts = {}; + } else { + service = mutationOrOpts.service; + connector = mutationOrOpts.connector; + operation = mutationOrOpts.operation; + opts = { ...mutationOrOpts }; + + delete (opts as any).service; + delete (opts as any).connector; + delete (opts as any).operation; + } + + return { + service, + connector, + operation, + opts, + }; +} + +function makeEndpoint( + eventType: string, + opts: EventHandlerOptions, + service: PathPattern, + connector: PathPattern, + operation: PathPattern +): ManifestEndpoint { + const baseOpts = optionsToEndpoint(getGlobalOptions()); + const specificOpts = optionsToEndpoint(opts); + + const eventFilters: Record = {}; + const eventFilterPathPatterns: Record = {}; + + service.hasWildcards() + ? (eventFilterPathPatterns.service = service.getValue()) + : (eventFilters.service = service.getValue()); + connector.hasWildcards() + ? (eventFilterPathPatterns.connector = connector.getValue()) + : (eventFilters.connector = connector.getValue()); + operation.hasWildcards() + ? (eventFilterPathPatterns.operation = operation.getValue()) + : (eventFilters.operation = operation.getValue()); + + return { + ...initV2Endpoint(getGlobalOptions(), opts), + platform: "gcfv2", + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType, + eventFilters, + eventFilterPathPatterns, + retry: opts.retry ?? false, + }, + }; +} + +function makeParams( + event: RawDataConnectEvent>, + service: PathPattern, + connector: PathPattern, + operation: PathPattern +) { + return { + ...service.extractMatches(event.service), + ...connector.extractMatches(event.connector), + ...operation.extractMatches(event.operation), + }; +} + +function onOperation( + eventType: string, + mutationOrOpts: string | OperationOptions, + handler: (event: DataConnectEvent>) => any | Promise +): CloudFunction>> { + const { service, connector, operation, opts } = getOpts(mutationOrOpts); + + const servicePattern = new PathPattern(service); + const connectorPattern = new PathPattern(connector); + const operationPattern = new PathPattern(operation); + + // wrap the handler + const func = (raw: CloudEvent) => { + const event = raw as RawDataConnectEvent>; + const params = makeParams(event, servicePattern, connectorPattern, operationPattern); + + const dataConnectEvent: DataConnectEvent> = { + ...event, + params, + }; + + return wrapTraceContext(withInit(handler))(dataConnectEvent); + }; + + func.run = handler; + + func.__endpoint = makeEndpoint( + eventType, + opts, + servicePattern, + connectorPattern, + operationPattern + ); + + return func; +} From c3844fce4bc7db0a6bbbd242969e93f3897f0369 Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Thu, 11 Sep 2025 10:11:22 -0700 Subject: [PATCH 02/11] Export dataconnect --- package.json | 3 +++ spec/v2/providers/dataconnect.spec.ts | 2 +- src/v2/index.ts | 2 ++ src/v2/providers/dataconnect.ts | 2 +- v2/dataconnect.js | 26 ++++++++++++++++++++++++++ 5 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 v2/dataconnect.js diff --git a/package.json b/package.json index b9736a051..9b48e029f 100644 --- a/package.json +++ b/package.json @@ -239,6 +239,9 @@ ], "v2/firestore": [ "lib/v2/providers/firestore" + ], + "v2/dataconnect": [ + "lib/v2/providers/dataconnect" ] } }, diff --git a/spec/v2/providers/dataconnect.spec.ts b/spec/v2/providers/dataconnect.spec.ts index ad3c910f7..9f99ce700 100644 --- a/spec/v2/providers/dataconnect.spec.ts +++ b/spec/v2/providers/dataconnect.spec.ts @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2023 Firebase +// Copyright (c) 2025 Firebase // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/src/v2/index.ts b/src/v2/index.ts index 23fc424b8..ccee302e5 100644 --- a/src/v2/index.ts +++ b/src/v2/index.ts @@ -41,6 +41,7 @@ import * as tasks from "./providers/tasks"; import * as remoteConfig from "./providers/remoteConfig"; import * as testLab from "./providers/testLab"; import * as firestore from "./providers/firestore"; +import * as dataconnect from "./providers/dataconnect"; export { alerts, @@ -56,6 +57,7 @@ export { remoteConfig, testLab, firestore, + dataconnect, }; export { diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts index 8e5c8d26f..54fe71b13 100644 --- a/src/v2/providers/dataconnect.ts +++ b/src/v2/providers/dataconnect.ts @@ -1,6 +1,6 @@ // The MIT License (MIT) // -// Copyright (c) 2023 Firebase +// Copyright (c) 2025 Firebase // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/v2/dataconnect.js b/v2/dataconnect.js new file mode 100644 index 000000000..3e4b26904 --- /dev/null +++ b/v2/dataconnect.js @@ -0,0 +1,26 @@ +// The MIT License (MIT) +// +// Copyright (c) 2025 Firebase +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// This file is not part of the firebase-functions SDK. It is used to silence the +// imports eslint plugin until it can understand import paths defined by node +// package exports. +// For more information, see github.com/import-js/eslint-plugin-import/issues/1810 From 96bff3f68c856a20b152b9c450b1115107c3903d Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Thu, 11 Sep 2025 10:22:12 -0700 Subject: [PATCH 03/11] Fix export --- package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9b48e029f..4da82dda3 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "./remoteConfig": "./lib/v2/providers/remoteConfig.js", "./testLab": "./lib/v2/providers/testLab.js", "./firestore": "./lib/v2/providers/firestore.js", + "./dataconnect": "./lib/v2/providers/dataconnect.js", "./v2": "./lib/v2/index.js", "./v2/core": "./lib/v2/core.js", "./v2/options": "./lib/v2/options.js", @@ -80,7 +81,8 @@ "./v2/scheduler": "./lib/v2/providers/scheduler.js", "./v2/remoteConfig": "./lib/v2/providers/remoteConfig.js", "./v2/testLab": "./lib/v2/providers/testLab.js", - "./v2/firestore": "./lib/v2/providers/firestore.js" + "./v2/firestore": "./lib/v2/providers/firestore.js", + "./v2/dataconnect": "./lib/v2/providers/dataconnect.js" }, "typesVersions": { "*": { @@ -180,6 +182,9 @@ "firestore": [ "./lib/v2/providers/firestore" ], + "dataconnect": [ + "./lib/v2/providers/dataconnect" + ], "v2": [ "lib/v2" ], From 85bf83f7916771fcba15859458a7efe4347b1a07 Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Fri, 12 Sep 2025 11:06:08 -0700 Subject: [PATCH 04/11] Rename Extract to VarName, since Extract is a built in type --- spec/common/params.spec.ts | 12 ++++++------ src/common/params.ts | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/spec/common/params.spec.ts b/spec/common/params.spec.ts index 595a5758f..9887c743e 100644 --- a/spec/common/params.spec.ts +++ b/spec/common/params.spec.ts @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE ignoreUnusedWarning OR OTHER DEALINGS IN THE // SOFTWARE. -import { Extract, ParamsOf, Split } from "../../src/common/params"; +import { VarName, ParamsOf, Split } from "../../src/common/params"; import { expectNever, expectType } from "./metaprogramming"; describe("Params namespace", () => { @@ -56,21 +56,21 @@ describe("Params namespace", () => { }); }); - describe("Extract", () => { + describe("VarName", () => { it("extracts nothing from strings without params", () => { - expectNever>(); + expectNever>(); }); it("extracts {segment} captures", () => { - expectType>("uid"); + expectType>("uid"); }); it("extracts {segment=*} captures", () => { - expectType>("uid"); + expectType>("uid"); }); it("extracts {segment=**} captures", () => { - expectType>("uid"); + expectType>("uid"); }); }); diff --git a/src/common/params.ts b/src/common/params.ts index b6b225cb1..8ff3f30a1 100644 --- a/src/common/params.ts +++ b/src/common/params.ts @@ -60,11 +60,11 @@ export type NullSafe = S extends null * A type that extracts parameter name enclosed in bracket as string. * Ignore wildcard matches * - * For example, Extract<"{uid}"> is "uid". - * For example, Extract<"{uid=*}"> is "uid". - * For example, Extract<"{uid=**}"> is "uid". + * For example, VarName<"{uid}"> is "uid". + * For example, VarName<"{uid=*}"> is "uid". + * For example, VarName<"{uid=**}"> is "uid". */ -export type Extract = Part extends `{${infer Param}=**}` +export type VarName = Part extends `{${infer Param}=**}` ? Param : Part extends `{${infer Param}=*}` ? Param @@ -90,7 +90,7 @@ export type ParamsOf> = // N.B. I'm not sure why PathPattern isn't detected to not be an // Expression per the check above. Since we have the check above // The Exclude call should be safe. - [Key in Extract< + [Key in VarName< Split>>, "/">[number] >]: string; }; From 4a4393375d2d51d3f95a8ed95d74dafcca2ea3e2 Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Sat, 4 Oct 2025 09:25:19 -0700 Subject: [PATCH 05/11] Include auth context in event payload --- src/v2/providers/dataconnect.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts index 54fe71b13..cb5bc7fc4 100644 --- a/src/v2/providers/dataconnect.ts +++ b/src/v2/providers/dataconnect.ts @@ -75,8 +75,18 @@ export interface RawDataConnectEvent extends CloudEvent { schema: string; connector: string; operation: string; + authtype: AuthType; + authid?: string; } +/** + * AuthType defines the possible values for the authType field in a Firebase Data Connect event. + * - app_user: an end user of an application.. + * - admin: an admin user of an application. In the context of impersonate endpoints used by the admin SDK, the impersonator. + * - unknown: a general type to capture all other principals not captured in the other auth types. + */ +export type AuthType = "app_user" | "admin" | "unknown"; + /** OperationOptions extend EventHandlerOptions with a provided service, connector, and operation. */ export interface OperationOptions extends EventHandlerOptions { /** Firebase Data Connect service ID */ @@ -97,6 +107,10 @@ export interface DataConnectEvent> extends Cl * Only named capture groups will be populated - {key}, {key=*}, {key=**}. */ params: Params; + /** The type of principal that triggered the event */ + authType: AuthType; + /** The unique identifier for the principal */ + authId?: string; } /** @@ -261,8 +275,12 @@ function onOperation( const dataConnectEvent: DataConnectEvent> = { ...event, + authType: event.authtype, + authId: event.authid, params, }; + delete (dataConnectEvent as any).authtype; + delete (dataConnectEvent as any).authid; return wrapTraceContext(withInit(handler))(dataConnectEvent); }; From 4c83ca427fbd705f8661f1cd1eee81588ff9f96a Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Sun, 5 Oct 2025 15:40:04 -0700 Subject: [PATCH 06/11] Stronger typing for data connect params --- spec/common/metaprogramming.ts | 1 + spec/v2/providers/dataconnect.spec.ts | 91 +++++++++++++++++++++++++++ src/v2/providers/dataconnect.ts | 63 ++++++++++--------- 3 files changed, 127 insertions(+), 28 deletions(-) diff --git a/spec/common/metaprogramming.ts b/spec/common/metaprogramming.ts index 11909ede8..5c16a710b 100644 --- a/spec/common/metaprogramming.ts +++ b/spec/common/metaprogramming.ts @@ -23,3 +23,4 @@ /* eslint-disable @typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-function */ export function expectType(value: Type) {} export function expectNever() {} +export function expectExtends() {} diff --git a/spec/v2/providers/dataconnect.spec.ts b/spec/v2/providers/dataconnect.spec.ts index 9f99ce700..30c1845ea 100644 --- a/spec/v2/providers/dataconnect.spec.ts +++ b/spec/v2/providers/dataconnect.spec.ts @@ -24,6 +24,7 @@ import { expect } from "chai"; import * as dataconnect from "../../../src/v2/providers/dataconnect"; import { CloudEvent } from "../../../src/v2"; import { onInit } from "../../../src/v2/core"; +import { expectExtends } from "../../common/metaprogramming"; const expectedEndpointBase = { platform: "gcfv2", @@ -51,6 +52,81 @@ function makeExpectedEndpoint(eventType: string, eventFilters, eventFilterPathPa } describe("dataconnect", () => { + describe("params", () => { + it("extracts {segment} captures", () => { + expectExtends< + Record<"myConnector", string>, + dataconnect.DataConnectParams<"/{myConnector}"> + >(); + }); + + it("extracts nothing from strings without params", () => { + expectExtends, dataconnect.DataConnectParams<"foo/bar">>(); + expectExtends, dataconnect.DataConnectParams<"/foo/bar">>(); + }); + + it("extracts {segment} captures from options", () => { + expectExtends< + Record<"myService", string>, + dataconnect.DataConnectParams<{ + service: "{myService}"; + connector: "connector"; + operation: "operation"; + }> + >(); + + expectExtends< + { myService: string; [key: string]: string }, + dataconnect.DataConnectParams< + dataconnect.OperationOptions<"{myService}", "connector", "operation"> + > + >(); + }); + + it("extracts {segment=*} captures from options", () => { + expectExtends< + Record<"myConnector", string>, + dataconnect.DataConnectParams< + dataconnect.OperationOptions + > + >(); + }); + + it("extracts {segment=**} captures from options", () => { + expectExtends< + Record<"myOperation", string>, + dataconnect.DataConnectParams< + dataconnect.OperationOptions + > + >(); + }); + + it("extracts multiple captures from options", () => { + expectExtends< + Record<"myService" | "myConnector" | "myOperation", string>, + dataconnect.DataConnectParams< + dataconnect.OperationOptions<"{myService}", "{myConnector=*}", "{myOperation=**}"> + > + >(); + }); + + it("extracts nothing from options without params", () => { + expectExtends< + Record, + dataconnect.DataConnectParams<{ + service: "service"; + connector: "connector"; + operation: "operation"; + }> + >(); + + expectExtends< + Record, + dataconnect.DataConnectParams> + >(); + }); + }); + describe("onMutationExecuted", () => { it("should create a func", () => { const expectedEndpoint = makeExpectedEndpoint( @@ -424,6 +500,21 @@ describe("dataconnect", () => { expect(func.__endpoint).to.deep.eq(expectedEndpoint); }); + it("should create a func in the absence of param opts", () => { + const expectedEndpoint = makeExpectedEndpoint( + dataconnect.mutationExecutedEventType, + { + connector: undefined, + operation: undefined, + service: undefined, + }, + {} + ); + + const func = dataconnect.onMutationExecuted({}, () => true); + expect(func.__endpoint).to.deep.eq(expectedEndpoint); + }); + it("calls init function", async () => { const event: CloudEvent = { specversion: "1.0", diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts index cb5bc7fc4..5d2afd547 100644 --- a/src/v2/providers/dataconnect.ts +++ b/src/v2/providers/dataconnect.ts @@ -21,7 +21,7 @@ // SOFTWARE. import { CloudEvent, CloudFunction } from "../core"; -import { ParamsOf } from "../../common/params"; +import { ParamsOf, VarName } from "../../common/params"; import { EventHandlerOptions, getGlobalOptions, optionsToEndpoint } from "../options"; import { normalizePath } from "../../common/utilities/path"; import { wrapTraceContext } from "../trace"; @@ -88,16 +88,23 @@ export interface RawDataConnectEvent extends CloudEvent { export type AuthType = "app_user" | "admin" | "unknown"; /** OperationOptions extend EventHandlerOptions with a provided service, connector, and operation. */ -export interface OperationOptions extends EventHandlerOptions { - /** Firebase Data Connect service ID */ - service: string; - /** Firebase Data Connect connector ID */ - connector: string; - /** Name of the operation */ - operation: string; +export interface OperationOptions extends EventHandlerOptions{ + /** Firebase Data Connect service ID */ + service?: Service; + /** Firebase Data Connect connector ID */ + connector?: Connector; + /** Name of the operation */ + operation?: Operation; } -export interface DataConnectEvent> extends CloudEvent { +export type DataConnectParams = + PathPatternOrOptions extends string + ? ParamsOf + : PathPatternOrOptions extends OperationOptions + ? Record | VarName | VarName, string> + : never; + +export interface DataConnectEvent> extends CloudEvent { /** The location of the Firebase Data Connect instance */ location: string; /** The project identifier */ @@ -126,9 +133,9 @@ export function onMutationExecuted< >( mutation: Mutation, handler: ( - event: DataConnectEvent, ParamsOf> + event: DataConnectEvent, DataConnectParams> ) => unknown | Promise -): CloudFunction, ParamsOf>>; +): CloudFunction, DataConnectParams>>; /** * Event handler that triggers when a mutation is executed in Firebase Data Connect. @@ -137,15 +144,15 @@ export function onMutationExecuted< * @param handler - Event handler which is run every time a mutation is executed. */ export function onMutationExecuted< - Mutation extends string, + Options extends OperationOptions, Variables = unknown, ResponseData = unknown >( - opts: OperationOptions, + opts: Options, handler: ( - event: DataConnectEvent, ParamsOf> + event: DataConnectEvent, DataConnectParams> ) => unknown | Promise -): CloudFunction, ParamsOf>>; +): CloudFunction, DataConnectParams>>; /** * Event handler that triggers when a mutation is executed in Firebase Data Connect. @@ -154,16 +161,16 @@ export function onMutationExecuted< * @param handler - Event handler which is run every time a mutation is executed. */ export function onMutationExecuted< - Mutation extends string, + PathPatternOrOptions extends string | OperationOptions, Variables = unknown, ResponseData = unknown >( - mutationOrOpts: Mutation | OperationOptions, + mutationOrOpts: PathPatternOrOptions, handler: ( - event: DataConnectEvent, ParamsOf> + event: DataConnectEvent, DataConnectParams> ) => unknown | Promise -): CloudFunction, ParamsOf>> { - return onOperation(mutationExecutedEventType, mutationOrOpts, handler); +): CloudFunction, DataConnectParams>> { + return onOperation(mutationExecutedEventType, mutationOrOpts, handler); } function getOpts(mutationOrOpts: string | OperationOptions) { @@ -257,11 +264,11 @@ function makeParams( }; } -function onOperation( +function onOperation( eventType: string, - mutationOrOpts: string | OperationOptions, - handler: (event: DataConnectEvent>) => any | Promise -): CloudFunction>> { + mutationOrOpts: PathPatternOrOptions, + handler: (event: DataConnectEvent, DataConnectParams>) => any | Promise +): CloudFunction, DataConnectParams>> { const { service, connector, operation, opts } = getOpts(mutationOrOpts); const servicePattern = new PathPattern(service); @@ -270,14 +277,14 @@ function onOperation( // wrap the handler const func = (raw: CloudEvent) => { - const event = raw as RawDataConnectEvent>; - const params = makeParams(event, servicePattern, connectorPattern, operationPattern); + const event = raw as RawDataConnectEvent>; + const params = makeParams(event, servicePattern, connectorPattern, operationPattern); - const dataConnectEvent: DataConnectEvent> = { + const dataConnectEvent: DataConnectEvent, DataConnectParams> = { ...event, authType: event.authtype, authId: event.authid, - params, + params: params as DataConnectParams, }; delete (dataConnectEvent as any).authtype; delete (dataConnectEvent as any).authid; From 6c63be54f7ebf64e4c52ae20d391fae69b4edc6b Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Wed, 8 Oct 2025 11:11:47 -0700 Subject: [PATCH 07/11] Fix formatting and address comments --- spec/v2/providers/dataconnect.spec.ts | 10 +- src/v2/providers/dataconnect.ts | 135 +++++++++++++++++--------- 2 files changed, 92 insertions(+), 53 deletions(-) diff --git a/spec/v2/providers/dataconnect.spec.ts b/spec/v2/providers/dataconnect.spec.ts index 30c1845ea..a85e4ee44 100644 --- a/spec/v2/providers/dataconnect.spec.ts +++ b/spec/v2/providers/dataconnect.spec.ts @@ -501,15 +501,7 @@ describe("dataconnect", () => { }); it("should create a func in the absence of param opts", () => { - const expectedEndpoint = makeExpectedEndpoint( - dataconnect.mutationExecutedEventType, - { - connector: undefined, - operation: undefined, - service: undefined, - }, - {} - ); + const expectedEndpoint = makeExpectedEndpoint(dataconnect.mutationExecutedEventType, {}, {}); const func = dataconnect.onMutationExecuted({}, () => true); expect(func.__endpoint).to.deep.eq(expectedEndpoint); diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts index 5d2afd547..9cb560067 100644 --- a/src/v2/providers/dataconnect.ts +++ b/src/v2/providers/dataconnect.ts @@ -88,19 +88,27 @@ export interface RawDataConnectEvent extends CloudEvent { export type AuthType = "app_user" | "admin" | "unknown"; /** OperationOptions extend EventHandlerOptions with a provided service, connector, and operation. */ -export interface OperationOptions extends EventHandlerOptions{ - /** Firebase Data Connect service ID */ - service?: Service; - /** Firebase Data Connect connector ID */ - connector?: Connector; - /** Name of the operation */ - operation?: Operation; +export interface OperationOptions< + Service extends string = string, + Connector extends string = string, + Operation extends string = string +> extends EventHandlerOptions { + /** Firebase Data Connect service ID */ + service?: Service; + /** Firebase Data Connect connector ID */ + connector?: Connector; + /** Name of the operation */ + operation?: Operation; } export type DataConnectParams = - PathPatternOrOptions extends string + PathPatternOrOptions extends string ? ParamsOf - : PathPatternOrOptions extends OperationOptions + : PathPatternOrOptions extends OperationOptions< + infer Service extends string, + infer Connector extends string, + infer Operation extends string + > ? Record | VarName | VarName, string> : never; @@ -135,7 +143,9 @@ export function onMutationExecuted< handler: ( event: DataConnectEvent, DataConnectParams> ) => unknown | Promise -): CloudFunction, DataConnectParams>>; +): CloudFunction< + DataConnectEvent, DataConnectParams> +>; /** * Event handler that triggers when a mutation is executed in Firebase Data Connect. @@ -152,7 +162,9 @@ export function onMutationExecuted< handler: ( event: DataConnectEvent, DataConnectParams> ) => unknown | Promise -): CloudFunction, DataConnectParams>>; +): CloudFunction< + DataConnectEvent, DataConnectParams> +>; /** * Event handler that triggers when a mutation is executed in Firebase Data Connect. @@ -167,18 +179,30 @@ export function onMutationExecuted< >( mutationOrOpts: PathPatternOrOptions, handler: ( - event: DataConnectEvent, DataConnectParams> + event: DataConnectEvent< + MutationEventData, + DataConnectParams + > ) => unknown | Promise -): CloudFunction, DataConnectParams>> { - return onOperation(mutationExecutedEventType, mutationOrOpts, handler); +): CloudFunction< + DataConnectEvent< + MutationEventData, + DataConnectParams + > +> { + return onOperation( + mutationExecutedEventType, + mutationOrOpts, + handler + ); } function getOpts(mutationOrOpts: string | OperationOptions) { const operationRegex = new RegExp("services/([^/]+)/connectors/([^/]+)/operations/([^/]+)"); - let service: string; - let connector: string; - let operation: string; + let service: string | undefined; + let connector: string | undefined; + let operation: string | undefined; let opts: EventHandlerOptions; if (typeof mutationOrOpts === "string") { const path = normalizePath(mutationOrOpts); @@ -213,9 +237,9 @@ function getOpts(mutationOrOpts: string | OperationOptions) { function makeEndpoint( eventType: string, opts: EventHandlerOptions, - service: PathPattern, - connector: PathPattern, - operation: PathPattern + service: PathPattern | undefined, + connector: PathPattern | undefined, + operation: PathPattern | undefined ): ManifestEndpoint { const baseOpts = optionsToEndpoint(getGlobalOptions()); const specificOpts = optionsToEndpoint(opts); @@ -223,16 +247,21 @@ function makeEndpoint( const eventFilters: Record = {}; const eventFilterPathPatterns: Record = {}; - service.hasWildcards() - ? (eventFilterPathPatterns.service = service.getValue()) - : (eventFilters.service = service.getValue()); - connector.hasWildcards() - ? (eventFilterPathPatterns.connector = connector.getValue()) - : (eventFilters.connector = connector.getValue()); - operation.hasWildcards() - ? (eventFilterPathPatterns.operation = operation.getValue()) - : (eventFilters.operation = operation.getValue()); - + if (service) { + service.hasWildcards() + ? (eventFilterPathPatterns.service = service.getValue()) + : (eventFilters.service = service.getValue()); + } + if (connector) { + connector.hasWildcards() + ? (eventFilterPathPatterns.connector = connector.getValue()) + : (eventFilters.connector = connector.getValue()); + } + if (operation) { + operation.hasWildcards() + ? (eventFilterPathPatterns.operation = operation.getValue()) + : (eventFilters.operation = operation.getValue()); + } return { ...initV2Endpoint(getGlobalOptions(), opts), platform: "gcfv2", @@ -253,34 +282,52 @@ function makeEndpoint( function makeParams( event: RawDataConnectEvent>, - service: PathPattern, - connector: PathPattern, - operation: PathPattern + service: PathPattern | undefined, + connector: PathPattern | undefined, + operation: PathPattern | undefined ) { return { - ...service.extractMatches(event.service), - ...connector.extractMatches(event.connector), - ...operation.extractMatches(event.operation), + ...service?.extractMatches(event.service), + ...connector?.extractMatches(event.connector), + ...operation?.extractMatches(event.operation), }; } function onOperation( eventType: string, mutationOrOpts: PathPatternOrOptions, - handler: (event: DataConnectEvent, DataConnectParams>) => any | Promise -): CloudFunction, DataConnectParams>> { + handler: ( + event: DataConnectEvent< + MutationEventData, + DataConnectParams + > + ) => any | Promise +): CloudFunction< + DataConnectEvent< + MutationEventData, + DataConnectParams + > +> { const { service, connector, operation, opts } = getOpts(mutationOrOpts); - const servicePattern = new PathPattern(service); - const connectorPattern = new PathPattern(connector); - const operationPattern = new PathPattern(operation); + const servicePattern = service ? new PathPattern(service) : undefined; + const connectorPattern = connector ? new PathPattern(connector) : undefined; + const operationPattern = operation ? new PathPattern(operation) : undefined; // wrap the handler const func = (raw: CloudEvent) => { const event = raw as RawDataConnectEvent>; - const params = makeParams(event, servicePattern, connectorPattern, operationPattern); - - const dataConnectEvent: DataConnectEvent, DataConnectParams> = { + const params = makeParams( + event, + servicePattern, + connectorPattern, + operationPattern + ); + + const dataConnectEvent: DataConnectEvent< + MutationEventData, + DataConnectParams + > = { ...event, authType: event.authtype, authId: event.authid, From c3ecbbc982cb30441778f7d8106fd4b1c472ef7b Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Wed, 8 Oct 2025 11:44:51 -0700 Subject: [PATCH 08/11] Apply another suggestion --- src/v2/providers/dataconnect.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts index 9cb560067..b465af3ae 100644 --- a/src/v2/providers/dataconnect.ts +++ b/src/v2/providers/dataconnect.ts @@ -324,17 +324,16 @@ function onOperation( operationPattern ); + const { authtype, authid, ...rest } = event; const dataConnectEvent: DataConnectEvent< MutationEventData, DataConnectParams > = { - ...event, - authType: event.authtype, - authId: event.authid, + ...rest, + authType: authtype, + authId: authid, params: params as DataConnectParams, }; - delete (dataConnectEvent as any).authtype; - delete (dataConnectEvent as any).authid; return wrapTraceContext(withInit(handler))(dataConnectEvent); }; From 5e591f98176976f8ffe84d25552e1c5edfb2c0fd Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Tue, 21 Oct 2025 21:31:18 -0700 Subject: [PATCH 09/11] Add region and fix bug in which event.service, event.connector, event.operation gets populated --- spec/v2/providers/dataconnect.spec.ts | 61 ++++++++++++++++++--------- src/v2/providers/dataconnect.ts | 27 +++++++++--- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/spec/v2/providers/dataconnect.spec.ts b/spec/v2/providers/dataconnect.spec.ts index a85e4ee44..9e799d92d 100644 --- a/spec/v2/providers/dataconnect.spec.ts +++ b/spec/v2/providers/dataconnect.spec.ts @@ -39,8 +39,13 @@ const expectedEndpointBase = { labels: {}, }; -function makeExpectedEndpoint(eventType: string, eventFilters, eventFilterPathPatterns) { - return { +function makeExpectedEndpoint( + eventType: string, + eventFilters, + eventFilterPathPatterns, + region?: string[] +) { + let endpoint = { ...expectedEndpointBase, eventTrigger: { eventType, @@ -49,6 +54,11 @@ function makeExpectedEndpoint(eventType: string, eventFilters, eventFilterPathPa retry: false, }, }; + + if (region) { + return { ...endpoint, region }; + } + return endpoint; } describe("dataconnect", () => { @@ -136,11 +146,12 @@ describe("dataconnect", () => { connector: "my-connector", operation: "my-operation", }, - {} + {}, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/my-service/connectors/my-connector/operations/my-operation", + "locations/us-central1/services/my-service/connectors/my-connector/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -177,11 +188,12 @@ describe("dataconnect", () => { }, { service: "{service}", - } + }, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/{service}/connectors/my-connector/operations/my-operation", + "locations/us-central1/services/{service}/connectors/my-connector/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -219,11 +231,12 @@ describe("dataconnect", () => { }, { connector: "{connector}", - } + }, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/my-service/connectors/{connector}/operations/my-operation", + "locations/us-central1/services/my-service/connectors/{connector}/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -261,11 +274,12 @@ describe("dataconnect", () => { }, { operation: "{operation}", - } + }, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/my-service/connectors/my-connector/operations/{operation}", + "locations/us-central1/services/my-service/connectors/my-connector/operations/{operation}", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -302,11 +316,12 @@ describe("dataconnect", () => { service: "{service}", connector: "{connector}", operation: "{operation}", - } + }, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/{service}/connectors/{connector}/operations/{operation}", + "locations/us-central1/services/{service}/connectors/{connector}/operations/{operation}", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -343,11 +358,12 @@ describe("dataconnect", () => { }, { service: "*", - } + }, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/*/connectors/my-connector/operations/my-operation", + "locations/us-central1/services/*/connectors/my-connector/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -385,11 +401,12 @@ describe("dataconnect", () => { }, { connector: "*", - } + }, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/my-service/connectors/*/operations/my-operation", + "locations/us-central1/services/my-service/connectors/*/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -427,11 +444,12 @@ describe("dataconnect", () => { }, { operation: "*", - } + }, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/my-service/connectors/my-connector/operations/*", + "locations/us-central1/services/my-service/connectors/my-connector/operations/*", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -468,11 +486,12 @@ describe("dataconnect", () => { service: "*", connector: "*", operation: "*", - } + }, + ["us-central1"] ); const func = dataconnect.onMutationExecuted( - "services/*/connectors/*/operations/*", + "locations/us-central1/services/*/connectors/*/operations/*", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -521,7 +540,7 @@ describe("dataconnect", () => { onInit(() => (hello = "world")); expect(hello).to.be.undefined; await dataconnect.onMutationExecuted( - "services/*/connectors/*/operations/*", + "locations/us-central1/services/*/connectors/*/operations/*", () => null )(event); expect(hello).to.equal("world"); diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts index b465af3ae..29b1ba6dd 100644 --- a/src/v2/providers/dataconnect.ts +++ b/src/v2/providers/dataconnect.ts @@ -22,12 +22,19 @@ import { CloudEvent, CloudFunction } from "../core"; import { ParamsOf, VarName } from "../../common/params"; -import { EventHandlerOptions, getGlobalOptions, optionsToEndpoint } from "../options"; +import { + EventHandlerOptions, + getGlobalOptions, + optionsToEndpoint, + SupportedRegion, +} from "../options"; import { normalizePath } from "../../common/utilities/path"; import { wrapTraceContext } from "../trace"; import { withInit } from "../../common/onInit"; import { initV2Endpoint, ManifestEndpoint } from "../../runtime/manifest"; import { PathPattern } from "../../common/utilities/path-pattern"; +import { Expression } from "../../params"; +import { ResetValue } from "../../common/options"; /** @internal */ export const mutationExecutedEventType = @@ -99,6 +106,10 @@ export interface OperationOptions< connector?: Connector; /** Name of the operation */ operation?: Operation; + /** + * Region where functions should be deployed. Defaults to us-central1. + */ + region?: SupportedRegion | string | Expression | ResetValue; } export type DataConnectParams = @@ -198,7 +209,9 @@ export function onMutationExecuted< } function getOpts(mutationOrOpts: string | OperationOptions) { - const operationRegex = new RegExp("services/([^/]+)/connectors/([^/]+)/operations/([^/]+)"); + const operationRegex = new RegExp( + "locations/([^/]+)/services/([^/]+)/connectors/([^/]*)/operations/([^/]+)" + ); let service: string | undefined; let connector: string | undefined; @@ -211,10 +224,10 @@ function getOpts(mutationOrOpts: string | OperationOptions) { throw new Error(`Invalid operation path: ${path}`); } - service = match[1]; - connector = match[2]; - operation = match[3]; - opts = {}; + service = match[2]; + connector = match[3]; + operation = match[4]; + opts = { region: match[1] }; } else { service = mutationOrOpts.service; connector = mutationOrOpts.connector; @@ -324,7 +337,7 @@ function onOperation( operationPattern ); - const { authtype, authid, ...rest } = event; + const { authtype, authid, service, connector, operation, ...rest } = event; const dataConnectEvent: DataConnectEvent< MutationEventData, DataConnectParams From 55930cae551f57f9fb72f878c473aa14c1c1f1f1 Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Thu, 23 Oct 2025 10:38:43 -0700 Subject: [PATCH 10/11] Remove locations/... segment --- spec/v2/providers/dataconnect.spec.ts | 61 +++++++++------------------ src/v2/providers/dataconnect.ts | 12 +++--- 2 files changed, 26 insertions(+), 47 deletions(-) diff --git a/spec/v2/providers/dataconnect.spec.ts b/spec/v2/providers/dataconnect.spec.ts index 9e799d92d..a85e4ee44 100644 --- a/spec/v2/providers/dataconnect.spec.ts +++ b/spec/v2/providers/dataconnect.spec.ts @@ -39,13 +39,8 @@ const expectedEndpointBase = { labels: {}, }; -function makeExpectedEndpoint( - eventType: string, - eventFilters, - eventFilterPathPatterns, - region?: string[] -) { - let endpoint = { +function makeExpectedEndpoint(eventType: string, eventFilters, eventFilterPathPatterns) { + return { ...expectedEndpointBase, eventTrigger: { eventType, @@ -54,11 +49,6 @@ function makeExpectedEndpoint( retry: false, }, }; - - if (region) { - return { ...endpoint, region }; - } - return endpoint; } describe("dataconnect", () => { @@ -146,12 +136,11 @@ describe("dataconnect", () => { connector: "my-connector", operation: "my-operation", }, - {}, - ["us-central1"] + {} ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/my-service/connectors/my-connector/operations/my-operation", + "services/my-service/connectors/my-connector/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -188,12 +177,11 @@ describe("dataconnect", () => { }, { service: "{service}", - }, - ["us-central1"] + } ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/{service}/connectors/my-connector/operations/my-operation", + "services/{service}/connectors/my-connector/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -231,12 +219,11 @@ describe("dataconnect", () => { }, { connector: "{connector}", - }, - ["us-central1"] + } ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/my-service/connectors/{connector}/operations/my-operation", + "services/my-service/connectors/{connector}/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -274,12 +261,11 @@ describe("dataconnect", () => { }, { operation: "{operation}", - }, - ["us-central1"] + } ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/my-service/connectors/my-connector/operations/{operation}", + "services/my-service/connectors/my-connector/operations/{operation}", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -316,12 +302,11 @@ describe("dataconnect", () => { service: "{service}", connector: "{connector}", operation: "{operation}", - }, - ["us-central1"] + } ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/{service}/connectors/{connector}/operations/{operation}", + "services/{service}/connectors/{connector}/operations/{operation}", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -358,12 +343,11 @@ describe("dataconnect", () => { }, { service: "*", - }, - ["us-central1"] + } ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/*/connectors/my-connector/operations/my-operation", + "services/*/connectors/my-connector/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -401,12 +385,11 @@ describe("dataconnect", () => { }, { connector: "*", - }, - ["us-central1"] + } ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/my-service/connectors/*/operations/my-operation", + "services/my-service/connectors/*/operations/my-operation", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -444,12 +427,11 @@ describe("dataconnect", () => { }, { operation: "*", - }, - ["us-central1"] + } ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/my-service/connectors/my-connector/operations/*", + "services/my-service/connectors/my-connector/operations/*", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -486,12 +468,11 @@ describe("dataconnect", () => { service: "*", connector: "*", operation: "*", - }, - ["us-central1"] + } ); const func = dataconnect.onMutationExecuted( - "locations/us-central1/services/*/connectors/*/operations/*", + "services/*/connectors/*/operations/*", () => true ); expect(func.__endpoint).to.deep.eq(expectedEndpoint); @@ -540,7 +521,7 @@ describe("dataconnect", () => { onInit(() => (hello = "world")); expect(hello).to.be.undefined; await dataconnect.onMutationExecuted( - "locations/us-central1/services/*/connectors/*/operations/*", + "services/*/connectors/*/operations/*", () => null )(event); expect(hello).to.equal("world"); diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts index 29b1ba6dd..37448f497 100644 --- a/src/v2/providers/dataconnect.ts +++ b/src/v2/providers/dataconnect.ts @@ -209,9 +209,7 @@ export function onMutationExecuted< } function getOpts(mutationOrOpts: string | OperationOptions) { - const operationRegex = new RegExp( - "locations/([^/]+)/services/([^/]+)/connectors/([^/]*)/operations/([^/]+)" - ); + const operationRegex = new RegExp("services/([^/]+)/connectors/([^/]*)/operations/([^/]+)"); let service: string | undefined; let connector: string | undefined; @@ -224,10 +222,10 @@ function getOpts(mutationOrOpts: string | OperationOptions) { throw new Error(`Invalid operation path: ${path}`); } - service = match[2]; - connector = match[3]; - operation = match[4]; - opts = { region: match[1] }; + service = match[1]; + connector = match[2]; + operation = match[3]; + opts = {}; } else { service = mutationOrOpts.service; connector = mutationOrOpts.connector; From 1649c9296a7e798447462867174013a95cc188fd Mon Sep 17 00:00:00 2001 From: Lisa Jian Date: Mon, 27 Oct 2025 13:52:09 -0700 Subject: [PATCH 11/11] Address comments --- src/v2/providers/dataconnect.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/v2/providers/dataconnect.ts b/src/v2/providers/dataconnect.ts index 37448f497..e5245bcf2 100644 --- a/src/v2/providers/dataconnect.ts +++ b/src/v2/providers/dataconnect.ts @@ -259,19 +259,25 @@ function makeEndpoint( const eventFilterPathPatterns: Record = {}; if (service) { - service.hasWildcards() - ? (eventFilterPathPatterns.service = service.getValue()) - : (eventFilters.service = service.getValue()); + if (service.hasWildcards()) { + eventFilterPathPatterns.service = service.getValue(); + } else { + eventFilters.service = service.getValue(); + } } if (connector) { - connector.hasWildcards() - ? (eventFilterPathPatterns.connector = connector.getValue()) - : (eventFilters.connector = connector.getValue()); + if (connector.hasWildcards()) { + eventFilterPathPatterns.connector = connector.getValue(); + } else { + eventFilters.connector = connector.getValue(); + } } if (operation) { - operation.hasWildcards() - ? (eventFilterPathPatterns.operation = operation.getValue()) - : (eventFilters.operation = operation.getValue()); + if (operation.hasWildcards()) { + eventFilterPathPatterns.operation = operation.getValue(); + } else { + eventFilters.operation = operation.getValue(); + } } return { ...initV2Endpoint(getGlobalOptions(), opts),