diff --git a/functions/dataconnect-resolver/fwdChatToEmail.ts b/functions/dataconnect-resolver/fwdChatToEmail.ts new file mode 100644 index 00000000..6886001d --- /dev/null +++ b/functions/dataconnect-resolver/fwdChatToEmail.ts @@ -0,0 +1,58 @@ +// Stub Data Connect function. Normally, this would be generated by the Firebase CLI. +async function getChatMessage({ chatMessageId }: { chatMessageId: string }): Promise<{ data: { chatMessage?: { content?: string } } }> { + return { + data: { + chatMessage: { + content: "" + } + } + }; +} + +// [START fdc_forward_to_email_resolver] +import { getAuth } from "firebase-admin/auth"; +import { + FirebaseContext, + GraphqlServerOptions, + onGraphRequest +} from "firebase-functions/dataconnect/graphql"; + +const opts: GraphqlServerOptions = { + schemaFilePath: "dataconnect/schema_resolver/schema.gql", + resolvers: { + query: { + async forwardToEmail( + _parent: unknown, + args: Record, + _contextValue: FirebaseContext, + _info: unknown + ) { + const chatMessageId = args.chatMessageId as string; + + let decodedToken; + try { + decodedToken = await getAuth().verifyIdToken(_contextValue.auth.token ?? ""); + } catch (error) { + return false; + } + + const email = decodedToken.email; + if (!email) { + return false; + } + + // Call generated admin SDK. + const response = await getChatMessage({chatMessageId}); + const messageContent = response.data.chatMessage?.content; + + // Here you call the cloud service of your choice to send the email with + // the message content. + + return true; + } + }, + }, +}; + +export const resolver = onGraphRequest(opts); +// [END fdc_forward_to_email_resolver] diff --git a/functions/dataconnect-resolver/index.ts b/functions/dataconnect-resolver/index.ts new file mode 100644 index 00000000..b0204dc9 --- /dev/null +++ b/functions/dataconnect-resolver/index.ts @@ -0,0 +1,56 @@ +// [START fdc_resolver_skeleton] +import { + FirebaseContext, + GraphqlServerOptions, + onGraphRequest, +} from "firebase-functions/dataconnect/graphql"; + +const opts: GraphqlServerOptions = { + // Points to the schema you defined earlier, relative to the root of your + // Firebase project. + schemaFilePath: "dataconnect/schema_resolver/schema.gql", + resolvers: { + query: { + // This resolver function populates the data for the "publicProfile" field + // defined in your GraphQL schema located at schemaFilePath. + publicProfile( + _parent: unknown, + args: Record, + _contextValue: FirebaseContext, + _info: unknown + ) { + const userId = args.userId; + + // Here you would use the user ID to retrieve the user profile from your data + // store. In this example, we just return a hard-coded value. + + return { + name: "Ulysses von Userberg", + photoUrl: "https://example.com/profiles/12345/photo.jpg", + bioLine: "Just a guy on a mountain. Ski fanatic.", + }; + }, + }, + mutation: { + // This resolver function updates data for the "updatePublicProfile" field + // defined in your GraphQL schema located at schemaFilePath. + updatePublicProfile( + _parent: unknown, + args: Record, + _contextValue: FirebaseContext, + _info: unknown + ) { + const { userId, name, photoUrl, bioLine } = args; + + // Here you would update in your datastore the user's profile using the + // arguments that were passed. In this example, we just return the profile + // as though the operation had been successful. + + return { name, photoUrl, bioLine }; + }, + }, + }, +}; + +export const resolver = onGraphRequest(opts); +// [END fdc_resolver_skeleton] diff --git a/functions/dataconnect-resolver/package.json b/functions/dataconnect-resolver/package.json new file mode 100644 index 00000000..9b421ec6 --- /dev/null +++ b/functions/dataconnect-resolver/package.json @@ -0,0 +1,14 @@ +{ + "name": "dataconnect-resolver", + "version": "1.0.0", + "description": "Skeleton implementations of Data Connect custom resolvers", + "main": "index.js", + "license": "Apache-2.0", + "scripts": { + "compile": "cp ../../tsconfig.template.json ./tsconfig.json && tsc" + }, + "dependencies": { + "firebase-admin": "^11.11.1", + "firebase-functions": "^7.1.0" + } +} diff --git a/functions/dataconnect-resolver/sendEmail.ts b/functions/dataconnect-resolver/sendEmail.ts new file mode 100644 index 00000000..17e58f5a --- /dev/null +++ b/functions/dataconnect-resolver/sendEmail.ts @@ -0,0 +1,30 @@ +// [START fdc_send_email_resolver] +import { + FirebaseContext, + GraphqlServerOptions, + onGraphRequest, +} from "firebase-functions/dataconnect/graphql"; + +const opts: GraphqlServerOptions = { + schemaFilePath: "dataconnect/schema_resolver/schema.gql", + resolvers: { + mutation: { + sendEmail( + _parent: unknown, + args: Record, + _contextValue: FirebaseContext, + _info: unknown + ) { + const { friendId, content } = args; + + // Look up the friend's email address and call the cloud service of your + // choice to send the friend an email with the given content. + + return true; + }, + }, + }, +}; + +export const resolver = onGraphRequest(opts); +// [END fdc_send_email_resolver] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f79892a7..6aa58971 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,6 +161,15 @@ importers: specifier: ^11.9.0 version: 11.11.1 + functions/dataconnect-resolver: + dependencies: + firebase-admin: + specifier: ^11.11.1 + version: 11.11.1 + firebase-functions: + specifier: ^7.1.0 + version: 7.1.1(firebase-admin@11.11.1) + functions/firestore-export: dependencies: firebase-admin: @@ -736,9 +745,15 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + '@types/express-serve-static-core@5.1.1': resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/express@4.17.3': resolution: {integrity: sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==} @@ -767,6 +782,9 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + '@types/mocha@10.0.10': resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} @@ -785,9 +803,15 @@ packages: '@types/rimraf@3.0.2': resolution: {integrity: sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==} + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} @@ -1786,6 +1810,23 @@ packages: peerDependencies: firebase-admin: ^10.0.0 || ^11.0.0 || ^12.0.0 + firebase-functions@7.1.1: + resolution: {integrity: sha512-blTBsAfp3f9hYN34SgQgUFiMqGqEb53xrWe+P3EVlhxkdYmcmFPIJ6S77FemtgWGW53lFdUdJErtwpMI4+1bYg==} + engines: {node: '>=18.0.0'} + hasBin: true + peerDependencies: + '@apollo/server': ^5.2.0 + '@as-integrations/express4': ^1.1.2 + firebase-admin: ^11.10.0 || ^12.0.0 || ^13.0.0 + graphql: ^16.12.0 + peerDependenciesMeta: + '@apollo/server': + optional: true + '@as-integrations/express4': + optional: true + graphql: + optional: true + firebase-tools@12.9.1: resolution: {integrity: sha512-t/oTgGnGm3sLT3wR80B7hY6vdAs6rTlZMsmnZGsP+GeKtVzaB5KHEwLbkZuRXtqij9f35IfkQm2a4TKjKY6xUQ==} engines: {node: '>=16.13.0 || >=18.0.0'} @@ -4544,6 +4585,13 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 24.12.0 + '@types/qs': 6.15.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + '@types/express-serve-static-core@5.1.1': dependencies: '@types/node': 24.12.0 @@ -4551,6 +4599,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 1.2.1 + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.15.0 + '@types/serve-static': 1.15.10 + '@types/express@4.17.3': dependencies: '@types/body-parser': 1.19.6 @@ -4581,6 +4636,8 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/mime@1.3.5': {} + '@types/mocha@10.0.10': {} '@types/ms@2.1.0': {} @@ -4598,10 +4655,21 @@ snapshots: '@types/glob': 9.0.0 '@types/node': 24.12.0 + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 24.12.0 + '@types/send@1.2.1': dependencies: '@types/node': 24.12.0 + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.12.0 + '@types/send': 0.17.6 + '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 @@ -5728,6 +5796,17 @@ snapshots: transitivePeerDependencies: - supports-color + firebase-functions@7.1.1(firebase-admin@11.11.1): + dependencies: + '@types/cors': 2.8.19 + '@types/express': 4.17.25 + cors: 2.8.6 + express: 4.22.1 + firebase-admin: 11.11.1 + protobufjs: 7.5.4 + transitivePeerDependencies: + - supports-color + firebase-tools@12.9.1(@types/node@24.12.0): dependencies: '@google-cloud/pubsub': 3.7.5