diff --git a/spec/v2/providers/firestore.spec.ts b/spec/v2/providers/firestore.spec.ts index 7e734c671..e672703a6 100644 --- a/spec/v2/providers/firestore.spec.ts +++ b/spec/v2/providers/firestore.spec.ts @@ -26,6 +26,7 @@ import { Timestamp } from "firebase-admin/firestore"; import * as firestore from "../../../src/v2/providers/firestore"; import { PathPattern } from "../../../src/common/utilities/path-pattern"; import { onInit } from "../../../src/v2/core"; +import * as params from "../../../src/params"; /** static-complied protobuf */ const DocumentEventData = google.events.cloud.firestore.v1.DocumentEventData; @@ -148,6 +149,20 @@ const writtenData = { const writtenProto = DocumentEventData.create(writtenData); describe("firestore", () => { + let docParam: params.Expression; + let nsParam: params.Expression; + let dbParam: params.Expression; + + before(() => { + docParam = params.defineString("DOCUMENT"); + nsParam = params.defineString("NAMESPACE"); + dbParam = params.defineString("DATABASE"); + }); + + after(() => { + params.clearParams(); + }); + describe("onDocumentWritten", () => { it("should create a func", () => { const expectedEp = makeExpectedEp( @@ -194,6 +209,29 @@ describe("firestore", () => { expect(func.__endpoint).to.deep.eq(expectedEp); }); + it("should create a func with param opts", () => { + const expectedEp = makeExpectedEp( + firestore.writtenEventType, + { + database: dbParam, + namespace: nsParam, + }, + { + document: docParam, + } + ); + + const func = firestore.onDocumentWritten( + { + database: dbParam, + namespace: nsParam, + document: docParam, + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + it("calls init function", async () => { const event: firestore.RawFirestoreEvent = { ...eventBase, @@ -258,6 +296,29 @@ describe("firestore", () => { expect(func.__endpoint).to.deep.eq(expectedEp); }); + it("should create a func with param opts", () => { + const expectedEp = makeExpectedEp( + firestore.createdEventType, + { + database: dbParam, + namespace: nsParam, + }, + { + document: docParam, + } + ); + + const func = firestore.onDocumentCreated( + { + database: dbParam, + namespace: nsParam, + document: docParam, + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + it("calls init function", async () => { const event: firestore.RawFirestoreEvent = { ...eventBase, @@ -322,6 +383,29 @@ describe("firestore", () => { expect(func.__endpoint).to.deep.eq(expectedEp); }); + it("should create a func with param opts", () => { + const expectedEp = makeExpectedEp( + firestore.updatedEventType, + { + database: dbParam, + namespace: nsParam, + }, + { + document: docParam, + } + ); + + const func = firestore.onDocumentUpdated( + { + database: dbParam, + namespace: nsParam, + document: docParam, + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + it("calls init function", async () => { const event: firestore.RawFirestoreEvent = { ...eventBase, @@ -386,6 +470,29 @@ describe("firestore", () => { expect(func.__endpoint).to.deep.eq(expectedEp); }); + it("should create a func with param opts", () => { + const expectedEp = makeExpectedEp( + firestore.deletedEventType, + { + database: dbParam, + namespace: nsParam, + }, + { + document: docParam, + } + ); + + const func = firestore.onDocumentDeleted( + { + database: dbParam, + namespace: nsParam, + document: docParam, + }, + () => true + ); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + it("calls init function", async () => { const event: firestore.RawFirestoreEvent = { ...eventBase, @@ -663,7 +770,7 @@ describe("firestore", () => { const ep = firestore.makeEndpoint( firestore.createdEventType, { region: "us-central1" }, - new PathPattern("foo/{bar}"), + "foo/{bar}", "my-db", "my-ns" ); @@ -686,7 +793,7 @@ describe("firestore", () => { const ep = firestore.makeEndpoint( firestore.createdEventType, { region: "us-central1" }, - new PathPattern("foo/fGRodw71mHutZ4wGDuT8"), + "foo/fGRodw71mHutZ4wGDuT8", "my-db", "my-ns" ); diff --git a/src/common/params.ts b/src/common/params.ts index a35a870b3..e0b0b8537 100644 --- a/src/common/params.ts +++ b/src/common/params.ts @@ -20,6 +20,8 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import { Expression } from "../params"; + /** * A type that splits literal string S with delimiter D. * @@ -78,10 +80,17 @@ export type Extract = Part extends `{${infer Param}=**}` * * For flexibility reasons, ParamsOf is Record */ -export type ParamsOf = +export type ParamsOf> = // if we have lost type information, revert back to an untyped dictionary - string extends PathPattern + PathPattern extends Expression + ? Record + : string extends PathPattern ? Record : { - [Key in Extract, "/">[number]>]: string; + // 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< + Split>>, "/">[number] + >]: string; }; diff --git a/src/v2/providers/firestore.ts b/src/v2/providers/firestore.ts index dd3c461ff..cc78aa87f 100644 --- a/src/v2/providers/firestore.ts +++ b/src/v2/providers/firestore.ts @@ -36,6 +36,7 @@ import { } from "../../common/providers/firestore"; import { wrapTraceContext } from "../trace"; import { withInit } from "../../common/onInit"; +import { Expression } from "../../params"; export { Change }; @@ -106,11 +107,11 @@ export interface FirestoreEvent> extends Clou /** DocumentOptions extend EventHandlerOptions with provided document and optional database and namespace. */ export interface DocumentOptions extends EventHandlerOptions { /** The document path */ - document: Document; + document: Document | Expression; /** The Firestore database */ - database?: string; + database?: string | Expression; /** The Firestore namespace */ - namespace?: string; + namespace?: string | Expression; } /** @@ -278,9 +279,9 @@ export function onDocumentDeleted( /** @internal */ export function getOpts(documentOrOpts: string | DocumentOptions) { - let document: string; - let database: string; - let namespace: string; + let document: string | Expression; + let database: string | Expression; + let namespace: string | Expression; let opts: EventHandlerOptions; if (typeof documentOrOpts === "string") { document = normalizePath(documentOrOpts); @@ -288,7 +289,10 @@ export function getOpts(documentOrOpts: string | DocumentOptions) { namespace = "(default)"; opts = {}; } else { - document = normalizePath(documentOrOpts.document); + document = + typeof documentOrOpts.document === "string" + ? normalizePath(documentOrOpts.document) + : documentOrOpts.document; database = documentOrOpts.database || "(default)"; namespace = documentOrOpts.namespace || "(default)"; opts = { ...documentOrOpts }; @@ -398,21 +402,25 @@ export function makeChangedFirestoreEvent( export function makeEndpoint( eventType: string, opts: EventHandlerOptions, - document: PathPattern, - database: string, - namespace: string + document: string | Expression, + database: string | Expression, + namespace: string | Expression ): ManifestEndpoint { const baseOpts = optionsToEndpoint(getGlobalOptions()); const specificOpts = optionsToEndpoint(opts); - const eventFilters: Record = { + const eventFilters: Record> = { database, namespace, }; - const eventFilterPathPatterns: Record = {}; - document.hasWildcards() - ? (eventFilterPathPatterns.document = document.getValue()) - : (eventFilters.document = document.getValue()); + const eventFilterPathPatterns: Record> = {}; + const maybePattern = + typeof document === "string" ? new PathPattern(document).hasWildcards() : true; + if (maybePattern) { + eventFilterPathPatterns.document = document; + } else { + eventFilters.document = document; + } return { ...initV2Endpoint(getGlobalOptions(), opts), @@ -440,11 +448,12 @@ export function onOperation( ): CloudFunction>> { const { document, database, namespace, opts } = getOpts(documentOrOpts); - const documentPattern = new PathPattern(document); - // wrap the handler const func = (raw: CloudEvent) => { const event = raw as RawFirestoreEvent; + const documentPattern = new PathPattern( + typeof document === "string" ? document : document.value() + ); const params = makeParams(event.document, documentPattern) as unknown as ParamsOf; const firestoreEvent = makeFirestoreEvent(eventType, event, params); return wrapTraceContext(withInit(handler))(firestoreEvent); @@ -452,7 +461,7 @@ export function onOperation( func.run = handler; - func.__endpoint = makeEndpoint(eventType, opts, documentPattern, database, namespace); + func.__endpoint = makeEndpoint(eventType, opts, document, database, namespace); return func; } @@ -467,11 +476,12 @@ export function onChangedOperation( ): CloudFunction, ParamsOf>> { const { document, database, namespace, opts } = getOpts(documentOrOpts); - const documentPattern = new PathPattern(document); - // wrap the handler const func = (raw: CloudEvent) => { const event = raw as RawFirestoreEvent; + const documentPattern = new PathPattern( + typeof document === "string" ? document : document.value() + ); const params = makeParams(event.document, documentPattern) as unknown as ParamsOf; const firestoreEvent = makeChangedFirestoreEvent(event, params); return wrapTraceContext(withInit(handler))(firestoreEvent); @@ -479,7 +489,7 @@ export function onChangedOperation( func.run = handler; - func.__endpoint = makeEndpoint(eventType, opts, documentPattern, database, namespace); + func.__endpoint = makeEndpoint(eventType, opts, document, database, namespace); return func; }