diff --git a/spec/v2/providers/firestore.spec.ts b/spec/v2/providers/firestore.spec.ts index 77ed0aa1d..954593903 100644 --- a/spec/v2/providers/firestore.spec.ts +++ b/spec/v2/providers/firestore.spec.ts @@ -86,6 +86,15 @@ function makeEvent(data?: any): firestore.RawFirestoreEvent { } as firestore.RawFirestoreEvent; } +function makeAuthEvent(data?: any): firestore.RawFirestoreAuthEvent { + return { + ...eventBase, + data, + userId: "userId", + authType: "user", + } as firestore.RawFirestoreAuthEvent; +} + const createdData = { value: { fields: { @@ -912,6 +921,38 @@ describe("firestore", () => { }); }); + describe("makeFirestoreAuthEvent", () => { + it("should make event from an event without data", () => { + const event = firestore.makeFirestoreAuthEvent( + firestore.createdEventType, + makeAuthEvent(), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event.data).to.eq(undefined); + }); + + it("should make event from a created event", () => { + const event = firestore.makeFirestoreAuthEvent( + firestore.createdEventType, + makeAuthEvent(makeEncodedProtobuf(createdProto)), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event.data.data()).to.deep.eq({ hello: "create world" }); + }); + + it("should make event from a deleted event", () => { + const event = firestore.makeFirestoreAuthEvent( + firestore.deletedEventType, + makeAuthEvent(makeEncodedProtobuf(deletedProto)), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event.data.data()).to.deep.eq({ hello: "delete world" }); + }); + }); + describe("makeChangedFirestoreEvent", () => { it("should make event from an event without data", () => { const event = firestore.makeChangedFirestoreEvent( @@ -943,6 +984,38 @@ describe("firestore", () => { }); }); + describe("makeChangedFirestoreEvent", () => { + it("should make event from an event without data", () => { + const event = firestore.makeChangedFirestoreAuthEvent( + makeAuthEvent(), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event.data).to.eq(undefined); + }); + + it("should make event from an updated event", () => { + const event = firestore.makeChangedFirestoreEvent( + makeAuthEvent(makeEncodedProtobuf(updatedProto)), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event.data.before.data()).to.deep.eq({ hello: "old world" }); + expect(event.data.after.data()).to.deep.eq({ hello: "new world" }); + }); + + it("should make event from a written event", () => { + const event = firestore.makeChangedFirestoreEvent( + makeAuthEvent(makeEncodedProtobuf(writtenProto)), + firestore.makeParams("foo/fGRodw71mHutZ4wGDuT8", new PathPattern("foo/{bar}")) + ); + + expect(event.data.before.data()).to.deep.eq({}); + expect(event.data.after.data()).to.deep.eq({ hello: "a new world" }); + }); + }); + + 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 2afcfc332..18d661f1d 100644 --- a/src/v2/providers/firestore.ts +++ b/src/v2/providers/firestore.ts @@ -93,7 +93,11 @@ export interface RawFirestoreEvent extends CloudEvent> extends Clou namespace: string; /** The document path */ document: string; - /** The type of principal that triggered the event */ - authType?: AuthType; - /** The unique identifier for the principal */ - authId?: string; /** * An object containing the values of the path patterns. * Only named capture groups will be populated - {key}, {key=*}, {key=**} @@ -136,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 */ @@ -197,9 +205,9 @@ export function onDocumentWritten( export function onDocumentWrittenWithAuthContext( document: Document, handler: ( - event: FirestoreEvent | undefined, ParamsOf> + event: FirestoreAuthEvent | undefined, ParamsOf> ) => any | Promise -): CloudFunction | undefined, ParamsOf>>; +): CloudFunction | undefined, ParamsOf>>; /** * Event handler that triggers when a document is created, updated, or deleted in Firestore. @@ -211,9 +219,9 @@ export function onDocumentWrittenWithAuthContext( export function onDocumentWrittenWithAuthContext( opts: DocumentOptions, handler: ( - event: FirestoreEvent | undefined, ParamsOf> + event: FirestoreAuthEvent | undefined, ParamsOf> ) => any | Promise -): CloudFunction | undefined, ParamsOf>>; +): CloudFunction | undefined, ParamsOf>>; /** * Event handler that triggers when a document is created, updated, or deleted in Firestore. @@ -223,12 +231,14 @@ export function onDocumentWrittenWithAuthContext( * @param handler - Event handler which is run every time a Firestore create, update, or delete occurs. */ export function onDocumentWrittenWithAuthContext( - documentorOpts: Document | DocumentOptions, + documentOrOpts: Document | DocumentOptions, handler: ( - event: FirestoreEvent | undefined, ParamsOf> + event: FirestoreAuthEvent | undefined, ParamsOf> ) => any | Promise -): CloudFunction | undefined, ParamsOf>> { - return onChangedOperation(writtenEventWithAuthContextType, documentorOpts, handler); +): CloudFunction< + FirestoreAuthEvent | undefined, ParamsOf> +> { + return onChangedOperation(writtenEventWithAuthContextType, documentOrOpts, handler); } /** @@ -282,9 +292,9 @@ export function onDocumentCreated( export function onDocumentCreatedWithAuthContext( document: Document, handler: ( - event: FirestoreEvent> + event: FirestoreAuthEvent> ) => any | Promise -): CloudFunction>>; +): CloudFunction>>; /** * Event handler that triggers when a document is created in Firestore. @@ -296,9 +306,9 @@ export function onDocumentCreatedWithAuthContext( export function onDocumentCreatedWithAuthContext( opts: DocumentOptions, handler: ( - event: FirestoreEvent> + event: FirestoreAuthEvent> ) => any | Promise -): CloudFunction>>; +): CloudFunction>>; /** * Event handler that triggers when a document is created in Firestore. @@ -309,9 +319,9 @@ export function onDocumentCreatedWithAuthContext( export function onDocumentCreatedWithAuthContext( documentOrOpts: Document | DocumentOptions, handler: ( - event: FirestoreEvent> + event: FirestoreAuthEvent> ) => any | Promise -): CloudFunction>> { +): CloudFunction>> { return onOperation(createdEventWithAuthContextType, documentOrOpts, handler); } @@ -365,9 +375,10 @@ export function onDocumentUpdated( export function onDocumentUpdatedWithAuthContext( document: Document, handler: ( - event: FirestoreEvent | undefined, ParamsOf> + event: FirestoreAuthEvent | undefined, ParamsOf> ) => any | Promise -): CloudFunction | undefined, ParamsOf>>; +): 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. @@ -378,9 +389,9 @@ export function onDocumentUpdatedWithAuthContext( export function onDocumentUpdatedWithAuthContext( opts: DocumentOptions, handler: ( - event: FirestoreEvent | undefined, ParamsOf> + event: FirestoreAuthEvent | undefined, ParamsOf> ) => any | Promise -): CloudFunction | undefined, ParamsOf>>; +): CloudFunction | undefined, ParamsOf>>; /** * Event handler that triggers when a document is updated in Firestore. @@ -391,9 +402,11 @@ export function onDocumentUpdatedWithAuthContext( export function onDocumentUpdatedWithAuthContext( documentOrOpts: Document | DocumentOptions, handler: ( - event: FirestoreEvent | undefined, ParamsOf> + event: FirestoreAuthEvent | undefined, ParamsOf> ) => any | Promise -): CloudFunction | undefined, ParamsOf>> { +): CloudFunction< + FirestoreAuthEvent | undefined, ParamsOf> +> { return onChangedOperation(updatedEventWithAuthContextType, documentOrOpts, handler); } @@ -448,9 +461,9 @@ export function onDocumentDeleted( export function onDocumentDeletedWithAuthContext( document: Document, handler: ( - event: FirestoreEvent> + event: FirestoreAuthEvent> ) => any | Promise -): CloudFunction>>; +): CloudFunction>>; /** * Event handler that triggers when a document is deleted in Firestore. @@ -462,9 +475,9 @@ export function onDocumentDeletedWithAuthContext( export function onDocumentDeletedWithAuthContext( opts: DocumentOptions, handler: ( - event: FirestoreEvent> + event: FirestoreAuthEvent> ) => any | Promise -): CloudFunction>>; +): CloudFunction>>; /** * Event handler that triggers when a document is deleted in Firestore. @@ -475,9 +488,9 @@ export function onDocumentDeletedWithAuthContext( export function onDocumentDeletedWithAuthContext( documentOrOpts: Document | DocumentOptions, handler: ( - event: FirestoreEvent> + event: FirestoreAuthEvent> ) => any | Promise -): CloudFunction>> { +): CloudFunction>> { return onOperation(deletedEventWithAuthContextType, documentOrOpts, handler); } @@ -578,9 +591,32 @@ export function makeFirestoreEvent( ...event, params, data, - authType: event.authtype as AuthType, + }; + + delete (firestoreEvent as any).datacontenttype; + delete (firestoreEvent as any).dataschema; + return firestoreEvent; +} + +/** @internal */ +export function makeFirestoreAuthEvent( + eventType: string, + event: RawFirestoreAuthEvent, + params: Params +): FirestoreAuthEvent { + const data = event.data + ? eventType === createdEventType + ? createSnapshot(event) + : createBeforeSnapshot(event) + : undefined; + const firestoreEvent: FirestoreAuthEvent = { + ...event, + params, + data, + authType: event.authtype, authId: event.authid, }; + delete (firestoreEvent as any).datacontenttype; delete (firestoreEvent as any).dataschema; return firestoreEvent; @@ -598,7 +634,24 @@ export function makeChangedFirestoreEvent( ...event, params, data, - authType: event.authtype as AuthType, + }; + delete (firestoreEvent as any).datacontenttype; + delete (firestoreEvent as any).dataschema; + return firestoreEvent; +} + +export function makeChangedFirestoreAuthEvent( + event: RawFirestoreAuthEvent, + params: Params +): FirestoreAuthEvent | undefined, Params> { + const data = event.data + ? Change.fromObjects(createBeforeSnapshot(event), createSnapshot(event)) + : undefined; + const firestoreEvent: FirestoreAuthEvent | undefined, Params> = { + ...event, + params, + data, + authType: event.authtype, authId: event.authid, }; delete (firestoreEvent as any).datacontenttype; @@ -649,21 +702,29 @@ export function makeEndpoint( } /** @internal */ -export function onOperation( +export function onOperation< + Document extends string, + Event extends + | FirestoreEvent> + | FirestoreAuthEvent> +>( 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() ); const params = makeParams(event.document, documentPattern) as unknown as ParamsOf; - const firestoreEvent = makeFirestoreEvent(eventType, event, params); + const firestoreEvent = + "authid" in raw + ? makeFirestoreAuthEvent(eventType, event, params) + : makeFirestoreEvent(eventType, event, params); return wrapTraceContext(withInit(handler))(firestoreEvent); }; @@ -675,23 +736,29 @@ export function onOperation( } /** @internal */ -export function onChangedOperation( +export function onChangedOperation< + Document extends string, + Event extends + | FirestoreEvent, ParamsOf> + | FirestoreAuthEvent, 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() ); const params = makeParams(event.document, documentPattern) as unknown as ParamsOf; - const firestoreEvent = makeChangedFirestoreEvent(event, params); + const firestoreEvent = + "authid" in raw + ? makeChangedFirestoreAuthEvent(event, params) + : makeChangedFirestoreEvent(event, params); return wrapTraceContext(withInit(handler))(firestoreEvent); };