diff --git a/spec/v2/providers/identity.spec.ts b/spec/v2/providers/identity.spec.ts index dbda1189c..956a42e68 100644 --- a/spec/v2/providers/identity.spec.ts +++ b/spec/v2/providers/identity.spec.ts @@ -65,6 +65,114 @@ const opts: identity.BlockingOptions = { }; describe("identity", () => { + describe("onUserCreated", () => { + it("should accept a handler", async () => { + let received: identity.UserRecord | undefined; + const fn = identity.onUserCreated((event) => { + received = event.data; + return null; + }); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: {}, + eventTrigger: { + eventType: identity.userCreatedEvent, + eventFilters: {}, + retry: false, + }, + }); + + const rawEvent = { + specversion: "1.0" as const, + id: "event-id", + source: "//identitytoolkit.googleapis.com/projects/project-id", + type: identity.userCreatedEvent, + time: new Date().toISOString(), + data: { + uid: "abc123", + metadata: { + creationTime: "2016-12-15T19:37:37.059Z", + lastSignInTime: "2017-01-01T00:00:00.000Z", + }, + }, + }; + + await fn(rawEvent as any); + expect(received).to.exist; + expect(received!.uid).to.equal("abc123"); + expect(received!.metadata).to.be.instanceof(identity.UserRecordMetadata); + }); + + it("should accept options and a handler", () => { + const fn = identity.onUserCreated( + { + region: "us-central1", + minInstances: 2, + memory: "512MiB", + retry: true, + }, + () => null + ); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + availableMemoryMb: 512, + region: ["us-central1"], + minInstances: 2, + labels: {}, + eventTrigger: { + eventType: identity.userCreatedEvent, + eventFilters: {}, + retry: true, + }, + }); + }); + }); + + describe("onUserDeleted", () => { + it("should accept a handler", () => { + const fn = identity.onUserDeleted(() => null); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + labels: {}, + eventTrigger: { + eventType: identity.userDeletedEvent, + eventFilters: {}, + retry: false, + }, + }); + }); + + it("should accept options and a handler", () => { + const fn = identity.onUserDeleted( + { + region: "europe-west3", + concurrency: 5, + retry: true, + }, + () => null + ); + + expect(fn.__endpoint).to.deep.equal({ + ...MINIMAL_V2_ENDPOINT, + platform: "gcfv2", + concurrency: 5, + region: ["europe-west3"], + labels: {}, + eventTrigger: { + eventType: identity.userDeletedEvent, + eventFilters: {}, + retry: true, + }, + }); + }); + }); + describe("beforeUserCreated", () => { it("should accept a handler", () => { const fn = identity.beforeUserCreated(() => Promise.resolve()); diff --git a/src/v2/providers/identity.ts b/src/v2/providers/identity.ts index 755cbf93f..0da0f7429 100644 --- a/src/v2/providers/identity.ts +++ b/src/v2/providers/identity.ts @@ -37,17 +37,23 @@ import { HttpsError, wrapHandler, MaybeAsync, + UserInfo, + UserRecord, + UserRecordMetadata, + userRecordConstructor, } from "../../common/providers/identity"; import { BlockingFunction } from "../../v1/cloud-functions"; import { wrapTraceContext } from "../trace"; import { Expression } from "../../params"; -import { initV2Endpoint } from "../../runtime/manifest"; +import { CloudEvent, CloudFunction } from "../core"; +import { initV2Endpoint, ManifestEndpoint } from "../../runtime/manifest"; import * as options from "../options"; import { SecretParam } from "../../params/types"; import { withInit } from "../../common/onInit"; export { HttpsError }; -export type { AuthUserRecord, AuthBlockingEvent }; +export { UserRecordMetadata, userRecordConstructor }; +export type { AuthUserRecord, AuthBlockingEvent, UserRecord, UserInfo }; /** @hidden Internally used when parsing the options. */ interface InternalOptions { @@ -166,6 +172,84 @@ export interface BlockingOptions { secrets?: (string | SecretParam)[]; } +/** @internal */ +export const userCreatedEvent = "google.firebase.auth.user.v1.created"; +/** @internal */ +export const userDeletedEvent = "google.firebase.auth.user.v1.deleted"; + +/** A CloudEvent that contains a Firebase Auth user record. */ +export type UserRecordEvent = CloudEvent; + +type UserEventHandler = (event: UserRecordEvent) => any | Promise; + +/** + * Event handler which triggers when a Firebase Auth user is created. + * + * @param handler - Event handler which is run every time a Firebase Auth user is created. + * @returns A function that you can export and deploy. + */ +export function onUserCreated(handler: UserEventHandler): CloudFunction; + +/** + * Event handler which triggers when a Firebase Auth user is created. + * + * @param opts - Options that can be set on an individual event-handling function. + * @param handler - Event handler which is run every time a Firebase Auth user is created. + * @returns A function that you can export and deploy. + */ +export function onUserCreated( + opts: options.EventHandlerOptions, + handler: UserEventHandler +): CloudFunction; + +/** + * Event handler which triggers when a Firebase Auth user is created. + * + * @param optsOrHandler - Options or an event handler. + * @param handler - Event handler which is run every time a Firebase Auth user is created. + * @returns A function that you can export and deploy. + */ +export function onUserCreated( + optsOrHandler: options.EventHandlerOptions | UserEventHandler, + handler?: UserEventHandler +): CloudFunction { + return onUserOperation(userCreatedEvent, optsOrHandler, handler); +} + +/** + * Event handler which triggers when a Firebase Auth user is deleted. + * + * @param handler - Event handler which is run every time a Firebase Auth user is deleted. + * @returns A function that you can export and deploy. + */ +export function onUserDeleted(handler: UserEventHandler): CloudFunction; + +/** + * Event handler which triggers when a Firebase Auth user is deleted. + * + * @param opts - Options that can be set on an individual event-handling function. + * @param handler - Event handler which is run every time a Firebase Auth user is deleted. + * @returns A function that you can export and deploy. + */ +export function onUserDeleted( + opts: options.EventHandlerOptions, + handler: UserEventHandler +): CloudFunction; + +/** + * Event handler which triggers when a Firebase Auth user is deleted. + * + * @param optsOrHandler - Options or an event handler. + * @param handler - Event handler which is run every time a Firebase Auth user is deleted. + * @returns A function that you can export and deploy. + */ +export function onUserDeleted( + optsOrHandler: options.EventHandlerOptions | UserEventHandler, + handler?: UserEventHandler +): CloudFunction { + return onUserOperation(userDeletedEvent, optsOrHandler, handler); +} + /** * Handles an event that is triggered before a user is created. * @param handler - Event handler which is run every time before a user is created. @@ -293,6 +377,53 @@ export function beforeSmsSent( return beforeOperation("beforeSendSms", optsOrHandler, handler); } +function onUserOperation( + eventType: string, + optsOrHandler: options.EventHandlerOptions | UserEventHandler, + handler?: UserEventHandler +): CloudFunction { + if (typeof optsOrHandler === "function") { + handler = optsOrHandler; + optsOrHandler = {}; + } + + const baseOpts = options.optionsToEndpoint(options.getGlobalOptions()); + const specificOpts = options.optionsToEndpoint(optsOrHandler); + + const func: any = (raw: CloudEvent) => { + const event = convertToUserRecordEvent(raw); + return wrapTraceContext(withInit(handler))(event); + }; + + func.run = handler; + + const endpoint: ManifestEndpoint = { + ...initV2Endpoint(options.getGlobalOptions(), optsOrHandler), + platform: "gcfv2", + ...baseOpts, + ...specificOpts, + labels: { + ...baseOpts?.labels, + ...specificOpts?.labels, + }, + eventTrigger: { + eventType, + eventFilters: {}, + retry: optsOrHandler.retry ?? false, + }, + }; + + func.__endpoint = endpoint; + + return func as CloudFunction; +} + +function convertToUserRecordEvent(raw: CloudEvent): UserRecordEvent { + const data = (raw.data ?? {}) as Record; + const user = userRecordConstructor(data); + return { ...raw, data: user } as UserRecordEvent; +} + /** @hidden */ export function beforeOperation( eventType: AuthBlockingEventType,