diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29bb..fdbf13bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1 @@ +- Add new 2nd gen Firestore auth context triggers. (#1519) diff --git a/package-lock.json b/package-lock.json index 6a51af0a1..9f23b098b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "@types/express": "4.17.3", "cors": "^2.8.5", "express": "^4.17.1", - "node-fetch": "^2.6.7", "protobufjs": "^7.2.2" }, "bin": { diff --git a/spec/v2/providers/firestore.spec.ts b/spec/v2/providers/firestore.spec.ts index e672703a6..e5406bdb3 100644 --- a/spec/v2/providers/firestore.spec.ts +++ b/spec/v2/providers/firestore.spec.ts @@ -84,6 +84,15 @@ function makeEvent(data?: any): firestore.RawFirestoreEvent { } as firestore.RawFirestoreEvent; } +function makeAuthEvent(data?: any): firestore.RawFirestoreAuthEvent { + return { + ...eventBase, + data, + authid: "userId", + authtype: "unknown", + } as firestore.RawFirestoreAuthEvent; +} + const createdData = { value: { fields: { @@ -511,6 +520,262 @@ describe("firestore", () => { }); }); + describe("onDocumentWrittenWithAuthContext", () => { + it("should create a func", () => { + const expectedEp = makeExpectedEp( + firestore.writtenEventWithAuthContextType, + { + database: "(default)", + namespace: "(default)", + }, + { + document: "foo/{bar}", + } + ); + + const func = firestore.onDocumentWrittenWithAuthContext("foo/{bar}", () => 2); + + expect(func.run(true as any)).to.eq(2); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + + it("should create a func with opts", () => { + const expectedEp = makeExpectedEp( + firestore.writtenEventWithAuthContextType, + { + database: "my-db", + namespace: "my-ns", + }, + { + document: "foo/{bar}", + } + ); + expectedEp["region"] = ["us-central1"]; + + const func = firestore.onDocumentWrittenWithAuthContext( + { + region: "us-central1", + document: "foo/{bar}", + database: "my-db", + namespace: "my-ns", + }, + () => 2 + ); + + expect(func.run(true as any)).to.eq(2); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + + it("calls init function", async () => { + const event: firestore.RawFirestoreEvent = { + ...eventBase, + datacontenttype: "application/json", + data: { + oldValue: null, + value: null, + }, + }; + + let hello; + onInit(() => (hello = "world")); + expect(hello).to.be.undefined; + await firestore.onDocumentWrittenWithAuthContext("path", () => null)(event); + expect(hello).to.equal("world"); + }); + }); + + describe("onDocumentCreatedWithAuthContext", () => { + it("should create a func", () => { + const expectedEp = makeExpectedEp( + firestore.createdEventWithAuthContextType, + { + database: "(default)", + namespace: "(default)", + }, + { + document: "foo/{bar}", + } + ); + + const func = firestore.onDocumentCreatedWithAuthContext("foo/{bar}", () => 2); + + expect(func.run(true as any)).to.eq(2); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + + it("should create a func with opts", () => { + const expectedEp = makeExpectedEp( + firestore.createdEventWithAuthContextType, + { + database: "my-db", + namespace: "my-ns", + }, + { + document: "foo/{bar}", + } + ); + expectedEp["region"] = ["us-central1"]; + + const func = firestore.onDocumentCreatedWithAuthContext( + { + region: "us-central1", + document: "foo/{bar}", + database: "my-db", + namespace: "my-ns", + }, + () => 2 + ); + + expect(func.run(true as any)).to.eq(2); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + + it("calls init function", async () => { + const event: firestore.RawFirestoreEvent = { + ...eventBase, + datacontenttype: "application/json", + data: { + oldValue: null, + value: null, + }, + }; + + let hello; + onInit(() => (hello = "world")); + expect(hello).to.be.undefined; + await firestore.onDocumentCreatedWithAuthContext("path", () => null)(event); + expect(hello).to.equal("world"); + }); + }); + + describe("onDocumentUpdatedWithAuthContext", () => { + it("should create a func", () => { + const expectedEp = makeExpectedEp( + firestore.updatedEventWithAuthContextType, + { + database: "(default)", + namespace: "(default)", + }, + { + document: "foo/{bar}", + } + ); + + const func = firestore.onDocumentUpdatedWithAuthContext("foo/{bar}", () => 2); + + expect(func.run(true as any)).to.eq(2); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + + it("should create a func with opts", () => { + const expectedEp = makeExpectedEp( + firestore.updatedEventWithAuthContextType, + { + database: "my-db", + namespace: "my-ns", + }, + { + document: "foo/{bar}", + } + ); + expectedEp["region"] = ["us-central1"]; + + const func = firestore.onDocumentUpdatedWithAuthContext( + { + region: "us-central1", + document: "foo/{bar}", + database: "my-db", + namespace: "my-ns", + }, + () => 2 + ); + + expect(func.run(true as any)).to.eq(2); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + + it("calls init function", async () => { + const event: firestore.RawFirestoreEvent = { + ...eventBase, + datacontenttype: "application/json", + data: { + oldValue: null, + value: null, + }, + }; + + let hello; + onInit(() => (hello = "world")); + expect(hello).to.be.undefined; + await firestore.onDocumentUpdatedWithAuthContext("path", () => null)(event); + expect(hello).to.equal("world"); + }); + }); + + describe("onDocumentDeletedWithAuthContext", () => { + it("should create a func", () => { + const expectedEp = makeExpectedEp( + firestore.deletedEventWithAuthContextType, + { + database: "(default)", + namespace: "(default)", + }, + { + document: "foo/{bar}", + } + ); + + const func = firestore.onDocumentDeletedWithAuthContext("foo/{bar}", () => 2); + + expect(func.run(true as any)).to.eq(2); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + + it("should create a func with opts", () => { + const expectedEp = makeExpectedEp( + firestore.deletedEventWithAuthContextType, + { + database: "my-db", + namespace: "my-ns", + }, + { + document: "foo/{bar}", + } + ); + expectedEp["region"] = ["us-central1"]; + + const func = firestore.onDocumentDeletedWithAuthContext( + { + region: "us-central1", + document: "foo/{bar}", + database: "my-db", + namespace: "my-ns", + }, + () => 2 + ); + + expect(func.run(true as any)).to.eq(2); + expect(func.__endpoint).to.deep.eq(expectedEp); + }); + + it("calls init function", async () => { + const event: firestore.RawFirestoreEvent = { + ...eventBase, + datacontenttype: "application/json", + data: { + oldValue: null, + value: null, + }, + }; + + let hello; + onInit(() => (hello = "world")); + expect(hello).to.be.undefined; + await firestore.onDocumentDeletedWithAuthContext("path", () => null)(event); + expect(hello).to.equal("world"); + }); + }); + describe("getOpts", () => { it("should handle document string", () => { const { document, database, namespace, opts } = firestore.getOpts("foo/{bar}"); @@ -720,6 +985,26 @@ describe("firestore", () => { expect(event.data.data()).to.deep.eq({ hello: "delete world" }); }); + + it("should make event from a created event with auth context", () => { + const event = firestore.makeFirestoreEvent( + firestore.createdEventWithAuthContextType, + makeAuthEvent(makeEncodedProtobuf(createdProto)), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event.data.data()).to.deep.eq({ hello: "create world" }); + }); + + it("should include auth fields if provided in raw event", () => { + const event = firestore.makeFirestoreEvent( + firestore.createdEventWithAuthContextType, + makeAuthEvent(makeEncodedProtobuf(createdProto)), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event).to.include({ authId: "userId", authType: "unknown" }); + }); }); describe("makeChangedFirestoreEvent", () => { @@ -753,6 +1038,15 @@ describe("firestore", () => { }); }); + it("should include auth fields if provided in raw event", () => { + const event = firestore.makeChangedFirestoreEvent( + makeAuthEvent(makeEncodedProtobuf(writtenProto)), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event).to.include({ authId: "userId", authType: "unknown" }); + }); + describe("makeEndpoint", () => { it("should make an endpoint with a document path pattern", () => { const expectedEp = makeExpectedEp( diff --git a/src/v2/providers/firestore.ts b/src/v2/providers/firestore.ts index cc78aa87f..1d8b823a1 100644 --- a/src/v2/providers/firestore.ts +++ b/src/v2/providers/firestore.ts @@ -52,6 +52,22 @@ export const updatedEventType = "google.cloud.firestore.document.v1.updated"; /** @internal */ export const deletedEventType = "google.cloud.firestore.document.v1.deleted"; +/** @internal */ +export const writtenEventWithAuthContextType = + "google.cloud.firestore.document.v1.written.withAuthContext"; + +/** @internal */ +export const createdEventWithAuthContextType = + "google.cloud.firestore.document.v1.created.withAuthContext"; + +/** @internal */ +export const updatedEventWithAuthContextType = + "google.cloud.firestore.document.v1.updated.withAuthContext"; + +/** @internal */ +export const deletedEventWithAuthContextType = + "google.cloud.firestore.document.v1.deleted.withAuthContext"; + // https://github.com/googleapis/google-cloudevents-nodejs/blob/main/cloud/firestore/v1/DocumentEventData.ts /** @internal */ export interface RawFirestoreDocument { @@ -79,12 +95,28 @@ export interface RawFirestoreEvent extends CloudEvent */ export interface FirestoreEvent> extends CloudEvent { /** The location of the Firestore instance */ @@ -104,6 +136,14 @@ export interface FirestoreEvent> extends Clou params: Params; } +export interface FirestoreAuthEvent> + extends FirestoreEvent { + /** The type of principal that triggered the event */ + authType: AuthType; + /** The unique identifier for the principal */ + authId?: string; +} + /** DocumentOptions extend EventHandlerOptions with provided document and optional database and namespace. */ export interface DocumentOptions extends EventHandlerOptions { /** The document path */ @@ -115,7 +155,7 @@ export interface DocumentOptions extends Event } /** - * Event handler which triggers when a document is created, updated, or deleted in Firestore. + * Event handler that triggers when a document is created, updated, or deleted in Firestore. * * @param document - The Firestore document path to trigger on. * @param handler - Event handler which is run every time a Firestore create, update, or delete occurs. @@ -128,7 +168,7 @@ export function onDocumentWritten( ): CloudFunction | undefined, ParamsOf>>; /** - * Event handler which triggers when a document is created, updated, or deleted in Firestore. + * Event handler that triggers when a document is created, updated, or deleted in Firestore. * * @param opts - Options that can be set on an individual event-handling function. * @param handler - Event handler which is run every time a Firestore create, update, or delete occurs. @@ -141,7 +181,7 @@ export function onDocumentWritten( ): CloudFunction | undefined, ParamsOf>>; /** - * Event handler which triggers when a document is created, updated, or deleted in Firestore. + * Event handler that triggers when a document is created, updated, or deleted in Firestore. * * @param documentOrOpts - Options or a string document path. * @param handler - Event handler which is run every time a Firestore create, update, or delete occurs. @@ -156,7 +196,51 @@ export function onDocumentWritten( } /** - * Event handler which triggers when a document is created in Firestore. + * Event handler that triggers when a document is created, updated, or deleted in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param document - The Firestore document path to trigger on. + * @param handler - Event handler which is run every time a Firestore create, update, or delete occurs. + */ +export function onDocumentWrittenWithAuthContext( + document: Document, + handler: ( + event: FirestoreAuthEvent | undefined, ParamsOf> + ) => any | Promise +): CloudFunction | undefined, ParamsOf>>; + +/** + * Event handler that triggers when a document is created, updated, or deleted in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param opts - Options that can be set on an individual event-handling function. + * @param handler - Event handler which is run every time a Firestore create, update, or delete occurs. + */ +export function onDocumentWrittenWithAuthContext( + opts: DocumentOptions, + handler: ( + event: FirestoreAuthEvent | undefined, ParamsOf> + ) => any | Promise +): CloudFunction | undefined, ParamsOf>>; + +/** + * Event handler that triggers when a document is created, updated, or deleted in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param opts - Options or a string document path. + * @param handler - Event handler which is run every time a Firestore create, update, or delete occurs. + */ +export function onDocumentWrittenWithAuthContext( + documentOrOpts: Document | DocumentOptions, + handler: ( + event: FirestoreAuthEvent | undefined, ParamsOf> + ) => any | Promise +): CloudFunction | undefined, ParamsOf>> { + return onChangedOperation(writtenEventWithAuthContextType, documentOrOpts, handler); +} + +/** + * Event handler that triggers when a document is created in Firestore. * * @param document - The Firestore document path to trigger on. * @param handler - Event handler which is run every time a Firestore create occurs. @@ -169,7 +253,7 @@ export function onDocumentCreated( ): CloudFunction>>; /** - * Event handler which triggers when a document is created in Firestore. + * Event handler that triggers when a document is created in Firestore. * * @param opts - Options that can be set on an individual event-handling function. * @param handler - Event handler which is run every time a Firestore create occurs. @@ -182,7 +266,7 @@ export function onDocumentCreated( ): CloudFunction>>; /** - * Event handler which triggers when a document is created in Firestore. + * Event handler that triggers when a document is created in Firestore. * * @param documentOrOpts - Options or a string document path. * @param handler - Event handler which is run every time a Firestore create occurs. @@ -197,7 +281,50 @@ export function onDocumentCreated( } /** - * Event handler which triggers when a document is updated in Firestore. + * Event handler that triggers when a document is created in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param document - The Firestore document path to trigger on. + * @param handler - Event handler which is run every time a Firestore create occurs. + */ +export function onDocumentCreatedWithAuthContext( + document: Document, + handler: ( + event: FirestoreAuthEvent> + ) => any | Promise +): CloudFunction>>; + +/** + * Event handler that triggers when a document is created in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param opts - Options that can be set on an individual event-handling function. + * @param handler - Event handler which is run every time a Firestore create occurs. + */ +export function onDocumentCreatedWithAuthContext( + opts: DocumentOptions, + handler: ( + event: FirestoreAuthEvent> + ) => any | Promise +): CloudFunction>>; + +/** + * Event handler that triggers when a document is created in Firestore. + * + * @param documentOrOpts - Options or a string document path. + * @param handler - Event handler which is run every time a Firestore create occurs. + */ +export function onDocumentCreatedWithAuthContext( + documentOrOpts: Document | DocumentOptions, + handler: ( + event: FirestoreAuthEvent> + ) => any | Promise +): CloudFunction>> { + return onOperation(createdEventWithAuthContextType, documentOrOpts, handler); +} + +/** + * Event handler that triggers when a document is updated in Firestore. * * @param document - The Firestore document path to trigger on. * @param handler - Event handler which is run every time a Firestore update occurs. @@ -209,7 +336,7 @@ export function onDocumentUpdated( ) => any | Promise ): CloudFunction | undefined, ParamsOf>>; /** - * Event handler which triggers when a document is updated in Firestore. + * Event handler that triggers when a document is updated in Firestore. * * @param opts - Options that can be set on an individual event-handling function. * @param handler - Event handler which is run every time a Firestore update occurs. @@ -222,7 +349,7 @@ export function onDocumentUpdated( ): CloudFunction | undefined, ParamsOf>>; /** - * Event handler which triggers when a document is updated in Firestore. + * Event handler that triggers when a document is updated in Firestore. * * @param documentOrOpts - Options or a string document path. * @param handler - Event handler which is run every time a Firestore update occurs. @@ -237,7 +364,52 @@ export function onDocumentUpdated( } /** - * Event handler which triggers when a document is deleted in Firestore. + * Event handler that triggers when a document is updated in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param document - The Firestore document path to trigger on. + * @param handler - Event handler which is run every time a Firestore update occurs. + */ +export function onDocumentUpdatedWithAuthContext( + document: Document, + handler: ( + event: FirestoreAuthEvent | undefined, ParamsOf> + ) => any | Promise +): CloudFunction | undefined, ParamsOf>>; + +/** + * Event handler that triggers when a document is updated in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param opts - Options that can be set on an individual event-handling function. + * @param handler - Event handler which is run every time a Firestore update occurs. + */ +export function onDocumentUpdatedWithAuthContext( + opts: DocumentOptions, + handler: ( + event: FirestoreAuthEvent | undefined, ParamsOf> + ) => any | Promise +): CloudFunction | undefined, ParamsOf>>; + +/** + * Event handler that triggers when a document is updated in Firestore. + * + * @param documentOrOpts - Options or a string document path. + * @param handler - Event handler which is run every time a Firestore update occurs. + */ +export function onDocumentUpdatedWithAuthContext( + documentOrOpts: Document | DocumentOptions, + handler: ( + event: FirestoreAuthEvent | undefined, ParamsOf> + ) => any | Promise +): CloudFunction< + FirestoreAuthEvent | undefined, ParamsOf> +> { + return onChangedOperation(updatedEventWithAuthContextType, documentOrOpts, handler); +} + +/** + * Event handler that triggers when a document is deleted in Firestore. * * @param document - The Firestore document path to trigger on. * @param handler - Event handler which is run every time a Firestore delete occurs. @@ -250,7 +422,7 @@ export function onDocumentDeleted( ): CloudFunction>>; /** - * Event handler which triggers when a document is deleted in Firestore. + * Event handler that triggers when a document is deleted in Firestore. * * @param opts - Options that can be set on an individual event-handling function. * @param handler - Event handler which is run every time a Firestore delete occurs. @@ -263,7 +435,7 @@ export function onDocumentDeleted( ): CloudFunction>>; /** - * Event handler which triggers when a document is deleted in Firestore. + * Event handler that triggers when a document is deleted in Firestore. * * @param documentOrOpts - Options or a string document path. * @param handler - Event handler which is run every time a Firestore delete occurs. @@ -277,6 +449,49 @@ export function onDocumentDeleted( return onOperation(deletedEventType, documentOrOpts, handler); } +/** + * Event handler that triggers when a document is deleted in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param document - The Firestore document path to trigger on. + * @param handler - Event handler which is run every time a Firestore delete occurs. + */ +export function onDocumentDeletedWithAuthContext( + document: Document, + handler: ( + event: FirestoreAuthEvent> + ) => any | Promise +): CloudFunction>>; + +/** + * Event handler that triggers when a document is deleted in Firestore. + * This trigger also provides the authentication context of the principal who triggered the event. + * + * @param opts - Options that can be set on an individual event-handling function. + * @param handler - Event handler which is run every time a Firestore delete occurs. + */ +export function onDocumentDeletedWithAuthContext( + opts: DocumentOptions, + handler: ( + event: FirestoreAuthEvent> + ) => any | Promise +): CloudFunction>>; + +/** + * Event handler that triggers when a document is deleted in Firestore. + * + * @param documentOrOpts - Options or a string document path. + * @param handler - Event handler which is run every time a Firestore delete occurs. + */ +export function onDocumentDeletedWithAuthContext( + documentOrOpts: Document | DocumentOptions, + handler: ( + event: FirestoreAuthEvent> + ) => any | Promise +): CloudFunction>> { + return onOperation(deletedEventWithAuthContextType, documentOrOpts, handler); +} + /** @internal */ export function getOpts(documentOrOpts: string | DocumentOptions) { let document: string | Expression; @@ -362,11 +577,13 @@ export function makeParams(document: string, documentPattern: PathPattern) { /** @internal */ export function makeFirestoreEvent( eventType: string, - event: RawFirestoreEvent, + event: RawFirestoreEvent | RawFirestoreAuthEvent, params: Params -): FirestoreEvent { +): + | FirestoreEvent + | FirestoreAuthEvent { const data = event.data - ? eventType === createdEventType + ? eventType === createdEventType || eventType === createdEventWithAuthContextType ? createSnapshot(event) : createBeforeSnapshot(event) : undefined; @@ -375,16 +592,31 @@ export function makeFirestoreEvent( params, data, }; + delete (firestoreEvent as any).datacontenttype; delete (firestoreEvent as any).dataschema; + + if ("authtype" in event) { + const eventWithAuth = { + ...firestoreEvent, + authType: event.authtype, + authId: event.authid, + }; + delete (eventWithAuth as any).authtype; + delete (eventWithAuth as any).authid; + return eventWithAuth; + } + return firestoreEvent; } /** @internal */ export function makeChangedFirestoreEvent( - event: RawFirestoreEvent, + event: RawFirestoreEvent | RawFirestoreAuthEvent, params: Params -): FirestoreEvent | undefined, Params> { +): + | FirestoreEvent | undefined, Params> + | FirestoreAuthEvent | undefined, Params> { const data = event.data ? Change.fromObjects(createBeforeSnapshot(event), createSnapshot(event)) : undefined; @@ -395,6 +627,18 @@ export function makeChangedFirestoreEvent( }; delete (firestoreEvent as any).datacontenttype; delete (firestoreEvent as any).dataschema; + + if ("authtype" in event) { + const eventWithAuth = { + ...firestoreEvent, + authType: event.authtype, + authId: event.authid, + }; + delete (eventWithAuth as any).authtype; + delete (eventWithAuth as any).authid; + return eventWithAuth; + } + return firestoreEvent; } @@ -441,16 +685,19 @@ export function makeEndpoint( } /** @internal */ -export function onOperation( +export function onOperation< + Document extends string, + Event extends FirestoreEvent> +>( eventType: string, documentOrOpts: Document | DocumentOptions, - handler: (event: FirestoreEvent>) => any | Promise -): CloudFunction>> { + handler: (event: Event) => any | Promise +): CloudFunction { const { document, database, namespace, opts } = getOpts(documentOrOpts); // wrap the handler const func = (raw: CloudEvent) => { - const event = raw as RawFirestoreEvent; + const event = raw as RawFirestoreEvent | RawFirestoreAuthEvent; const documentPattern = new PathPattern( typeof document === "string" ? document : document.value() ); @@ -467,18 +714,19 @@ export function onOperation( } /** @internal */ -export function onChangedOperation( +export function onChangedOperation< + Document extends string, + Event extends FirestoreEvent, ParamsOf> +>( eventType: string, documentOrOpts: Document | DocumentOptions, - handler: ( - event: FirestoreEvent, ParamsOf> - ) => any | Promise -): CloudFunction, ParamsOf>> { + handler: (event: Event) => any | Promise +): CloudFunction { const { document, database, namespace, opts } = getOpts(documentOrOpts); // wrap the handler const func = (raw: CloudEvent) => { - const event = raw as RawFirestoreEvent; + const event = raw as RawFirestoreEvent | RawFirestoreAuthEvent; const documentPattern = new PathPattern( typeof document === "string" ? document : document.value() );